mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-21 13:00:33 +00:00
Add syntax highlighting to HTML export using highlight.js
This commit is contained in:
parent
914898c58f
commit
9851ee3bdb
3 changed files with 954 additions and 61 deletions
743
out.html
Normal file
743
out.html
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -6359,23 +6359,6 @@ export const MODELS = {
|
||||||
contextWindow: 128000,
|
contextWindow: 128000,
|
||||||
maxTokens: 16384,
|
maxTokens: 16384,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"meta-llama/llama-3.1-70b-instruct": {
|
|
||||||
id: "meta-llama/llama-3.1-70b-instruct",
|
|
||||||
name: "Meta: Llama 3.1 70B Instruct",
|
|
||||||
api: "openai-completions",
|
|
||||||
provider: "openrouter",
|
|
||||||
baseUrl: "https://openrouter.ai/api/v1",
|
|
||||||
reasoning: false,
|
|
||||||
input: ["text"],
|
|
||||||
cost: {
|
|
||||||
input: 0.39999999999999997,
|
|
||||||
output: 0.39999999999999997,
|
|
||||||
cacheRead: 0,
|
|
||||||
cacheWrite: 0,
|
|
||||||
},
|
|
||||||
contextWindow: 131072,
|
|
||||||
maxTokens: 4096,
|
|
||||||
} satisfies Model<"openai-completions">,
|
|
||||||
"meta-llama/llama-3.1-8b-instruct": {
|
"meta-llama/llama-3.1-8b-instruct": {
|
||||||
id: "meta-llama/llama-3.1-8b-instruct",
|
id: "meta-llama/llama-3.1-8b-instruct",
|
||||||
name: "Meta: Llama 3.1 8B Instruct",
|
name: "Meta: Llama 3.1 8B Instruct",
|
||||||
|
|
@ -6410,6 +6393,23 @@ export const MODELS = {
|
||||||
contextWindow: 10000,
|
contextWindow: 10000,
|
||||||
maxTokens: 4096,
|
maxTokens: 4096,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
|
"meta-llama/llama-3.1-70b-instruct": {
|
||||||
|
id: "meta-llama/llama-3.1-70b-instruct",
|
||||||
|
name: "Meta: Llama 3.1 70B Instruct",
|
||||||
|
api: "openai-completions",
|
||||||
|
provider: "openrouter",
|
||||||
|
baseUrl: "https://openrouter.ai/api/v1",
|
||||||
|
reasoning: false,
|
||||||
|
input: ["text"],
|
||||||
|
cost: {
|
||||||
|
input: 0.39999999999999997,
|
||||||
|
output: 0.39999999999999997,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
},
|
||||||
|
contextWindow: 131072,
|
||||||
|
maxTokens: 4096,
|
||||||
|
} satisfies Model<"openai-completions">,
|
||||||
"mistralai/mistral-nemo": {
|
"mistralai/mistral-nemo": {
|
||||||
id: "mistralai/mistral-nemo",
|
id: "mistralai/mistral-nemo",
|
||||||
name: "Mistral: Mistral Nemo",
|
name: "Mistral: Mistral Nemo",
|
||||||
|
|
@ -6546,6 +6546,23 @@ export const MODELS = {
|
||||||
contextWindow: 128000,
|
contextWindow: 128000,
|
||||||
maxTokens: 4096,
|
maxTokens: 4096,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
|
"openai/gpt-4o-2024-05-13": {
|
||||||
|
id: "openai/gpt-4o-2024-05-13",
|
||||||
|
name: "OpenAI: GPT-4o (2024-05-13)",
|
||||||
|
api: "openai-completions",
|
||||||
|
provider: "openrouter",
|
||||||
|
baseUrl: "https://openrouter.ai/api/v1",
|
||||||
|
reasoning: false,
|
||||||
|
input: ["text", "image"],
|
||||||
|
cost: {
|
||||||
|
input: 5,
|
||||||
|
output: 15,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
},
|
||||||
|
contextWindow: 128000,
|
||||||
|
maxTokens: 4096,
|
||||||
|
} satisfies Model<"openai-completions">,
|
||||||
"openai/gpt-4o": {
|
"openai/gpt-4o": {
|
||||||
id: "openai/gpt-4o",
|
id: "openai/gpt-4o",
|
||||||
name: "OpenAI: GPT-4o",
|
name: "OpenAI: GPT-4o",
|
||||||
|
|
@ -6580,23 +6597,6 @@ export const MODELS = {
|
||||||
contextWindow: 128000,
|
contextWindow: 128000,
|
||||||
maxTokens: 64000,
|
maxTokens: 64000,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"openai/gpt-4o-2024-05-13": {
|
|
||||||
id: "openai/gpt-4o-2024-05-13",
|
|
||||||
name: "OpenAI: GPT-4o (2024-05-13)",
|
|
||||||
api: "openai-completions",
|
|
||||||
provider: "openrouter",
|
|
||||||
baseUrl: "https://openrouter.ai/api/v1",
|
|
||||||
reasoning: false,
|
|
||||||
input: ["text", "image"],
|
|
||||||
cost: {
|
|
||||||
input: 5,
|
|
||||||
output: 15,
|
|
||||||
cacheRead: 0,
|
|
||||||
cacheWrite: 0,
|
|
||||||
},
|
|
||||||
contextWindow: 128000,
|
|
||||||
maxTokens: 4096,
|
|
||||||
} satisfies Model<"openai-completions">,
|
|
||||||
"meta-llama/llama-3-70b-instruct": {
|
"meta-llama/llama-3-70b-instruct": {
|
||||||
id: "meta-llama/llama-3-70b-instruct",
|
id: "meta-llama/llama-3-70b-instruct",
|
||||||
name: "Meta: Llama 3 70B Instruct",
|
name: "Meta: Llama 3 70B Instruct",
|
||||||
|
|
@ -6716,23 +6716,6 @@ export const MODELS = {
|
||||||
contextWindow: 128000,
|
contextWindow: 128000,
|
||||||
maxTokens: 4096,
|
maxTokens: 4096,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"openai/gpt-4-turbo-preview": {
|
|
||||||
id: "openai/gpt-4-turbo-preview",
|
|
||||||
name: "OpenAI: GPT-4 Turbo Preview",
|
|
||||||
api: "openai-completions",
|
|
||||||
provider: "openrouter",
|
|
||||||
baseUrl: "https://openrouter.ai/api/v1",
|
|
||||||
reasoning: false,
|
|
||||||
input: ["text"],
|
|
||||||
cost: {
|
|
||||||
input: 10,
|
|
||||||
output: 30,
|
|
||||||
cacheRead: 0,
|
|
||||||
cacheWrite: 0,
|
|
||||||
},
|
|
||||||
contextWindow: 128000,
|
|
||||||
maxTokens: 4096,
|
|
||||||
} satisfies Model<"openai-completions">,
|
|
||||||
"openai/gpt-3.5-turbo-0613": {
|
"openai/gpt-3.5-turbo-0613": {
|
||||||
id: "openai/gpt-3.5-turbo-0613",
|
id: "openai/gpt-3.5-turbo-0613",
|
||||||
name: "OpenAI: GPT-3.5 Turbo (older v0613)",
|
name: "OpenAI: GPT-3.5 Turbo (older v0613)",
|
||||||
|
|
@ -6750,6 +6733,23 @@ export const MODELS = {
|
||||||
contextWindow: 4095,
|
contextWindow: 4095,
|
||||||
maxTokens: 4096,
|
maxTokens: 4096,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
|
"openai/gpt-4-turbo-preview": {
|
||||||
|
id: "openai/gpt-4-turbo-preview",
|
||||||
|
name: "OpenAI: GPT-4 Turbo Preview",
|
||||||
|
api: "openai-completions",
|
||||||
|
provider: "openrouter",
|
||||||
|
baseUrl: "https://openrouter.ai/api/v1",
|
||||||
|
reasoning: false,
|
||||||
|
input: ["text"],
|
||||||
|
cost: {
|
||||||
|
input: 10,
|
||||||
|
output: 30,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
},
|
||||||
|
contextWindow: 128000,
|
||||||
|
maxTokens: 4096,
|
||||||
|
} satisfies Model<"openai-completions">,
|
||||||
"mistralai/mistral-tiny": {
|
"mistralai/mistral-tiny": {
|
||||||
id: "mistralai/mistral-tiny",
|
id: "mistralai/mistral-tiny",
|
||||||
name: "Mistral Tiny",
|
name: "Mistral Tiny",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import type { AgentState } from "@mariozechner/pi-agent-core";
|
import type { AgentState } from "@mariozechner/pi-agent-core";
|
||||||
import type { AssistantMessage, ImageContent, Message, ToolResultMessage, UserMessage } from "@mariozechner/pi-ai";
|
import type { AssistantMessage, ImageContent, Message, ToolResultMessage, UserMessage } from "@mariozechner/pi-ai";
|
||||||
import { existsSync, readFileSync, writeFileSync } from "fs";
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
||||||
|
import hljs from "highlight.js";
|
||||||
import { marked } from "marked";
|
import { marked } from "marked";
|
||||||
import { homedir } from "os";
|
import { homedir } from "os";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
|
|
@ -322,17 +323,123 @@ function formatTimestamp(timestamp: number | string | undefined): string {
|
||||||
return date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
return date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Render markdown to HTML server-side with TUI-style code block formatting. */
|
/** Highlight code using highlight.js. Returns HTML with syntax highlighting spans. */
|
||||||
|
function highlightCode(code: string, lang?: string): string {
|
||||||
|
if (!lang) {
|
||||||
|
return escapeHtml(code);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// Check if language is supported
|
||||||
|
if (hljs.getLanguage(lang)) {
|
||||||
|
return hljs.highlight(code, { language: lang, ignoreIllegals: true }).value;
|
||||||
|
}
|
||||||
|
// Try common aliases
|
||||||
|
const aliases: Record<string, string> = {
|
||||||
|
ts: "typescript",
|
||||||
|
js: "javascript",
|
||||||
|
py: "python",
|
||||||
|
rb: "ruby",
|
||||||
|
sh: "bash",
|
||||||
|
yml: "yaml",
|
||||||
|
md: "markdown",
|
||||||
|
};
|
||||||
|
const aliasedLang = aliases[lang];
|
||||||
|
if (aliasedLang && hljs.getLanguage(aliasedLang)) {
|
||||||
|
return hljs.highlight(code, { language: aliasedLang, ignoreIllegals: true }).value;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fall through to escaped output
|
||||||
|
}
|
||||||
|
return escapeHtml(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get language from file path extension. */
|
||||||
|
function getLanguageFromPath(filePath: string): string | undefined {
|
||||||
|
const ext = filePath.split(".").pop()?.toLowerCase();
|
||||||
|
if (!ext) return undefined;
|
||||||
|
|
||||||
|
const extToLang: Record<string, string> = {
|
||||||
|
ts: "typescript",
|
||||||
|
tsx: "typescript",
|
||||||
|
js: "javascript",
|
||||||
|
jsx: "javascript",
|
||||||
|
mjs: "javascript",
|
||||||
|
cjs: "javascript",
|
||||||
|
py: "python",
|
||||||
|
rb: "ruby",
|
||||||
|
rs: "rust",
|
||||||
|
go: "go",
|
||||||
|
java: "java",
|
||||||
|
kt: "kotlin",
|
||||||
|
swift: "swift",
|
||||||
|
c: "c",
|
||||||
|
h: "c",
|
||||||
|
cpp: "cpp",
|
||||||
|
cc: "cpp",
|
||||||
|
cxx: "cpp",
|
||||||
|
hpp: "cpp",
|
||||||
|
cs: "csharp",
|
||||||
|
php: "php",
|
||||||
|
sh: "bash",
|
||||||
|
bash: "bash",
|
||||||
|
zsh: "bash",
|
||||||
|
fish: "bash",
|
||||||
|
ps1: "powershell",
|
||||||
|
sql: "sql",
|
||||||
|
html: "html",
|
||||||
|
htm: "html",
|
||||||
|
xml: "xml",
|
||||||
|
css: "css",
|
||||||
|
scss: "scss",
|
||||||
|
sass: "scss",
|
||||||
|
less: "less",
|
||||||
|
json: "json",
|
||||||
|
yaml: "yaml",
|
||||||
|
yml: "yaml",
|
||||||
|
toml: "toml",
|
||||||
|
ini: "ini",
|
||||||
|
md: "markdown",
|
||||||
|
markdown: "markdown",
|
||||||
|
dockerfile: "dockerfile",
|
||||||
|
makefile: "makefile",
|
||||||
|
cmake: "cmake",
|
||||||
|
lua: "lua",
|
||||||
|
r: "r",
|
||||||
|
scala: "scala",
|
||||||
|
clj: "clojure",
|
||||||
|
cljs: "clojure",
|
||||||
|
ex: "elixir",
|
||||||
|
exs: "elixir",
|
||||||
|
erl: "erlang",
|
||||||
|
hrl: "erlang",
|
||||||
|
hs: "haskell",
|
||||||
|
ml: "ocaml",
|
||||||
|
mli: "ocaml",
|
||||||
|
fs: "fsharp",
|
||||||
|
fsx: "fsharp",
|
||||||
|
vue: "vue",
|
||||||
|
svelte: "xml",
|
||||||
|
tf: "hcl",
|
||||||
|
hcl: "hcl",
|
||||||
|
proto: "protobuf",
|
||||||
|
graphql: "graphql",
|
||||||
|
gql: "graphql",
|
||||||
|
};
|
||||||
|
|
||||||
|
return extToLang[ext];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Render markdown to HTML server-side with TUI-style code block formatting and syntax highlighting. */
|
||||||
function renderMarkdown(text: string): string {
|
function renderMarkdown(text: string): string {
|
||||||
// Custom renderer for code blocks to match TUI style
|
// Custom renderer for code blocks to match TUI style
|
||||||
const renderer = new marked.Renderer();
|
const renderer = new marked.Renderer();
|
||||||
renderer.code = ({ text: code, lang }: { text: string; lang?: string }) => {
|
renderer.code = ({ text: code, lang }: { text: string; lang?: string }) => {
|
||||||
const language = lang || "";
|
const language = lang || "";
|
||||||
const escaped = escapeHtml(code);
|
const highlighted = highlightCode(code, lang);
|
||||||
return (
|
return (
|
||||||
'<div class="code-block-wrapper">' +
|
'<div class="code-block-wrapper">' +
|
||||||
`<div class="code-block-header">\`\`\`${language}</div>` +
|
`<div class="code-block-header">\`\`\`${language}</div>` +
|
||||||
`<pre><code>${escaped}</code></pre>` +
|
`<pre><code class="hljs">${highlighted}</code></pre>` +
|
||||||
'<div class="code-block-footer">```</div>' +
|
'<div class="code-block-footer">```</div>' +
|
||||||
"</div>"
|
"</div>"
|
||||||
);
|
);
|
||||||
|
|
@ -348,10 +455,32 @@ function renderMarkdown(text: string): string {
|
||||||
return marked.parse(text, { renderer }) as string;
|
return marked.parse(text, { renderer }) as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatExpandableOutput(lines: string[], maxLines: number): string {
|
function formatExpandableOutput(lines: string[], maxLines: number, lang?: string): string {
|
||||||
const displayLines = lines.slice(0, maxLines);
|
const displayLines = lines.slice(0, maxLines);
|
||||||
const remaining = lines.length - maxLines;
|
const remaining = lines.length - maxLines;
|
||||||
|
|
||||||
|
// If language is provided, highlight the entire code block
|
||||||
|
if (lang) {
|
||||||
|
const code = lines.join("\n");
|
||||||
|
const highlighted = highlightCode(code, lang);
|
||||||
|
|
||||||
|
if (remaining > 0) {
|
||||||
|
// For expandable, we need preview and full versions
|
||||||
|
const previewCode = displayLines.join("\n");
|
||||||
|
const previewHighlighted = highlightCode(previewCode, lang);
|
||||||
|
|
||||||
|
let out = '<div class="tool-output expandable" onclick="this.classList.toggle(\'expanded\')">';
|
||||||
|
out += `<div class="output-preview"><pre><code class="hljs">${previewHighlighted}</code></pre>`;
|
||||||
|
out += `<div class="expand-hint">... (${remaining} more lines) - click to expand</div>`;
|
||||||
|
out += "</div>";
|
||||||
|
out += `<div class="output-full"><pre><code class="hljs">${highlighted}</code></pre></div></div>`;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<div class="tool-output"><pre><code class="hljs">${highlighted}</code></pre></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No language - plain text output
|
||||||
if (remaining > 0) {
|
if (remaining > 0) {
|
||||||
let out = '<div class="tool-output expandable" onclick="this.classList.toggle(\'expanded\')">';
|
let out = '<div class="tool-output expandable" onclick="this.classList.toggle(\'expanded\')">';
|
||||||
out += '<div class="output-preview">';
|
out += '<div class="output-preview">';
|
||||||
|
|
@ -601,12 +730,14 @@ function formatToolExecution(
|
||||||
}
|
}
|
||||||
|
|
||||||
case "read": {
|
case "read": {
|
||||||
const filePath = shortenPath((args?.file_path as string) || (args?.path as string) || "");
|
const filePath = (args?.file_path as string) || (args?.path as string) || "";
|
||||||
|
const shortenedPath = shortenPath(filePath);
|
||||||
const offset = args?.offset as number | undefined;
|
const offset = args?.offset as number | undefined;
|
||||||
const limit = args?.limit as number | undefined;
|
const limit = args?.limit as number | undefined;
|
||||||
|
const lang = getLanguageFromPath(filePath);
|
||||||
|
|
||||||
// Build path display with offset/limit suffix
|
// Build path display with offset/limit suffix
|
||||||
let pathHtml = escapeHtml(filePath || "...");
|
let pathHtml = escapeHtml(shortenedPath || "...");
|
||||||
if (offset !== undefined || limit !== undefined) {
|
if (offset !== undefined || limit !== undefined) {
|
||||||
const startLine = offset ?? 1;
|
const startLine = offset ?? 1;
|
||||||
const endLine = limit !== undefined ? startLine + limit - 1 : "";
|
const endLine = limit !== undefined ? startLine + limit - 1 : "";
|
||||||
|
|
@ -617,25 +748,27 @@ function formatToolExecution(
|
||||||
if (result) {
|
if (result) {
|
||||||
const output = getTextOutput();
|
const output = getTextOutput();
|
||||||
if (output) {
|
if (output) {
|
||||||
html += formatExpandableOutput(output.split("\n"), 10);
|
html += formatExpandableOutput(output.split("\n"), 10, lang);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "write": {
|
case "write": {
|
||||||
const path = shortenPath((args?.file_path as string) || (args?.path as string) || "");
|
const filePath = (args?.file_path as string) || (args?.path as string) || "";
|
||||||
|
const shortenedPath = shortenPath(filePath);
|
||||||
const fileContent = (args?.content as string) || "";
|
const fileContent = (args?.content as string) || "";
|
||||||
const lines = fileContent ? fileContent.split("\n") : [];
|
const lines = fileContent ? fileContent.split("\n") : [];
|
||||||
|
const lang = getLanguageFromPath(filePath);
|
||||||
|
|
||||||
html = `<div class="tool-header"><span class="tool-name">write</span> <span class="tool-path">${escapeHtml(path || "...")}</span>`;
|
html = `<div class="tool-header"><span class="tool-name">write</span> <span class="tool-path">${escapeHtml(shortenedPath || "...")}</span>`;
|
||||||
if (lines.length > 10) {
|
if (lines.length > 10) {
|
||||||
html += ` <span class="line-count">(${lines.length} lines)</span>`;
|
html += ` <span class="line-count">(${lines.length} lines)</span>`;
|
||||||
}
|
}
|
||||||
html += "</div>";
|
html += "</div>";
|
||||||
|
|
||||||
if (fileContent) {
|
if (fileContent) {
|
||||||
html += formatExpandableOutput(lines, 10);
|
html += formatExpandableOutput(lines, 10, lang);
|
||||||
}
|
}
|
||||||
if (result) {
|
if (result) {
|
||||||
const output = getTextOutput().trim();
|
const output = getTextOutput().trim();
|
||||||
|
|
@ -1108,6 +1241,23 @@ function generateHtml(data: ParsedSessionData, filename: string, colors: ThemeCo
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Syntax highlighting (highlight.js) */
|
||||||
|
.hljs { background: transparent; }
|
||||||
|
.hljs-comment, .hljs-quote { color: ${colors.syntaxComment}; }
|
||||||
|
.hljs-keyword, .hljs-selector-tag, .hljs-addition { color: ${colors.syntaxKeyword}; }
|
||||||
|
.hljs-number, .hljs-literal, .hljs-symbol, .hljs-bullet { color: ${colors.syntaxNumber}; }
|
||||||
|
.hljs-string, .hljs-doctag, .hljs-regexp { color: ${colors.syntaxString}; }
|
||||||
|
.hljs-title, .hljs-section, .hljs-name, .hljs-selector-id, .hljs-selector-class { color: ${colors.syntaxFunction}; }
|
||||||
|
.hljs-type, .hljs-class, .hljs-built_in { color: ${colors.syntaxType}; }
|
||||||
|
.hljs-attr, .hljs-variable, .hljs-template-variable, .hljs-params { color: ${colors.syntaxVariable}; }
|
||||||
|
.hljs-attribute { color: ${colors.syntaxVariable}; }
|
||||||
|
.hljs-meta { color: ${colors.syntaxKeyword}; }
|
||||||
|
.hljs-formula { background: rgba(${isLight ? "0, 0, 0" : "255, 255, 255"}, 0.05); }
|
||||||
|
.hljs-deletion { color: ${colors.toolDiffRemoved}; }
|
||||||
|
.hljs-emphasis { font-style: italic; }
|
||||||
|
.hljs-strong { font-weight: bold; }
|
||||||
|
.hljs-link { color: ${colors.mdLink}; text-decoration: underline; }
|
||||||
|
|
||||||
@media print { body { background: white; color: black; } .tool-execution { border: 1px solid #ddd; } }
|
@media print { body { background: white; color: black; } .tool-execution { border: 1px solid #ddd; } }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue