diff --git a/.lane/memory/packages/ui/src/components/timeline/layout.ts/01M53FDJQ4C0BGDGXNAY5GK09T-a-file-block-s-cache-key-inc.md b/.lane/memory/packages/ui/src/components/timeline/layout.ts/01M53FDJQ4C0BGDGXNAY5GK09T-a-file-block-s-cache-key-inc.md index d5b2fbce..b980ddd1 100644 --- a/.lane/memory/packages/ui/src/components/timeline/layout.ts/01M53FDJQ4C0BGDGXNAY5GK09T-a-file-block-s-cache-key-inc.md +++ b/.lane/memory/packages/ui/src/components/timeline/layout.ts/01M53FDJQ4C0BGDGXNAY5GK09T-a-file-block-s-cache-key-inc.md @@ -4,9 +4,9 @@ anchor: '@file' created: 2026-10-16T23:05:41Z norm: '1' sig: ce515648b5bedef2 -body_hash: 433c39db9d6384fd -raw_hash: ea74d2e9c0710659 -vouched: 2026-10-22T22:05:34Z +body_hash: 91ade5f09dcd49cf +raw_hash: 2c9b0b22266f1342 +vouched: 2026-10-27T14:07:44Z lines: 1-912 --- diff --git a/.lane/memory/packages/ui/src/components/timeline/layout.ts/01M5JTBNVCVRCB6KWWK6JSWF67-bubble-rows-measure-at-conte.md b/.lane/memory/packages/ui/src/components/timeline/layout.ts/01M5JTBNVCVRCB6KWWK6JSWF67-bubble-rows-measure-at-conte.md index dda78807..a4cde673 100644 --- a/.lane/memory/packages/ui/src/components/timeline/layout.ts/01M5JTBNVCVRCB6KWWK6JSWF67-bubble-rows-measure-at-conte.md +++ b/.lane/memory/packages/ui/src/components/timeline/layout.ts/01M5JTBNVCVRCB6KWWK6JSWF67-bubble-rows-measure-at-conte.md @@ -4,8 +4,9 @@ anchor: function itemHeight created: 2026-10-22T22:05:29Z norm: '1' sig: 3c40d8dd5f73026a -body_hash: 446268a0d25debb4 -raw_hash: e696d2f48fa62bb6 +body_hash: 9b7f5ad1d6fa2633 +raw_hash: c051d6140c4c9315 +vouched: 2026-10-27T14:07:44Z lines: 742-831 --- diff --git a/.lane/memory/packages/ui/src/components/timeline/layout.ts/01M5YV02MCCK1PGFNC7FJ58XPJ-itemheight-and-the-timeline.md b/.lane/memory/packages/ui/src/components/timeline/layout.ts/01M5YV02MCCK1PGFNC7FJ58XPJ-itemheight-and-the-timeline.md new file mode 100644 index 00000000..2489de6f --- /dev/null +++ b/.lane/memory/packages/ui/src/components/timeline/layout.ts/01M5YV02MCCK1PGFNC7FJ58XPJ-itemheight-and-the-timeline.md @@ -0,0 +1,12 @@ +--- +id: 01M5YV02MCCK1PGFNC7FJ58XPJ +anchor: function toolGroupOpen +created: 2026-10-27T14:07:31Z +norm: '1' +sig: 609e7680e155462e +body_hash: 97cc55c00fcde4b1 +raw_hash: 34d7822a7841af58 +lines: 859-861 +--- + +itemHeight and the timeline render must both decide a tool group's openness with toolGroupOpen, or measured heights drift from what renders diff --git a/packages/channel/src/seed.ts b/packages/channel/src/seed.ts index 96bca22d..9fe43e8b 100644 --- a/packages/channel/src/seed.ts +++ b/packages/channel/src/seed.ts @@ -72,7 +72,7 @@ async function history(harness: Harness, chat: ConversationId): Promise, header: number, expanded: boolean, + live: boolean, ): number { let h = BUBBLE_PAD_Y * 2 + BUBBLE_BORDER * 2 + header; if (!expanded) return h; let content = 0; let count = 0; for (let item of items) { - content += workingHeight(item, blocks, width, open); + content += workingHeight(item, blocks, width, open, live); count++; } if (count > 1) content += (count - 1) * CONTENT_GAP; @@ -851,6 +852,19 @@ function workingSectionHeight( return h + content; } +/** + * While a turn runs, its tool groups are open so the work stays visible; once it finishes they + * fold. `open` holds the groups someone toggled away from that default, keyed by toolGroupKey. + */ +function toolGroupOpen(id: string, open: ReadonlySet, live: boolean): boolean { + return open.has(toolGroupKey(id, live)) !== live; +} + +/** A separate key while live, so closing a running group doesn't open it once the turn folds. */ +function toolGroupKey(id: string, live: boolean): string { + return live ? id + ":live" : id; +} + /** The folded turn's label row, below its recap when there is one. */ function workingHeaderHeight(recap: string | undefined, width: number): number { if (!recap) return WORKING_SUMMARY_HEIGHT; @@ -864,6 +878,7 @@ function workingHeight( blocks: Map, width: number, open: Set, + live: boolean, ): number { let b = blocks.get(item.id) || []; if (item.type === "text") { @@ -873,7 +888,7 @@ function workingHeight( } let h = TOOL_GROUP_ROW_HEIGHT + TOOL_GROUP_PAD_Y * 2; - if (!open.has(item.id) || !b.length) return h; + if (!toolGroupOpen(item.id, open, live) || !b.length) return h; h += measureRow(b, width - TOOL_LIST_INDENT).height; h += b.length * CONTENT_GAP; return h; @@ -910,6 +925,8 @@ export { TOOL_GROUP_PAD_Y, TOOL_GROUP_ROW_HEIGHT, TOOL_LIST_INDENT, + toolGroupKey, + toolGroupOpen, TURN_GAP, useBlockMap, WORKING_SUMMARY_HEIGHT, diff --git a/packages/ui/src/components/timeline/timeline.tsx b/packages/ui/src/components/timeline/timeline.tsx index 8b810679..9ed22868 100644 --- a/packages/ui/src/components/timeline/timeline.tsx +++ b/packages/ui/src/components/timeline/timeline.tsx @@ -80,6 +80,8 @@ import { TOOL_GROUP_PAD_Y, TOOL_GROUP_ROW_HEIGHT, TOOL_LIST_INDENT, + toolGroupKey, + toolGroupOpen, TURN_GAP, useBlockMap, WORKING_SUMMARY_HEIGHT, @@ -469,7 +471,7 @@ function AgentWorkingContextView( style={{ gap: CONTENT_GAP }} > {turn.working.map((w, index) => { - let open = w.type === "tools" && openGroups.has(w.id); + let open = w.type === "tools" && toolGroupOpen(w.id, openGroups, live); return ( onToggleGroup(toolGroupKey(id, live))} live={live} index={index} />