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", () => {
469469 expect(assistantRow?.assistantTurnDiffSummary).toBe(assistantTurnDiffSummary);
470470 });
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", () => {
473473 const timelineEntries = [
474474 {
475475 id: "user-entry",
......@@ -545,7 +545,6 @@ describe("deriveMessagesTimelineRows", () => {
545545 expect(foldRow?.label).toBe("Worked for 22s");
546546 expect(collapsedRows.map((row) => row.id)).toEqual([
547547 "user-entry",
548 "assistant-first-entry",
549548 "turn-fold:turn-1",
550549 "assistant-final-entry",
551550 ]);
......@@ -561,8 +560,8 @@ describe("deriveMessagesTimelineRows", () => {
561560
562561 expect(expandedRows.map((row) => row.id)).toEqual([
563562 "user-entry",
564 "assistant-first-entry",
565563 "turn-fold:turn-1",
564 "assistant-first-entry",
566565 "work-toggle:work-entry-1",
567566 "assistant-final-entry",
568567 ]);
......@@ -571,7 +570,7 @@ describe("deriveMessagesTimelineRows", () => {
571570 ).toBeDefined();
572571 });
573572
574 it("folds assistant messages between the first and terminal messages", () => {
573 it("folds all assistant messages before the terminal message", () => {
575574 const timelineEntries = [
576575 {
577576 id: "assistant-first-entry",
......@@ -625,11 +624,7 @@ describe("deriveMessagesTimelineRows", () => {
625624 revertTurnCountByUserMessageId: new Map(),
626625 });
627626
628 expect(rows.map((row) => row.id)).toEqual([
629 "assistant-first-entry",
630 "turn-fold:turn-1",
631 "assistant-final-entry",
632 ]);
627 expect(rows.map((row) => row.id)).toEqual(["turn-fold:turn-1", "assistant-final-entry"]);
633628 });
634629
635630 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 {
523523}
524524
525525/**
526 * Settled turns keep their first and terminal assistant messages visible.
527 * Everything between them folds behind a "Worked for ..." row anchored at
528 * the first hidden entry. Keeping both ends prevents a short follow-up from
529 * hiding a substantive opening response while still bounding noisy turns.
526 * Settled turns keep only their terminal assistant message visible.
527 * Everything before it folds behind a "Worked for ..." row anchored at the
528 * first hidden entry, so the duration leads directly into the final response.
530529 */
531530function deriveTurnFolds(input: {
532531 timelineEntries: ReadonlyArray<TimelineEntry>;
......@@ -596,12 +595,9 @@ function deriveTurnFolds(input: {
596595 if (group.hasStreamingMessage) {
597596 continue;
598597 }
599 const firstAssistantEntry = group.entries.find(
600 (entry): entry is Extract<TimelineEntry, { kind: "message" }> => entry.kind === "message",
601 );
602598 const hiddenEntryIds = new Set<string>();
603599 for (const entry of group.entries) {
604 if (entry.id === firstAssistantEntry?.id || entry.id === group.terminalEntry?.id) {
600 if (entry.id === group.terminalEntry?.id) {
605601 continue;
606602 }
607603 // Agent-spawn CTA rows never fold: workflows outlive their launching