diff --git a/packages/app/src/pages/session/message-timeline.data.test.ts b/packages/app/src/pages/session/message-timeline.data.test.ts index a2a64d49..2206a80b 100644 --- a/packages/app/src/pages/session/message-timeline.data.test.ts +++ b/packages/app/src/pages/session/message-timeline.data.test.ts @@ -8,7 +8,8 @@ mock.module("@deepagent-code/ui/message-part", () => ({ type: "part", ref: { messageID: item.messageID, partID: item.part.id }, })), - renderable: () => true, + renderable: (part: Part, showReasoningSummaries = true) => + part.type !== "reasoning" || showReasoningSummaries, })) afterAll(() => mock.restore()) @@ -122,7 +123,7 @@ describe("message timeline activity progress", () => { }, }) as Part - test("shows only the latest settled progress for one activity", async () => { + test("renders every progress revision for one activity", async () => { const { Timeline } = await import("./message-timeline.data") const messages = [assistant("msg_a0"), assistant("msg_a1")] const parts = new Map([ @@ -133,10 +134,10 @@ describe("message timeline activity progress", () => { const rows = Timeline.constructMessageRows(user, (id) => parts.get(id) ?? [], messages, 0, false, "idle", false) expect( rows.flatMap((row) => (row._tag === "AssistantPart" && row.group.type === "part" ? [row.group.ref.partID] : [])), - ).toEqual(["prt_progress_1"]) + ).toEqual(["prt_progress_0", "prt_progress_1"]) }) - test("replaces settled progress with the activity final", async () => { + test("keeps progress revisions when the activity final arrives", async () => { const { Timeline } = await import("./message-timeline.data") const messages = [assistant("msg_a0"), assistant("msg_a1"), { ...assistant("msg_a2"), finish: "stop" }] const parts = new Map([ @@ -148,10 +149,10 @@ describe("message timeline activity progress", () => { const rows = Timeline.constructMessageRows(user, (id) => parts.get(id) ?? [], messages, 0, false, "idle", false) expect( rows.flatMap((row) => (row._tag === "AssistantPart" && row.group.type === "part" ? [row.group.ref.partID] : [])), - ).toEqual(["prt_final_2"]) + ).toEqual(["prt_progress_0", "prt_progress_1", "prt_final_2"]) }) - test("collapses every text part in one activity across separate parent user rows", async () => { + test("keeps text parts across separate parent user rows", async () => { const { Timeline } = await import("./message-timeline.data") const user2 = { ...user, id: "msg_user_2" } const firstAssistant = assistant("msg_cross_a0") @@ -173,7 +174,6 @@ describe("message timeline activity progress", () => { ], ]) const getParts = (id: string) => parts.get(id) ?? [] - const visibility = Timeline.activityProgressVisibility(messages, getParts) const firstRows = Timeline.constructMessageRows( user, getParts, @@ -182,7 +182,6 @@ describe("message timeline activity progress", () => { false, "idle", false, - visibility, ) const secondRows = Timeline.constructMessageRows( user2, @@ -192,9 +191,12 @@ describe("message timeline activity progress", () => { false, "idle", false, - visibility, ) - expect(firstRows.some((row) => row._tag === "AssistantPart")).toBe(false) + expect( + firstRows.flatMap((row) => + row._tag === "AssistantPart" && row.group.type === "part" ? [row.group.ref.partID] : [], + ), + ).toEqual(["prt_progress_0", "prt_cross_old_plain"]) expect( secondRows.flatMap((row) => row._tag === "AssistantPart" && row.group.type === "part" ? [row.group.ref.partID] : [], @@ -202,7 +204,7 @@ describe("message timeline activity progress", () => { ).toEqual(["prt_progress_1", "prt_cross_latest_plain"]) }) - test("applies one revision marker to every text part in the assistant message", async () => { + test("keeps every text part in every revision", async () => { const { Timeline } = await import("./message-timeline.data") const messages = [assistant("msg_multi_a0"), { ...assistant("msg_multi_a1"), finish: "stop" }] const plain = (messageID: string, id: string, text: string) => @@ -219,10 +221,10 @@ describe("message timeline activity progress", () => { expect( rows.flatMap((row) => (row._tag === "AssistantPart" && row.group.type === "part" ? [row.group.ref.partID] : [])), - ).toEqual(["prt_final_1", "prt_final_plain"]) + ).toEqual(["prt_progress_0", "prt_old_plain", "prt_final_1", "prt_final_plain"]) }) - test("uses the message marker to collapse reasoning-only revisions", async () => { + test("keeps reasoning summaries from every revision when enabled", async () => { const { Timeline } = await import("./message-timeline.data") const messages = [ { @@ -245,19 +247,34 @@ describe("message timeline activity progress", () => { expect( rows.flatMap((row) => (row._tag === "AssistantPart" && row.group.type === "part" ? [row.group.ref.partID] : [])), - ).toEqual(["prt_reasoning_latest"]) + ).toEqual(["prt_reasoning_old", "prt_reasoning_latest"]) }) - test("hides an older tool revision when the latest terminal message has no renderable parts", async () => { + test("uses showReasoning as the explicit reasoning visibility control", async () => { + const { Timeline } = await import("./message-timeline.data") + const message = assistant("msg_reasoning_setting") + const reasoning = { + id: "prt_reasoning_setting", + sessionID: user.sessionID, + messageID: message.id, + type: "reasoning", + text: "summary", + } as Part + const getParts = () => [reasoning] + + const hidden = Timeline.constructMessageRows(user, getParts, [message], 0, false, "idle", false) + const shown = Timeline.constructMessageRows(user, getParts, [message], 0, true, "idle", false) + + expect(hidden.some((row) => row._tag === "AssistantPart")).toBe(false) + expect(shown.some((row) => row._tag === "AssistantPart")).toBe(true) + }) + + test("keeps an older tool revision when a later terminal message has no renderable parts", async () => { const { Timeline } = await import("./message-timeline.data") const old = { ...assistant("msg_tool_old"), activityProgress: { activityID: "activity-tool", revision: 0, state: "progress" as const }, } - const terminal = { - ...assistant("msg_tool_terminal"), - activityProgress: { activityID: "activity-tool", revision: 1, state: "final" as const }, - } const tool = { id: "prt_tool_old", sessionID: user.sessionID, @@ -268,10 +285,8 @@ describe("message timeline activity progress", () => { state: { status: "completed", input: {}, output: "pending", title: "poll", time: { start: 1, end: 2 } }, } as Part const parts = new Map([[old.id, [tool]]]) - const visibility = Timeline.activityProgressVisibility([old, terminal], (id) => parts.get(id) ?? []) - - const rows = Timeline.constructMessageRows(user, (id) => parts.get(id) ?? [], [old], 0, true, "idle", false, visibility) + const rows = Timeline.constructMessageRows(user, (id) => parts.get(id) ?? [], [old], 0, true, "idle", false) - expect(rows.some((row) => row._tag === "AssistantPart")).toBe(false) + expect(rows.some((row) => row._tag === "AssistantPart")).toBe(true) }) }) diff --git a/packages/app/src/pages/session/message-timeline.data.ts b/packages/app/src/pages/session/message-timeline.data.ts index 98688c54..cf4b98d9 100644 --- a/packages/app/src/pages/session/message-timeline.data.ts +++ b/packages/app/src/pages/session/message-timeline.data.ts @@ -147,7 +147,6 @@ export namespace Timeline { showReasoning: boolean, status: SessionStatus["type"], isActive: boolean, - activityProgressVisibility?: ReadonlySet, ) { const rows: TimelineRow.TimelineRow[] = [] @@ -159,14 +158,10 @@ export namespace Timeline { const interrupted = interruptedMessageIndex !== -1 const error = assistantMessages.find((m) => m.error && m.error.name !== "MessageAbortedError")?.error - const assistantPartRefs = latestActivityProgress( - assistantMessages, - assistantMessages.flatMap((message, messageIndex) => - getMessageParts(message.id) - .filter((part) => renderable(part, showReasoning)) - .map((part) => ({ messageID: message.id, messageIndex, part })), - ), - activityProgressVisibility, + const assistantPartRefs = assistantMessages.flatMap((message, messageIndex) => + getMessageParts(message.id) + .filter((part) => renderable(part, showReasoning)) + .map((part) => ({ messageID: message.id, messageIndex, part })), ) const assistantItems = interrupted && !compaction @@ -291,100 +286,6 @@ export namespace Timeline { return rows } - export function activityProgressVisibility( - assistantMessages: AssistantMessage[], - getMessageParts: (messageID: string) => Part[], - ) { - const refs = assistantMessages.flatMap((message, messageIndex) => - getMessageParts(message.id).map((part) => ({ messageID: message.id, messageIndex, part })), - ) - return new Set( - latestActivityProgress(assistantMessages, refs).map((ref) => `${ref.messageID}:${ref.part.id}`), - ) - } - - function latestActivityProgress( - assistantMessages: AssistantMessage[], - refs: T[], - visibility?: ReadonlySet, - ) { - const progressByMessage = new Map>>() - assistantMessages.forEach((message) => { - const marker = messageActivityProgress(message) - if (marker) progressByMessage.set(message.id, marker) - }) - refs.forEach((ref) => { - const marker = partActivityProgress(ref.part) - const projected = progressByMessage.get(ref.messageID) - if ( - marker && - projected && - (marker.activityID !== projected.activityID || marker.revision !== projected.revision) - ) - console.error("Conflicting activity progress projection", { - messageID: ref.messageID, - projected, - legacy: marker, - }) - if (projected) return - if (marker) progressByMessage.set(ref.messageID, marker) - }) - const markerFor = (ref: T) => progressByMessage.get(ref.messageID) ?? partActivityProgress(ref.part) - if (visibility) - return refs.filter((ref) => { - if (!markerFor(ref)) return true - return visibility.has(`${ref.messageID}:${ref.part.id}`) - }) - const selected = new Map() - progressByMessage.forEach((marker) => { - if (!marker) return - const terminal = marker.state !== "provisional" && marker.state !== "progress" - const current = selected.get(marker.activityID) - if ( - current && - ((current.terminal && !terminal) || (current.terminal === terminal && current.revision > marker.revision)) - ) - return - selected.set(marker.activityID, { revision: marker.revision, terminal }) - }) - return refs.filter((ref) => { - const marker = markerFor(ref) - if (!marker) return true - const current = selected.get(marker.activityID) - return ( - current?.revision === marker.revision && - current.terminal === (marker.state !== "provisional" && marker.state !== "progress") - ) - }) - } - - function messageActivityProgress(message: AssistantMessage) { - const marker = message.activityProgress - if (!marker) return - if (!marker.activityID || !Number.isInteger(marker.revision) || marker.revision < 0) return - if ( - !["provisional", "progress", "final", "interrupted", "recovery_required", "failed"].includes(marker.state) - ) - return - return marker - } - - function partActivityProgress(part: Part) { - if (part.type !== "text") return - const value = part.metadata?.deepagent_activity_progress - if (!value || typeof value !== "object") return - const marker = value as Record - if (typeof marker.activity_id !== "string" || marker.activity_id.length === 0) return - if (typeof marker.revision !== "number" || !Number.isInteger(marker.revision) || marker.revision < 0) return - if (!["provisional", "progress", "final", "interrupted", "recovery_required"].includes(String(marker.state))) - return - return { - activityID: marker.activity_id, - revision: marker.revision, - state: marker.state as "provisional" | "progress" | "final" | "interrupted" | "recovery_required", - } - } - function isSummaryDiff(value: SnapshotFileDiff): value is SummaryDiff { return typeof value.file === "string" } diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx index 9615ff6d..b3ee9d86 100644 --- a/packages/app/src/pages/session/message-timeline.tsx +++ b/packages/app/src/pages/session/message-timeline.tsx @@ -586,12 +586,6 @@ export function MessageTimeline(props: { }) const parentTitle = createMemo(() => sessionTitle(parent()?.title) ?? language.t("command.session.new")) const getMsgParts = (msgId: string) => sync.data.part[msgId] ?? emptyParts - const activityProgressVisibility = createMemo(() => - Timeline.activityProgressVisibility( - sessionMessages().filter((message): message is AssistantMessage => message.role === "assistant"), - getMsgParts, - ), - ) const childTaskDescription = createMemo(() => { const id = sessionID() if (!id) return @@ -622,7 +616,6 @@ export function MessageTimeline(props: { settings.general.showReasoningSummaries(), sessionStatus().type, activeMessageID() === userMessage.id, - activityProgressVisibility(), ) return reuseTimelineRows(previous, rows) diff --git a/packages/desktop/electron.vite.config.ts b/packages/desktop/electron.vite.config.ts index 4a9d5850..ba765d4a 100644 --- a/packages/desktop/electron.vite.config.ts +++ b/packages/desktop/electron.vite.config.ts @@ -30,7 +30,7 @@ const sentry = }) : false -export default defineConfig(({ command }) => ({ +export default defineConfig({ main: { define: { "import.meta.env.DEEPAGENT_CODE_CHANNEL": JSON.stringify(channel), @@ -47,8 +47,7 @@ export default defineConfig(({ command }) => ({ enforce: "pre", resolveId(id) { if (id !== "virtual:deepagent-code-server") return - if (command === "build") return { id: "./chunks/node.js", external: true } - return this.resolve(`${DEEPAGENT_CODE_SERVER_DIST}/node.js`) + return { id: "./chunks/node.js", external: true } }, }, { @@ -56,10 +55,7 @@ export default defineConfig(({ command }) => ({ async writeBundle() { await mkdir("./out/main/chunks", { recursive: true }) for (const file of await readdir(DEEPAGENT_CODE_SERVER_DIST)) { - if ( - !file.endsWith(".wasm") && - (command !== "build" || !["node.js", "node.js.map", "models-dev.build.json"].includes(file)) - ) + if (!file.endsWith(".wasm") && !["node.js", "node.js.map", "models-dev.build.json"].includes(file)) continue await copyFile(`${DEEPAGENT_CODE_SERVER_DIST}/${file}`, `./out/main/chunks/${file}`) } @@ -93,4 +89,4 @@ export default defineConfig(({ command }) => ({ }, }, }, -})) +})