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" });
}
// =========================================================================