mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-14 12:10:45 +03:00
fix(app): keep the activity pill clear of Working on touch (#61794)
This commit is contained in:
committed by
GitHub
Unverified
parent
5ff7cfb679
commit
3ca0739383
@@ -633,10 +633,28 @@ async function captureCardEdges(page: Page, card: Locator) {
|
||||
)
|
||||
}
|
||||
|
||||
test("scrolls the timeline behind the composer region, fading at the card, and keeps its end above the region", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Without hover the activity pill grows to its 44px touch size, and the tail it floats in grows with it.
|
||||
for (const input of [
|
||||
{ name: "a pointer", touch: false, viewport: { width: 1400, height: 900 }, pill: 32 },
|
||||
{ name: "touch", touch: true, viewport: { width: 390, height: 844 }, pill: 44 },
|
||||
]) {
|
||||
test.describe(`with ${input.name}`, () => {
|
||||
test.use({ hasTouch: input.touch, isMobile: input.touch })
|
||||
|
||||
test("scrolls the timeline behind the composer region, fading at the card, and keeps its end above the region", async ({
|
||||
page,
|
||||
}) => {
|
||||
await scrollsBehindComposerRegion(page, input)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function scrollsBehindComposerRegion(
|
||||
page: Page,
|
||||
input: { viewport: { width: number; height: number }; pill: number },
|
||||
) {
|
||||
await setupTimeline(page, {
|
||||
viewport: input.viewport,
|
||||
sessionMessages: [
|
||||
...validateTimelineMessages(historyMessages(18)),
|
||||
{ id: "msg_9000_user", type: "user", text: "Split the work.", time: { created: 1 } },
|
||||
@@ -681,7 +699,10 @@ test("scrolls the timeline behind the composer region, fading at the card, and k
|
||||
const dock = region.getBoundingClientRect()
|
||||
const card = region.querySelector('[data-component="composer"]')!.getBoundingClientRect()
|
||||
const pill = region.querySelector('[data-slot="session-activity-chip"]')!.getBoundingClientRect()
|
||||
const hint = viewport.querySelector('[data-component="session-background-hint-row"]')!.getBoundingClientRect()
|
||||
const row = viewport.querySelector('[data-component="session-background-hint-row"]')!
|
||||
const hint = row.getBoundingClientRect()
|
||||
// The live status lane that holds Working and the background hint.
|
||||
const lane = row.parentElement!.getBoundingClientRect()
|
||||
const parts = [...viewport.querySelectorAll("[data-timeline-part-id]")]
|
||||
const last = parts.find((element) => element.textContent?.includes("The last answer ends"))!
|
||||
|
||||
@@ -695,13 +716,25 @@ test("scrolls the timeline behind the composer region, fading at the card, and k
|
||||
),
|
||||
// The pill floats above the region out of flow, so it never changes the inset the timeline scrolls above.
|
||||
floats: pill.bottom <= dock.top,
|
||||
// It floats in the timeline's reserved tail, below the background hint and the last answer.
|
||||
reserved: hint.bottom <= pill.top && last.getBoundingClientRect().bottom <= hint.top,
|
||||
pill: Math.round(pill.height),
|
||||
// It floats in the timeline's reserved tail, a clear gap below the live status lane and the last answer.
|
||||
clearance: Math.round(pill.top - lane.bottom),
|
||||
reserved: lane.top <= hint.top && last.getBoundingClientRect().bottom <= hint.top,
|
||||
atEnd: viewport.scrollHeight - viewport.clientHeight - viewport.scrollTop <= 1,
|
||||
}
|
||||
})
|
||||
|
||||
const expected = { behind: 0, inset: 0, fade: 0, floats: true, reserved: true, atEnd: true }
|
||||
const expected = {
|
||||
behind: 0,
|
||||
inset: 0,
|
||||
fade: 0,
|
||||
floats: true,
|
||||
pill: input.pill,
|
||||
clearance: 14,
|
||||
reserved: true,
|
||||
atEnd: true,
|
||||
}
|
||||
|
||||
await expect.poll(geometry).toEqual(expected)
|
||||
const before = (await dock.boundingBox())!.height
|
||||
|
||||
@@ -715,7 +748,7 @@ test("scrolls the timeline behind the composer region, fading at the card, and k
|
||||
|
||||
await expect.poll(async () => (await dock.boundingBox())!.height).toBeGreaterThan(before + 40)
|
||||
await expect.poll(geometry).toEqual(expected)
|
||||
})
|
||||
}
|
||||
|
||||
test("groups consecutive sent prompts under the last one's meta line, with each bubble's actions beside it", async ({
|
||||
page,
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* The timeline's tail reserves this height (tailSize in timeline/virtualizer.tsx); change both together. */
|
||||
@media (hover: none) {
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
@@ -35,10 +35,6 @@ import { defersAdjustments, END_EPSILON, MIN_OVERFLOW, needsEndProbe, ZOOM_SLACK
|
||||
|
||||
const fallbackItemSize = 60
|
||||
|
||||
// Reserved below the last row: a 36px lane for the live status row and its background hint, a 14px gap, then a 32px
|
||||
// activity pill and its 10px composer gap. Both come and go inside it, so neither moves the rows above.
|
||||
const tailSize = 92
|
||||
|
||||
const pendingMarkdown = '[data-component="markdown"]:not([data-markdown-ready])'
|
||||
|
||||
const upwardKeys = new Set(["up", "page-up", "home"])
|
||||
@@ -116,6 +112,11 @@ export function createTimelineVirtualizer(input: Input) {
|
||||
const active = () => input.active?.() !== false
|
||||
const isDesktop = createMediaQuery("(min-width: 768px)")
|
||||
const topOffset = () => (input.showHeader() ? 64 : isDesktop() ? 0 : 16)
|
||||
// Reserved below the last row: a 36px lane for the live status row and its background hint, a 14px gap, then the
|
||||
// activity pill and its 10px composer gap. Without hover the pill grows from 32px to its 44px touch size
|
||||
// (composer-lane.css), and the tail grows with it. Both come and go inside it, so neither moves the rows above.
|
||||
const touch = createMediaQuery("(hover: none)")
|
||||
const tailSize = () => (touch() ? 104 : 92)
|
||||
const ownerSessionKey = input.sessionKey()
|
||||
const entry = cache.get(ownerSessionKey)
|
||||
const cached = entry?.presentationKey === input.presentationKey?.() ? entry : undefined
|
||||
@@ -415,7 +416,9 @@ export function createTimelineVirtualizer(input: Input) {
|
||||
// Empty projections still need the bottom spacer for running status.
|
||||
return rows().length > 0 ? topOffset() : 0
|
||||
},
|
||||
paddingEnd: tailSize,
|
||||
get paddingEnd() {
|
||||
return tailSize()
|
||||
},
|
||||
// Revealed prompts sit below the solid header and its 16px fade.
|
||||
get scrollPaddingStart() {
|
||||
return input.showHeader() ? headerHeight() + 16 : 0
|
||||
@@ -1190,8 +1193,8 @@ export function createTimelineVirtualizer(input: Input) {
|
||||
data-timeline-row="bottom-spacer"
|
||||
class="absolute top-0 left-0 w-full"
|
||||
style={{
|
||||
height: `${tailSize}px`,
|
||||
transform: `translateY(${virtualizer.getTotalSize() - tailSize - rendering.scrollAdjustment}px)`,
|
||||
height: `${tailSize()}px`,
|
||||
transform: `translateY(${virtualizer.getTotalSize() - tailSize() - rendering.scrollAdjustment}px)`,
|
||||
}}
|
||||
>
|
||||
{props.bottomSpacer}
|
||||
|
||||
Reference in New Issue
Block a user