From 6a5f04ce1fb13f282f837fd55df690d0a4dddbbf Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 6 Jan 2026 14:21:34 +0100 Subject: [PATCH 1/2] Add the codex bridge prompt in the html export --- packages/ai/src/index.ts | 1 + .../ai/src/providers/openai-codex/index.ts | 7 +++ .../coding-agent/src/core/agent-session.ts | 4 +- .../src/core/export-html/index.ts | 51 ++++++++++++++++++- .../src/core/export-html/template.css | 11 ++++ .../src/core/export-html/template.js | 29 ++++++++++- .../src/modes/interactive/interactive-mode.ts | 8 +-- .../coding-agent/src/modes/rpc/rpc-mode.ts | 2 +- 8 files changed, 103 insertions(+), 10 deletions(-) create mode 100644 packages/ai/src/providers/openai-codex/index.ts diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts index 0fab4dbd..73a56538 100644 --- a/packages/ai/src/index.ts +++ b/packages/ai/src/index.ts @@ -3,6 +3,7 @@ export * from "./providers/anthropic.js"; export * from "./providers/google.js"; export * from "./providers/google-gemini-cli.js"; export * from "./providers/google-vertex.js"; +export * from "./providers/openai-codex/index.js"; export * from "./providers/openai-completions.js"; export * from "./providers/openai-responses.js"; export * from "./stream.js"; diff --git a/packages/ai/src/providers/openai-codex/index.ts b/packages/ai/src/providers/openai-codex/index.ts new file mode 100644 index 00000000..86575ded --- /dev/null +++ b/packages/ai/src/providers/openai-codex/index.ts @@ -0,0 +1,7 @@ +/** + * OpenAI Codex utilities - exported for use by coding-agent export + */ + +export { type CacheMetadata, getCodexInstructions, getModelFamily, type ModelFamily } from "./prompts/codex.js"; +export { buildCodexPiBridge } from "./prompts/pi-codex-bridge.js"; +export { buildCodexSystemPrompt, type CodexSystemPrompt } from "./prompts/system-prompt.js"; diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index 60868127..9deedc53 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -2057,9 +2057,9 @@ export class AgentSession { * @param outputPath Optional output path (defaults to session directory) * @returns Path to exported file */ - exportToHtml(outputPath?: string): string { + async exportToHtml(outputPath?: string): Promise { const themeName = this.settingsManager.getTheme(); - return exportSessionToHtml(this.sessionManager, this.state, { outputPath, themeName }); + return await exportSessionToHtml(this.sessionManager, this.state, { outputPath, themeName }); } // ========================================================================= diff --git a/packages/coding-agent/src/core/export-html/index.ts b/packages/coding-agent/src/core/export-html/index.ts index 577f262c..3af74df6 100644 --- a/packages/coding-agent/src/core/export-html/index.ts +++ b/packages/coding-agent/src/core/export-html/index.ts @@ -1,4 +1,5 @@ import type { AgentState } from "@mariozechner/pi-agent-core"; +import { buildCodexPiBridge, getCodexInstructions, getModelFamily } from "@mariozechner/pi-ai"; import { existsSync, readFileSync, writeFileSync } from "fs"; import { basename, join } from "path"; import { APP_NAME, getExportTemplateDir } from "../../config.js"; @@ -10,6 +11,47 @@ export interface ExportOptions { themeName?: string; } +interface ProviderSystemPrompt { + title: string; + content: string; + note?: string; +} + +/** + * Build the provider-specific system prompt for display in exports. + * Currently only supports OpenAI Codex provider. + */ +async function buildProviderSystemPrompt(state?: AgentState): Promise { + if (!state?.model || state.model.provider !== "openai-codex") { + return undefined; + } + + let instructions: string | null = null; + try { + instructions = await getCodexInstructions(state.model.id); + } catch { + // Cache miss or fetch failed - that's fine + } + + const bridgeText = buildCodexPiBridge(state.tools); + const userPrompt = state.systemPrompt || ""; + const modelFamily = getModelFamily(state.model.id); + + const instructionsText = + instructions || "(Codex instructions not cached. Run a Codex request to populate the local cache.)"; + const note = instructions + ? `Injected by the OpenAI Codex provider for model family "${modelFamily}" (instructions + bridge + user system prompt).` + : "Codex instructions unavailable; showing bridge and user system prompt only."; + + const content = `# Codex Instructions\n${instructionsText}\n\n# Codex-Pi Bridge\n${bridgeText}\n\n# User System Prompt\n${userPrompt || "(empty)"}`; + + return { + title: "Injected Prompt (OpenAI Codex)", + content, + note, + }; +} + /** Parse a color string to RGB values. Supports hex (#RRGGBB) and rgb(r,g,b) formats. */ function parseColor(color: string): { r: number; g: number; b: number } | undefined { const hexMatch = color.match(/^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/); @@ -103,6 +145,7 @@ interface SessionData { entries: ReturnType; leafId: string | null; systemPrompt?: string; + providerSystemPrompt?: ProviderSystemPrompt; tools?: { name: string; description: string }[]; } @@ -146,7 +189,11 @@ function generateHtml(sessionData: SessionData, themeName?: string): string { * Export session to HTML using SessionManager and AgentState. * Used by TUI's /export command. */ -export function exportSessionToHtml(sm: SessionManager, state?: AgentState, options?: ExportOptions | string): string { +export async function exportSessionToHtml( + sm: SessionManager, + state?: AgentState, + options?: ExportOptions | string, +): Promise { const opts: ExportOptions = typeof options === "string" ? { outputPath: options } : options || {}; const sessionFile = sm.getSessionFile(); @@ -162,6 +209,7 @@ export function exportSessionToHtml(sm: SessionManager, state?: AgentState, opti entries: sm.getEntries(), leafId: sm.getLeafId(), systemPrompt: state?.systemPrompt, + providerSystemPrompt: await buildProviderSystemPrompt(state), tools: state?.tools?.map((t) => ({ name: t.name, description: t.description })), }; @@ -195,6 +243,7 @@ export function exportFromFile(inputPath: string, options?: ExportOptions | stri entries: sm.getEntries(), leafId: sm.getLeafId(), systemPrompt: undefined, + providerSystemPrompt: undefined, tools: undefined, }; diff --git a/packages/coding-agent/src/core/export-html/template.css b/packages/coding-agent/src/core/export-html/template.css index 8fbdcd01..eac001a1 100644 --- a/packages/coding-agent/src/core/export-html/template.css +++ b/packages/coding-agent/src/core/export-html/template.css @@ -593,6 +593,17 @@ display: block; } + .system-prompt.provider-prompt { + border-left: 3px solid var(--warning); + } + + .system-prompt-note { + font-size: 10px; + font-style: italic; + color: var(--muted); + margin-top: 4px; + } + /* Tools list */ .tools-list { background: var(--customMessageBg); diff --git a/packages/coding-agent/src/core/export-html/template.js b/packages/coding-agent/src/core/export-html/template.js index 6d3e5929..52c2e493 100644 --- a/packages/coding-agent/src/core/export-html/template.js +++ b/packages/coding-agent/src/core/export-html/template.js @@ -12,7 +12,7 @@ bytes[i] = binary.charCodeAt(i); } const data = JSON.parse(new TextDecoder('utf-8').decode(bytes)); - const { header, entries, leafId: defaultLeafId, systemPrompt, tools } = data; + const { header, entries, leafId: defaultLeafId, systemPrompt, providerSystemPrompt, tools } = data; // ============================================================ // URL PARAMETER HANDLING @@ -1060,7 +1060,32 @@ `; - if (systemPrompt) { + // Render provider-injected system prompt (e.g., Codex) if present + if (providerSystemPrompt) { + const lines = providerSystemPrompt.content.split('\n'); + const previewLines = 10; + const noteHtml = providerSystemPrompt.note + ? `
${escapeHtml(providerSystemPrompt.note)}
` + : ''; + if (lines.length > previewLines) { + const preview = lines.slice(0, previewLines).join('\n'); + const remaining = lines.length - previewLines; + html += ``; + } else { + html += `
+
${escapeHtml(providerSystemPrompt.title)}
+ ${noteHtml} +
${escapeHtml(providerSystemPrompt.content)}
+
`; + } + } else if (systemPrompt) { + // Standard system prompt (non-Codex providers) const lines = systemPrompt.split('\n'); const previewLines = 10; if (lines.length > previewLines) { diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 6242ef52..c52e1daf 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -999,7 +999,7 @@ export class InteractiveMode { return; } if (text.startsWith("/export")) { - this.handleExportCommand(text); + await this.handleExportCommand(text); this.editor.setText(""); return; } @@ -2402,12 +2402,12 @@ export class InteractiveMode { // Command handlers // ========================================================================= - private handleExportCommand(text: string): void { + private async handleExportCommand(text: string): Promise { const parts = text.split(/\s+/); const outputPath = parts.length > 1 ? parts[1] : undefined; try { - const filePath = this.session.exportToHtml(outputPath); + const filePath = await this.session.exportToHtml(outputPath); this.showStatus(`Session exported to: ${filePath}`); } catch (error: unknown) { this.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`); @@ -2430,7 +2430,7 @@ export class InteractiveMode { // Export to a temp file const tmpFile = path.join(os.tmpdir(), "session.html"); try { - this.session.exportToHtml(tmpFile); + await this.session.exportToHtml(tmpFile); } catch (error: unknown) { this.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`); return; diff --git a/packages/coding-agent/src/modes/rpc/rpc-mode.ts b/packages/coding-agent/src/modes/rpc/rpc-mode.ts index fb32de59..b3ac5c38 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-mode.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-mode.ts @@ -431,7 +431,7 @@ export async function runRpcMode(session: AgentSession): Promise { } case "export_html": { - const path = session.exportToHtml(command.outputPath); + const path = await session.exportToHtml(command.outputPath); return success(id, "export_html", { path }); } From 17d863c08285a51c47ec9e91fe4d11ae4d6a9e10 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 6 Jan 2026 22:35:02 +0100 Subject: [PATCH 2/2] Show the bridge prompt as a standalone thing --- .../src/core/export-html/index.ts | 51 ++++++++----------- .../src/core/export-html/template.css | 33 ++++++++++++ .../src/core/export-html/template.js | 42 +++++---------- packages/coding-agent/src/main.ts | 2 +- 4 files changed, 69 insertions(+), 59 deletions(-) diff --git a/packages/coding-agent/src/core/export-html/index.ts b/packages/coding-agent/src/core/export-html/index.ts index 3af74df6..a25d39b7 100644 --- a/packages/coding-agent/src/core/export-html/index.ts +++ b/packages/coding-agent/src/core/export-html/index.ts @@ -1,5 +1,5 @@ -import type { AgentState } from "@mariozechner/pi-agent-core"; -import { buildCodexPiBridge, getCodexInstructions, getModelFamily } from "@mariozechner/pi-ai"; +import type { AgentState, AgentTool } from "@mariozechner/pi-agent-core"; +import { buildCodexPiBridge, getCodexInstructions } from "@mariozechner/pi-ai"; import { existsSync, readFileSync, writeFileSync } from "fs"; import { basename, join } from "path"; import { APP_NAME, getExportTemplateDir } from "../../config.js"; @@ -11,44 +11,34 @@ export interface ExportOptions { themeName?: string; } -interface ProviderSystemPrompt { - title: string; - content: string; - note?: string; +/** Info about Codex injection to show inline with model_change entries */ +interface CodexInjectionInfo { + /** Codex instructions text */ + instructions: string; + /** Bridge text (tool list) */ + bridge: string; } /** - * Build the provider-specific system prompt for display in exports. - * Currently only supports OpenAI Codex provider. + * Build Codex injection info for display inline with model_change entries. */ -async function buildProviderSystemPrompt(state?: AgentState): Promise { - if (!state?.model || state.model.provider !== "openai-codex") { - return undefined; - } - +async function buildCodexInjectionInfo(tools?: AgentTool[]): Promise { + // Try to get cached instructions for default model family let instructions: string | null = null; try { - instructions = await getCodexInstructions(state.model.id); + instructions = await getCodexInstructions("gpt-5.1-codex"); } catch { - // Cache miss or fetch failed - that's fine + // Cache miss - that's fine } - const bridgeText = buildCodexPiBridge(state.tools); - const userPrompt = state.systemPrompt || ""; - const modelFamily = getModelFamily(state.model.id); + const bridgeText = buildCodexPiBridge(tools); const instructionsText = instructions || "(Codex instructions not cached. Run a Codex request to populate the local cache.)"; - const note = instructions - ? `Injected by the OpenAI Codex provider for model family "${modelFamily}" (instructions + bridge + user system prompt).` - : "Codex instructions unavailable; showing bridge and user system prompt only."; - - const content = `# Codex Instructions\n${instructionsText}\n\n# Codex-Pi Bridge\n${bridgeText}\n\n# User System Prompt\n${userPrompt || "(empty)"}`; return { - title: "Injected Prompt (OpenAI Codex)", - content, - note, + instructions: instructionsText, + bridge: bridgeText, }; } @@ -145,7 +135,8 @@ interface SessionData { entries: ReturnType; leafId: string | null; systemPrompt?: string; - providerSystemPrompt?: ProviderSystemPrompt; + /** Info for rendering Codex injection inline with model_change entries */ + codexInjectionInfo?: CodexInjectionInfo; tools?: { name: string; description: string }[]; } @@ -209,7 +200,7 @@ export async function exportSessionToHtml( entries: sm.getEntries(), leafId: sm.getLeafId(), systemPrompt: state?.systemPrompt, - providerSystemPrompt: await buildProviderSystemPrompt(state), + codexInjectionInfo: await buildCodexInjectionInfo(state?.tools), tools: state?.tools?.map((t) => ({ name: t.name, description: t.description })), }; @@ -229,7 +220,7 @@ export async function exportSessionToHtml( * Export session file to HTML (standalone, without AgentState). * Used by CLI for exporting arbitrary session files. */ -export function exportFromFile(inputPath: string, options?: ExportOptions | string): string { +export async function exportFromFile(inputPath: string, options?: ExportOptions | string): Promise { const opts: ExportOptions = typeof options === "string" ? { outputPath: options } : options || {}; if (!existsSync(inputPath)) { @@ -243,7 +234,7 @@ export function exportFromFile(inputPath: string, options?: ExportOptions | stri entries: sm.getEntries(), leafId: sm.getLeafId(), systemPrompt: undefined, - providerSystemPrompt: undefined, + codexInjectionInfo: await buildCodexInjectionInfo(undefined), tools: undefined, }; diff --git a/packages/coding-agent/src/core/export-html/template.css b/packages/coding-agent/src/core/export-html/template.css index eac001a1..354ae74a 100644 --- a/packages/coding-agent/src/core/export-html/template.css +++ b/packages/coding-agent/src/core/export-html/template.css @@ -512,6 +512,39 @@ font-weight: bold; } + .codex-bridge-toggle { + color: var(--muted); + cursor: pointer; + text-decoration: underline; + font-size: 10px; + } + + .codex-bridge-toggle:hover { + color: var(--accent); + } + + .codex-bridge-content { + display: none; + margin-top: 8px; + padding: 8px; + background: var(--exportCardBg); + border-radius: 4px; + font-size: 11px; + max-height: 300px; + overflow: auto; + } + + .codex-bridge-content pre { + margin: 0; + white-space: pre-wrap; + word-break: break-word; + color: var(--muted); + } + + .model-change.show-bridge .codex-bridge-content { + display: block; + } + /* Compaction / Branch Summary - matches customMessage colors from TUI */ .compaction { background: var(--customMessageBg); diff --git a/packages/coding-agent/src/core/export-html/template.js b/packages/coding-agent/src/core/export-html/template.js index 52c2e493..48311d87 100644 --- a/packages/coding-agent/src/core/export-html/template.js +++ b/packages/coding-agent/src/core/export-html/template.js @@ -12,7 +12,7 @@ bytes[i] = binary.charCodeAt(i); } const data = JSON.parse(new TextDecoder('utf-8').decode(bytes)); - const { header, entries, leafId: defaultLeafId, systemPrompt, providerSystemPrompt, tools } = data; + const { header, entries, leafId: defaultLeafId, systemPrompt, codexInjectionInfo, tools } = data; // ============================================================ // URL PARAMETER HANDLING @@ -954,7 +954,17 @@ } if (entry.type === 'model_change') { - return `
${tsHtml}Switched to model: ${escapeHtml(entry.provider)}/${escapeHtml(entry.modelId)}
`; + let html = `
${tsHtml}Switched to model: ${escapeHtml(entry.provider)}/${escapeHtml(entry.modelId)}`; + + // Show expandable bridge prompt info when switching to openai-codex + if (entry.provider === 'openai-codex' && codexInjectionInfo) { + const fullContent = `# Codex Instructions\n${codexInjectionInfo.instructions}\n\n# Codex-Pi Bridge\n${codexInjectionInfo.bridge}`; + html += ` [bridge prompt]`; + html += `
${escapeHtml(fullContent)}
`; + } + + html += '
'; + return html; } if (entry.type === 'compaction') { @@ -1060,32 +1070,8 @@ `; - // Render provider-injected system prompt (e.g., Codex) if present - if (providerSystemPrompt) { - const lines = providerSystemPrompt.content.split('\n'); - const previewLines = 10; - const noteHtml = providerSystemPrompt.note - ? `
${escapeHtml(providerSystemPrompt.note)}
` - : ''; - if (lines.length > previewLines) { - const preview = lines.slice(0, previewLines).join('\n'); - const remaining = lines.length - previewLines; - html += ``; - } else { - html += `
-
${escapeHtml(providerSystemPrompt.title)}
- ${noteHtml} -
${escapeHtml(providerSystemPrompt.content)}
-
`; - } - } else if (systemPrompt) { - // Standard system prompt (non-Codex providers) + // Render system prompt (user's base prompt, applies to all providers) + if (systemPrompt) { const lines = systemPrompt.split('\n'); const previewLines = 10; if (lines.length > previewLines) { diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index eaa164cd..099c47ae 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -345,7 +345,7 @@ export async function main(args: string[]) { if (parsed.export) { try { const outputPath = parsed.messages.length > 0 ? parsed.messages[0] : undefined; - const result = exportFromFile(parsed.export, outputPath); + const result = await exportFromFile(parsed.export, outputPath); console.log(`Exported to: ${result}`); return; } catch (error: unknown) {