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