mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 14:03:49 +00:00
WIP: Rewrite export-html with tree sidebar, client-side rendering
- Add tree sidebar with search and filter (Default/All/Labels) - Client-side markdown/syntax highlighting via vendored marked.js + highlight.js - Base64 encode session data to avoid escaping issues - Reuse theme.ts color tokens via getResolvedThemeColors() - Sticky sidebar, responsive mobile layout with overlay - Click tree node to scroll to message - Keyboard shortcuts: Esc to reset, Ctrl/Cmd+F to search
This commit is contained in:
parent
a073477555
commit
256fa575fb
11 changed files with 3195 additions and 1446 deletions
|
|
@ -3263,7 +3263,7 @@ export const MODELS = {
|
|||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 163840,
|
||||
maxTokens: 163840,
|
||||
maxTokens: 65536,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"deepseek/deepseek-r1-distill-llama-70b": {
|
||||
id: "deepseek/deepseek-r1-distill-llama-70b",
|
||||
|
|
@ -3563,13 +3563,13 @@ export const MODELS = {
|
|||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0.04,
|
||||
output: 0.15,
|
||||
input: 0.036,
|
||||
output: 0.064,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 96000,
|
||||
maxTokens: 96000,
|
||||
contextWindow: 131072,
|
||||
maxTokens: 4096,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"google/gemma-3-27b-it:free": {
|
||||
id: "google/gemma-3-27b-it:free",
|
||||
|
|
@ -5297,8 +5297,8 @@ export const MODELS = {
|
|||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.039,
|
||||
output: 0.19,
|
||||
input: 0.02,
|
||||
output: 0.09999999999999999,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
|
|
@ -5348,8 +5348,8 @@ export const MODELS = {
|
|||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.03,
|
||||
output: 0.14,
|
||||
input: 0.016,
|
||||
output: 0.06,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
|
|
@ -5994,8 +5994,8 @@ export const MODELS = {
|
|||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.09,
|
||||
output: 1.1,
|
||||
input: 0.06,
|
||||
output: 0.6,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@
|
|||
"clean": "rm -rf dist",
|
||||
"build": "tsgo -p tsconfig.build.json && chmod +x dist/cli.js && npm run copy-assets",
|
||||
"build:binary": "npm run build && bun build --compile ./dist/cli.js --outfile dist/pi && npm run copy-binary-assets",
|
||||
"copy-assets": "mkdir -p dist/modes/interactive/theme && cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/",
|
||||
"copy-binary-assets": "cp package.json dist/ && cp README.md dist/ && cp CHANGELOG.md dist/ && mkdir -p dist/theme && cp src/modes/interactive/theme/*.json dist/theme/ && cp -r docs dist/ && cp -r examples dist/",
|
||||
"copy-assets": "mkdir -p dist/modes/interactive/theme && cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/ && mkdir -p dist/core/export-html/vendor && cp src/core/export-html/template.html dist/core/export-html/ && cp src/core/export-html/vendor/*.js dist/core/export-html/vendor/",
|
||||
"copy-binary-assets": "cp package.json dist/ && cp README.md dist/ && cp CHANGELOG.md dist/ && mkdir -p dist/theme && cp src/modes/interactive/theme/*.json dist/theme/ && mkdir -p dist/export-html/vendor && cp src/core/export-html/template.html dist/export-html/ && cp src/core/export-html/vendor/*.js dist/export-html/vendor/ && cp -r docs dist/ && cp -r examples dist/",
|
||||
"test": "vitest --run",
|
||||
"prepublishOnly": "npm run clean && npm run build"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -60,6 +60,21 @@ export function getThemesDir(): string {
|
|||
return join(packageDir, srcOrDist, "modes", "interactive", "theme");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get path to HTML export template directory (shipped with package)
|
||||
* - For Bun binary: export-html/ next to executable
|
||||
* - For Node.js (dist/): dist/core/export-html/
|
||||
* - For tsx (src/): src/core/export-html/
|
||||
*/
|
||||
export function getExportTemplateDir(): string {
|
||||
if (isBunBinary) {
|
||||
return join(dirname(process.execPath), "export-html");
|
||||
}
|
||||
const packageDir = getPackageDir();
|
||||
const srcOrDist = existsSync(join(packageDir, "src")) ? "src" : "dist";
|
||||
return join(packageDir, srcOrDist, "core", "export-html");
|
||||
}
|
||||
|
||||
/** Get path to package.json */
|
||||
export function getPackageJsonPath(): string {
|
||||
return join(getPackageDir(), "package.json");
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import {
|
|||
shouldCompact,
|
||||
} from "./compaction/index.js";
|
||||
import type { CustomToolContext, CustomToolSessionEvent, LoadedCustomTool } from "./custom-tools/index.js";
|
||||
import { exportSessionToHtml } from "./export-html.js";
|
||||
import { exportSessionToHtml } from "./export-html/index.js";
|
||||
import type {
|
||||
HookContext,
|
||||
HookRunner,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
130
packages/coding-agent/src/core/export-html/index.ts
Normal file
130
packages/coding-agent/src/core/export-html/index.ts
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
import type { AgentState } from "@mariozechner/pi-agent-core";
|
||||
import { existsSync, readFileSync, writeFileSync } from "fs";
|
||||
import { basename, join } from "path";
|
||||
import { APP_NAME, getExportTemplateDir, VERSION } from "../../config.js";
|
||||
import { getResolvedThemeColors, isLightTheme } from "../../modes/interactive/theme/theme.js";
|
||||
import { SessionManager } from "../session-manager.js";
|
||||
|
||||
export interface ExportOptions {
|
||||
outputPath?: string;
|
||||
themeName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate CSS custom property declarations from theme colors.
|
||||
*/
|
||||
function generateThemeVars(themeName?: string): string {
|
||||
const colors = getResolvedThemeColors(themeName);
|
||||
const lines: string[] = [];
|
||||
for (const [key, value] of Object.entries(colors)) {
|
||||
lines.push(`--${key}: ${value};`);
|
||||
}
|
||||
return lines.join("\n ");
|
||||
}
|
||||
|
||||
interface SessionData {
|
||||
header: ReturnType<SessionManager["getHeader"]>;
|
||||
entries: ReturnType<SessionManager["getEntries"]>;
|
||||
leafId: string | null;
|
||||
systemPrompt?: string;
|
||||
tools?: { name: string; description: string }[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Core HTML generation logic shared by both export functions.
|
||||
*/
|
||||
function generateHtml(sessionData: SessionData, themeName?: string): string {
|
||||
const templateDir = getExportTemplateDir();
|
||||
const template = readFileSync(join(templateDir, "template.html"), "utf-8");
|
||||
const markedJs = readFileSync(join(templateDir, "vendor", "marked.min.js"), "utf-8");
|
||||
const hljsJs = readFileSync(join(templateDir, "vendor", "highlight.min.js"), "utf-8");
|
||||
|
||||
const themeVars = generateThemeVars(themeName);
|
||||
const light = isLightTheme(themeName);
|
||||
const bodyBg = light ? "#f8f8f8" : "#18181e";
|
||||
const containerBg = light ? "#ffffff" : "#1e1e24";
|
||||
|
||||
const title = `Session ${sessionData.header?.id ?? "export"} - ${APP_NAME}`;
|
||||
|
||||
// Base64 encode session data to avoid escaping issues
|
||||
const sessionDataBase64 = Buffer.from(JSON.stringify(sessionData)).toString("base64");
|
||||
|
||||
return template
|
||||
.replace("{{TITLE}}", title)
|
||||
.replace("{{THEME_VARS}}", themeVars)
|
||||
.replace("{{BODY_BG}}", bodyBg)
|
||||
.replace("{{CONTAINER_BG}}", containerBg)
|
||||
.replace("{{SESSION_DATA}}", sessionDataBase64)
|
||||
.replace("{{MARKED_JS}}", markedJs)
|
||||
.replace("{{HIGHLIGHT_JS}}", hljsJs)
|
||||
.replace("{{APP_NAME}}", `${APP_NAME} v${VERSION}`)
|
||||
.replace("{{GENERATED_DATE}}", new Date().toLocaleString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Export session to HTML using SessionManager and AgentState.
|
||||
* Used by TUI's /export command.
|
||||
*/
|
||||
export function exportSessionToHtml(sm: SessionManager, state?: AgentState, options?: ExportOptions | string): string {
|
||||
const opts: ExportOptions = typeof options === "string" ? { outputPath: options } : options || {};
|
||||
|
||||
const sessionFile = sm.getSessionFile();
|
||||
if (!sessionFile) {
|
||||
throw new Error("Cannot export in-memory session to HTML");
|
||||
}
|
||||
if (!existsSync(sessionFile)) {
|
||||
throw new Error("Nothing to export yet - start a conversation first");
|
||||
}
|
||||
|
||||
const sessionData: SessionData = {
|
||||
header: sm.getHeader(),
|
||||
entries: sm.getEntries(),
|
||||
leafId: sm.getLeafId(),
|
||||
systemPrompt: state?.systemPrompt,
|
||||
tools: state?.tools?.map((t) => ({ name: t.name, description: t.description })),
|
||||
};
|
||||
|
||||
const html = generateHtml(sessionData, opts.themeName);
|
||||
|
||||
let outputPath = opts.outputPath;
|
||||
if (!outputPath) {
|
||||
const sessionBasename = basename(sessionFile, ".jsonl");
|
||||
outputPath = `${APP_NAME}-session-${sessionBasename}.html`;
|
||||
}
|
||||
|
||||
writeFileSync(outputPath, html, "utf8");
|
||||
return outputPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export session file to HTML (standalone, without AgentState).
|
||||
* Used by CLI for exporting arbitrary session files.
|
||||
*/
|
||||
export function exportFromFile(inputPath: string, options?: ExportOptions | string): string {
|
||||
const opts: ExportOptions = typeof options === "string" ? { outputPath: options } : options || {};
|
||||
|
||||
if (!existsSync(inputPath)) {
|
||||
throw new Error(`File not found: ${inputPath}`);
|
||||
}
|
||||
|
||||
const sm = SessionManager.open(inputPath);
|
||||
|
||||
const sessionData: SessionData = {
|
||||
header: sm.getHeader(),
|
||||
entries: sm.getEntries(),
|
||||
leafId: sm.getLeafId(),
|
||||
systemPrompt: undefined,
|
||||
tools: undefined,
|
||||
};
|
||||
|
||||
const html = generateHtml(sessionData, opts.themeName);
|
||||
|
||||
let outputPath = opts.outputPath;
|
||||
if (!outputPath) {
|
||||
const inputBasename = basename(inputPath, ".jsonl");
|
||||
outputPath = `${APP_NAME}-session-${inputBasename}.html`;
|
||||
}
|
||||
|
||||
writeFileSync(outputPath, html, "utf8");
|
||||
return outputPath;
|
||||
}
|
||||
1731
packages/coding-agent/src/core/export-html/template.html
Normal file
1731
packages/coding-agent/src/core/export-html/template.html
Normal file
File diff suppressed because it is too large
Load diff
1213
packages/coding-agent/src/core/export-html/vendor/highlight.min.js
vendored
Normal file
1213
packages/coding-agent/src/core/export-html/vendor/highlight.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
6
packages/coding-agent/src/core/export-html/vendor/marked.min.js
vendored
Normal file
6
packages/coding-agent/src/core/export-html/vendor/marked.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -17,7 +17,7 @@ import { CONFIG_DIR_NAME, getAgentDir, getModelsPath, VERSION } from "./config.j
|
|||
import type { AgentSession } from "./core/agent-session.js";
|
||||
|
||||
import type { LoadedCustomTool } from "./core/custom-tools/index.js";
|
||||
import { exportFromFile } from "./core/export-html.js";
|
||||
import { exportFromFile } from "./core/export-html/index.js";
|
||||
import type { HookUIContext } from "./core/index.js";
|
||||
import type { ModelRegistry } from "./core/model-registry.js";
|
||||
import { resolveModelScope, type ScopedModel } from "./core/model-resolver.js";
|
||||
|
|
|
|||
|
|
@ -652,6 +652,91 @@ export function stopThemeWatcher(): void {
|
|||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HTML Export Helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Convert a 256-color index to hex string.
|
||||
* Indices 0-15: basic colors (approximate)
|
||||
* Indices 16-231: 6x6x6 color cube
|
||||
* Indices 232-255: grayscale ramp
|
||||
*/
|
||||
function ansi256ToHex(index: number): string {
|
||||
// Basic colors (0-15) - approximate common terminal values
|
||||
const basicColors = [
|
||||
"#000000",
|
||||
"#800000",
|
||||
"#008000",
|
||||
"#808000",
|
||||
"#000080",
|
||||
"#800080",
|
||||
"#008080",
|
||||
"#c0c0c0",
|
||||
"#808080",
|
||||
"#ff0000",
|
||||
"#00ff00",
|
||||
"#ffff00",
|
||||
"#0000ff",
|
||||
"#ff00ff",
|
||||
"#00ffff",
|
||||
"#ffffff",
|
||||
];
|
||||
if (index < 16) {
|
||||
return basicColors[index];
|
||||
}
|
||||
|
||||
// Color cube (16-231): 6x6x6 = 216 colors
|
||||
if (index < 232) {
|
||||
const cubeIndex = index - 16;
|
||||
const r = Math.floor(cubeIndex / 36);
|
||||
const g = Math.floor((cubeIndex % 36) / 6);
|
||||
const b = cubeIndex % 6;
|
||||
const toHex = (n: number) => (n === 0 ? 0 : 55 + n * 40).toString(16).padStart(2, "0");
|
||||
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
||||
}
|
||||
|
||||
// Grayscale (232-255): 24 shades
|
||||
const gray = 8 + (index - 232) * 10;
|
||||
const grayHex = gray.toString(16).padStart(2, "0");
|
||||
return `#${grayHex}${grayHex}${grayHex}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get resolved theme colors as CSS-compatible hex strings.
|
||||
* Used by HTML export to generate CSS custom properties.
|
||||
*/
|
||||
export function getResolvedThemeColors(themeName?: string): Record<string, string> {
|
||||
const name = themeName ?? getDefaultTheme();
|
||||
const isLight = name === "light";
|
||||
const themeJson = loadThemeJson(name);
|
||||
const resolved = resolveThemeColors(themeJson.colors, themeJson.vars);
|
||||
|
||||
// Default text color for empty values (terminal uses default fg color)
|
||||
const defaultText = isLight ? "#000000" : "#e5e5e7";
|
||||
|
||||
const cssColors: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(resolved)) {
|
||||
if (typeof value === "number") {
|
||||
cssColors[key] = ansi256ToHex(value);
|
||||
} else if (value === "") {
|
||||
// Empty means default terminal color - use sensible fallback for HTML
|
||||
cssColors[key] = defaultText;
|
||||
} else {
|
||||
cssColors[key] = value;
|
||||
}
|
||||
}
|
||||
return cssColors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a theme is a "light" theme (for CSS that needs light/dark variants).
|
||||
*/
|
||||
export function isLightTheme(themeName?: string): boolean {
|
||||
// Currently just check the name - could be extended to analyze colors
|
||||
return themeName === "light";
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TUI Helpers
|
||||
// ============================================================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue