From 52ffc7dcc1dc4682ec1d52cdadb6a07d71c6ec55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=9F=E6=9C=88?= Date: Sat, 23 May 2026 22:01:24 +0800 Subject: [PATCH] fix(thread-read): remove ### Output section and deduplicate ### Prompt globally --- .../cli-workflow/src/__tests__/thread.test.ts | 93 +++++++++++++++++-- packages/cli-workflow/src/commands/thread.ts | 6 +- 2 files changed, 89 insertions(+), 10 deletions(-) diff --git a/packages/cli-workflow/src/__tests__/thread.test.ts b/packages/cli-workflow/src/__tests__/thread.test.ts index 721750e..4596d9e 100644 --- a/packages/cli-workflow/src/__tests__/thread.test.ts +++ b/packages/cli-workflow/src/__tests__/thread.test.ts @@ -266,12 +266,7 @@ describe("cmdThreadRead ### Content section", () => { expect(markdown).toContain("### Content"); expect(markdown).toContain("The assistant response text"); - - const contentIdx = markdown.indexOf("### Content"); - const outputIdx = markdown.indexOf("### Output"); - expect(contentIdx).toBeGreaterThanOrEqual(0); - expect(outputIdx).toBeGreaterThanOrEqual(0); - expect(contentIdx).toBeLessThan(outputIdx); + expect(markdown).not.toContain("### Output"); }); test("omits ### Content when detail has no matching assistant turns", async () => { @@ -314,7 +309,7 @@ describe("cmdThreadRead ### Content section", () => { const markdown = await cmdThreadRead(tmpDir, threadId, THREAD_READ_DEFAULT_QUOTA, null, false); expect(markdown).not.toContain("### Content"); - expect(markdown).toContain("### Output"); + expect(markdown).not.toContain("### Output"); }); }); @@ -392,3 +387,87 @@ describe("cmdThreadStepDetails", () => { await expect(cmdThreadStepDetails(tmpDir, "nonexistenth0" as CasRef)).rejects.toThrow(); }); }); + +// ── cmdThreadRead: ### Prompt deduplication ─────────────────────────────────── + +describe("cmdThreadRead ### Prompt deduplication", () => { + async function makeThreadWithRoles(uwf: UwfStore, roles: string[]): Promise { + const roleMap: Record = {}; + for (const r of [...new Set(roles)]) { + roleMap[r] = { + description: r, + goal: `Goal for ${r}`, + capabilities: [], + procedure: "Do stuff.", + output: "Output.", + meta: "placeholder00" as CasRef, + }; + } + const workflowHash = await uwf.store.put(uwf.schemas.workflow, { + name: "dedup-wf", + description: "desc", + roles: roleMap, + conditions: {}, + graph: {}, + }); + const startHash = await uwf.store.put(uwf.schemas.startNode, { + workflow: workflowHash, + prompt: "Start", + }); + const outputHash = await uwf.store.put(uwf.schemas.workflow, { + name: "out", + description: "", + roles: {}, + conditions: {}, + graph: {}, + }); + + let prev: string | null = null; + let stepHash = ""; + for (const role of roles) { + stepHash = await uwf.store.put(uwf.schemas.stepNode, { + start: startHash, + prev: prev as CasRef | null, + role, + output: outputHash, + detail: null, + agent: "uwf-test", + }); + prev = stepHash; + } + return stepHash; + } + + test("same consecutive role shows ### Prompt once", async () => { + const uwf = await makeUwfStore(tmpDir); + const headHash = await makeThreadWithRoles(uwf, ["writer", "writer"]); + const threadId = "01JTEST0000000000000003" as ThreadId; + await saveThreadsIndex(tmpDir, { [threadId]: headHash }); + + const markdown = await cmdThreadRead(tmpDir, threadId, THREAD_READ_DEFAULT_QUOTA, null, false); + const count = (markdown.match(/### Prompt/g) ?? []).length; + expect(count).toBe(1); + }); + + test("different consecutive roles each show ### Prompt", async () => { + const uwf = await makeUwfStore(tmpDir); + const headHash = await makeThreadWithRoles(uwf, ["planner", "coder"]); + const threadId = "01JTEST0000000000000004" as ThreadId; + await saveThreadsIndex(tmpDir, { [threadId]: headHash }); + + const markdown = await cmdThreadRead(tmpDir, threadId, THREAD_READ_DEFAULT_QUOTA, null, false); + const count = (markdown.match(/### Prompt/g) ?? []).length; + expect(count).toBe(2); + }); + + test("non-consecutive same role shows ### Prompt twice", async () => { + const uwf = await makeUwfStore(tmpDir); + const headHash = await makeThreadWithRoles(uwf, ["roleA", "roleB", "roleA"]); + const threadId = "01JTEST0000000000000005" as ThreadId; + await saveThreadsIndex(tmpDir, { [threadId]: headHash }); + + const markdown = await cmdThreadRead(tmpDir, threadId, THREAD_READ_DEFAULT_QUOTA, null, false); + const count = (markdown.match(/### Prompt/g) ?? []).length; + expect(count).toBe(2); + }); +}); diff --git a/packages/cli-workflow/src/commands/thread.ts b/packages/cli-workflow/src/commands/thread.ts index 60a0c72..92bd9b3 100644 --- a/packages/cli-workflow/src/commands/thread.ts +++ b/packages/cli-workflow/src/commands/thread.ts @@ -655,11 +655,11 @@ function formatThreadReadMarkdown(options: { // Step blocks const startIndex = candidates.length - selected.length; + const shownPromptRoles = new Set(); for (let i = 0; i < selected.length; i++) { const item = selected[i]; if (item === undefined) continue; const stepNum = startIndex + i + 1; - const outputYaml = formatYaml(expandOutput(uwf, item.payload.output)); const ts = new Date(item.timestamp) .toISOString() .replace("T", " ") @@ -669,9 +669,10 @@ function formatThreadReadMarkdown(options: { `**Agent:** ${item.payload.agent} | **Time:** ${ts}`, ]; const roleDef = workflow.roles[item.payload.role]; - if (roleDef) { + if (roleDef && !shownPromptRoles.has(item.payload.role)) { const prompt = roleDef.goal; stepLines.push("", "### Prompt", "", prompt); + shownPromptRoles.add(item.payload.role); } if (item.payload.detail) { const content = extractLastAssistantContent(uwf, item.payload.detail); @@ -679,7 +680,6 @@ function formatThreadReadMarkdown(options: { stepLines.push("", "### Content", "", content); } } - stepLines.push("", "### Output", "", "```yaml", outputYaml, "```"); parts.push(stepLines.join("\n")); }