From c8d4f22b5967db1a0abbc5d66fb30ac4d2e0b52e Mon Sep 17 00:00:00 2001 From: David Marshall Date: Mon, 11 May 2026 10:42:17 -0500 Subject: [PATCH] fix(coding-agent): render developer-role messages in /export and /tree The HTML export feed, sidebar tree, and TUI session tree did not handle the developer role, so plan content injected after /plan approval (and any other developer messages) was hidden from /export and shown as a bare [developer] label in /tree. Renders developer messages in the main feed with a dimmed .developer-message style, labels them in the sidebar tree, counts them in header stats, and shows their content in the TUI tree selector so search matches the body. Closes #753. Co-Authored-By: omp --- packages/coding-agent/CHANGELOG.md | 1 + .../coding-agent/src/export/html/template.css | 12 ++++ .../src/export/html/template.generated.ts | 2 +- .../coding-agent/src/export/html/template.js | 22 ++++++- .../src/modes/components/tree-selector.ts | 4 ++ .../export/html-template-developer.test.ts | 18 +++++ .../tree-selector-developer.test.ts | 65 +++++++++++++++++++ 7 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 packages/coding-agent/test/export/html-template-developer.test.ts create mode 100644 packages/coding-agent/test/modes/components/tree-selector-developer.test.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index fff5b55bf..20421cb9f 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixed - Fixed subagents launched in the same parallel batch not seeing each other in their initial `# IRC Peers` system-prompt block by pre-registering the agent in the global `AgentRegistry` before `rebuildSystemPrompt` runs and attaching the live session afterwards +- Fixed `/export` and `/tree` not showing developer-role messages (including the plan content injected after `/plan` approval) so the HTML export and TUI session tree now render developer messages dimmed with their actual content instead of hiding them entirely ([#753](https://github.com/can1357/oh-my-pi/issues/753)) ## [14.9.3] - 2026-05-10 ### Breaking Changes diff --git a/packages/coding-agent/src/export/html/template.css b/packages/coding-agent/src/export/html/template.css index 63decc2cb..de869c410 100644 --- a/packages/coding-agent/src/export/html/template.css +++ b/packages/coding-agent/src/export/html/template.css @@ -179,6 +179,10 @@ color: var(--accent); } + .tree-role-developer { + color: var(--dim); + } + .tree-role-assistant { color: var(--success); } @@ -316,6 +320,14 @@ position: relative; } + .user-message.developer-message { + opacity: 0.7; + } + + .user-message.developer-message .markdown-content { + color: var(--dim); + } + .assistant-message { padding: 0; position: relative; diff --git a/packages/coding-agent/src/export/html/template.generated.ts b/packages/coding-agent/src/export/html/template.generated.ts index 40edaa6b3..cbb1aa425 100644 --- a/packages/coding-agent/src/export/html/template.generated.ts +++ b/packages/coding-agent/src/export/html/template.generated.ts @@ -1,2 +1,2 @@ // Auto-generated by scripts/generate-template.ts - DO NOT EDIT -export const TEMPLATE = "\n\n\n \n \n Session Export\n \n \n\n\n \n
\n
\n \n
\n
\n
\n
\n
\n
\n \"\"\n
\n
\n\n \n \n \n \n\n\n"; +export const TEMPLATE = "\n\n\n \n \n Session Export\n \n \n\n\n \n
\n
\n \n
\n
\n
\n
\n
\n
\n \"\"\n
\n
\n\n \n \n \n \n\n\n"; diff --git a/packages/coding-agent/src/export/html/template.js b/packages/coding-agent/src/export/html/template.js index d093252e5..a61fcd217 100644 --- a/packages/coding-agent/src/export/html/template.js +++ b/packages/coding-agent/src/export/html/template.js @@ -456,6 +456,10 @@ const content = truncate(normalize(extractContent(msg.content))); return labelHtml + `user: ${escapeHtml(content)}`; } + if (msg.role === 'developer') { + const content = truncate(normalize(extractContent(msg.content))); + return labelHtml + `developer: ${escapeHtml(content)}`; + } if (msg.role === 'assistant') { const textContent = truncate(normalize(extractContent(msg.content))); if (textContent) { @@ -1648,6 +1652,18 @@ return html; } + if (msg.role === 'developer') { + let html = `
${copyBtnHtml}${tsHtml}`; + const content = msg.content; + const text = typeof content === 'string' ? content : + content.filter(c => c.type === 'text').map(c => c.text).join('\n'); + if (text.trim()) { + html += `
${safeMarkedParse(text)}
`; + } + html += '
'; + return html; + } + if (msg.role === 'assistant') { let html = `
${copyBtnHtml}${tsHtml}`; @@ -1750,7 +1766,7 @@ // ============================================================ function computeStats(entryList) { - let userMessages = 0, assistantMessages = 0, toolResults = 0; + let userMessages = 0, developerMessages = 0, assistantMessages = 0, toolResults = 0; let customMessages = 0, compactions = 0, branchSummaries = 0, toolCalls = 0; const tokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }; const cost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }; @@ -1760,6 +1776,7 @@ if (entry.type === 'message') { const msg = entry.message; if (msg.role === 'user') userMessages++; + if (msg.role === 'developer') developerMessages++; if (msg.role === 'assistant') { assistantMessages++; if (msg.model) models.add(msg.provider ? `${msg.provider}/${msg.model}` : msg.model); @@ -1787,7 +1804,7 @@ } } - return { userMessages, assistantMessages, toolResults, customMessages, compactions, branchSummaries, toolCalls, tokens, cost, models: Array.from(models) }; + return { userMessages, developerMessages, assistantMessages, toolResults, customMessages, compactions, branchSummaries, toolCalls, tokens, cost, models: Array.from(models) }; } const globalStats = computeStats(entries); @@ -1803,6 +1820,7 @@ const msgParts = []; if (globalStats.userMessages) msgParts.push(`${globalStats.userMessages} user`); + if (globalStats.developerMessages) msgParts.push(`${globalStats.developerMessages} developer`); if (globalStats.assistantMessages) msgParts.push(`${globalStats.assistantMessages} assistant`); if (globalStats.toolResults) msgParts.push(`${globalStats.toolResults} tool results`); if (globalStats.customMessages) msgParts.push(`${globalStats.customMessages} custom`); diff --git a/packages/coding-agent/src/modes/components/tree-selector.ts b/packages/coding-agent/src/modes/components/tree-selector.ts index 9a803749b..2d9693dea 100644 --- a/packages/coding-agent/src/modes/components/tree-selector.ts +++ b/packages/coding-agent/src/modes/components/tree-selector.ts @@ -539,6 +539,10 @@ class TreeList implements Component { const msgWithContent = msg as { content?: unknown }; const content = normalize(this.#extractContent(msgWithContent.content)); result = theme.fg("accent", "user: ") + content; + } else if (role === "developer") { + const msgWithContent = msg as { content?: unknown }; + const content = normalize(this.#extractContent(msgWithContent.content)); + result = theme.fg("dim", "developer: ") + theme.fg("muted", content); } else if (role === "assistant") { const msgWithContent = msg as { content?: unknown; stopReason?: string; errorMessage?: string }; const textContent = normalize(this.#extractContent(msgWithContent.content)); diff --git a/packages/coding-agent/test/export/html-template-developer.test.ts b/packages/coding-agent/test/export/html-template-developer.test.ts new file mode 100644 index 000000000..b1f1916aa --- /dev/null +++ b/packages/coding-agent/test/export/html-template-developer.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from "bun:test"; +import { TEMPLATE } from "../../src/export/html/template.generated"; + +describe("HTML export template developer message support", () => { + it("renders developer-role messages in the main feed", () => { + expect(TEMPLATE).toContain("msg.role === 'developer'"); + expect(TEMPLATE).toContain("developer-message"); + }); + + it("labels developer entries in the sidebar tree", () => { + expect(TEMPLATE).toContain("tree-role-developer"); + expect(TEMPLATE).toContain("developer:"); + }); + + it("counts developer messages in header stats", () => { + expect(TEMPLATE).toContain("developerMessages"); + }); +}); diff --git a/packages/coding-agent/test/modes/components/tree-selector-developer.test.ts b/packages/coding-agent/test/modes/components/tree-selector-developer.test.ts new file mode 100644 index 000000000..fc84cd1ee --- /dev/null +++ b/packages/coding-agent/test/modes/components/tree-selector-developer.test.ts @@ -0,0 +1,65 @@ +import { beforeAll, describe, expect, it } from "bun:test"; +import type { AgentMessage } from "@oh-my-pi/pi-agent-core"; +import { TreeSelectorComponent } from "../../../src/modes/components/tree-selector"; +import * as themeModule from "../../../src/modes/theme/theme"; +import type { SessionEntry, SessionTreeNode } from "../../../src/session/session-manager"; + +let counter = 0; +function makeMessageNode(message: AgentMessage, parentId: string | null = null, label?: string): SessionTreeNode { + const id = `entry-${counter++}`; + const entry: SessionEntry = { + type: "message", + id, + parentId, + timestamp: new Date().toISOString(), + message, + }; + return { entry, children: [], label }; +} + +function render(tree: SessionTreeNode[], width = 120): string { + const selector = new TreeSelectorComponent( + tree, + tree[tree.length - 1]?.entry.id ?? null, + 60, + () => {}, + () => {}, + ); + return Bun.stripANSI(selector.render(width).join("\n")); +} + +describe("TreeSelectorComponent developer message rendering", () => { + beforeAll(async () => { + await themeModule.initTheme(false, undefined, undefined, "dark", "light"); + }); + + it("renders developer messages with their content, not just [developer]", () => { + const planContent = "## Plan\n\n1. Fix the tree selector\n2. Update the HTML export"; + const root = makeMessageNode({ role: "user", content: "/plan", timestamp: 1 }); + const developer = makeMessageNode( + { role: "developer", content: [{ type: "text", text: planContent }], timestamp: 2 }, + root.entry.id, + ); + root.children.push(developer); + + const rendered = render([root]); + + expect(rendered).toContain("developer:"); + expect(rendered).toContain("Fix the tree selector"); + expect(rendered).toContain("Update the HTML export"); + expect(rendered).not.toMatch(/^\s*\[developer\]\s*$/m); + }); + + it("matches developer messages in search (content is searchable)", () => { + const planContent = "ZZZ_UNIQUE_PLAN_TOKEN approved plan body"; + const root = makeMessageNode({ role: "user", content: "/plan", timestamp: 1 }); + const developer = makeMessageNode( + { role: "developer", content: [{ type: "text", text: planContent }], timestamp: 2 }, + root.entry.id, + ); + root.children.push(developer); + + const rendered = render([root]); + expect(rendered).toContain("ZZZ_UNIQUE_PLAN_TOKEN"); + }); +});