From 5973b4d38abe6352af810ad34a8af16f758ba1a6 Mon Sep 17 00:00:00 2001 From: deepagent-ai Date: Fri, 14 Aug 2026 17:16:02 +0800 Subject: [PATCH] v4.0.7-r2 bugfix (#117) ### Issue for this PR Closes # ### Type of change - [ ] Bug fix - [ ] New feature - [ ] Refactor / code improvement - [ ] Documentation ### What does this PR do? Please provide a description of the issue, the changes you made to fix it, and why they work. It is expected that you understand why your changes work and if you do not understand why at least say as much so a maintainer knows how much to value the PR. **If you paste a large clearly AI generated description here your PR may be IGNORED or CLOSED!** ### How did you verify your code works? ### Screenshots / recordings _If this is a UI change, please include a screenshot or recording._ ### Checklist - [ ] I have tested my changes locally - [ ] I have not included unrelated changes in this PR _If you do not follow this template your PR will be automatically rejected._ --- .../session/message-timeline.data.test.ts | 61 ++++++---- .../pages/session/message-timeline.data.ts | 107 +----------------- .../src/pages/session/message-timeline.tsx | 7 -- packages/desktop/electron.vite.config.ts | 12 +- 4 files changed, 46 insertions(+), 141 deletions(-) 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 }) => ({ }, }, }, -})) +})