diff --git a/apps/web/src/components/ChatView.logic.test.ts b/apps/web/src/components/ChatView.logic.test.ts index 5d03a782..128b174c 100644 --- a/apps/web/src/components/ChatView.logic.test.ts +++ b/apps/web/src/components/ChatView.logic.test.ts @@ -1,5 +1,31 @@ +import { EventId, TurnId, type OrchestrationThreadActivity } from "@t3tools/contracts"; import { describe, expect, it } from "vitest"; -import { resolveProviderHealthBannerProvider } from "./ChatView.logic"; +import { + deriveVisibleThreadWorkLogEntries, + resolveProviderHealthBannerProvider, +} from "./ChatView.logic"; + +function makeActivity(overrides: { + id?: string; + createdAt?: string; + kind?: string; + summary?: string; + tone?: OrchestrationThreadActivity["tone"]; + payload?: Record; + turnId?: string; + sequence?: number; +}): OrchestrationThreadActivity { + return { + id: EventId.makeUnsafe(overrides.id ?? crypto.randomUUID()), + createdAt: overrides.createdAt ?? "2026-02-23T00:00:00.000Z", + kind: overrides.kind ?? "tool.started", + summary: overrides.summary ?? "Tool call", + tone: overrides.tone ?? "tool", + payload: overrides.payload ?? {}, + turnId: overrides.turnId ? TurnId.makeUnsafe(overrides.turnId) : null, + ...(overrides.sequence !== undefined ? { sequence: overrides.sequence } : {}), + }; +} describe("resolveProviderHealthBannerProvider", () => { it("uses the active session provider when a session exists", () => { @@ -20,3 +46,29 @@ describe("resolveProviderHealthBannerProvider", () => { ).toBe("copilot"); }); }); + +describe("deriveVisibleThreadWorkLogEntries", () => { + it("keeps completed tool calls from previous turns visible in the thread timeline", () => { + const activities: OrchestrationThreadActivity[] = [ + makeActivity({ + id: "tool-1", + createdAt: "2026-02-23T00:00:01.000Z", + turnId: "turn-1", + kind: "tool.completed", + summary: "First tool call", + }), + makeActivity({ + id: "tool-2", + createdAt: "2026-02-23T00:00:03.000Z", + turnId: "turn-2", + kind: "tool.completed", + summary: "Second tool call", + }), + ]; + + expect(deriveVisibleThreadWorkLogEntries(activities).map((entry) => entry.id)).toEqual([ + "tool-1", + "tool-2", + ]); + }); +}); diff --git a/apps/web/src/components/ChatView.logic.ts b/apps/web/src/components/ChatView.logic.ts index 11da5b33..7a1c60a3 100644 --- a/apps/web/src/components/ChatView.logic.ts +++ b/apps/web/src/components/ChatView.logic.ts @@ -1,9 +1,15 @@ -import { ProjectId, type ProviderKind, type ThreadId } from "@t3tools/contracts"; +import { + ProjectId, + type OrchestrationThreadActivity, + type ProviderKind, + type ThreadId, +} from "@t3tools/contracts"; import { type ChatMessage, type Thread } from "../types"; import { randomUUID } from "~/lib/utils"; import { getAppModelOptions } from "../appSettings"; import { type ComposerImageAttachment, type DraftThreadState } from "../composerDraftStore"; import { Schema } from "effect"; +import { deriveWorkLogEntries, type WorkLogEntry } from "../session-logic"; export const LAST_INVOKED_SCRIPT_BY_PROJECT_KEY = "t3code:last-invoked-script-by-project"; const WORKTREE_BRANCH_PREFIX = "t3code"; @@ -132,3 +138,9 @@ export function resolveProviderHealthBannerProvider(input: { }): ProviderKind { return input.sessionProvider ?? input.selectedProvider; } + +export function deriveVisibleThreadWorkLogEntries( + activities: ReadonlyArray, +): WorkLogEntry[] { + return deriveWorkLogEntries(activities, undefined); +} diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 68622d2e..2a9a2760 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -80,7 +80,6 @@ import { type PendingUserInput, type ProviderPickerKind, PROVIDER_OPTIONS, - deriveWorkLogEntries, hasToolActivityForTurn, isLatestTurnSettled, formatElapsed, @@ -249,7 +248,10 @@ import { computeMessageDurationStart, normalizeCompactToolLabel, } from "./chat/MessagesTimeline.logic"; -import { resolveProviderHealthBannerProvider } from "./ChatView.logic"; +import { + deriveVisibleThreadWorkLogEntries, + resolveProviderHealthBannerProvider, +} from "./ChatView.logic"; function formatMessageMeta( createdAt: string, @@ -1526,8 +1528,8 @@ export default function ChatView({ threadId }: ChatViewProps) { ); const threadActivities = activeThread?.activities ?? EMPTY_ACTIVITIES; const workLogEntries = useMemo( - () => deriveWorkLogEntries(threadActivities, activeLatestTurn?.turnId ?? undefined), - [activeLatestTurn?.turnId, threadActivities], + () => deriveVisibleThreadWorkLogEntries(threadActivities), + [threadActivities], ); const latestTurnHasToolActivity = useMemo( () => hasToolActivityForTurn(threadActivities, activeLatestTurn?.turnId),