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:
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user