Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
cfe4928
Agent chat: one action footer per turn, at the bottom; its Copy copie…
teamleaderleo Oct 31, 2026
536abe1
Tests and gallery note: a reply inside a turn keeps the PR fallback; …
teamleaderleo Oct 31, 2026
62522ec
Merge origin/feat-cmux-next (take its deletions of the PR dock tests …
teamleaderleo Oct 31, 2026
5c7ed48
Test: use translatorFor (i18n has no createT)
teamleaderleo Oct 31, 2026
b679ddf
Test: format
teamleaderleo Oct 31, 2026
fe3cf09
Keep transcript bottom pinned while tool folds open
teamleaderleo Oct 31, 2026
8a409a7
Include fold room cleanup in transcript placement deps
teamleaderleo Oct 31, 2026
a5c70cf
Include bottom fold room cleanup dependency
teamleaderleo Oct 31, 2026
764f4d2
Merge remote-tracking branch 'origin/feat-cmux-next' into turn-footer…
teamleaderleo Oct 31, 2026
fca8d25
Gallery: remove the per-reply action row page (no reply draws its own…
teamleaderleo Oct 31, 2026
c322f89
Keep transcript bottom room after tool collapse
teamleaderleo Oct 31, 2026
4ee8970
Format transcript room browser test
teamleaderleo Oct 31, 2026
6bc2412
Keep line anchors for nonanimated transcript folds
teamleaderleo Oct 31, 2026
03cc3c0
fix(transcript): keep copied tool runs split by steers
teamleaderleo Oct 31, 2026
8b5d236
test(transcript): match tool summary order
teamleaderleo Oct 31, 2026
94c6023
test(transcript): assert rendered read path
teamleaderleo Oct 31, 2026
23f20a4
Count transcript stack animation in popup audit
teamleaderleo Oct 31, 2026
3c76215
test(transcript): render exactly one footer per turn
teamleaderleo Oct 31, 2026
b6888cd
style(transcript): format footer regression test
teamleaderleo Oct 31, 2026
fd1fe34
Keep received reports out of steer fold coverage
teamleaderleo Oct 31, 2026
d60126e
fix(transcript): keep pre-steer replies expanded
teamleaderleo Oct 31, 2026
e2e26a1
Merge commit 'd60126ece51f5ad91e4543f52398349c40b9ce27' into lane/tra…
teamleaderleo Oct 31, 2026
506e050
Preserve expanded extent through collapse and cover visible steer rep…
teamleaderleo Oct 31, 2026
7563b9d
Keep bottom room when reader scrolls during collapse
teamleaderleo Oct 31, 2026
ea3db30
Integrate landed bottom expansion motion with retained collapse room
teamleaderleo Oct 31, 2026
6b9cb38
Keep incoming report fixtures in their connected turn
teamleaderleo Oct 31, 2026
a83f3e8
Merge remote-tracking branch 'origin/feat-cmux-next' into lane/transc…
teamleaderleo Oct 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 35 additions & 3 deletions webviews/src/agent-session/acpmux/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3166,18 +3166,23 @@ export function VirtualTranscript({
/// when the line was clicked. Closing a body at the transcript's end leaves that much room under
/// the rows instead of letting the browser clamp the offset, which brought every row down by what
/// closed (Leo, ask-a736047107: "WHEN I COLLAPSE IT JUMPS ME UP"). The room shrinks with the view's
/// end as the reader scrolls up, and goes once the rows reach it again (`trimFoldRoom`), on a send
/// and on Jump to latest; a fold that left the rows past it gives it up at the next pass.
/// end as the reader scrolls up unless this was a bottom tool collapse. Bottom room stays until
/// the rows grow into it again; a send or Jump to latest releases either kind.
const [foldRoom, setFoldRoom] = useState(0);
const foldRoomRef = useRef(0);
foldRoomRef.current = foldRoom;
/// A bottom collapse keeps the end created by its open body. Unlike the transient room used to
/// hold a line clicked away from the end, this room remains while the reader scrolls up and back.
/// It is released only when the rows grow into it, or an explicit latest/send transition drops it.
const stickyFoldRoom = useRef<{ shrunk: boolean } | undefined>(undefined);
/// The content's end without that room, as of the last render.
const rowsEndRef = useRef(0);
/// Shrinks the kept room to the view's end at `node`'s offset, and lets it go once nothing holds
/// it (no fold in flight, the rows reach the view's end). O(1): a read of the offset and a compare.
/// Lets the kept room go: a send or Jump to latest heads for the rows' own end. `now` commits it
/// before the caller reads the layout (Jump to latest's target).
const dropFoldRoom = useCallback((now = false) => {
stickyFoldRoom.current = undefined;
if (!foldRoomRef.current) return;
foldRoomRef.current = 0;
if (now) flushSync(() => setFoldRoom(0));
Expand All @@ -3188,6 +3193,20 @@ export function VirtualTranscript({
const trimFoldRoom = useCallback((node: HTMLElement) => {
const kept = foldRoomRef.current;
if (!kept) return;
// A tool body opened at the end gives the transcript a larger physical end. Keep that end when
// its body closes, including through an upward scroll and a return to the latest row. The room
// is still released once the rows themselves grow back to it.
if (stickyFoldRoom.current) {
// A wheel can release the clicked-line anchor before the collapse's first measurement.
// Only growth after an observed shrink may consume this room.
if (rowsEndRef.current < kept - 0.5) stickyFoldRoom.current.shrunk = true;
if (stickyFoldRoom.current.shrunk && !foldHolding(foldAnchor.current) && rowsEndRef.current >= kept - 0.5) {
stickyFoldRoom.current = undefined;
foldRoomRef.current = 0;
setFoldRoom(0);
}
return;
}
const holding = foldHolding(foldAnchor.current);
// A view that follows the latest row again (the reader scrolled back to it) ends at the rows.
if (followingRef.current && !holding) {
Expand Down Expand Up @@ -4586,6 +4605,14 @@ export function VirtualTranscript({
foldRoomRef.current = end;
setFoldRoom(end);
};
const keepBottomFoldRoom = () => {
if (!node) return;
const end = foldRoomFor(node.scrollTop, node.clientHeight, foldRoomRef.current);
stickyFoldRoom.current = { shrunk: rowsEndRef.current < end - 0.5 };
if (end === foldRoomRef.current) return;
foldRoomRef.current = end;
setFoldRoom(end);
};
const beginBottomStackFold = (toggle: HTMLElement, line: HTMLElement): boolean => {
// This ask is for tool/work disclosures. Message Show more/less and other controls retain
// their established row anchor, including the room they need when closing at the end.
Expand All @@ -4602,7 +4629,12 @@ export function VirtualTranscript({
stopBottomStackFold();
followGlide.current?.stop();
stopJump();
dropFoldRoom();
// A closing tool keeps its expanded extent and clicked line rather than sliding down.
if (toggle.matches(".cv-tool.is-toggle") && toggle.getAttribute("aria-expanded") === "true") {
keepBottomFoldRoom();
return false;
}
if (!stickyFoldRoom.current) dropFoldRoom();
const stack: BottomStackFold = { toggle: line, row, beforeTop: layoutScrollTop(node), delta: 0, from: 0 };
bottomStackFold.current = stack;
row.setAttribute("data-bottom-fold", "");
Expand Down
91 changes: 78 additions & 13 deletions webviews/test/transcript-fold-anchor-browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ await requireBrowserLane("transcript-fold-anchor-browser.test.ts", async () => {
const node = document.querySelector<HTMLElement>(".acpmux-scroll")!;
return node.scrollHeight - node.clientHeight - node.scrollTop <= 2;
});
const scrollHeight = (page: Page) =>
page.evaluate(() => document.querySelector<HTMLElement>(".acpmux-scroll")!.scrollHeight);
/// Marks the line `selector` finds last in the transcript, and returns where it is drawn.
const probe = (page: Page, selector: string) =>
page.evaluate((query) => {
Expand Down Expand Up @@ -225,26 +227,58 @@ await requireBrowserLane("transcript-fold-anchor-browser.test.ts", async () => {
expect(opened.threadDuration).toBeLessThanOrEqual(200);
expect(await atLatest(page)).toBe(true);

// The reply streams on; the bottom edge remains pinned.
for (let version = 2; version < 6; version += 1) {
await show(page, [...done, ...live(version)]);
await settle(page);
expect(await atLatest(page)).toBe(true);
}

// Closing at the end gets the same one-shot stack motion.
const expandedEnd = await scrollHeight(page);
// Closing keeps the expanded end and the clicked line, with no downward stack motion.
const closed = await clickAndWatch(page);
expect(await page.locator("[data-probe]").getAttribute("aria-expanded")).toBe("false");
expect(closed.bottomDrift).toBeLessThanOrEqual(2);
expect(closed.threadAnimations).toBe(1);
expect(closed.folded).toBe(false);
expect(closed.animatedHeights).toBe(1);
expect(closed.threadDuration).toBeGreaterThanOrEqual(150);
expect(closed.threadDuration).toBeLessThanOrEqual(200);
expect(closed.moved).toBeLessThanOrEqual(1);
expect(closed.threadAnimations).toBe(0);
expect(await scrollHeight(page)).toBe(expandedEnd);
expect(await atLatest(page)).toBe(true);

// The room created by the expanded body remains after collapse. Scrolling up and back to the
// latest row must not trim it or reintroduce a jump.
const retainedEnd = await scrollHeight(page);
await page.mouse.wheel(0, -260);
await settle(page);
expect(await atLatest(page)).toBe(false);
expect(await scrollHeight(page)).toBe(retainedEnd);
await page.mouse.wheel(0, 10_000);
await settle(page);
expect(await scrollHeight(page)).toBe(retainedEnd);
expect(await atLatest(page)).toBe(true);
await page.close();
});

test("bottom room survives upward scrolling before a collapse is measured", async () => {
const page = await open("no-preference");
await show(page, [...done, ...live(1)]);
await settle(page);
await probe(page, `[data-row-id="live-run"] button.cv-tool.is-toggle[aria-expanded="false"]`);
await clickAndWatch(page);
const expandedEnd = await scrollHeight(page);
// Deliver reader intent and a scroll synchronously with the close, before ResizeObserver
// can report the shrinking body. This releases the line hold but must not release its room.
await page.evaluate(() => {
const toggle = document.querySelector<HTMLElement>("[data-probe]")!;
const scroller = document.querySelector<HTMLElement>(".acpmux-scroll")!;
toggle.click();
scroller.dispatchEvent(new WheelEvent("wheel", { deltaY: -160, bubbles: true }));
scroller.scrollTop -= 160;
scroller.dispatchEvent(new Event("scroll"));
});
await settle(page);
expect(await page.locator("[data-probe]").getAttribute("aria-expanded")).toBe("false");
expect(await atLatest(page)).toBe(false);
expect(await scrollHeight(page)).toBe(expandedEnd);
await page.mouse.wheel(0, 10_000);
await settle(page);
expect(await atLatest(page)).toBe(true);
expect(await scrollHeight(page)).toBe(expandedEnd);
await page.close();
});

test("a tool toggle just above the end reacquires bottom follow, and user scroll cancels its motion", async () => {
const page = await open("no-preference");
await show(page, [...done, ...live(1)]);
Expand Down Expand Up @@ -289,6 +323,37 @@ await requireBrowserLane("transcript-fold-anchor-browser.test.ts", async () => {
await page.close();
});

test("a completed turn keeps the reply before a personal steer visible across fold toggles", async () => {
const page = await open("reduce");
await show(page, [
...done,
row("steer-user", "user", "Read the log."),
row("steer-work-a", "activity", undefined, { items: [read("before-steer")] }),
row("steer-answer-a", "assistant", "Reply A, before the steer."),
row("steer-input", "user", "Also check the tests.", { steer: "landed" }),
row("steer-work-b", "activity", undefined, { items: [read("after-steer")] }),
row("steer-answer-b", "assistant", "Reply B, the final answer."),
row("steer-summary", "turnSummary", undefined, { status: "completed", durationMs: 9_000 }),
]);
await settle(page);
const line = page.locator('[data-row-id="worked-steer-user"] .cv-worked');
const visibleDialogue = async () => {
for (const id of ["steer-answer-a", "steer-input", "steer-answer-b"])
expect(await page.locator(`[data-row-id="${id}"]`).isVisible()).toBe(true);
};
expect(await line.getAttribute("aria-expanded")).toBe("false");
await visibleDialogue();
await line.click();
await settle(page);
expect(await line.getAttribute("aria-expanded")).toBe("true");
await visibleDialogue();
await line.click();
await settle(page);
expect(await line.getAttribute("aria-expanded")).toBe("false");
await visibleDialogue();
await page.close();
});

test("completion folds received reports without remounting the answer, and keeps one-click open state", async () => {
const page = await open("reduce");
const reports = Array.from({ length: 24 }, (_, index) =>
Expand Down
Loading