{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/export-html/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAC;AAUrE,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEvD;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,oFAAoF;IACpF,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;IAChE,sFAAsF;IACtF,YAAY,CACV,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,KAAK,CAAC;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC,EACF,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,OAAO,GACf,MAAM,GAAG,SAAS,CAAC;CACvB;AAQD,MAAM,WAAW,aAAa;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8CAA8C;IAC9C,YAAY,CAAC,EAAE,gBAAgB,CAAC;CACjC;AAwND;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,cAAc,EAClB,KAAK,CAAC,EAAE,UAAU,EAClB,OAAO,CAAC,EAAE,aAAa,GAAG,MAAM,GAC/B,OAAO,CAAC,MAAM,CAAC,CA+CjB;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAClC,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,aAAa,GAAG,MAAM,GAC/B,OAAO,CAAC,MAAM,CAAC,CA4BjB","sourcesContent":["import type { AgentState } from \"@harivansh-afk/clanker-agent-core\";\nimport { existsSync, readFileSync, writeFileSync } from \"fs\";\nimport { basename, join } from \"path\";\nimport { APP_NAME, getExportTemplateDir } from \"../../config.js\";\nimport {\n getResolvedThemeColors,\n getThemeExportColors,\n} from \"../../modes/interactive/theme/theme.js\";\nimport type { ToolInfo } from \"../extensions/types.js\";\nimport type { SessionEntry } from \"../session-manager.js\";\nimport { SessionManager } from \"../session-manager.js\";\n\n/**\n * Interface for rendering custom tools to HTML.\n * Used by agent-session to pre-render extension tool output.\n */\nexport interface ToolHtmlRenderer {\n /** Render a tool call to HTML. Returns undefined if tool has no custom renderer. */\n renderCall(toolName: string, args: unknown): string | undefined;\n /** Render a tool result to HTML. Returns undefined if tool has no custom renderer. */\n renderResult(\n toolName: string,\n result: Array<{\n type: string;\n text?: string;\n data?: string;\n mimeType?: string;\n }>,\n details: unknown,\n isError: boolean,\n ): string | undefined;\n}\n\n/** Pre-rendered HTML for a custom tool call and result */\ninterface RenderedToolHtml {\n callHtml?: string;\n resultHtml?: string;\n}\n\nexport interface ExportOptions {\n outputPath?: string;\n themeName?: string;\n /** Optional tool renderer for custom tools */\n toolRenderer?: ToolHtmlRenderer;\n}\n\n/** Parse a color string to RGB values. Supports hex (#RRGGBB) and rgb(r,g,b) formats. */\nfunction parseColor(\n color: string,\n): { r: number; g: number; b: number } | undefined {\n const hexMatch = color.match(\n /^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,\n );\n if (hexMatch) {\n return {\n r: Number.parseInt(hexMatch[1], 16),\n g: Number.parseInt(hexMatch[2], 16),\n b: Number.parseInt(hexMatch[3], 16),\n };\n }\n const rgbMatch = color.match(\n /^rgb\\s*\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*\\)$/,\n );\n if (rgbMatch) {\n return {\n r: Number.parseInt(rgbMatch[1], 10),\n g: Number.parseInt(rgbMatch[2], 10),\n b: Number.parseInt(rgbMatch[3], 10),\n };\n }\n return undefined;\n}\n\n/** Calculate relative luminance of a color (0-1, higher = lighter). */\nfunction getLuminance(r: number, g: number, b: number): number {\n const toLinear = (c: number) => {\n const s = c / 255;\n return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;\n };\n return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);\n}\n\n/** Adjust color brightness. Factor > 1 lightens, < 1 darkens. */\nfunction adjustBrightness(color: string, factor: number): string {\n const parsed = parseColor(color);\n if (!parsed) return color;\n const adjust = (c: number) =>\n Math.min(255, Math.max(0, Math.round(c * factor)));\n return `rgb(${adjust(parsed.r)}, ${adjust(parsed.g)}, ${adjust(parsed.b)})`;\n}\n\n/** Derive export background colors from a base color (e.g., userMessageBg). */\nfunction deriveExportColors(baseColor: string): {\n pageBg: string;\n cardBg: string;\n infoBg: string;\n} {\n const parsed = parseColor(baseColor);\n if (!parsed) {\n return {\n pageBg: \"rgb(24, 24, 30)\",\n cardBg: \"rgb(30, 30, 36)\",\n infoBg: \"rgb(60, 55, 40)\",\n };\n }\n\n const luminance = getLuminance(parsed.r, parsed.g, parsed.b);\n const isLight = luminance > 0.5;\n\n if (isLight) {\n return {\n pageBg: adjustBrightness(baseColor, 0.96),\n cardBg: baseColor,\n infoBg: `rgb(${Math.min(255, parsed.r + 10)}, ${Math.min(255, parsed.g + 5)}, ${Math.max(0, parsed.b - 20)})`,\n };\n }\n return {\n pageBg: adjustBrightness(baseColor, 0.7),\n cardBg: adjustBrightness(baseColor, 0.85),\n infoBg: `rgb(${Math.min(255, parsed.r + 20)}, ${Math.min(255, parsed.g + 15)}, ${parsed.b})`,\n };\n}\n\n/**\n * Generate CSS custom property declarations from theme colors.\n */\nfunction generateThemeVars(themeName?: string): string {\n const colors = getResolvedThemeColors(themeName);\n const lines: string[] = [];\n for (const [key, value] of Object.entries(colors)) {\n lines.push(`--${key}: ${value};`);\n }\n\n // Use explicit theme export colors if available, otherwise derive from userMessageBg\n const themeExport = getThemeExportColors(themeName);\n const userMessageBg = colors.userMessageBg || \"#343541\";\n const derivedColors = deriveExportColors(userMessageBg);\n\n lines.push(`--exportPageBg: ${themeExport.pageBg ?? derivedColors.pageBg};`);\n lines.push(`--exportCardBg: ${themeExport.cardBg ?? derivedColors.cardBg};`);\n lines.push(`--exportInfoBg: ${themeExport.infoBg ?? derivedColors.infoBg};`);\n\n return lines.join(\"\\n \");\n}\n\ninterface SessionData {\n header: ReturnType;\n entries: ReturnType;\n leafId: string | null;\n systemPrompt?: string;\n tools?: ToolInfo[];\n /** Pre-rendered HTML for custom tool calls/results, keyed by tool call ID */\n renderedTools?: Record;\n}\n\n/**\n * Core HTML generation logic shared by both export functions.\n */\nfunction generateHtml(sessionData: SessionData, themeName?: string): string {\n const templateDir = getExportTemplateDir();\n const template = readFileSync(join(templateDir, \"template.html\"), \"utf-8\");\n const templateCss = readFileSync(join(templateDir, \"template.css\"), \"utf-8\");\n const templateJs = readFileSync(join(templateDir, \"template.js\"), \"utf-8\");\n const markedJs = readFileSync(\n join(templateDir, \"vendor\", \"marked.min.js\"),\n \"utf-8\",\n );\n const hljsJs = readFileSync(\n join(templateDir, \"vendor\", \"highlight.min.js\"),\n \"utf-8\",\n );\n\n const themeVars = generateThemeVars(themeName);\n const colors = getResolvedThemeColors(themeName);\n const exportColors = deriveExportColors(colors.userMessageBg || \"#343541\");\n const bodyBg = exportColors.pageBg;\n const containerBg = exportColors.cardBg;\n const infoBg = exportColors.infoBg;\n\n // Base64 encode session data to avoid escaping issues\n const sessionDataBase64 = Buffer.from(JSON.stringify(sessionData)).toString(\n \"base64\",\n );\n\n // Build the CSS with theme variables injected\n const css = templateCss\n .replace(\"{{THEME_VARS}}\", themeVars)\n .replace(\"{{BODY_BG}}\", bodyBg)\n .replace(\"{{CONTAINER_BG}}\", containerBg)\n .replace(\"{{INFO_BG}}\", infoBg);\n\n return template\n .replace(\"{{CSS}}\", css)\n .replace(\"{{JS}}\", templateJs)\n .replace(\"{{SESSION_DATA}}\", sessionDataBase64)\n .replace(\"{{MARKED_JS}}\", markedJs)\n .replace(\"{{HIGHLIGHT_JS}}\", hljsJs);\n}\n\n/** Built-in tool names that have custom rendering in template.js */\nconst BUILTIN_TOOLS = new Set([\n \"bash\",\n \"read\",\n \"write\",\n \"edit\",\n \"ls\",\n \"find\",\n \"grep\",\n]);\n\n/**\n * Pre-render custom tools to HTML using their TUI renderers.\n */\nfunction preRenderCustomTools(\n entries: SessionEntry[],\n toolRenderer: ToolHtmlRenderer,\n): Record {\n const renderedTools: Record = {};\n\n for (const entry of entries) {\n if (entry.type !== \"message\") continue;\n const msg = entry.message;\n\n // Find tool calls in assistant messages\n if (msg.role === \"assistant\" && Array.isArray(msg.content)) {\n for (const block of msg.content) {\n if (block.type === \"toolCall\" && !BUILTIN_TOOLS.has(block.name)) {\n const callHtml = toolRenderer.renderCall(block.name, block.arguments);\n if (callHtml) {\n renderedTools[block.id] = { callHtml };\n }\n }\n }\n }\n\n // Find tool results\n if (msg.role === \"toolResult\" && msg.toolCallId) {\n const toolName = msg.toolName || \"\";\n // Only render if we have a pre-rendered call OR it's not a built-in tool\n const existing = renderedTools[msg.toolCallId];\n if (existing || !BUILTIN_TOOLS.has(toolName)) {\n const resultHtml = toolRenderer.renderResult(\n toolName,\n msg.content,\n msg.details,\n msg.isError || false,\n );\n if (resultHtml) {\n renderedTools[msg.toolCallId] = {\n ...existing,\n resultHtml,\n };\n }\n }\n }\n }\n\n return renderedTools;\n}\n\n/**\n * Export session to HTML using SessionManager and AgentState.\n * Used by TUI's /export command.\n */\nexport async function exportSessionToHtml(\n sm: SessionManager,\n state?: AgentState,\n options?: ExportOptions | string,\n): Promise {\n const opts: ExportOptions =\n typeof options === \"string\" ? { outputPath: options } : options || {};\n\n const sessionFile = sm.getSessionFile();\n if (!sessionFile) {\n throw new Error(\"Cannot export in-memory session to HTML\");\n }\n if (!existsSync(sessionFile)) {\n throw new Error(\"Nothing to export yet - start a conversation first\");\n }\n\n const entries = sm.getEntries();\n\n // Pre-render custom tools if a tool renderer is provided\n let renderedTools: Record | undefined;\n if (opts.toolRenderer) {\n renderedTools = preRenderCustomTools(entries, opts.toolRenderer);\n // Only include if we actually rendered something\n if (Object.keys(renderedTools).length === 0) {\n renderedTools = undefined;\n }\n }\n\n const sessionData: SessionData = {\n header: sm.getHeader(),\n entries,\n leafId: sm.getLeafId(),\n systemPrompt: state?.systemPrompt,\n tools: state?.tools?.map((t) => ({\n name: t.name,\n description: t.description,\n parameters: t.parameters,\n })),\n renderedTools,\n };\n\n const html = generateHtml(sessionData, opts.themeName);\n\n let outputPath = opts.outputPath;\n if (!outputPath) {\n const sessionBasename = basename(sessionFile, \".jsonl\");\n outputPath = `${APP_NAME}-session-${sessionBasename}.html`;\n }\n\n writeFileSync(outputPath, html, \"utf8\");\n return outputPath;\n}\n\n/**\n * Export session file to HTML (standalone, without AgentState).\n * Used by CLI for exporting arbitrary session files.\n */\nexport async function exportFromFile(\n inputPath: string,\n options?: ExportOptions | string,\n): Promise {\n const opts: ExportOptions =\n typeof options === \"string\" ? { outputPath: options } : options || {};\n\n if (!existsSync(inputPath)) {\n throw new Error(`File not found: ${inputPath}`);\n }\n\n const sm = SessionManager.open(inputPath);\n\n const sessionData: SessionData = {\n header: sm.getHeader(),\n entries: sm.getEntries(),\n leafId: sm.getLeafId(),\n systemPrompt: undefined,\n tools: undefined,\n };\n\n const html = generateHtml(sessionData, opts.themeName);\n\n let outputPath = opts.outputPath;\n if (!outputPath) {\n const inputBasename = basename(inputPath, \".jsonl\");\n outputPath = `${APP_NAME}-session-${inputBasename}.html`;\n }\n\n writeFileSync(outputPath, html, \"utf8\");\n return outputPath;\n}\n"]}