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.
This commit is contained in:
can1357
2026-06-21 18:31:03 +02:00
parent 1cb433cbde
commit 838b5162bc
6 changed files with 206 additions and 14 deletions
+50 -8
View File
@@ -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/<id>` 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<string> {
/**
* 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<string> {
// 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<string> {
const themeVars = await generateThemeVars(themeName);
async function generateHtml(sessionData: SessionData, palette: "web" | "theme", themeName?: string): Promise<string> {
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);
@@ -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/<id>`).
*
* 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<string, string>;
/** 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();
}