authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-08-30 20:32:59-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-30 17:32:59-07:00
log17c48f7fc1af66110706b013bf41901ebcd8aedb
treeee1a3b8a5097e1aadf21a8f78823e3ac084510c9
parente4f7b14fab0850bff134a3f6bfaf5b71cc8ac9fc
signature Signed by PGP key B5690EEEBB952194

fix(web): fold interim turn responses (#8828)


2 files changed, 8 insertions(+), 17 deletions(-)

apps/web/src/components/chat/MessagesTimeline.logic.test.ts+4-9
...@@ -469,7 +469,7 @@ describe("deriveMessagesTimelineRows", () => {...@@ -469,7 +469,7 @@ describe("deriveMessagesTimelineRows", () => {
469 expect(assistantRow?.assistantTurnDiffSummary).toBe(assistantTurnDiffSummary);469 expect(assistantRow?.assistantTurnDiffSummary).toBe(assistantTurnDiffSummary);
470 });470 });
471471
472 it("keeps the first and terminal assistant messages visible around settled work", () => {472 it("folds the first assistant message and settled work before the terminal response", () => {
473 const timelineEntries = [473 const timelineEntries = [
474 {474 {
475 id: "user-entry",475 id: "user-entry",
...@@ -545,7 +545,6 @@ describe("deriveMessagesTimelineRows", () => {...@@ -545,7 +545,6 @@ describe("deriveMessagesTimelineRows", () => {
545 expect(foldRow?.label).toBe("Worked for 22s");545 expect(foldRow?.label).toBe("Worked for 22s");
546 expect(collapsedRows.map((row) => row.id)).toEqual([546 expect(collapsedRows.map((row) => row.id)).toEqual([
547 "user-entry",547 "user-entry",
548 "assistant-first-entry",
549 "turn-fold:turn-1",548 "turn-fold:turn-1",
550 "assistant-final-entry",549 "assistant-final-entry",
551 ]);550 ]);
...@@ -561,8 +560,8 @@ describe("deriveMessagesTimelineRows", () => {...@@ -561,8 +560,8 @@ describe("deriveMessagesTimelineRows", () => {
561560
562 expect(expandedRows.map((row) => row.id)).toEqual([561 expect(expandedRows.map((row) => row.id)).toEqual([
563 "user-entry",562 "user-entry",
564 "assistant-first-entry",
565 "turn-fold:turn-1",563 "turn-fold:turn-1",
564 "assistant-first-entry",
566 "work-toggle:work-entry-1",565 "work-toggle:work-entry-1",
567 "assistant-final-entry",566 "assistant-final-entry",
568 ]);567 ]);
...@@ -571,7 +570,7 @@ describe("deriveMessagesTimelineRows", () => {...@@ -571,7 +570,7 @@ describe("deriveMessagesTimelineRows", () => {
571 ).toBeDefined();570 ).toBeDefined();
572 });571 });
573572
574 it("folds assistant messages between the first and terminal messages", () => {573 it("folds all assistant messages before the terminal message", () => {
575 const timelineEntries = [574 const timelineEntries = [
576 {575 {
577 id: "assistant-first-entry",576 id: "assistant-first-entry",
...@@ -625,11 +624,7 @@ describe("deriveMessagesTimelineRows", () => {...@@ -625,11 +624,7 @@ describe("deriveMessagesTimelineRows", () => {
625 revertTurnCountByUserMessageId: new Map(),624 revertTurnCountByUserMessageId: new Map(),
626 });625 });
627626
628 expect(rows.map((row) => row.id)).toEqual([627 expect(rows.map((row) => row.id)).toEqual(["turn-fold:turn-1", "assistant-final-entry"]);
629 "assistant-first-entry",
630 "turn-fold:turn-1",
631 "assistant-final-entry",
632 ]);
633 });628 });
634629
635 it("derives a sane duration for a steer-superseded turn with one instant commentary message", () => {630 it("derives a sane duration for a steer-superseded turn with one instant commentary message", () => {
apps/web/src/components/chat/MessagesTimeline.logic.ts+4-8
...@@ -523,10 +523,9 @@ function timelineEntryTurnId(entry: TimelineEntry): TurnId | null {...@@ -523,10 +523,9 @@ function timelineEntryTurnId(entry: TimelineEntry): TurnId | null {
523}523}
524524
525/**525/**
526 * Settled turns keep their first and terminal assistant messages visible.526 * Settled turns keep only their terminal assistant message visible.
527 * Everything between them folds behind a "Worked for ..." row anchored at527 * Everything before it folds behind a "Worked for ..." row anchored at the
528 * the first hidden entry. Keeping both ends prevents a short follow-up from528 * first hidden entry, so the duration leads directly into the final response.
529 * hiding a substantive opening response while still bounding noisy turns.
530 */529 */
531function deriveTurnFolds(input: {530function deriveTurnFolds(input: {
532 timelineEntries: ReadonlyArray<TimelineEntry>;531 timelineEntries: ReadonlyArray<TimelineEntry>;
...@@ -596,12 +595,9 @@ function deriveTurnFolds(input: {...@@ -596,12 +595,9 @@ function deriveTurnFolds(input: {
596 if (group.hasStreamingMessage) {595 if (group.hasStreamingMessage) {
597 continue;596 continue;
598 }597 }
599 const firstAssistantEntry = group.entries.find(
600 (entry): entry is Extract<TimelineEntry, { kind: "message" }> => entry.kind === "message",
601 );
602 const hiddenEntryIds = new Set<string>();598 const hiddenEntryIds = new Set<string>();
603 for (const entry of group.entries) {599 for (const entry of group.entries) {
604 if (entry.id === firstAssistantEntry?.id || entry.id === group.terminalEntry?.id) {600 if (entry.id === group.terminalEntry?.id) {
605 continue;601 continue;
606 }602 }
607 // Agent-spawn CTA rows never fold: workflows outlive their launching603 // Agent-spawn CTA rows never fold: workflows outlive their launching