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: {