From 6729848ba56c009197b7a7f3015694b9b3d8255d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 21 Sep 2026 21:42:41 +1000 Subject: [PATCH 1/2] fix(web): show the agent's question on user-input timeline rows --- .../components/chat/MessagesTimeline.test.tsx | 59 +++++++++++++++++-- .../src/components/chat/MessagesTimeline.tsx | 33 ++++++----- .../src/work-log/userInput.test.ts | 37 ++++++++++++ .../client-runtime/src/work-log/userInput.ts | 7 +++ 4 files changed, 118 insertions(+), 18 deletions(-) create mode 100644 packages/client-runtime/src/work-log/userInput.test.ts diff --git a/apps/web/src/components/chat/MessagesTimeline.test.tsx b/apps/web/src/components/chat/MessagesTimeline.test.tsx index dae27ccdba47..fcee5883ccd7 100644 --- a/apps/web/src/components/chat/MessagesTimeline.test.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.test.tsx @@ -361,28 +361,79 @@ describe("MessagesTimeline", () => { }); const questionToggle = renderer!.root.find( (node) => - node.props["aria-label"]?.startsWith("Question answer submitted:") && + node.props["aria-label"]?.startsWith("Provide a spec") && node.props["aria-expanded"] === false, ); expect(questionToggle.props["aria-label"]).toContain( Object.values(answers)[0] ?? "spec.txt", ); - expect(JSON.stringify(renderer!.toJSON())).not.toContain("Provide a spec"); + // The question leads the collapsed row so the exchange reads as a + // question and answer without expanding (heading + accessible label). + expect(JSON.stringify(renderer!.toJSON()).match(/Provide a spec/g)).toHaveLength(2); await act(() => questionToggle.props.onClick()); const markup = JSON.stringify(renderer!.toJSON()); - expect(markup.match(/Provide a spec/g)).toHaveLength(1); + // Expanded, the question also appears in the history: label, heading, history. + expect(markup.match(/Provide a spec/g)).toHaveLength(3); expect(markup).toContain("spec.txt"); expect(markup).toContain("Provide a screenshot"); expect(markup).toContain("shot.png"); for (const answer of Object.values(answers)) expect(markup).toContain(answer); await act(() => questionToggle.props.onClick()); - expect(JSON.stringify(renderer!.toJSON())).not.toContain("Provide a spec"); + // Collapsing hides the history but keeps the question heading. + expect(JSON.stringify(renderer!.toJSON())).toContain("Provide a spec"); } finally { await act(() => renderer?.unmount()); } }, ); + it("leads an unanswered question row with the question text", async () => { + vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); + vi.stubGlobal("requestAnimationFrame", () => 0); + vi.stubGlobal("cancelAnimationFrame", () => {}); + let renderer: ReactTestRenderer | undefined; + try { + await act(() => { + renderer = create( + , + ); + }); + const questionToggle = renderer!.root.find( + (node) => + node.props["aria-label"] === "Which repository?" && node.props["aria-expanded"] === false, + ); + const markup = JSON.stringify(renderer!.toJSON()); + // Heading + accessible label. + expect(markup.match(/Which repository\?/g)).toHaveLength(2); + await act(() => questionToggle.props.onClick()); + // Expanded history adds a third occurrence alongside heading and label. + expect(JSON.stringify(renderer!.toJSON()).match(/Which repository\?/g)).toHaveLength(3); + } finally { + await act(() => renderer?.unmount()); + } + }); + it.each([ { toolLifecycleStatus: "inProgress", isAtEnd: true }, { toolLifecycleStatus: "inProgress", isAtEnd: false }, diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index 161adc8be00d..af1e0a90f1d6 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -4,6 +4,7 @@ import { useRightPanelStore } from "~/rightPanelStore"; import { getQuestionAnswerPreview, getQuestionAnswerText, + getQuestionTextPreview, hasQuestionAnswer, } from "@t3tools/client-runtime/work-log/user-input"; import { @@ -3245,7 +3246,10 @@ function LiveWorkEntryTimelineRow({ row }: { row: Extract ); } - const label = liveWorkEntryLabel(row.entry, ctx.workspaceRoot, row.active); + const questionHeading = row.entry.questionAnswer + ? getQuestionTextPreview(row.entry.questionAnswer) + : ""; + const label = questionHeading || liveWorkEntryLabel(row.entry, ctx.workspaceRoot, row.active); const failed = workEntryDisplayIndicatesToolFailure(row.entry); return ( @@ -3258,17 +3262,10 @@ function LiveWorkEntryTimelineRow({ row }: { row: Extract {label} - + {getQuestionAnswerPreview(row.entry.questionAnswer)} @@ -4851,10 +4848,18 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: { showWarningIndicator || showDestructiveRowStyle ? undefined : (workEntry.toolIcon ?? workEntry.toolSource?.icon); - const previewText = displayLabel ?? workEntryDisplayLabel(workEntry, workspaceRoot); - const answerPreview = workEntry.questionAnswer - ? getQuestionAnswerPreview(workEntry.questionAnswer) - : null; + // The question is the row's identity: a generic "User input submitted" + // label buries what was asked, so lead with the question text and keep the + // answer as the trailing preview. + const questionHeading = workEntry.questionAnswer + ? getQuestionTextPreview(workEntry.questionAnswer) + : ""; + const previewText = + displayLabel ?? (questionHeading || workEntryDisplayLabel(workEntry, workspaceRoot)); + const answerPreview = + workEntry.questionAnswer && hasQuestionAnswer(workEntry.questionAnswer) + ? getQuestionAnswerPreview(workEntry.questionAnswer) + : null; const viewedImagePath = workEntryViewedImagePath(workEntry); const viewedImage = viewedImagePath && threadRef diff --git a/packages/client-runtime/src/work-log/userInput.test.ts b/packages/client-runtime/src/work-log/userInput.test.ts new file mode 100644 index 000000000000..bfc7ea75296f --- /dev/null +++ b/packages/client-runtime/src/work-log/userInput.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from "vite-plus/test"; +import { ApprovalRequestId, type UserInputAttachmentAnswerPayload } from "@t3tools/contracts"; +import { getQuestionTextPreview } from "./userInput.ts"; + +function answer( + overrides: Partial = {}, +): UserInputAttachmentAnswerPayload { + return { + requestId: ApprovalRequestId.make("request-1"), + questionTextById: { scope: "Which repository?" }, + answers: { scope: "Use the private repository" }, + attachmentsByQuestionId: {}, + ...overrides, + }; +} + +describe("getQuestionTextPreview", () => { + it("joins the question texts", () => { + expect( + getQuestionTextPreview( + answer({ questionTextById: { scope: "Which repository?", name: "What name?" } }), + ), + ).toBe("Which repository? · What name?"); + }); + + it("normalizes whitespace and skips blank texts", () => { + expect( + getQuestionTextPreview( + answer({ questionTextById: { scope: "Which\nrepository?", x: " " } }), + ), + ).toBe("Which repository?"); + }); + + it("returns an empty string without question texts", () => { + expect(getQuestionTextPreview(answer({ questionTextById: undefined }))).toBe(""); + }); +}); diff --git a/packages/client-runtime/src/work-log/userInput.ts b/packages/client-runtime/src/work-log/userInput.ts index 18dea7dd83c2..d1bb4370efbf 100644 --- a/packages/client-runtime/src/work-log/userInput.ts +++ b/packages/client-runtime/src/work-log/userInput.ts @@ -163,6 +163,13 @@ export function getQuestionAnswerText(value: unknown): string { return nested ? getQuestionAnswerText(nested.answers) : ""; } +export function getQuestionTextPreview(answer: UserInputAttachmentAnswerPayload): string { + return Object.values(answer.questionTextById ?? {}) + .map((text) => text.replace(/\s+/g, " ").trim()) + .filter(Boolean) + .join(" · "); +} + export function getQuestionAnswerPreview(answer: UserInputAttachmentAnswerPayload): string { const answers = Object.values(answer.answers).map(getQuestionAnswerText).filter(Boolean); const attachments = Object.values(answer.attachmentsByQuestionId) From 0047fac0b0286c5cf240c08e730202c9190b57eb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 21 Sep 2026 22:00:01 +1000 Subject: [PATCH 2/2] fix(web): let long question headings shrink beside the answer preview --- apps/web/src/components/chat/MessagesTimeline.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index af1e0a90f1d6..11fd82be1aa4 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -3264,8 +3264,8 @@ function LiveWorkEntryTimelineRow({ row }: { row: Extract - {label} - + {label} + {getQuestionAnswerPreview(row.entry.questionAnswer)} @@ -4956,7 +4956,7 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: {