diff --git a/packages/coding-agent/docs/theme.md b/packages/coding-agent/docs/theme.md index aba7643b..3eaffc83 100644 --- a/packages/coding-agent/docs/theme.md +++ b/packages/coding-agent/docs/theme.md @@ -104,6 +104,27 @@ These create a visual hierarchy: off → minimal → low → medium → high → **Total: 50 color tokens** (all required) +### HTML Export Colors (optional) + +The `export` section is optional and controls colors used when exporting sessions to HTML via `/export`. If not specified, these colors are automatically derived from `userMessageBg` based on luminance detection. + +| Token | Purpose | +|-------|---------| +| `pageBg` | Page background color | +| `cardBg` | Card/container background (headers, stats boxes) | +| `infoBg` | Info sections background (system prompt, notices, compaction) | + +Example: +```json +{ + "export": { + "pageBg": "#18181e", + "cardBg": "#1e1e24", + "infoBg": "#3c3728" + } +} +``` + ## Theme Format Themes are defined in JSON files with the following structure: diff --git a/packages/coding-agent/src/core/export-html/index.ts b/packages/coding-agent/src/core/export-html/index.ts index ac860171..577f262c 100644 --- a/packages/coding-agent/src/core/export-html/index.ts +++ b/packages/coding-agent/src/core/export-html/index.ts @@ -2,7 +2,7 @@ import type { AgentState } from "@mariozechner/pi-agent-core"; import { existsSync, readFileSync, writeFileSync } from "fs"; import { basename, join } from "path"; import { APP_NAME, getExportTemplateDir } from "../../config.js"; -import { getResolvedThemeColors } from "../../modes/interactive/theme/theme.js"; +import { getResolvedThemeColors, getThemeExportColors } from "../../modes/interactive/theme/theme.js"; import { SessionManager } from "../session-manager.js"; export interface ExportOptions { @@ -86,12 +86,14 @@ function generateThemeVars(themeName?: string): string { lines.push(`--${key}: ${value};`); } - // Add derived export colors + // Use explicit theme export colors if available, otherwise derive from userMessageBg + const themeExport = getThemeExportColors(themeName); const userMessageBg = colors.userMessageBg || "#343541"; - const exportColors = deriveExportColors(userMessageBg); - lines.push(`--exportPageBg: ${exportColors.pageBg};`); - lines.push(`--exportCardBg: ${exportColors.cardBg};`); - lines.push(`--exportInfoBg: ${exportColors.infoBg};`); + const derivedColors = deriveExportColors(userMessageBg); + + lines.push(`--exportPageBg: ${themeExport.pageBg ?? derivedColors.pageBg};`); + lines.push(`--exportCardBg: ${themeExport.cardBg ?? derivedColors.cardBg};`); + lines.push(`--exportInfoBg: ${themeExport.infoBg ?? derivedColors.infoBg};`); return lines.join("\n "); } diff --git a/packages/coding-agent/src/core/export-html/template.css b/packages/coding-agent/src/core/export-html/template.css index 1e8ab138..0763a3fd 100644 --- a/packages/coding-agent/src/core/export-html/template.css +++ b/packages/coding-agent/src/core/export-html/template.css @@ -363,6 +363,7 @@ } .tool-output { + margin-top: var(--line-height); color: var(--toolOutput); word-wrap: break-word; overflow-wrap: break-word; diff --git a/packages/coding-agent/src/modes/interactive/theme/dark.json b/packages/coding-agent/src/modes/interactive/theme/dark.json index 069e32fd..cc909781 100644 --- a/packages/coding-agent/src/modes/interactive/theme/dark.json +++ b/packages/coding-agent/src/modes/interactive/theme/dark.json @@ -76,5 +76,10 @@ "thinkingXhigh": "#d183e8", "bashMode": "green" + }, + "export": { + "pageBg": "#18181e", + "cardBg": "#1e1e24", + "infoBg": "#3c3728" } } diff --git a/packages/coding-agent/src/modes/interactive/theme/light.json b/packages/coding-agent/src/modes/interactive/theme/light.json index 138af303..9154b161 100644 --- a/packages/coding-agent/src/modes/interactive/theme/light.json +++ b/packages/coding-agent/src/modes/interactive/theme/light.json @@ -75,5 +75,10 @@ "thinkingXhigh": "#8b008b", "bashMode": "green" + }, + "export": { + "pageBg": "#f8f8f8", + "cardBg": "#ffffff", + "infoBg": "#fffae6" } } diff --git a/packages/coding-agent/src/modes/interactive/theme/theme-schema.json b/packages/coding-agent/src/modes/interactive/theme/theme-schema.json index f561ecb1..820813e0 100644 --- a/packages/coding-agent/src/modes/interactive/theme/theme-schema.json +++ b/packages/coding-agent/src/modes/interactive/theme/theme-schema.json @@ -267,6 +267,25 @@ } }, "additionalProperties": false + }, + "export": { + "type": "object", + "description": "Optional colors for HTML export (defaults derived from userMessageBg if not specified)", + "properties": { + "pageBg": { + "$ref": "#/$defs/colorValue", + "description": "Page background color" + }, + "cardBg": { + "$ref": "#/$defs/colorValue", + "description": "Card/container background color" + }, + "infoBg": { + "$ref": "#/$defs/colorValue", + "description": "Info sections background (system prompt, notices)" + } + }, + "additionalProperties": false } }, "additionalProperties": false, diff --git a/packages/coding-agent/src/modes/interactive/theme/theme.ts b/packages/coding-agent/src/modes/interactive/theme/theme.ts index 84b9d02e..5b5155e6 100644 --- a/packages/coding-agent/src/modes/interactive/theme/theme.ts +++ b/packages/coding-agent/src/modes/interactive/theme/theme.ts @@ -82,6 +82,13 @@ const ThemeJsonSchema = Type.Object({ // Bash Mode (1 color) bashMode: ColorValueSchema, }), + export: Type.Optional( + Type.Object({ + pageBg: Type.Optional(ColorValueSchema), + cardBg: Type.Optional(ColorValueSchema), + infoBg: Type.Optional(ColorValueSchema), + }), + ), }); type ThemeJson = Static; @@ -737,6 +744,44 @@ export function isLightTheme(themeName?: string): boolean { return themeName === "light"; } +/** + * Get explicit export colors from theme JSON, if specified. + * Returns undefined for each color that isn't explicitly set. + */ +export function getThemeExportColors(themeName?: string): { + pageBg?: string; + cardBg?: string; + infoBg?: string; +} { + const name = themeName ?? getDefaultTheme(); + try { + const themeJson = loadThemeJson(name); + const exportSection = themeJson.export; + if (!exportSection) return {}; + + const vars = themeJson.vars ?? {}; + const resolve = (value: string | number | undefined): string | undefined => { + if (value === undefined) return undefined; + if (typeof value === "number") return ansi256ToHex(value); + if (value.startsWith("$")) { + const resolved = vars[value]; + if (resolved === undefined) return undefined; + if (typeof resolved === "number") return ansi256ToHex(resolved); + return resolved; + } + return value; + }; + + return { + pageBg: resolve(exportSection.pageBg), + cardBg: resolve(exportSection.cardBg), + infoBg: resolve(exportSection.infoBg), + }; + } catch { + return {}; + } +} + // ============================================================================ // TUI Helpers // ============================================================================