diff --git a/apps/app/src/views/ToolsView.plugin-detail.test.tsx b/apps/app/src/views/ToolsView.plugin-detail.test.tsx index b8badbdddc1..1d78403b3b3 100644 --- a/apps/app/src/views/ToolsView.plugin-detail.test.tsx +++ b/apps/app/src/views/ToolsView.plugin-detail.test.tsx @@ -686,18 +686,18 @@ describe("BB Official plugin detail routing", () => { const { wrapper: QueryClientWrapper } = createQueryClientTestHarness(); render( - + - } /> + } /> , { wrapper: QueryClientWrapper }, ); await waitFor(() => { - expect( - document.querySelector("[data-plugin-summary]")?.textContent, - ).toBe("Description from the installed catalog."); + expect(document.querySelector("[data-plugin-summary]")?.textContent).toBe( + "Description from the installed catalog.", + ); }); }); @@ -760,18 +760,18 @@ describe("BB Official plugin detail routing", () => { const { wrapper: QueryClientWrapper } = createQueryClientTestHarness(); render( - + - } /> + } /> , { wrapper: QueryClientWrapper }, ); await waitFor(() => { - expect( - document.querySelector("[data-plugin-summary]")?.textContent, - ).toBe("Description from installed metadata."); + expect(document.querySelector("[data-plugin-summary]")?.textContent).toBe( + "Description from installed metadata.", + ); }); }); diff --git a/apps/server/test/services/threads/timeline-in-turn-window.test.ts b/apps/server/test/services/threads/timeline-in-turn-window.test.ts index 4e1d9b3f9f6..7f2fb5ffeb5 100644 --- a/apps/server/test/services/threads/timeline-in-turn-window.test.ts +++ b/apps/server/test/services/threads/timeline-in-turn-window.test.ts @@ -2053,7 +2053,10 @@ describe("turn details for an item that finishes in a later turn", () => { unfinishedLatest.timelinePage.olderCursor!, ).response; const beforeThought = unfinishedOlder.rows.find( - (row) => row.kind === "system" && row.title.startsWith("Thought for"), + (row) => + row.kind === "system" && + row.systemKind === "operation" && + row.operationKind === "reasoning", ); expect(beforeThought).toMatchObject({ detail: "Checking" }); @@ -2077,7 +2080,10 @@ describe("turn details for an item that finishes in a later turn", () => { expect(latest.rows.some((row) => row.kind === "system")).toBe(false); const after = collectTurnDetailsAndChildren(db, thread).get(turnId); const thoughts = after?.details.filter( - (row) => row.kind === "system" && row.title.startsWith("Thought for"), + (row) => + row.kind === "system" && + row.systemKind === "operation" && + row.operationKind === "reasoning", ); expect(thoughts).toEqual([ expect.objectContaining({ diff --git a/packages/thread-view/src/reasoning-lifecycle-projection.ts b/packages/thread-view/src/reasoning-lifecycle-projection.ts index 3c5119ac940..7cf240029c7 100644 --- a/packages/thread-view/src/reasoning-lifecycle-projection.ts +++ b/packages/thread-view/src/reasoning-lifecycle-projection.ts @@ -219,6 +219,7 @@ function finalizeReasoningLifecycleByKey( return null; } + const durationMs = args.meta.createdAt - lifecycle.startedAt; const message: EventProjectionOperationMessage = { kind: "operation", id: messageId( @@ -237,9 +238,10 @@ function finalizeReasoningLifecycleByKey( ? { parentToolCallId: lifecycle.parentToolCallId } : {}), opType: "reasoning", - title: `Thought for ${durationToCompactString( - args.meta.createdAt - lifecycle.startedAt, - )}`, + title: + durationMs > 0 + ? `Thought for ${durationToCompactString(durationMs)}` + : "Thought", detail: normalizeReasoningDetail(detail), status: args.status, }; diff --git a/packages/thread-view/test/reasoning-zero-duration.test.ts b/packages/thread-view/test/reasoning-zero-duration.test.ts new file mode 100644 index 00000000000..20cc5a8a165 --- /dev/null +++ b/packages/thread-view/test/reasoning-zero-duration.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from "vitest"; +import { + createTimelineEventFactory, + renderTimelineFixture, +} from "./timeline-test-harness.js"; + +describe("completed reasoning duration", () => { + it.each([ + { completedAt: 4_000, title: "Thought" }, + { completedAt: 3_000, title: "Thought" }, + { completedAt: 4_500, title: "Thought for 500ms" }, + ])( + "renders $title when completion is $completedAt", + ({ completedAt, title }) => { + const event = createTimelineEventFactory({ threadId: "thread-1" }); + const persistedAt = 4_000; + const timeline = renderTimelineFixture({ + events: [ + event.turnStarted({ createdAt: 0 }), + event.reasoningStarted({ + createdAt: persistedAt, + itemId: "reasoning-1", + }), + event.reasoningDelta({ + createdAt: persistedAt, + delta: "Checking the projection.", + itemId: "reasoning-1", + }), + event.reasoningCompleted({ + createdAt: completedAt, + itemId: "reasoning-1", + text: "Checked the projection.", + }), + event.turnCompleted({ createdAt: 5_000 }), + ], + projectionOptions: { + threadStatus: "idle", + turnMessageDetail: "summary", + }, + }); + + const reasoning = timeline.messages.find( + (message) => + message.kind === "operation" && + message.detail === "Checked the projection.", + ); + expect(reasoning).toMatchObject({ + completedAt, + startedAt: persistedAt, + title, + }); + expect(timeline.text).toContain(`── ${title}\n`); + expect(timeline.text).not.toContain("Thought for 0ms"); + }, + ); +});