diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 536e7fe87..d19ffadf0 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -572,6 +572,9 @@ - Fixed `--reasoning-slide-plan` silently ending the run with no code written when the model answered with a text-only reply. - Fixed launch tool rendering issues, including stacked pending headers and confusing start/wait results when readiness timed out. - Fixed the in-process `stat` and other GNU-flavored shell builtins (such as `date`, `sed`, `mktemp`, `tail`, `find`, `base64`, and `ln`) mangling or failing on macOS/BSD-style invocations. +### Added + +- Added system-aware light and dark themes to HTML session exports, with a `/export --themes` option to bundle the user's selected TUI themes. ## [16.5.1] - 2026-07-14 diff --git a/packages/coding-agent/scripts/generate-share-viewer.ts b/packages/coding-agent/scripts/generate-share-viewer.ts index e941c7f49..185223df3 100755 --- a/packages/coding-agent/scripts/generate-share-viewer.ts +++ b/packages/coding-agent/scripts/generate-share-viewer.ts @@ -10,7 +10,7 @@ * The relay repo's build script runs this and embeds the output via go:embed. */ import * as path from "node:path"; -import { generateThemeVars, getTemplate } from "../src/export/html"; +import { generateThemeStyles, getTemplate } from "../src/export/html"; const outPath = process.argv[2]; if (!outPath) { @@ -19,13 +19,11 @@ if (!outPath) { } const loaderJs = await Bun.file(new URL("../src/export/html/share-loader.js", import.meta.url).pathname).text(); -// Pin the omp brand palette (collab-web pink/purple identity) — the viewer is -// a public artifact matching the live my.omp.sh client, not a per-user export -// that should mirror the host's terminal theme. -const themeVars = await generateThemeVars("web"); +// Public artifacts use the bundled omp web themes rather than TUI themes. +const themeStyles = await generateThemeStyles("web"); const html = getTemplate() - .replace("", () => ``) + .replace("", () => ``) .replace("Session Export", () => "omp session") .replace("{{SESSION_DATA}}", () => `\n `); diff --git a/packages/coding-agent/src/export/html/index.ts b/packages/coding-agent/src/export/html/index.ts index 5b7b06e03..b23fad364 100644 --- a/packages/coding-agent/src/export/html/index.ts +++ b/packages/coding-agent/src/export/html/index.ts @@ -35,22 +35,28 @@ export function getTemplate(): string { return cachedTemplate; } +export interface ExportThemeNames { + dark: string; + light: string; +} + +/** Parse `/export [--themes] [path]`; paths containing spaces were never supported. */ +export function parseExportArgs(args: string): { outputPath?: string; useUserThemes: boolean } { + const parts = args.trim().split(/\s+/).filter(Boolean); + const useUserThemes = parts.includes("--themes"); + const paths = parts.filter(part => part !== "--themes"); + if (paths.length > 1) throw new Error("Usage: /export [--themes] [path]"); + return { outputPath: paths[0], useUserThemes }; +} + export interface ExportOptions { outputPath?: string; - /** - * Which color palette the export ships with. - * - `"web"` (default) — the omp brand identity (collab-web pink/purple), - * so public HTML exports and the `/s/` share viewer match the live - * `my.omp.sh` client. See `web-palette.ts`. - * - `"theme"` — derive from `themeName` (or the active TUI theme), preserving - * the pre-15.12 behavior where an export mirrored the user's terminal. - */ + /** `"web"` bundles the omp web themes; `"theme"` bundles TUI themes. */ palette?: "web" | "theme"; - /** - * TUI theme to derive colors from when `palette: "theme"`. Ignored for the - * default `"web"` palette. Resolves to the active TUI theme when omitted. - */ + /** Legacy single TUI theme name. Prefer `themeNames` for dual-theme exports. */ themeName?: string; + /** Dark and light TUI themes to bundle when `palette` is `"theme"`. */ + themeNames?: ExportThemeNames; /** Embed subagent session transcripts found next to the session file (default true). */ includeSubSessions?: boolean; } @@ -116,50 +122,49 @@ function deriveExportColors(baseColor: string): { pageBg: string; cardBg: string } /** - * Generate CSS custom properties for the export `:root`. + * Generate CSS custom properties for one export theme. * - * Two call shapes: - * • `generateThemeVars("web" | "theme", themeName?)` — explicit palette. - * `"web"` (the default for public artifacts) returns the fixed omp brand - * palette from `web-palette.ts` — collab-web pink/purple identity, shared - * with the live `my.omp.sh` client, so exports and the share viewer render - * identically to it. `"theme"` derives from the TUI theme via - * `getResolvedThemeColors(themeName)` plus the three - * `export.{pageBg,cardBg,infoBg}` surface overrides. - * • `generateThemeVars(themeName)` — legacy single-arg form: derive from the - * named TUI theme. Kept so existing callers (and the theme-islight test) - * keep working; equivalent to `generateThemeVars("theme", themeName)`. - * - * Exported for the share-viewer build script. + * The single-argument theme-name form remains available to callers that need + * one TUI palette. Standalone HTML uses `generateThemeStyles()` below. */ export async function generateThemeVars( palette: "web" | "theme" | (string & {}) = "web", themeName?: string, ): Promise { - // Legacy single-arg form: `generateThemeVars("my-theme")` — the first arg - // is a theme name, not a palette. Route it to the themed path. - if (palette !== "web" && palette !== "theme") { - return generateThemeVars("theme", palette); - } - if (palette === "web") return webExportThemeVars(); + if (palette !== "web" && palette !== "theme") return generateThemeVars("theme", palette); + if (palette === "web") return webExportThemeVars("dark"); const colors = await getResolvedThemeColors(themeName); - const lines: string[] = []; - for (const key in colors) { - lines.push(`--${key}: ${colors[key]};`); - } - + const lines = Object.entries(colors).map(([key, value]) => `--${key}: ${value};`); const themeExport = await getThemeExportColors(themeName); - const userMessageBg = colors.userMessageBg || "#343541"; - const derived = deriveExportColors(userMessageBg); + const derived = deriveExportColors(colors.userMessageBg || "#343541"); lines.push(`--body-bg: ${themeExport.pageBg ?? derived.pageBg};`); lines.push(`--container-bg: ${themeExport.cardBg ?? derived.cardBg};`); lines.push(`--info-bg: ${themeExport.infoBg ?? derived.infoBg};`); - return lines.join(" "); } +/** Generate dark, light, and system-following CSS rules for a standalone viewer. */ +export async function generateThemeStyles( + palette: "web" | "theme", + themeNames?: ExportThemeNames, + legacyThemeName?: string, +): Promise { + const [dark, light] = + palette === "web" + ? [webExportThemeVars("dark"), webExportThemeVars("light")] + : await Promise.all([ + generateThemeVars("theme", themeNames?.dark ?? legacyThemeName ?? "dark"), + generateThemeVars("theme", themeNames?.light ?? legacyThemeName ?? "light"), + ]); + return [ + `:root, :root[data-theme="dark"] { color-scheme: dark; ${dark} }`, + `:root[data-theme="light"] { color-scheme: light; ${light} }`, + `@media (prefers-color-scheme: light) { :root:not([data-theme]) { color-scheme: light; ${light} } }`, + ].join("\n"); +} + /** Embedded subagent session transcript, keyed by slash-joined agent path in `SessionData.subSessions`. */ export interface SubSession { /** Bare agent id (session file stem), e.g. "ToolAsk". */ @@ -240,15 +245,19 @@ async function collectSubSessionsFromDir( } /** Generate HTML from bundled template with runtime substitutions. */ -async function generateHtml(sessionData: SessionData, palette: "web" | "theme", themeName?: string): Promise { - const themeVars = await generateThemeVars(palette, themeName); +async function generateHtml( + sessionData: SessionData, + palette: "web" | "theme", + themeNames?: ExportThemeNames, + themeName?: string, +): Promise { + const themeStyles = await generateThemeStyles(palette, themeNames, themeName); const sessionDataBase64 = Buffer.from(JSON.stringify(sessionData)).toBase64(); // Use function replacements so `$'`, `$&`, `$$`, `$n`, etc. in the - // substituted CSS/base64 are not interpreted as substitution patterns - // (see https://mdn.io/String.replace). + // substituted CSS/base64 are not interpreted as substitution patterns. return getTemplate() - .replace("", () => ``) + .replace("", () => ``) .replace("{{SESSION_DATA}}", () => sessionDataBase64); } @@ -270,7 +279,7 @@ export async function exportSessionToHtml( } const palette = opts.palette ?? (opts.themeName ? "theme" : "web"); - const html = await generateHtml(sessionData, palette, opts.themeName); + const html = await generateHtml(sessionData, palette, opts.themeNames, opts.themeName); const outputPath = opts.outputPath || `${APP_NAME}-session-${path.basename(sessionFile, ".jsonl")}.html`; await Bun.write(outputPath, html); @@ -300,7 +309,7 @@ export async function exportFromFile(inputPath: string, options?: ExportOptions } const palette = opts.palette ?? (opts.themeName ? "theme" : "web"); - const html = await generateHtml(sessionData, palette, opts.themeName); + const html = await generateHtml(sessionData, palette, opts.themeNames, opts.themeName); const outputPath = opts.outputPath || `${APP_NAME}-session-${path.basename(inputPath, ".jsonl")}.html`; await Bun.write(outputPath, html); diff --git a/packages/coding-agent/src/export/html/template.css b/packages/coding-agent/src/export/html/template.css index 4f491b222..2e1fefb83 100644 --- a/packages/coding-agent/src/export/html/template.css +++ b/packages/coding-agent/src/export/html/template.css @@ -104,11 +104,14 @@ } .sidebar-controls { + display: flex; + gap: 6px; padding: 0 0 6px; } .sidebar-search { - width: 100%; + flex: 1; + min-width: 0; box-sizing: border-box; padding: 5px 9px; font-size: 11px; @@ -138,6 +141,21 @@ color: var(--muted); } + .theme-select { + padding: 5px 8px; + font: inherit; + font-size: 10px; + background: var(--body-bg); + color: var(--text); + border: 1px solid var(--hairline); + border-radius: var(--radius-ctl); + cursor: pointer; + } + + .theme-select:hover { + border-color: var(--hairline-strong); + } + .sidebar-filters { display: flex; gap: 4px; diff --git a/packages/coding-agent/src/export/html/template.html b/packages/coding-agent/src/export/html/template.html index a07ae1be8..adf752f65 100644 --- a/packages/coding-agent/src/export/html/template.html +++ b/packages/coding-agent/src/export/html/template.html @@ -8,6 +8,7 @@ Session Export + @@ -17,6 +18,11 @@