diff --git a/ui/src/components/TaskChatThread.test.tsx b/ui/src/components/TaskChatThread.test.tsx index 063472e28a..c9cb995574 100644 --- a/ui/src/components/TaskChatThread.test.tsx +++ b/ui/src/components/TaskChatThread.test.tsx @@ -1552,7 +1552,7 @@ describe("TaskChatThread runtime transcript selection", () => { ?.textContent, ).toContain(repeated); expect(container.textContent).toContain( - `Queued ${new Date("2026-08-25T17:59:32.000Z").toLocaleTimeString([], { hour: "numeric", minute: "2-digit" })} · Steered ${new Date("2026-08-25T18:00:02.000Z").toLocaleTimeString([], { hour: "numeric", minute: "2-digit" })}`, + new Date("2026-08-25T17:59:32.000Z").toLocaleTimeString([], { hour: "numeric", minute: "2-digit" }), ); const turnHeaders = Array.from( container.querySelectorAll('[data-testid="task-chat-turn-summary"]'), @@ -1630,7 +1630,7 @@ describe("TaskChatThread runtime transcript selection", () => { ?.textContent, ).toContain("Continued after steering · Working for"); expect(container.textContent).toContain( - `Queued ${new Date("2026-08-25T17:59:32.000Z").toLocaleTimeString([], { hour: "numeric", minute: "2-digit" })} · Steered ${new Date("2026-08-25T18:00:02.000Z").toLocaleTimeString([], { hour: "numeric", minute: "2-digit" })}`, + new Date("2026-08-25T17:59:32.000Z").toLocaleTimeString([], { hour: "numeric", minute: "2-digit" }), ); }); diff --git a/ui/src/components/task-chat/TaskChatStatusPill.test.tsx b/ui/src/components/task-chat/TaskChatStatusPill.test.tsx index 4ef908e3bf..1a09919ca9 100644 --- a/ui/src/components/task-chat/TaskChatStatusPill.test.tsx +++ b/ui/src/components/task-chat/TaskChatStatusPill.test.tsx @@ -77,6 +77,30 @@ describe("TaskChatStatusPill whimsy", () => { expect(container.textContent).toContain("Terminal · ls -la"); }); + it("centers the reconnecting caret and status dot in equal lead slots", () => { + act(() => { + root.render( + undefined} + />, + ); + }); + const caret = container.querySelector( + '[data-testid="task-chat-status-caret-slot"]', + ); + const dot = container.querySelector( + '[data-testid="task-chat-status-dot-slot"]', + ); + for (const slot of [caret, dot]) { + expect(slot?.classList.contains("h-3.5")).toBe(true); + expect(slot?.classList.contains("w-3.5")).toBe(true); + expect(slot?.classList.contains("items-center")).toBe(true); + expect(slot?.classList.contains("justify-center")).toBe(true); + } + }); + it("keeps Queued copy untouched", () => { render(liveStatus({ label: "Queued", detail: "Waiting to start" })); expect(container.textContent).toContain("Queued…"); diff --git a/ui/src/components/task-chat/TaskChatStatusPill.tsx b/ui/src/components/task-chat/TaskChatStatusPill.tsx index 6f2a33d6c9..6d2b4204f3 100644 --- a/ui/src/components/task-chat/TaskChatStatusPill.tsx +++ b/ui/src/components/task-chat/TaskChatStatusPill.tsx @@ -318,14 +318,22 @@ export function TaskChatStatusPill({ const statusLine = (
{chevronOpen !== undefined ? ( - + + + ) : null} {/* Fixed-size lead slot keeps the label from moving as tool icons come and go; the pulse dot renders unconditionally. */} - + { expect(header?.textContent).toContain("Editing files…"); expect(header?.textContent).toContain("Edit · server/src/routes/auth.ts"); expect(header?.getAttribute("aria-expanded")).toBe("false"); - expect(header?.firstElementChild?.firstElementChild?.tagName).toBe("svg"); + expect( + header?.querySelector('[data-testid="task-chat-status-caret-slot"] svg') + ?.tagName, + ).toBe("svg"); // All activity is folded behind it — no rows visible, no summary line. expect(fold()?.getAttribute("data-folded")).toBe("true"); expect(summaryBtn()).toBeNull(); diff --git a/ui/src/components/task-chat/task-chat-adapter.test.ts b/ui/src/components/task-chat/task-chat-adapter.test.ts index f31ef7e958..a9bb5deb28 100644 --- a/ui/src/components/task-chat/task-chat-adapter.test.ts +++ b/ui/src/components/task-chat/task-chat-adapter.test.ts @@ -129,7 +129,7 @@ describe("commentsToTaskChatItems", () => { expect(agent.createdAtIso).toBeUndefined(); }); - it("shows both queue and steer times for a causally repositioned follow-up", () => { + it("keeps the regular comment time for a causally repositioned steered follow-up", () => { const createdAt = "2026-09-04T14:09:33.000Z"; const conversationAnchorAt = "2026-09-04T14:10:14.000Z"; const [item] = commentsToTaskChatItems([ @@ -149,11 +149,11 @@ describe("commentsToTaskChatItems", () => { expect(item).toMatchObject({ kind: "message", - timestamp: `Queued ${formatTaskChatTimestamp(createdAt)} · Steered ${formatTaskChatTimestamp(conversationAnchorAt)}`, + timestamp: formatTaskChatTimestamp(createdAt), }); }); - it("shows the successor-run delivery time for a queued follow-up", () => { + it("keeps the regular comment time for a successor-run follow-up", () => { const createdAt = "2026-09-04T14:09:33.000Z"; const conversationAnchorAt = "2026-09-04T14:10:35.000Z"; const [item] = commentsToTaskChatItems([ @@ -172,7 +172,7 @@ describe("commentsToTaskChatItems", () => { expect(item).toMatchObject({ kind: "message", - timestamp: `Queued ${formatTaskChatTimestamp(createdAt)} · Delivered ${formatTaskChatTimestamp(conversationAnchorAt)}`, + timestamp: formatTaskChatTimestamp(createdAt), }); }); diff --git a/ui/src/components/task-chat/task-chat-adapter.ts b/ui/src/components/task-chat/task-chat-adapter.ts index 2402d6ba62..ed600d77fb 100644 --- a/ui/src/components/task-chat/task-chat-adapter.ts +++ b/ui/src/components/task-chat/task-chat-adapter.ts @@ -54,28 +54,12 @@ export function formatTaskChatTimestamp(value: unknown): string | undefined { return d.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" }); } -/** - * Follow-up inputs render at the causal slot where a runner consumed them. - * Keep their original submission time visible as well so the reordered bubble - * cannot look like it travelled backwards in the conversation. - */ +/** Keep every comment footer on the same compact, user-visible timestamp. */ export function formatTaskChatCommentTimestamp( comment: IssueChatComment, - kind: TaskChatAuthorKind, + _kind: TaskChatAuthorKind, ): string | undefined { - const queuedAt = formatTaskChatTimestamp(comment.createdAt); - const deliveredAt = formatTaskChatTimestamp(comment.conversationAnchorAt); - const isDeliveredFollowUp = Boolean( - kind === "human" && - comment.conversationAnchorAt && - comment.consumedByRunId && - (comment.followUpRequested || comment.steeredIntoRunId), - ); - if (!isDeliveredFollowUp) return queuedAt; - - if (!queuedAt || !deliveredAt) return queuedAt ?? deliveredAt; - const action = comment.steeredIntoRunId ? "Steered" : "Delivered"; - return `Queued ${queuedAt} · ${action} ${deliveredAt}`; + return formatTaskChatTimestamp(comment.createdAt); } export function commentsToTaskChatItems( diff --git a/ui/storybook/stories/task-chat-chain-of-thought.stories.tsx b/ui/storybook/stories/task-chat-chain-of-thought.stories.tsx new file mode 100644 index 0000000000..3a728e19f9 --- /dev/null +++ b/ui/storybook/stories/task-chat-chain-of-thought.stories.tsx @@ -0,0 +1,162 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { useState } from "react"; +import { TaskChatBubble } from "@/components/task-chat/TaskChatBubble"; +import { TaskChatRunnerTurn } from "@/components/task-chat/TaskChatRunnerTurn"; +import { TaskChatStatusPill } from "@/components/task-chat/TaskChatStatusPill"; +import { commentsToTaskChatItems } from "@/components/task-chat/task-chat-adapter"; +import type { TaskChatItem, TaskChatMessageItem } from "@/components/task-chat/task-chat-model"; +import type { IssueChatComment } from "@/lib/issue-chat-messages"; + +const runningItemSteps: TaskChatItem[][] = [ + [ + { + id: "reasoning-current", + kind: "thinking", + lines: ["Inspecting the task chat layout."], + streaming: true, + channel: "summary", + transcriptIndex: 1, + }, + ], + [ + { + id: "reasoning-current", + kind: "thinking", + lines: ["Inspecting the task chat layout."], + streaming: false, + channel: "summary", + transcriptIndex: 1, + }, + { + id: "tool-read", + kind: "tool", + name: "Read", + rawName: "read_file", + target: "ui/src/components/task-chat/TaskChatRunnerTurn.tsx", + status: "completed", + }, + ], + [ + { + id: "reasoning-current", + kind: "thinking", + lines: ["Inspecting the task chat layout."], + streaming: false, + channel: "summary", + transcriptIndex: 1, + }, + { + id: "tool-read", + kind: "tool", + name: "Read", + rawName: "read_file", + target: "ui/src/components/task-chat/TaskChatRunnerTurn.tsx", + status: "completed", + }, + { + id: "tool-test", + kind: "tool", + name: "Bash", + rawName: "bash", + target: "pnpm exec vitest run ui/src/components/task-chat/TaskChatRunnerTurn.test.tsx --runInBand", + status: "in_progress", + }, + ], +]; + +function ChainOfThoughtReview() { + const [step, setStep] = useState(0); + return ( +
+
+ +
+
+ + + Update {step + 1} of {runningItemSteps.length} + +
+
+ ); +} + +const steeredComment: IssueChatComment = { + id: "steered-comment", + companyId: "storybook-company", + issueId: "storybook-issue", + authorAgentId: null, + authorUserId: "storybook-user", + authorType: "user", + body: "Keep the regular timestamp after steering this follow-up.", + presentation: null, + metadata: null, + createdAt: new Date("2026-09-10T21:09:33.000Z"), + updatedAt: new Date("2026-09-10T21:09:33.000Z"), + conversationAnchorAt: "2026-09-10T21:10:14.000Z", + consumedByRunId: "run-live", + followUpRequested: true, + steeredIntoRunId: "run-live", +}; + +function TimestampReview() { + const [item] = commentsToTaskChatItems([steeredComment]); + return ( +
+ +
+ ); +} + +function ReconnectingAlignmentReview() { + const [open, setOpen] = useState(false); + return ( +
+ setOpen((value) => !value)} + /> +
+ ); +} + +const meta = { + title: "Tasks/Task chat review fixes", + component: ChainOfThoughtReview, + parameters: { layout: "padded" }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const CollapsedRunningChainOfThought: Story = {}; + +export const RegularTimestampAfterSteering: Story = { + render: () => , +}; + +export const ReconnectingCaretAndDotAlignment: Story = { + render: () => , +};