From 838b5162bc1c9508f519a18e0dc0cfeb79c8459b Mon Sep 17 00:00:00 2001 From: can1357 Date: Sun, 21 Jun 2026 18:31:03 +0200 Subject: [PATCH] feat(coding-agent): standardized html export styling with brand palette - Introduced `web-palette.ts` to implement the collab-web pink/purple brand identity for HTML exports. - Updated `generateThemeVars` to support a `palette` option, allowing users to choose between the brand-web aesthetic and a specific TUI theme. - Configured public exports and the share-viewer script to default to the brand-web palette rather than inheriting the user's terminal theme. - Refactored `AgentSession.exportToHtml` to align with the new branding defaults while allowing for per-export theme overrides. - Adjusted `dark.json` background values to ensure better visual consistency across internal surfaces. --- packages/coding-agent/CHANGELOG.md | 3 + .../scripts/generate-share-viewer.ts | 6 +- .../coding-agent/src/export/html/index.ts | 58 ++++++- .../src/export/html/web-palette.ts | 142 ++++++++++++++++++ .../coding-agent/src/modes/theme/dark.json | 2 +- .../coding-agent/src/session/agent-session.ts | 9 +- 6 files changed, 206 insertions(+), 14 deletions(-) create mode 100644 packages/coding-agent/src/export/html/web-palette.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 8fc55d13a..9d6d9c409 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## [Unreleased] + ### Added - Added `tab.waitForSelector` for more robust element-wait behavior @@ -8,6 +9,8 @@ ### Changed +- Standardized public HTML exports to use the brand web palette instead of the local TUI theme +- Updated export logic to allow choosing between brand-web or local TUI color palettes - Clamped browser tool timeouts to fail fast with named errors instead of opaque cell timeouts - Added strict validation to block unsupported Playwright-only selector engines in browser tools diff --git a/packages/coding-agent/scripts/generate-share-viewer.ts b/packages/coding-agent/scripts/generate-share-viewer.ts index 5815d0da6..e941c7f49 100755 --- a/packages/coding-agent/scripts/generate-share-viewer.ts +++ b/packages/coding-agent/scripts/generate-share-viewer.ts @@ -19,8 +19,10 @@ if (!outPath) { } const loaderJs = await Bun.file(new URL("../src/export/html/share-loader.js", import.meta.url).pathname).text(); -// Pin a built-in theme: the viewer is a public artifact, not a per-user export. -const themeVars = await generateThemeVars("dark"); +// 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"); const html = getTemplate() .replace("", () => ``) diff --git a/packages/coding-agent/src/export/html/index.ts b/packages/coding-agent/src/export/html/index.ts index 03fe28535..e22f2e2ee 100644 --- a/packages/coding-agent/src/export/html/index.ts +++ b/packages/coding-agent/src/export/html/index.ts @@ -12,6 +12,7 @@ import templateJs from "./template.js" with { type: "text" }; // Pre-built React tool renderers: built by `bun --cwd=packages/collab-web run build:tool-views`, // run automatically by root `prepare` on install and by `prepack` at publish. import toolViewsJs from "./tool-views.generated.js" with { type: "text" }; +import { webExportThemeVars } from "./web-palette"; let cachedTemplate: string | undefined; @@ -36,6 +37,19 @@ export function getTemplate(): string { 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. + */ + 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. + */ themeName?: string; /** Embed subagent session transcripts found next to the session file (default true). */ includeSubSessions?: boolean; @@ -101,12 +115,38 @@ function deriveExportColors(baseColor: string): { pageBg: string; cardBg: string }; } -/** Generate CSS custom properties for theme. Exported for the share-viewer build script. */ -export async function generateThemeVars(themeName?: string): Promise { +/** + * Generate CSS custom properties for the export `:root`. + * + * 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. + */ +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(); + const colors = await getResolvedThemeColors(themeName); const lines: string[] = []; - for (const [key, value] of Object.entries(colors)) { - lines.push(`--${key}: ${value};`); + for (const key in colors) { + lines.push(`--${key}: ${colors[key]};`); } const themeExport = await getThemeExportColors(themeName); @@ -200,8 +240,8 @@ async function collectSubSessionsFromDir( } /** Generate HTML from bundled template with runtime substitutions. */ -async function generateHtml(sessionData: SessionData, themeName?: string): Promise { - const themeVars = await generateThemeVars(themeName); +async function generateHtml(sessionData: SessionData, palette: "web" | "theme", themeName?: string): Promise { + const themeVars = await generateThemeVars(palette, themeName); const sessionDataBase64 = Buffer.from(JSON.stringify(sessionData)).toBase64(); // Use function replacements so `$'`, `$&`, `$$`, `$n`, etc. in the @@ -229,7 +269,8 @@ export async function exportSessionToHtml( if (Object.keys(subSessions).length > 0) sessionData.subSessions = subSessions; } - const html = await generateHtml(sessionData, opts.themeName); + const palette = opts.palette ?? (opts.themeName ? "theme" : "web"); + const html = await generateHtml(sessionData, palette, opts.themeName); const outputPath = opts.outputPath || `${APP_NAME}-session-${path.basename(sessionFile, ".jsonl")}.html`; await Bun.write(outputPath, html); @@ -258,7 +299,8 @@ export async function exportFromFile(inputPath: string, options?: ExportOptions if (Object.keys(subSessions).length > 0) sessionData.subSessions = subSessions; } - const html = await generateHtml(sessionData, opts.themeName); + const palette = opts.palette ?? (opts.themeName ? "theme" : "web"); + const html = await generateHtml(sessionData, palette, 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/web-palette.ts b/packages/coding-agent/src/export/html/web-palette.ts new file mode 100644 index 000000000..ce79e994b --- /dev/null +++ b/packages/coding-agent/src/export/html/web-palette.ts @@ -0,0 +1,142 @@ +/** + * Web/export palette — the omp brand identity shared by the collab-web live + * client (`my.omp.sh/`) and every public HTML export / share viewer (`/s/`). + * + * Why this exists separately from `modes/theme/dark.json`: the `dark` theme is + * the **default TUI theme** — its amber accent (`#febc38`) drives the terminal + * status line, syntax highlighting, thinking levels, and bash/python mode + * colors for every omp user. The public web artifacts want the collab-web + * pink/purple identity instead, so they pin this palette rather than inheriting + * the TUI's. Editing `dark.json` to repurpose it for the web would repaint + * every terminal; this file keeps the two surfaces decoupled. + * + * Token layout — emitted as CSS custom properties on `:root`: + * • Legacy export names consumed by `template.css` / `template.js` + * (`--text`, `--body-bg`, `--container-bg`, `--info-bg`, `--accent`, + * `--border`, `--success`, `--error`, `--warning`, `--muted`, `--dim`, + * `--borderAccent`, `--selectedBg`, `--userMessageBg`, `--customMessageBg`, + * `--customMessageLabel`, `--mdHeading`, `--mdLink`, `--mdCode`, + * `--mdListBullet`, `--toolOutput`, `--thinkingText`, syntax*, …). + * • collab-web-native aliases consumed by the `tv-` tool-render bridge + * (`tool-render.css`: `var(--bg-inset, …)`, `var(--fg, …)`, …) so embedded + * tool cards resolve to the *real* collab-web tokens and render + * pixel-identical to the live client. + * + * Alpha-bearing tokens (`--border`, `--ring`, `--accent-muted`, …) keep their + * `oklch(… / N%)` form — flattening them to opaque hex would produce harsh + * white borders and non-matching translucent focus rings. Opaque surfaces are + * sRGB hex (the collab-web `tokens.css` OKLCH dark-theme tokens converted via + * the standard OKLab→linear-sRGB→gamma path); if the live client palette + * changes, regenerate those from there. + */ +export const WEB_EXPORT_PALETTE = { + // --- collab-web-native aliases (tv- bridge) --- + "--bg": "#0f0b14", + "--bg-raised": "#16111c", + "--bg-inset": "#09060c", + "--bg-overlay": "#211b28", + "--fg": "#e6e3ea", + "--fg-muted": "#a49faa", + "--fg-faint": "#6e6974", + "--accent": "#ed4abf", + "--accent-muted": "oklch(0.674 0.23 341 / 18%)", + "--ok": "#68ca80", + "--err": "#f05653", + "--warn": "#e4b33f", + "--ring": "oklch(0.817 0.112 205 / 70%)", + "--font-mono": 'ui-monospace, "SF Mono", "JetBrains Mono", "Cascadia Mono", Menlo, Consolas, monospace', + + // --- legacy export names (template.css / template.js) --- + // surfaces — map onto the collab-web purple ramp + "--body-bg": "#0f0b14", // = --bg + "--container-bg": "#16111c", // = --bg-raised + "--info-bg": "#09060c", // = --bg-inset (recessed wells: code blocks, tool output) + // text + "--text": "#e6e3ea", // = --fg + "--muted": "#a49faa", // = --fg-muted + "--dim": "#6e6974", // = --fg-faint + "--thinkingText": "#a49faa", + // hairlines — white-alpha, matching collab-web's --border/--border-strong + "--border": "oklch(1 0 0 / 9%)", + "--borderMuted": "oklch(1 0 0 / 6%)", + // accent + brand purple (the gradient's mid stop) for secondary highlights + "--borderAccent": "#945ff9", + "--selectedBg": "#2d2535", + // status — semantic, used sparingly (cancelled / exit-code / success dots) + "--success": "#68ca80", // = --ok + "--error": "#f05653", // = --err + "--warning": "#e4b33f", // = --warn + // message bubbles + "--userMessageBg": "oklch(0.674 0.23 341 / 6%)", // accent-tinted, like template.css user-message + "--userMessageText": "#e6e3ea", + "--customMessageBg": "#211b28", // = --bg-overlay + "--customMessageText": "#a49faa", // = --fg-muted + "--customMessageLabel": "#b281d6", // lilac, matching dark.json's label hue + // tool surfaces + "--toolPendingBg": "#16111c", + "--toolSuccessBg": "#09060c", + "--toolErrorBg": "oklch(0.66 0.19 25 / 14%)", + "--toolTitle": "#e6e3ea", + "--toolOutput": "#a49faa", // = --fg-muted + // markdown + "--mdHeading": "#ed4abf", // accent — headings carry the brand + "--mdLink": "#5ad8e5", // ring cyan — links distinct from the pink accent + "--mdLinkUrl": "#6e6974", // = --fg-faint + "--mdCode": "#e6e3ea", + "--mdCodeBlock": "#e6e3ea", + "--mdCodeBlockBorder": "oklch(1 0 0 / 9%)", + "--mdQuote": "#a49faa", + "--mdQuoteBorder": "oklch(1 0 0 / 13%)", + "--mdHr": "oklch(1 0 0 / 9%)", + "--mdListBullet": "#ed4abf", // accent — bullets carry the brand + // diff + "--toolDiffAdded": "#68ca80", + "--toolDiffRemoved": "#f05653", + "--toolDiffContext": "#6e6974", + // syntax — cool-neutral with pink/purple accents for keywords/types + "--syntaxComment": "#6e6974", // = --fg-faint + "--syntaxKeyword": "#945ff9", // brand purple + "--syntaxFunction": "#e4b33f", // warn amber (analog of dark.json's DCDCAA) + "--syntaxVariable": "#5ad8e5", // ring cyan + "--syntaxString": "#68ca80", // ok green + "--syntaxNumber": "#ed4abf", // accent + "--syntaxType": "#b281d6", // lilac + "--syntaxOperator": "#e6e3ea", + "--syntaxPunctuation": "#a49faa", + // thinking-level ramp — purple → lilac, matching the brand gradient + "--thinkingOff": "#6e6974", + "--thinkingMinimal": "#6e6974", + "--thinkingLow": "#945ff9", + "--thinkingMedium": "#b281d6", + "--thinkingHigh": "#ed4abf", + "--thinkingXhigh": "#e4b33f", + // mode tints (sidebar/role tags) — not surfaced in the export tree but + // emitted for completeness so template.js role classes resolve cleanly + "--bashMode": "#5ad8e5", + "--pythonMode": "#e4b33f", + // status-line tokens are TUI-only; not consumed by the export template, but + // emitted so any future surface that reads them inherits the brand. + "--statusLineBg": "#0f0b14", + "--statusLineSep": "#6e6974", + "--statusLineModel": "#ed4abf", + "--statusLinePath": "#5ad8e5", + "--statusLineGitClean": "#68ca80", + "--statusLineGitDirty": "#e4b33f", + "--statusLineContext": "#a49faa", + "--statusLineSpend": "#5ad8e5", + "--statusLineStaged": "#68ca80", + "--statusLineDirty": "#e4b33f", + "--statusLineUntracked": "#945ff9", + "--statusLineOutput": "#b281d6", + "--statusLineCost": "#b281d6", + "--statusLineSubagents": "#ed4abf", +} as const satisfies Record; + +/** Serialize the palette as `--key: value;` declarations for `:root { … }`. */ +export function webExportThemeVars(): string { + let out = ""; + for (const k in WEB_EXPORT_PALETTE) { + out += `${k}: ${WEB_EXPORT_PALETTE[k as keyof typeof WEB_EXPORT_PALETTE]}; `; + } + return out.trimEnd(); +} diff --git a/packages/coding-agent/src/modes/theme/dark.json b/packages/coding-agent/src/modes/theme/dark.json index 080fb1238..649acc2b6 100644 --- a/packages/coding-agent/src/modes/theme/dark.json +++ b/packages/coding-agent/src/modes/theme/dark.json @@ -90,6 +90,6 @@ "export": { "pageBg": "#18181e", "cardBg": "#1e1e24", - "infoBg": "#3c3728" + "infoBg": "#26262e" } } diff --git a/packages/coding-agent/src/session/agent-session.ts b/packages/coding-agent/src/session/agent-session.ts index 8637a56cb..a6b91853c 100644 --- a/packages/coding-agent/src/session/agent-session.ts +++ b/packages/coding-agent/src/session/agent-session.ts @@ -210,7 +210,7 @@ import { resolveMemoryBackend } from "../memory-backend"; import { shutdownMnemopiEmbedClient } from "../mnemopi/embed-client"; import { getMnemopiSessionState, type MnemopiSessionState, setMnemopiSessionState } from "../mnemopi/state"; import { containsOrchestrate, ORCHESTRATE_NOTICE } from "../modes/orchestrate"; -import { getCurrentThemeName, theme } from "../modes/theme/theme"; +import { theme } from "../modes/theme/theme"; import { parseTurnBudget } from "../modes/turn-budget"; import { containsUltrathink, ULTRATHINK_NOTICE } from "../modes/ultrathink"; import { computeNonMessageBreakdown, computeNonMessageTokens } from "../modes/utils/context-usage"; @@ -12534,9 +12534,12 @@ export class AgentSession { * @returns Path to exported file */ async exportToHtml(outputPath?: string): Promise { - const themeName = getCurrentThemeName(); + // Public HTML export ships in the omp brand palette (collab-web + // pink/purple), matching my.omp.sh — not the host's terminal theme. + // Callers who want a themed export can pass `palette: "theme"` with + // `themeName` directly to `exportSessionToHtml`. const { exportSessionToHtml } = await import("../export/html"); - return exportSessionToHtml(this.sessionManager, this.state, { outputPath, themeName }); + return exportSessionToHtml(this.sessionManager, this.state, { outputPath, palette: "web" }); } // =========================================================================