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");
+ });
+});