diff --git a/.gitignore b/.gitignore index d320e0b1d..c0a2bf893 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ compaction-results/ syntax.jsonl out.jsonl pi-*.html +out.html diff --git a/.pi/commands/review.md b/.pi/commands/review.md deleted file mode 100644 index 3e1db7791..000000000 --- a/.pi/commands/review.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -description: Review a file for issues ---- -Please review the following file for potential issues, bugs, or improvements: - -$1 - -Focus on: -- Logic errors -- Edge cases -- Code style -- Performance concerns diff --git a/.pi/hooks/test-command.ts b/.pi/hooks/test-command.ts deleted file mode 100644 index 7f7374989..000000000 --- a/.pi/hooks/test-command.ts +++ /dev/null @@ -1,86 +0,0 @@ -/** - * Test hook demonstrating custom commands, message rendering, and before_agent_start. - */ -import type { BeforeAgentStartEvent, HookAPI } from "@mariozechner/pi-coding-agent"; -import { Box, Spacer, Text } from "@mariozechner/pi-tui"; - -export default function (pi: HookAPI) { - // Track whether injection is enabled - let injectEnabled = false; - - // Register a custom message renderer for our "test-info" type - pi.registerMessageRenderer("test-info", (message, options, theme) => { - const box = new Box(1, 1, (t) => theme.bg("customMessageBg", t)); - - const label = theme.fg("success", "[TEST INFO]"); - box.addChild(new Text(label, 0, 0)); - box.addChild(new Spacer(1)); - - const content = - typeof message.content === "string" - ? message.content - : message.content.map((c) => (c.type === "text" ? c.text : "[image]")).join(""); - - box.addChild(new Text(theme.fg("text", content), 0, 0)); - - if (options.expanded && message.details) { - box.addChild(new Spacer(1)); - box.addChild(new Text(theme.fg("dim", `Details: ${JSON.stringify(message.details)}`), 0, 0)); - } - - return box; - }); - - // Register /test-msg command - pi.registerCommand("test-msg", { - description: "Send a test custom message", - handler: async () => { - pi.sendMessage( - { - customType: "test-info", - content: "This is a test message with custom rendering!", - display: true, - details: { timestamp: Date.now(), source: "test-command hook" }, - }, - true, // triggerTurn: start agent run - ); - }, - }); - - // Register /test-hidden command - pi.registerCommand("test-hidden", { - description: "Send a hidden message (display: false)", - handler: async (ctx) => { - pi.sendMessage({ - customType: "test-info", - content: "This message is in context but not displayed", - display: false, - }); - ctx.ui.notify("Sent hidden message (check session file)"); - }, - }); - - // Register /test-inject command to toggle before_agent_start injection - pi.registerCommand("test-inject", { - description: "Toggle context injection before agent starts", - handler: async (ctx) => { - injectEnabled = !injectEnabled; - ctx.ui.notify(`Context injection ${injectEnabled ? "enabled" : "disabled"}`); - }, - }); - - // Demonstrate before_agent_start: inject context when enabled - pi.on("before_agent_start", async (event: BeforeAgentStartEvent) => { - if (!injectEnabled) return; - - // Return a message to inject before the user's prompt - return { - message: { - customType: "test-info", - content: `[Injected context for prompt: "${event.prompt.slice(0, 50)}..."]`, - display: true, - details: { injectedAt: Date.now() }, - }, - }; - }); -} diff --git a/AGENTS.md b/AGENTS.md index 87d6ea624..bc6a60891 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,6 +21,256 @@ read README.md, then ask which module(s) to work on. Based on the answer, read t - NEVER remove or downgrade code to fix type errors from outdated dependencies; upgrade the dependency instead - Always ask before removing functionality or code that appears to be intentional +## Bun Over Node + +This project uses Bun as its runtime. Always prefer Bun APIs over Node.js equivalents. + +### Process Spawning + +```typescript +// GOOD: Bun.spawn with ReadableStream API +import type { Subprocess } from "bun"; + +const child: Subprocess = Bun.spawn(["cmd", ...args], { + stdin: "ignore", + stdout: "pipe", + stderr: "pipe", +}); + +const reader = (child.stdout as ReadableStream).getReader(); +while (true) { + const { done, value } = await reader.read(); + if (done) break; + // process Buffer.from(value) +} +const exitCode = await child.exited; + +// BAD: Node child_process +import { spawn } from "node:child_process"; +const child = spawn("cmd", args); +child.stdout.on("data", (chunk) => { ... }); +child.on("close", (code) => { ... }); +``` + +### Sync Process Execution + +```typescript +// GOOD: Bun.spawnSync +const result = Bun.spawnSync(["cmd", ...args], { + stdin: "ignore", + stdout: "pipe", + stderr: "pipe", +}); +if (result.exitCode === 0) { ... } + +// BAD: Node execSync/spawnSync +import { spawnSync } from "node:child_process"; +``` + +### File I/O + +```typescript +// GOOD: Bun.file and Bun.write +const content = await Bun.file("path.txt").text(); +const binary = await Bun.file("image.png").arrayBuffer(); +const exists = await Bun.file("maybe.txt").exists(); +await Bun.write("path.txt", content); + +// BAD: Node fs +import { readFileSync, writeFileSync, existsSync } from "node:fs"; +``` + +### Scripts and package.json + +```json +// GOOD: Use bun in scripts +{ + "scripts": { + "start": "bun run src/index.ts", + "test": "bun test", + "check": "bunx tsc --noEmit" + } +} + +// BAD: Use node/npm/npx +{ + "scripts": { + "start": "node dist/index.js", + "test": "npm test", + "check": "npx tsc --noEmit" + } +} +``` + +### Running Commands + +```bash +# GOOD +bun run script.ts +bun test +bunx tsc --noEmit +bun install + +# BAD +node script.js +npm test +npx tsc --noEmit +npm install +``` + +### Type Imports for Bun APIs + +```typescript +// Import Bun types when needed +import type { Subprocess } from "bun"; + +// Bun globals are available without import +Bun.spawn(...) +Bun.file(...) +Bun.write(...) +Bun.stdin.stream() +``` + +### Casting Bun Subprocess Streams + +Bun's `Subprocess.stdout`/`stderr` types can be `number | ReadableStream | undefined`. Cast when using pipe mode: + +```typescript +const child = Bun.spawn(["cmd"], { stdout: "pipe", stderr: "pipe" }); +const stdoutReader = (child.stdout as ReadableStream).getReader(); +const stderrReader = (child.stderr as ReadableStream).getReader(); +``` + +### Binary Existence Checks + +```typescript +// GOOD: Bun.which (cross-platform) +const gitPath = Bun.which("git"); +if (!gitPath) throw new Error("git not found"); + +// BAD: Spawning which/where (platform-specific) +Bun.spawnSync(["which", "git"]); // Unix only +Bun.spawnSync(["where", "git"]); // Windows only +``` + +### Path Resolution + +```typescript +// GOOD: import.meta (Bun ESM) +const thisDir = import.meta.dir; // equivalent to __dirname +const thisFile = import.meta.path; // equivalent to __filename + +// BAD: fileURLToPath dance +import { fileURLToPath } from "url"; +import { dirname } from "path"; +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +``` + +### Crypto + +```typescript +// GOOD: Web Crypto (standard) or Bun.hash +const uuid = crypto.randomUUID(); +const bytes = crypto.getRandomValues(new Uint8Array(32)); +const hash = Bun.hash("sha256", data); + +// BAD: Node crypto +import { randomBytes, randomUUID } from "node:crypto"; +``` + +### Environment Variables + +Bun auto-loads `.env` files. Do NOT import dotenv. + +```typescript +// GOOD: Direct access +const apiKey = process.env.API_KEY; + +// BAD: dotenv +import dotenv from "dotenv"; +dotenv.config(); +``` + +### HTTP Servers + +```typescript +// GOOD: Bun.serve +const server = Bun.serve({ + port: 3000, + fetch(req) { + return new Response("OK"); + }, +}); +// later: server.stop(); + +// BAD: Node http +import http from "node:http"; +const server = http.createServer((req, res) => { ... }); +``` + +### HTTP Requests + +```typescript +// GOOD: Native fetch (built into Bun) +const response = await fetch("https://api.example.com/data"); +const json = await response.json(); + +// BAD: External packages +import fetch from "node-fetch"; +import axios from "axios"; +``` + +### Password Hashing + +```typescript +// GOOD: Bun.password (bcrypt/argon2 built-in) +const hash = await Bun.password.hash("password", "bcrypt"); +const valid = await Bun.password.verify("password", hash); + +// BAD: External packages +import bcrypt from "bcrypt"; +import argon2 from "argon2"; +``` + +### Watch Mode + +```bash +# GOOD: Bun built-in watch/hot reload +bun --watch src/index.ts # Restart on changes +bun --hot src/index.ts # Hot reload without restart + +# BAD: External tools +npx nodemon src/index.ts +npx ts-node-dev src/index.ts +``` + +### SQLite + +```typescript +// GOOD: Bun built-in SQLite +import { Database } from "bun:sqlite"; +const db = new Database("mydb.sqlite"); +db.run("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)"); + +// BAD: External packages +import Database from "better-sqlite3"; +``` + +### Testing + +```bash +# GOOD: Bun built-in test runner +bun test + +# BAD: External test runners +npx jest +npx vitest +npx mocha +``` + +See `docs/bun-migration-guide.md` for full migration reference. + ## Commands - After code changes: `npm run check` (get full output, no tail) diff --git a/bun.lock b/bun.lock index f32e5fbed..54ebddfa0 100644 --- a/bun.lock +++ b/bun.lock @@ -12,13 +12,14 @@ "@biomejs/biome": "2.3.10", "@types/bun": "^1.2.17", "@types/node": "^22.10.5", + "bun-types": "^1.3.5", "concurrently": "^9.2.1", "husky": "^9.1.7", }, }, "packages/agent": { "name": "@mariozechner/pi-agent-core", - "version": "1.337.0", + "version": "0.31.1", "dependencies": { "@mariozechner/pi-ai": "workspace:*", "@mariozechner/pi-tui": "workspace:*", @@ -30,7 +31,7 @@ }, "packages/ai": { "name": "@mariozechner/pi-ai", - "version": "1.337.0", + "version": "0.31.1", "bin": { "pi-ai": "./src/cli.ts", }, @@ -54,7 +55,7 @@ }, "packages/coding-agent": { "name": "@mariozechner/pi-coding-agent", - "version": "1.337.0", + "version": "0.31.1", "bin": { "pi": "src/cli.ts", }, @@ -62,7 +63,6 @@ "@mariozechner/pi-agent-core": "workspace:*", "@mariozechner/pi-ai": "workspace:*", "@mariozechner/pi-tui": "workspace:*", - "ajv": "^8.17.1", "chalk": "^5.5.0", "cli-highlight": "^2.1.11", "diff": "^8.0.2", @@ -82,7 +82,7 @@ }, "packages/mom": { "name": "@mariozechner/pi-mom", - "version": "1.337.0", + "version": "0.31.1", "bin": { "mom": "src/main.ts", }, @@ -105,7 +105,7 @@ }, "packages/pods": { "name": "@mariozechner/pi", - "version": "1.337.0", + "version": "0.31.1", "bin": { "pi-pods": "dist/cli.js", }, @@ -116,13 +116,13 @@ }, "packages/tui": { "name": "@mariozechner/pi-tui", - "version": "1.337.0", + "version": "0.31.1", "dependencies": { "@types/mime-types": "^2.1.4", "chalk": "^5.5.0", + "get-east-asian-width": "^1.3.0", "marked": "^15.0.12", "mime-types": "^3.0.1", - "string-width": "^8.1.0", }, "devDependencies": { "@xterm/headless": "^5.5.0", @@ -131,7 +131,7 @@ }, "packages/web-ui": { "name": "@mariozechner/pi-web-ui", - "version": "1.337.0", + "version": "0.31.1", "dependencies": { "@lmstudio/sdk": "^1.5.0", "@mariozechner/pi-agent-core": "workspace:*", @@ -157,7 +157,7 @@ }, "packages/web-ui/example": { "name": "pi-web-ui-example", - "version": "1.18.2", + "version": "1.19.1", "dependencies": { "@mariozechner/mini-lit": "^0.2.0", "@mariozechner/pi-ai": "workspace:*", @@ -908,7 +908,7 @@ "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], - "string-width": ["string-width@8.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg=="], + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -1058,8 +1058,6 @@ "cli-highlight/yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="], - "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "concurrently/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -1082,6 +1080,8 @@ "rimraf/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], + "string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -1090,16 +1090,10 @@ "tunnel-agent/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - "wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], @@ -1130,8 +1124,6 @@ "cli-highlight/yargs/cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="], - "cli-highlight/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "cli-highlight/yargs/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -1148,28 +1140,22 @@ "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "tar-stream/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "bl/readable-stream/string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "cli-highlight/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "cli-highlight/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "rimraf/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "tar-stream/readable-stream/string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "cli-highlight/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "cli-highlight/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], } } diff --git a/package.json b/package.json index de416d15f..02b4e4d71 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@biomejs/biome": "2.3.10", "@types/bun": "^1.2.17", "@types/node": "^22.10.5", + "bun-types": "^1.3.5", "concurrently": "^9.2.1", "husky": "^9.1.7" }, diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 5d20a7bb0..991be7cb9 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +## [0.31.1] - 2026-01-02 + +## [0.31.0] - 2026-01-02 + ### Breaking Changes - **Transport abstraction removed**: `ProviderTransport`, `AppTransport`, and `AgentTransport` interface have been removed. Use the `streamFn` option directly for custom streaming implementations. diff --git a/packages/agent/src/proxy.ts b/packages/agent/src/proxy.ts index 0a3224c2b..1e37cf7a2 100644 --- a/packages/agent/src/proxy.ts +++ b/packages/agent/src/proxy.ts @@ -148,12 +148,12 @@ export function streamProxy(model: Model, context: Context, options: ProxyS throw new Error(errorMessage); } - reader = response.body!.getReader(); + reader = response.body!.getReader() as ReadableStreamDefaultReader; const decoder = new TextDecoder(); let buffer = ""; while (true) { - const { done, value } = await reader.read(); + const { done, value } = await reader!.read(); if (done) break; if (options.signal?.aborted) { diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 6d822a711..88e15624a 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,14 @@ ## [Unreleased] +### Fixed + +- **Gemini CLI rate limit handling**: Added automatic retry with server-provided delay for 429 errors. Parses delay from error messages like "Your quota will reset after 39s" and waits accordingly. Falls back to exponential backoff for other transient errors. ([#370](https://github.com/badlogic/pi-mono/issues/370)) + +## [0.31.1] - 2026-01-02 + +## [0.31.0] - 2026-01-02 + ### Breaking Changes - **Agent API moved**: All agent functionality (`agentLoop`, `agentLoopContinue`, `AgentContext`, `AgentEvent`, `AgentTool`, `AgentToolResult`, etc.) has moved to `@mariozechner/pi-agent-core`. Import from that package instead of `@mariozechner/pi-ai`. diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index 34bc73d87..17a63b881 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -1,12 +1,9 @@ #!/usr/bin/env bun -import { join, dirname } from "path"; -import { fileURLToPath } from "url"; +import { join } from "path"; import { Api, KnownProvider, Model } from "../src/types.js"; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); -const packageRoot = join(__dirname, ".."); +const packageRoot = join(import.meta.dir, ".."); interface ModelsDevModel { id: string; diff --git a/packages/ai/scripts/generate-test-image.ts b/packages/ai/scripts/generate-test-image.ts index 00615443a..549ad21d3 100644 --- a/packages/ai/scripts/generate-test-image.ts +++ b/packages/ai/scripts/generate-test-image.ts @@ -1,11 +1,7 @@ #!/usr/bin/env bun import { createCanvas } from "canvas"; -import { join, dirname } from "path"; -import { fileURLToPath } from "url"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); +import { join } from "path"; // Create a 200x200 canvas const canvas = createCanvas(200, 200); @@ -23,10 +19,10 @@ ctx.fill(); // Save the image const buffer = canvas.toBuffer("image/png"); -const outputPath = join(__dirname, "..", "test", "data", "red-circle.png"); +const outputPath = join(import.meta.dir, "..", "test", "data", "red-circle.png"); // Ensure the directory exists -await Bun.write(join(__dirname, "..", "test", "data", ".keep"), ""); +await Bun.write(join(import.meta.dir, "..", "test", "data", ".keep"), ""); await Bun.write(outputPath, buffer); console.log(`Generated test image at: ${outputPath}`); diff --git a/packages/ai/src/providers/google-gemini-cli.ts b/packages/ai/src/providers/google-gemini-cli.ts index 85d644dd3..7bc8ef9a3 100644 --- a/packages/ai/src/providers/google-gemini-cli.ts +++ b/packages/ai/src/providers/google-gemini-cli.ts @@ -72,6 +72,83 @@ const ANTIGRAVITY_HEADERS = { // Counter for generating unique tool call IDs let toolCallCounter = 0; +// Retry configuration +const MAX_RETRIES = 3; +const BASE_DELAY_MS = 1000; + +/** + * Extract retry delay from Gemini error response (in milliseconds). + * Parses patterns like: + * - "Your quota will reset after 39s" + * - "Your quota will reset after 18h31m10s" + * - "Please retry in Xs" or "Please retry in Xms" + * - "retryDelay": "34.074824224s" (JSON field) + */ +function extractRetryDelay(errorText: string): number | undefined { + // Pattern 1: "Your quota will reset after ..." (formats: "18h31m10s", "10m15s", "6s", "39s") + const durationMatch = errorText.match(/reset after (?:(\d+)h)?(?:(\d+)m)?(\d+(?:\.\d+)?)s/i); + if (durationMatch) { + const hours = durationMatch[1] ? parseInt(durationMatch[1], 10) : 0; + const minutes = durationMatch[2] ? parseInt(durationMatch[2], 10) : 0; + const seconds = parseFloat(durationMatch[3]); + if (!Number.isNaN(seconds)) { + const totalMs = ((hours * 60 + minutes) * 60 + seconds) * 1000; + if (totalMs > 0) { + return Math.ceil(totalMs + 1000); // Add 1s buffer + } + } + } + + // Pattern 2: "Please retry in X[ms|s]" + const retryInMatch = errorText.match(/Please retry in ([0-9.]+)(ms|s)/i); + if (retryInMatch?.[1]) { + const value = parseFloat(retryInMatch[1]); + if (!Number.isNaN(value) && value > 0) { + const ms = retryInMatch[2].toLowerCase() === "ms" ? value : value * 1000; + return Math.ceil(ms + 1000); + } + } + + // Pattern 3: "retryDelay": "34.074824224s" (JSON field in error details) + const retryDelayMatch = errorText.match(/"retryDelay":\s*"([0-9.]+)(ms|s)"/i); + if (retryDelayMatch?.[1]) { + const value = parseFloat(retryDelayMatch[1]); + if (!Number.isNaN(value) && value > 0) { + const ms = retryDelayMatch[2].toLowerCase() === "ms" ? value : value * 1000; + return Math.ceil(ms + 1000); + } + } + + return undefined; +} + +/** + * Check if an error is retryable (rate limit, server error, etc.) + */ +function isRetryableError(status: number, errorText: string): boolean { + if (status === 429 || status === 500 || status === 502 || status === 503 || status === 504) { + return true; + } + return /resource.?exhausted|rate.?limit|overloaded|service.?unavailable/i.test(errorText); +} + +/** + * Sleep for a given number of milliseconds, respecting abort signal. + */ +function sleep(ms: number, signal?: AbortSignal): Promise { + return new Promise((resolve, reject) => { + if (signal?.aborted) { + reject(new Error("Request was aborted")); + return; + } + const timeout = setTimeout(resolve, ms); + signal?.addEventListener("abort", () => { + clearTimeout(timeout); + reject(new Error("Request was aborted")); + }); + }); +} + interface CloudCodeAssistRequest { project: string; model: string; @@ -181,21 +258,62 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = ( const isAntigravity = endpoint.includes("sandbox.googleapis.com"); const headers = isAntigravity ? ANTIGRAVITY_HEADERS : GEMINI_CLI_HEADERS; - const response = await fetch(url, { - method: "POST", - headers: { - Authorization: `Bearer ${accessToken}`, - "Content-Type": "application/json", - Accept: "text/event-stream", - ...headers, - }, - body: JSON.stringify(requestBody), - signal: options?.signal, - }); + // Fetch with retry logic for rate limits and transient errors + let response: Response | undefined; + let lastError: Error | undefined; - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`Cloud Code Assist API error (${response.status}): ${errorText}`); + for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) { + if (options?.signal?.aborted) { + throw new Error("Request was aborted"); + } + + try { + response = await fetch(url, { + method: "POST", + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + Accept: "text/event-stream", + ...headers, + }, + body: JSON.stringify(requestBody), + signal: options?.signal, + }); + + if (response.ok) { + break; // Success, exit retry loop + } + + const errorText = await response.text(); + + // Check if retryable + if (attempt < MAX_RETRIES && isRetryableError(response.status, errorText)) { + // Use server-provided delay or exponential backoff + const serverDelay = extractRetryDelay(errorText); + const delayMs = serverDelay ?? BASE_DELAY_MS * 2 ** attempt; + await sleep(delayMs, options?.signal); + continue; + } + + // Not retryable or max retries exceeded + throw new Error(`Cloud Code Assist API error (${response.status}): ${errorText}`); + } catch (error) { + if (error instanceof Error && error.message === "Request was aborted") { + throw error; + } + lastError = error instanceof Error ? error : new Error(String(error)); + // Network errors are retryable + if (attempt < MAX_RETRIES) { + const delayMs = BASE_DELAY_MS * 2 ** attempt; + await sleep(delayMs, options?.signal); + continue; + } + throw lastError; + } + } + + if (!response || !response.ok) { + throw lastError ?? new Error("Failed to get response after retries"); } if (!response.body) { diff --git a/packages/ai/test/context-overflow.test.ts b/packages/ai/test/context-overflow.test.ts index 5af3831f1..22a6d6f75 100644 --- a/packages/ai/test/context-overflow.test.ts +++ b/packages/ai/test/context-overflow.test.ts @@ -424,13 +424,7 @@ describe("Context overflow error handling", () => { // ============================================================================= // Check if ollama is installed - let ollamaInstalled = false; - try { - Bun.spawnSync(["which", "ollama"]); - ollamaInstalled = true; - } catch { - ollamaInstalled = false; - } + const ollamaInstalled = Bun.which("ollama") !== null; describe.skipIf(!ollamaInstalled)("Ollama (local)", () => { let ollamaProcess: Subprocess | null = null; diff --git a/packages/ai/test/image-limits.test.ts b/packages/ai/test/image-limits.test.ts index b87c984b6..c352a32f7 100644 --- a/packages/ai/test/image-limits.test.ts +++ b/packages/ai/test/image-limits.test.ts @@ -67,26 +67,24 @@ * ============================================================================ */ -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; +import { join } from "node:path"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { getModel } from "../src/models.js"; import { complete } from "../src/stream.js"; import type { Api, Context, ImageContent, Model, OptionsForApi, UserMessage } from "../src/types.js"; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); +const dir = import.meta.dir; // Temp directory for generated images -const TEMP_DIR = join(__dirname, ".temp-images"); +const TEMP_DIR = join(dir, ".temp-images"); /** * Generate a valid PNG image of specified dimensions using ImageMagick */ -function generateImage(width: number, height: number, filename: string): string { +async function generateImage(width: number, height: number, filename: string): Promise { const filepath = join(TEMP_DIR, filename); Bun.spawnSync(["magick", "-size", `${width}x${height}`, "xc:red", filepath]); - const buffer = Bun.file(filepath).arrayBuffer(); + const buffer = await Bun.file(filepath).arrayBuffer(); return Buffer.from(buffer).toString("base64"); } @@ -275,7 +273,7 @@ describe("Image Limits E2E Tests", () => { await Bun.write(join(TEMP_DIR, ".keep"), ""); // Generate small test image for count tests - smallImage = generateImage(100, 100, "small.png"); + smallImage = await generateImage(100, 100, "small.png"); }); afterAll(async () => { @@ -313,7 +311,7 @@ describe("Image Limits E2E Tests", () => { for (const sizeMB of sizes) { console.log(` Testing size: ${sizeMB}MB...`); - const imageBase64 = generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`); + const imageBase64 = await generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`); const result = await testImageSize(model, imageBase64); if (result.success) { lastSuccess = sizeMB; @@ -338,7 +336,7 @@ describe("Image Limits E2E Tests", () => { for (const dim of dimensions) { console.log(` Testing dimension: ${dim}x${dim}...`); - const imageBase64 = generateImage(dim, dim, `dim-${dim}.png`); + const imageBase64 = await generateImage(dim, dim, `dim-${dim}.png`); const result = await testImageDimensions(model, imageBase64); if (result.success) { lastSuccess = dim; @@ -391,7 +389,7 @@ describe("Image Limits E2E Tests", () => { for (const sizeMB of sizes) { console.log(` Testing size: ${sizeMB}MB...`); - const imageBase64 = generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`); + const imageBase64 = await generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`); const result = await testImageSize(model, imageBase64); if (result.success) { lastSuccess = sizeMB; @@ -415,7 +413,7 @@ describe("Image Limits E2E Tests", () => { for (const dim of dimensions) { console.log(` Testing dimension: ${dim}x${dim}...`); - const imageBase64 = generateImage(dim, dim, `dim-${dim}.png`); + const imageBase64 = await generateImage(dim, dim, `dim-${dim}.png`); const result = await testImageDimensions(model, imageBase64); if (result.success) { lastSuccess = dim; @@ -466,7 +464,7 @@ describe("Image Limits E2E Tests", () => { for (const sizeMB of sizes) { console.log(` Testing size: ${sizeMB}MB...`); - const imageBase64 = generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`); + const imageBase64 = await generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`); const result = await testImageSize(model, imageBase64); if (result.success) { lastSuccess = sizeMB; @@ -490,7 +488,7 @@ describe("Image Limits E2E Tests", () => { for (const dim of dimensions) { console.log(` Testing dimension: ${dim}x${dim}...`); - const imageBase64 = generateImage(dim, dim, `dim-${dim}.png`); + const imageBase64 = await generateImage(dim, dim, `dim-${dim}.png`); const result = await testImageDimensions(model, imageBase64); if (result.success) { lastSuccess = dim; @@ -535,7 +533,7 @@ describe("Image Limits E2E Tests", () => { for (const sizeMB of sizes) { console.log(` Testing size: ${sizeMB}MB...`); - const imageBase64 = generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`); + const imageBase64 = await generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`); const result = await testImageSize(model, imageBase64); if (result.success) { lastSuccess = sizeMB; @@ -559,7 +557,7 @@ describe("Image Limits E2E Tests", () => { for (const dim of dimensions) { console.log(` Testing dimension: ${dim}x${dim}...`); - const imageBase64 = generateImage(dim, dim, `dim-${dim}.png`); + const imageBase64 = await generateImage(dim, dim, `dim-${dim}.png`); const result = await testImageDimensions(model, imageBase64); if (result.success) { lastSuccess = dim; @@ -604,7 +602,7 @@ describe("Image Limits E2E Tests", () => { for (const sizeMB of sizes) { console.log(` Testing size: ${sizeMB}MB...`); - const imageBase64 = generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`); + const imageBase64 = await generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`); const result = await testImageSize(model, imageBase64); if (result.success) { lastSuccess = sizeMB; @@ -628,7 +626,7 @@ describe("Image Limits E2E Tests", () => { for (const dim of dimensions) { console.log(` Testing dimension: ${dim}x${dim}...`); - const imageBase64 = generateImage(dim, dim, `dim-${dim}.png`); + const imageBase64 = await generateImage(dim, dim, `dim-${dim}.png`); const result = await testImageDimensions(model, imageBase64); if (result.success) { lastSuccess = dim; @@ -671,7 +669,7 @@ describe("Image Limits E2E Tests", () => { for (const sizeMB of sizes) { console.log(` Testing size: ${sizeMB}MB...`); - const imageBase64 = generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`); + const imageBase64 = await generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`); const result = await testImageSize(model, imageBase64); if (result.success) { lastSuccess = sizeMB; @@ -695,7 +693,7 @@ describe("Image Limits E2E Tests", () => { for (const dim of dimensions) { console.log(` Testing dimension: ${dim}x${dim}...`); - const imageBase64 = generateImage(dim, dim, `dim-${dim}.png`); + const imageBase64 = await generateImage(dim, dim, `dim-${dim}.png`); const result = await testImageDimensions(model, imageBase64); if (result.success) { lastSuccess = dim; @@ -738,7 +736,7 @@ describe("Image Limits E2E Tests", () => { for (const sizeMB of sizes) { console.log(` Testing size: ${sizeMB}MB...`); - const imageBase64 = generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`); + const imageBase64 = await generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`); const result = await testImageSize(model, imageBase64); if (result.success) { lastSuccess = sizeMB; @@ -762,7 +760,7 @@ describe("Image Limits E2E Tests", () => { for (const dim of dimensions) { console.log(` Testing dimension: ${dim}x${dim}...`); - const imageBase64 = generateImage(dim, dim, `dim-${dim}.png`); + const imageBase64 = await generateImage(dim, dim, `dim-${dim}.png`); const result = await testImageDimensions(model, imageBase64); if (result.success) { lastSuccess = dim; @@ -805,7 +803,7 @@ describe("Image Limits E2E Tests", () => { for (const sizeMB of sizes) { console.log(` Testing size: ${sizeMB}MB...`); - const imageBase64 = generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`); + const imageBase64 = await generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`); const result = await testImageSize(model, imageBase64); if (result.success) { lastSuccess = sizeMB; @@ -829,7 +827,7 @@ describe("Image Limits E2E Tests", () => { for (const dim of dimensions) { console.log(` Testing dimension: ${dim}x${dim}...`); - const imageBase64 = generateImage(dim, dim, `dim-${dim}.png`); + const imageBase64 = await generateImage(dim, dim, `dim-${dim}.png`); const result = await testImageDimensions(model, imageBase64); if (result.success) { lastSuccess = dim; @@ -860,12 +858,12 @@ describe("Image Limits E2E Tests", () => { // Generate images at specific sizes for each provider's limit const imageCache: Map = new Map(); - function getImageAtSize(targetMB: number): string { + async function getImageAtSize(targetMB: number): Promise { if (imageCache.has(targetMB)) { return imageCache.get(targetMB)!; } console.log(` Generating ~${targetMB}MB noise image...`); - const imageBase64 = generateImageWithSize(targetMB * 1024 * 1024, `stress-${targetMB}mb.png`); + const imageBase64 = await generateImageWithSize(targetMB * 1024 * 1024, `stress-${targetMB}mb.png`); const actualSize = Buffer.from(imageBase64, "base64").length; console.log(` Actual size: ${(actualSize / 1024 / 1024).toFixed(2)}MB`); imageCache.set(targetMB, imageBase64); @@ -879,7 +877,7 @@ describe("Image Limits E2E Tests", () => { { timeout: 900000 }, async () => { const model = getModel("anthropic", "claude-3-5-haiku-20241022"); - const image3mb = getImageAtSize(3); + const image3mb = await getImageAtSize(3); // 32MB total limit / ~4MB actual = ~8 images const counts = [1, 2, 4, 6, 8, 10, 12]; @@ -911,7 +909,7 @@ describe("Image Limits E2E Tests", () => { { timeout: 1800000 }, async () => { const model = getModel("openai", "gpt-4o-mini"); - const image15mb = getImageAtSize(15); + const image15mb = await getImageAtSize(15); // Test progressively const counts = [1, 2, 5, 10, 20]; @@ -943,7 +941,7 @@ describe("Image Limits E2E Tests", () => { { timeout: 1800000 }, async () => { const model = getModel("google", "gemini-2.5-flash"); - const image20mb = getImageAtSize(20); + const image20mb = await getImageAtSize(20); // Test progressively const counts = [1, 2, 5, 10, 20, 50]; @@ -975,7 +973,7 @@ describe("Image Limits E2E Tests", () => { { timeout: 600000 }, async () => { const model = getModel("mistral", "pixtral-12b"); - const image10mb = getImageAtSize(10); + const image10mb = await getImageAtSize(10); // Known limit is 8 images const counts = [1, 2, 4, 6, 8, 9]; @@ -1004,7 +1002,7 @@ describe("Image Limits E2E Tests", () => { // Test with 20MB images (safely under limit) it.skipIf(!process.env.XAI_API_KEY)("xAI: max ~20MB images before rejection", { timeout: 1200000 }, async () => { const model = getModel("xai", "grok-2-vision"); - const image20mb = getImageAtSize(20); + const image20mb = await getImageAtSize(20); // Test progressively const counts = [1, 2, 5, 10, 20]; @@ -1037,7 +1035,7 @@ describe("Image Limits E2E Tests", () => { const model = getModel("groq", "meta-llama/llama-4-scout-17b-16e-instruct"); // Generate 5760x5760 image (33177600 pixels = Groq's limit) console.log(" Generating 5760x5760 test image for Groq..."); - const image5760 = generateImage(5760, 5760, "stress-5760.png"); + const image5760 = await generateImage(5760, 5760, "stress-5760.png"); // Known limit is 5 images const counts = [1, 2, 3, 4, 5, 6]; @@ -1067,7 +1065,7 @@ describe("Image Limits E2E Tests", () => { // Test with 15MB images it.skipIf(!process.env.ZAI_API_KEY)("zAI: max ~15MB images before rejection", { timeout: 1200000 }, async () => { const model = getModel("zai", "glm-4.5v"); - const image15mb = getImageAtSize(15); + const image15mb = await getImageAtSize(15); // Context-limited, test progressively const counts = [1, 2, 5, 10, 20]; @@ -1098,7 +1096,7 @@ describe("Image Limits E2E Tests", () => { { timeout: 900000 }, async () => { const model = getModel("openrouter", "z-ai/glm-4.5v"); - const image5mb = getImageAtSize(5); + const image5mb = await getImageAtSize(5); // Context-limited, test progressively const counts = [1, 2, 5, 10, 20]; diff --git a/packages/ai/test/stream.test.ts b/packages/ai/test/stream.test.ts index dd25d9c63..85b8921ab 100644 --- a/packages/ai/test/stream.test.ts +++ b/packages/ai/test/stream.test.ts @@ -1,7 +1,6 @@ import { Type } from "@sinclair/typebox"; import type { Subprocess } from "bun"; -import { dirname, join } from "path"; -import { fileURLToPath } from "url"; +import { join } from "path"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { getModel } from "../src/models.js"; import { complete, stream } from "../src/stream.js"; @@ -9,8 +8,7 @@ import type { Api, Context, ImageContent, Model, OptionsForApi, Tool, ToolResult import { StringEnum } from "../src/utils/typebox-helpers.js"; import { resolveApiKey } from "./oauth.js"; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); +const dir = import.meta.dir; // Resolve OAuth tokens at module level (async, runs before tests) const oauthTokens = await Promise.all([ @@ -218,7 +216,7 @@ async function handleImage(model: Model, options?: Optio } // Read the test image - const imagePath = join(__dirname, "data", "red-circle.png"); + const imagePath = join(dir, "data", "red-circle.png"); const imageBuffer = await Bun.file(imagePath).arrayBuffer(); const base64Image = Buffer.from(imageBuffer).toString("base64"); @@ -838,13 +836,7 @@ describe("Generate E2E Tests", () => { }); // Check if ollama is installed - let ollamaInstalled = false; - try { - Bun.spawnSync(["which", "ollama"]); - ollamaInstalled = true; - } catch { - ollamaInstalled = false; - } + const ollamaInstalled = Bun.which("ollama") !== null; describe.skipIf(!ollamaInstalled)("Ollama Provider (gpt-oss-20b via OpenAI Completions)", () => { let llm: Model<"openai-completions">; diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index d939a0746..dda47046f 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,9 +2,14 @@ ## [Unreleased] +## [0.31.1] - 2026-01-02 + ### Fixed -- Crash when displaying bash output containing Unicode format characters like U+0600-U+0604 ([#372](https://github.com/badlogic/pi-mono/pull/372) by [@HACKE-RC](https://github.com/HACKE-RC)) +- Model selector no longer allows negative index when pressing arrow keys before models finish loading ([#398](https://github.com/badlogic/pi-mono/pull/398) by [@mitsuhiko](https://github.com/mitsuhiko)) +- Type guard functions (`isBashToolResult`, etc.) now exported at runtime, not just in type declarations ([#397](https://github.com/badlogic/pi-mono/issues/397)) + +## [0.31.0] - 2026-01-02 This release introduces session trees for in-place branching, major API changes to hooks and custom tools, and structured compaction with file tracking. @@ -24,10 +29,11 @@ The hooks API has been restructured with more granular events and better session **Type renames:** - `HookEventContext` → `HookContext` -- `HookCommandContext` removed (use `HookContext` for command handlers) +- `HookCommandContext` is now a new interface extending `HookContext` with session control methods **Event changes:** -- The monolithic `session` event is now split into granular events: `session_start`, `session_before_switch`, `session_switch`, `session_before_new`, `session_new`, `session_before_branch`, `session_branch`, `session_before_compact`, `session_compact`, `session_shutdown` +- The monolithic `session` event is now split into granular events: `session_start`, `session_before_switch`, `session_switch`, `session_before_branch`, `session_branch`, `session_before_compact`, `session_compact`, `session_shutdown` +- `session_before_switch` and `session_switch` events now include `reason: "new" | "resume"` to distinguish between `/new` and `/resume` - New `session_before_tree` and `session_tree` events for `/tree` navigation (hook can provide custom branch summary) - New `before_agent_start` event: inject messages before the agent loop starts - New `context` event: modify messages non-destructively before each LLM call @@ -36,13 +42,25 @@ The hooks API has been restructured with more granular events and better session **API changes:** - `pi.send(text, attachments?)` → `pi.sendMessage(message, triggerTurn?)` (creates `CustomMessageEntry`) - New `pi.appendEntry(customType, data?)` for hook state persistence (not in LLM context) -- New `pi.registerCommand(name, options)` for custom slash commands +- New `pi.registerCommand(name, options)` for custom slash commands (handler receives `HookCommandContext`) - New `pi.registerMessageRenderer(customType, renderer)` for custom TUI rendering +- New `ctx.isIdle()`, `ctx.abort()`, `ctx.hasQueuedMessages()` for agent state (available in all events) +- New `ctx.ui.editor(title, prefill?)` for multi-line text editing with Ctrl+G external editor support - New `ctx.ui.custom(component)` for full TUI component rendering with keyboard focus +- New `ctx.ui.setStatus(key, text)` for persistent status text in footer (multiple hooks can set their own) +- New `ctx.ui.theme` getter for styling text with theme colors - `ctx.exec()` moved to `pi.exec()` - `ctx.sessionFile` → `ctx.sessionManager.getSessionFile()` - New `ctx.modelRegistry` and `ctx.model` for API key resolution +**HookCommandContext (slash commands only):** +- `ctx.waitForIdle()` - wait for agent to finish streaming +- `ctx.newSession(options?)` - create new sessions with optional setup callback +- `ctx.branch(entryId)` - branch from a specific entry +- `ctx.navigateTree(targetId, options?)` - navigate the session tree + +These methods are only on `HookCommandContext` (not `HookContext`) because they can deadlock if called from event handlers that run inside the agent loop. + **Removed:** - `hookTimeout` setting (hooks no longer have timeouts; use Ctrl+C to abort) - `resolveApiKey` parameter (use `ctx.modelRegistry.getApiKey(model)`) @@ -68,12 +86,15 @@ execute(toolCallId, params, signal, onUpdate) execute(toolCallId, params, onUpdate, ctx, signal?) ``` -The new `ctx: CustomToolContext` provides `sessionManager`, `modelRegistry`, and `model`. +The new `ctx: CustomToolContext` provides `sessionManager`, `modelRegistry`, `model`, and agent state methods: +- `ctx.isIdle()` - check if agent is streaming +- `ctx.hasQueuedMessages()` - check if user has queued messages (skip interactive prompts) +- `ctx.abort()` - abort current operation (fire-and-forget) **Session event changes:** - `CustomToolSessionEvent` now only has `reason` and `previousSessionFile` - Session entries are no longer in the event. Use `ctx.sessionManager.getBranch()` or `ctx.sessionManager.getEntries()` to reconstruct state -- New reasons: `"tree"` (for `/tree` navigation) and `"shutdown"` (for cleanup on exit) +- Reasons: `"start" | "switch" | "branch" | "tree" | "shutdown"` (no separate `"new"` reason; `/new` triggers `"switch"`) - `dispose()` method removed. Use `onSession` with `reason: "shutdown"` for cleanup See [docs/custom-tools.md](docs/custom-tools.md) and [examples/custom-tools/](examples/custom-tools/) for the current API. @@ -87,10 +108,11 @@ See [docs/custom-tools.md](docs/custom-tools.md) and [examples/custom-tools/](ex - `model` returns `Model | undefined` (was `Model | null`) - `Attachment` type removed. Use `ImageContent` from `@mariozechner/pi-ai` instead. Add images directly to message content arrays. -**AgentSession branching API:** +**AgentSession API:** - `branch(entryIndex: number)` → `branch(entryId: string)` - `getUserMessagesForBranching()` returns `{ entryId, text }` instead of `{ entryIndex, text }` -- `reset()` and `switchSession()` now return `Promise` (false if cancelled by hook) +- `reset()` → `newSession(options?)` where options has optional `parentSession` for lineage tracking +- `newSession()` and `switchSession()` now return `Promise` (false if cancelled by hook) - New `navigateTree(targetId, options?)` for in-place tree navigation **Hook integration:** @@ -99,8 +121,9 @@ See [docs/custom-tools.md](docs/custom-tools.md) and [examples/custom-tools/](ex **SessionManager API:** - Method renames: `saveXXX()` → `appendXXX()` (e.g., `appendMessage`, `appendCompaction`) - `branchInPlace()` → `branch()` -- `reset()` → `newSession()` +- `reset()` → `newSession(options?)` with optional `parentSession` for lineage tracking - `createBranchedSessionFromEntries(entries, index)` → `createBranchedSession(leafId)` +- `SessionHeader.branchedFrom` → `SessionHeader.parentSession` - `saveCompaction(entry)` → `appendCompaction(summary, firstKeptEntryId, tokensBefore, details?)` - `getEntries()` now excludes the session header (use `getHeader()` separately) - `getSessionFile()` returns `string | undefined` (undefined for in-memory sessions) @@ -141,6 +164,9 @@ See [docs/sdk.md](docs/sdk.md) and [examples/sdk/](examples/sdk/) for the curren ### RPC Migration +**Session commands:** +- `reset` command → `new_session` command with optional `parentSession` field + **Branching commands:** - `branch` command: `entryIndex` → `entryId` - `get_branch_messages` response: `entryIndex` → `entryId` @@ -193,6 +219,13 @@ Total color count increased from 46 to 50. See [docs/theme.md](docs/theme.md) fo ### Added +- `ctx.ui.setStatus(key, text)` for hooks to display persistent status text in the footer ([#385](https://github.com/badlogic/pi-mono/pull/385) by [@prateekmedia](https://github.com/prateekmedia)) +- `ctx.ui.theme` getter for styling status text and other output with theme colors +- `/share` command to upload session as a secret GitHub gist and get a shareable URL via shittycodingagent.ai ([#380](https://github.com/badlogic/pi-mono/issues/380)) +- HTML export now includes a tree visualization sidebar for navigating session branches ([#375](https://github.com/badlogic/pi-mono/issues/375)) +- HTML export supports keyboard shortcuts: Ctrl+T to toggle thinking blocks, Ctrl+O to toggle tool outputs +- HTML export supports theme-configurable background colors via optional `export` section in theme JSON ([#387](https://github.com/badlogic/pi-mono/pull/387) by [@mitsuhiko](https://github.com/mitsuhiko)) +- HTML export syntax highlighting now uses theme colors and matches TUI rendering - **Snake game example hook**: Demonstrates `ui.custom()`, `registerCommand()`, and session persistence. See [examples/hooks/snake.ts](examples/hooks/snake.ts). - **`thinkingText` theme token**: Configurable color for thinking block text. ([#366](https://github.com/badlogic/pi-mono/pull/366) by [@paulbettner](https://github.com/paulbettner)) @@ -200,15 +233,19 @@ Total color count increased from 46 to 50. See [docs/theme.md](docs/theme.md) fo - **Entry IDs**: Session entries now use short 8-character hex IDs instead of full UUIDs - **API key priority**: `ANTHROPIC_OAUTH_TOKEN` now takes precedence over `ANTHROPIC_API_KEY` +- HTML export template split into separate files (template.html, template.css, template.js) for easier maintenance ### Fixed +- HTML export now properly sanitizes user messages containing HTML tags like ` - - -
-
-

${APP_NAME} v${VERSION}

-
-
Session:${escapeHtml(data.sessionId)}
-
Date:${new Date(data.timestamp).toLocaleString()}
-
Models:${ - Array.from(data.modelsUsed) - .map((m) => escapeHtml(m)) - .join(", ") || "unknown" - }
-
-
- -
-

Messages

-
-
User:${userMessages}
-
Assistant:${assistantMessages}
-
Tool Calls:${toolCallsCount}
-
-
- -
-

Tokens & Cost

-
-
Input:${data.tokenStats.input.toLocaleString()} tokens
-
Output:${data.tokenStats.output.toLocaleString()} tokens
-
Cache Read:${data.tokenStats.cacheRead.toLocaleString()} tokens
-
Cache Write:${data.tokenStats.cacheWrite.toLocaleString()} tokens
-
Total:${(data.tokenStats.input + data.tokenStats.output + data.tokenStats.cacheRead + data.tokenStats.cacheWrite).toLocaleString()} tokens
-
Input Cost:$${data.costStats.input.toFixed(4)}
-
Output Cost:$${data.costStats.output.toFixed(4)}
-
Cache Read Cost:$${data.costStats.cacheRead.toFixed(4)}
-
Cache Write Cost:$${data.costStats.cacheWrite.toFixed(4)}
-
Total Cost:$${(data.costStats.input + data.costStats.output + data.costStats.cacheRead + data.costStats.cacheWrite).toFixed(4)}
-
Context Usage:${contextUsageText}
-
-
- - ${systemPromptHtml} - ${toolsHtml} - ${streamingNotice} - -
- ${messagesHtml} -
- - -
- -`; -} - -// ============================================================================ -// Public API -// ============================================================================ - -export interface ExportOptions { - outputPath?: string; - themeName?: string; -} - -/** - * Export session to HTML using SessionManager and AgentState. - * Used by TUI's /export command. - * @param sessionManager The session manager - * @param state The agent state - * @param options Export options including output path and theme name - */ -export function exportSessionToHtml( - sessionManager: SessionManager, - state: AgentState, - options?: ExportOptions | string, -): string { - // Handle backwards compatibility: options can be just the output path string - const opts: ExportOptions = typeof options === "string" ? { outputPath: options } : options || {}; - - const sessionFile = sessionManager.getSessionFile(); - if (!sessionFile) { - throw new Error("Cannot export in-memory session to HTML"); - } - const content = readFileSync(sessionFile, "utf8"); - const data = parseSessionFile(content); - - // Enrich with data from AgentState (tools, context window) - data.tools = state.tools.map((t: { name: string; description: string }) => ({ - name: t.name, - description: t.description, - })); - data.contextWindow = state.model?.contextWindow; - if (!data.systemPrompt) { - data.systemPrompt = state.systemPrompt; - } - - let outputPath = opts.outputPath; - if (!outputPath) { - const sessionBasename = basename(sessionFile, ".jsonl"); - outputPath = `${APP_NAME}-session-${sessionBasename}.html`; - } - - const colors = getThemeColors(opts.themeName); - const isLight = isLightTheme(opts.themeName); - const html = generateHtml(data, basename(sessionFile), colors, isLight); - writeFileSync(outputPath, html, "utf8"); - return outputPath; -} - -/** - * Export session file to HTML (standalone, without AgentState). - * Auto-detects format: session manager format or streaming event format. - * Used by CLI for exporting arbitrary session files. - * @param inputPath Path to the session file - * @param options Export options including output path and theme name - */ -export function exportFromFile(inputPath: string, options?: ExportOptions | string): string { - // Handle backwards compatibility: options can be just the output path string - const opts: ExportOptions = typeof options === "string" ? { outputPath: options } : options || {}; - - if (!existsSync(inputPath)) { - throw new Error(`File not found: ${inputPath}`); - } - - const content = readFileSync(inputPath, "utf8"); - const data = parseSessionFile(content); - - let outputPath = opts.outputPath; - if (!outputPath) { - const inputBasename = basename(inputPath, ".jsonl"); - outputPath = `${APP_NAME}-session-${inputBasename}.html`; - } - - const colors = getThemeColors(opts.themeName); - const isLight = isLightTheme(opts.themeName); - const html = generateHtml(data, basename(inputPath), colors, isLight); - writeFileSync(outputPath, html, "utf8"); - return outputPath; -} diff --git a/packages/coding-agent/src/core/export-html/index.ts b/packages/coding-agent/src/core/export-html/index.ts new file mode 100644 index 000000000..577f262c3 --- /dev/null +++ b/packages/coding-agent/src/core/export-html/index.ts @@ -0,0 +1,211 @@ +import type { AgentState } from "@mariozechner/pi-agent-core"; +import { existsSync, readFileSync, writeFileSync } from "fs"; +import { basename, join } from "path"; +import { APP_NAME, getExportTemplateDir } from "../../config.js"; +import { getResolvedThemeColors, getThemeExportColors } from "../../modes/interactive/theme/theme.js"; +import { SessionManager } from "../session-manager.js"; + +export interface ExportOptions { + outputPath?: string; + themeName?: string; +} + +/** Parse a color string to RGB values. Supports hex (#RRGGBB) and rgb(r,g,b) formats. */ +function parseColor(color: string): { r: number; g: number; b: number } | undefined { + const hexMatch = color.match(/^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/); + if (hexMatch) { + return { + r: Number.parseInt(hexMatch[1], 16), + g: Number.parseInt(hexMatch[2], 16), + b: Number.parseInt(hexMatch[3], 16), + }; + } + const rgbMatch = color.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/); + if (rgbMatch) { + return { + r: Number.parseInt(rgbMatch[1], 10), + g: Number.parseInt(rgbMatch[2], 10), + b: Number.parseInt(rgbMatch[3], 10), + }; + } + return undefined; +} + +/** Calculate relative luminance of a color (0-1, higher = lighter). */ +function getLuminance(r: number, g: number, b: number): number { + const toLinear = (c: number) => { + const s = c / 255; + return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4; + }; + return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b); +} + +/** Adjust color brightness. Factor > 1 lightens, < 1 darkens. */ +function adjustBrightness(color: string, factor: number): string { + const parsed = parseColor(color); + if (!parsed) return color; + const adjust = (c: number) => Math.min(255, Math.max(0, Math.round(c * factor))); + return `rgb(${adjust(parsed.r)}, ${adjust(parsed.g)}, ${adjust(parsed.b)})`; +} + +/** Derive export background colors from a base color (e.g., userMessageBg). */ +function deriveExportColors(baseColor: string): { pageBg: string; cardBg: string; infoBg: string } { + const parsed = parseColor(baseColor); + if (!parsed) { + return { + pageBg: "rgb(24, 24, 30)", + cardBg: "rgb(30, 30, 36)", + infoBg: "rgb(60, 55, 40)", + }; + } + + const luminance = getLuminance(parsed.r, parsed.g, parsed.b); + const isLight = luminance > 0.5; + + if (isLight) { + return { + pageBg: adjustBrightness(baseColor, 0.96), + cardBg: baseColor, + infoBg: `rgb(${Math.min(255, parsed.r + 10)}, ${Math.min(255, parsed.g + 5)}, ${Math.max(0, parsed.b - 20)})`, + }; + } + return { + pageBg: adjustBrightness(baseColor, 0.7), + cardBg: adjustBrightness(baseColor, 0.85), + infoBg: `rgb(${Math.min(255, parsed.r + 20)}, ${Math.min(255, parsed.g + 15)}, ${parsed.b})`, + }; +} + +/** + * 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};`); + } + + // Use explicit theme export colors if available, otherwise derive from userMessageBg + const themeExport = getThemeExportColors(themeName); + const userMessageBg = colors.userMessageBg || "#343541"; + const derivedColors = deriveExportColors(userMessageBg); + + lines.push(`--exportPageBg: ${themeExport.pageBg ?? derivedColors.pageBg};`); + lines.push(`--exportCardBg: ${themeExport.cardBg ?? derivedColors.cardBg};`); + lines.push(`--exportInfoBg: ${themeExport.infoBg ?? derivedColors.infoBg};`); + + return lines.join("\n "); +} + +interface SessionData { + header: ReturnType; + entries: ReturnType; + 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 templateCss = readFileSync(join(templateDir, "template.css"), "utf-8"); + const templateJs = readFileSync(join(templateDir, "template.js"), "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 colors = getResolvedThemeColors(themeName); + const exportColors = deriveExportColors(colors.userMessageBg || "#343541"); + const bodyBg = exportColors.pageBg; + const containerBg = exportColors.cardBg; + const infoBg = exportColors.infoBg; + + // Base64 encode session data to avoid escaping issues + const sessionDataBase64 = Buffer.from(JSON.stringify(sessionData)).toString("base64"); + + // Build the CSS with theme variables injected + const css = templateCss + .replace("{{THEME_VARS}}", themeVars) + .replace("{{BODY_BG}}", bodyBg) + .replace("{{CONTAINER_BG}}", containerBg) + .replace("{{INFO_BG}}", infoBg); + + return template + .replace("{{CSS}}", css) + .replace("{{JS}}", templateJs) + .replace("{{SESSION_DATA}}", sessionDataBase64) + .replace("{{MARKED_JS}}", markedJs) + .replace("{{HIGHLIGHT_JS}}", hljsJs); +} + +/** + * 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; +} diff --git a/packages/coding-agent/src/core/export-html/template.css b/packages/coding-agent/src/core/export-html/template.css new file mode 100644 index 000000000..0763a3fdb --- /dev/null +++ b/packages/coding-agent/src/core/export-html/template.css @@ -0,0 +1,781 @@ + :root { + {{THEME_VARS}} + --body-bg: {{BODY_BG}}; + --container-bg: {{CONTAINER_BG}}; + --info-bg: {{INFO_BG}}; + } + + * { margin: 0; padding: 0; box-sizing: border-box; } + + :root { + --line-height: 18px; /* 12px font * 1.5 */ + } + + body { + font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace; + font-size: 12px; + line-height: var(--line-height); + color: var(--text); + background: var(--body-bg); + } + + #app { + display: flex; + min-height: 100vh; + } + + /* Sidebar */ + #sidebar { + width: 400px; + background: var(--container-bg); + flex-shrink: 0; + display: flex; + flex-direction: column; + position: sticky; + top: 0; + height: 100vh; + border-right: 1px solid var(--dim); + } + + .sidebar-header { + padding: 8px 12px; + flex-shrink: 0; + } + + .sidebar-controls { + padding: 8px 8px 4px 8px; + } + + .sidebar-search { + width: 100%; + box-sizing: border-box; + padding: 4px 8px; + font-size: 11px; + font-family: inherit; + background: var(--body-bg); + color: var(--text); + border: 1px solid var(--dim); + border-radius: 3px; + } + + .sidebar-filters { + display: flex; + padding: 4px 8px 8px 8px; + gap: 4px; + align-items: center; + flex-wrap: wrap; + } + + .sidebar-search:focus { + outline: none; + border-color: var(--accent); + } + + .sidebar-search::placeholder { + color: var(--muted); + } + + .filter-btn { + padding: 3px 8px; + font-size: 10px; + font-family: inherit; + background: transparent; + color: var(--muted); + border: 1px solid var(--dim); + border-radius: 3px; + cursor: pointer; + } + + .filter-btn:hover { + color: var(--text); + border-color: var(--text); + } + + .filter-btn.active { + background: var(--accent); + color: var(--body-bg); + border-color: var(--accent); + } + + .sidebar-close { + display: none; + padding: 3px 8px; + font-size: 12px; + font-family: inherit; + background: transparent; + color: var(--muted); + border: 1px solid var(--dim); + border-radius: 3px; + cursor: pointer; + margin-left: auto; + } + + .sidebar-close:hover { + color: var(--text); + border-color: var(--text); + } + + .tree-container { + flex: 1; + overflow: auto; + padding: 4px 0; + } + + .tree-node { + padding: 0 8px; + cursor: pointer; + display: flex; + align-items: baseline; + font-size: 11px; + line-height: 13px; + white-space: nowrap; + } + + .tree-node:hover { + background: var(--selectedBg); + } + + .tree-node.active { + background: var(--selectedBg); + } + + .tree-node.active .tree-content { + font-weight: bold; + } + + .tree-node.in-path { + } + + .tree-prefix { + color: var(--muted); + flex-shrink: 0; + font-family: monospace; + white-space: pre; + } + + .tree-marker { + color: var(--accent); + flex-shrink: 0; + } + + .tree-content { + color: var(--text); + } + + .tree-role-user { + color: var(--accent); + } + + .tree-role-assistant { + color: var(--success); + } + + .tree-role-tool { + color: var(--muted); + } + + .tree-muted { + color: var(--muted); + } + + .tree-error { + color: var(--error); + } + + .tree-compaction { + color: var(--borderAccent); + } + + .tree-branch-summary { + color: var(--warning); + } + + .tree-custom-message { + color: var(--customMessageLabel); + } + + .tree-status { + padding: 4px 12px; + font-size: 10px; + color: var(--muted); + flex-shrink: 0; + } + + /* Main content */ + #content { + flex: 1; + overflow-y: auto; + padding: var(--line-height) calc(var(--line-height) * 2); + display: flex; + flex-direction: column; + align-items: center; + } + + #content > * { + width: 100%; + max-width: 800px; + } + + /* Help bar */ + .help-bar { + font-size: 11px; + color: var(--warning); + margin-bottom: var(--line-height); + } + + /* Header */ + .header { + background: var(--container-bg); + border-radius: 4px; + padding: var(--line-height); + margin-bottom: var(--line-height); + } + + .header h1 { + font-size: 12px; + font-weight: bold; + color: var(--borderAccent); + margin-bottom: var(--line-height); + } + + .header-info { + display: flex; + flex-direction: column; + gap: 0; + font-size: 11px; + } + + .info-item { + color: var(--dim); + display: flex; + align-items: baseline; + } + + .info-label { + font-weight: 600; + margin-right: 8px; + min-width: 100px; + } + + .info-value { + color: var(--text); + flex: 1; + } + + /* Messages */ + #messages { + display: flex; + flex-direction: column; + gap: var(--line-height); + } + + .message-timestamp { + font-size: 10px; + color: var(--dim); + opacity: 0.8; + } + + .user-message { + background: var(--userMessageBg); + color: var(--userMessageText); + padding: var(--line-height); + border-radius: 4px; + } + + .assistant-message { + padding: 0; + } + + .assistant-message > .message-timestamp { + padding-left: var(--line-height); + } + + .assistant-text { + padding: var(--line-height); + padding-bottom: 0; + } + + .message-timestamp + .assistant-text, + .message-timestamp + .thinking-block { + padding-top: 0; + } + + .thinking-block + .assistant-text { + padding-top: 0; + } + + .thinking-text { + padding: var(--line-height); + color: var(--thinkingText); + font-style: italic; + white-space: pre-wrap; + } + + .message-timestamp + .thinking-block .thinking-text, + .message-timestamp + .thinking-block .thinking-collapsed { + padding-top: 0; + } + + .thinking-collapsed { + display: none; + padding: var(--line-height); + color: var(--thinkingText); + font-style: italic; + } + + /* Tool execution */ + .tool-execution { + padding: var(--line-height); + border-radius: 4px; + } + + .tool-execution + .tool-execution { + margin-top: var(--line-height); + } + + .tool-execution.pending { background: var(--toolPendingBg); } + .tool-execution.success { background: var(--toolSuccessBg); } + .tool-execution.error { background: var(--toolErrorBg); } + + .tool-header, .tool-name { + font-weight: bold; + } + + .tool-path { + color: var(--accent); + word-break: break-all; + } + + .line-numbers { + color: var(--warning); + } + + .line-count { + color: var(--dim); + } + + .tool-command { + font-weight: bold; + white-space: pre-wrap; + word-wrap: break-word; + overflow-wrap: break-word; + word-break: break-word; + } + + .tool-output { + margin-top: var(--line-height); + color: var(--toolOutput); + word-wrap: break-word; + overflow-wrap: break-word; + word-break: break-word; + font-family: inherit; + overflow-x: auto; + } + + .tool-output > div, + .output-preview, + .output-full { + margin: 0; + padding: 0; + line-height: var(--line-height); + } + + .tool-output pre { + margin: 0; + padding: 0; + font-family: inherit; + color: inherit; + white-space: pre-wrap; + word-wrap: break-word; + overflow-wrap: break-word; + } + + .tool-output code { + padding: 0; + background: none; + color: var(--text); + } + + .tool-output.expandable { + cursor: pointer; + } + + .tool-output.expandable:hover { + opacity: 0.9; + } + + .tool-output.expandable .output-full { + display: none; + } + + .tool-output.expandable.expanded .output-preview { + display: none; + } + + .tool-output.expandable.expanded .output-full { + display: block; + } + + .tool-images { + } + + .tool-image { + max-width: 100%; + max-height: 500px; + border-radius: 4px; + margin: var(--line-height) 0; + } + + .expand-hint { + color: var(--toolOutput); + } + + /* Diff */ + .tool-diff { + font-size: 11px; + overflow-x: auto; + white-space: pre; + } + + .diff-added { color: var(--toolDiffAdded); } + .diff-removed { color: var(--toolDiffRemoved); } + .diff-context { color: var(--toolDiffContext); } + + /* Model change */ + .model-change { + padding: 0 var(--line-height); + color: var(--dim); + font-size: 11px; + } + + .model-name { + color: var(--borderAccent); + font-weight: bold; + } + + /* Compaction / Branch Summary - matches customMessage colors from TUI */ + .compaction { + background: var(--customMessageBg); + border-radius: 4px; + padding: var(--line-height); + cursor: pointer; + } + + .compaction-label { + color: var(--customMessageLabel); + font-weight: bold; + } + + .compaction-collapsed { + color: var(--customMessageText); + } + + .compaction-content { + display: none; + color: var(--customMessageText); + white-space: pre-wrap; + margin-top: var(--line-height); + } + + .compaction.expanded .compaction-collapsed { + display: none; + } + + .compaction.expanded .compaction-content { + display: block; + } + + /* System prompt */ + .system-prompt { + background: var(--customMessageBg); + padding: var(--line-height); + border-radius: 4px; + margin-bottom: var(--line-height); + } + + .system-prompt-header { + font-weight: bold; + color: var(--customMessageLabel); + } + + .system-prompt-content { + color: var(--customMessageText); + white-space: pre-wrap; + word-wrap: break-word; + font-size: 11px; + max-height: 200px; + overflow-y: auto; + margin-top: var(--line-height); + } + + /* Tools list */ + .tools-list { + background: var(--customMessageBg); + padding: var(--line-height); + border-radius: 4px; + margin-bottom: var(--line-height); + } + + .tools-header { + font-weight: bold; + color: var(--warning); + margin-bottom: var(--line-height); + } + + .tool-item { + font-size: 11px; + } + + .tool-item-name { + font-weight: bold; + color: var(--text); + } + + .tool-item-desc { + color: var(--dim); + } + + /* Hook/custom messages */ + .hook-message { + background: var(--customMessageBg); + color: var(--customMessageText); + padding: var(--line-height); + border-radius: 4px; + } + + .hook-type { + color: var(--customMessageLabel); + font-weight: bold; + } + + /* Branch summary */ + .branch-summary { + background: var(--customMessageBg); + padding: var(--line-height); + border-radius: 4px; + } + + .branch-summary-header { + font-weight: bold; + color: var(--borderAccent); + } + + /* Error */ + .error-text { + color: var(--error); + padding: 0 var(--line-height); + } + + /* Images */ + .message-images { + margin-bottom: 12px; + } + + .message-image { + max-width: 100%; + max-height: 400px; + border-radius: 4px; + margin: var(--line-height) 0; + } + + /* Markdown content */ + .markdown-content h1, + .markdown-content h2, + .markdown-content h3, + .markdown-content h4, + .markdown-content h5, + .markdown-content h6 { + color: var(--mdHeading); + margin: var(--line-height) 0 0 0; + font-weight: bold; + } + + .markdown-content h1 { font-size: 1em; } + .markdown-content h2 { font-size: 1em; } + .markdown-content h3 { font-size: 1em; } + .markdown-content h4 { font-size: 1em; } + .markdown-content h5 { font-size: 1em; } + .markdown-content h6 { font-size: 1em; } + .markdown-content p { margin: 0; } + .markdown-content p + p { margin-top: var(--line-height); } + + .markdown-content a { + color: var(--mdLink); + text-decoration: underline; + } + + .markdown-content code { + background: rgba(128, 128, 128, 0.2); + color: var(--mdCode); + padding: 0 4px; + border-radius: 3px; + font-family: inherit; + } + + .markdown-content pre { + background: transparent; + margin: var(--line-height) 0; + overflow-x: auto; + } + + .markdown-content pre code { + display: block; + background: none; + color: var(--text); + } + + .markdown-content blockquote { + border-left: 3px solid var(--mdQuoteBorder); + padding-left: var(--line-height); + margin: var(--line-height) 0; + color: var(--mdQuote); + font-style: italic; + } + + .markdown-content ul, + .markdown-content ol { + margin: var(--line-height) 0; + padding-left: calc(var(--line-height) * 2); + } + + .markdown-content li { margin: 0; } + .markdown-content li::marker { color: var(--mdListBullet); } + + .markdown-content hr { + border: none; + border-top: 1px solid var(--mdHr); + margin: var(--line-height) 0; + } + + .markdown-content table { + border-collapse: collapse; + margin: 0.5em 0; + width: 100%; + } + + .markdown-content th, + .markdown-content td { + border: 1px solid var(--mdCodeBlockBorder); + padding: 6px 10px; + text-align: left; + } + + .markdown-content th { + background: rgba(128, 128, 128, 0.1); + font-weight: bold; + } + + .markdown-content img { + max-width: 100%; + border-radius: 4px; + } + + /* Syntax highlighting */ + .hljs { background: transparent; color: var(--text); } + .hljs-comment, .hljs-quote { color: var(--syntaxComment); } + .hljs-keyword, .hljs-selector-tag { color: var(--syntaxKeyword); } + .hljs-number, .hljs-literal { color: var(--syntaxNumber); } + .hljs-string, .hljs-doctag { color: var(--syntaxString); } + /* Function names: hljs v11 uses .hljs-title.function_ compound class */ + .hljs-function, .hljs-title, .hljs-title.function_, .hljs-section, .hljs-name { color: var(--syntaxFunction); } + /* Types: hljs v11 uses .hljs-title.class_ for class names */ + .hljs-type, .hljs-class, .hljs-title.class_, .hljs-built_in { color: var(--syntaxType); } + .hljs-attr, .hljs-variable, .hljs-variable.language_, .hljs-params, .hljs-property { color: var(--syntaxVariable); } + .hljs-meta, .hljs-meta .hljs-keyword, .hljs-meta .hljs-string { color: var(--syntaxKeyword); } + .hljs-operator { color: var(--syntaxOperator); } + .hljs-punctuation { color: var(--syntaxPunctuation); } + .hljs-subst { color: var(--text); } + + /* Footer */ + .footer { + margin-top: 48px; + padding: 20px; + text-align: center; + color: var(--dim); + font-size: 10px; + } + + /* Mobile */ + #hamburger { + display: none; + position: fixed; + top: 10px; + left: 10px; + z-index: 100; + padding: 3px 8px; + font-size: 12px; + font-family: inherit; + background: transparent; + color: var(--muted); + border: 1px solid var(--dim); + border-radius: 3px; + cursor: pointer; + } + + #hamburger:hover { + color: var(--text); + border-color: var(--text); + } + + + + #sidebar-overlay { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 98; + } + + @media (max-width: 900px) { + #sidebar { + position: fixed; + left: -400px; + width: 400px; + top: 0; + bottom: 0; + height: 100vh; + z-index: 99; + transition: left 0.3s; + } + + #sidebar.open { + left: 0; + } + + #sidebar-overlay.open { + display: block; + } + + #hamburger { + display: block; + } + + .sidebar-close { + display: block; + } + + #content { + padding: var(--line-height) 16px; + } + + #content > * { + max-width: 100%; + } + } + + @media (max-width: 500px) { + #sidebar { + width: 100vw; + left: -100vw; + } + } + + @media print { + #sidebar, #sidebar-toggle { display: none !important; } + body { background: white; color: black; } + #content { max-width: none; } + } diff --git a/packages/coding-agent/src/core/export-html/template.html b/packages/coding-agent/src/core/export-html/template.html new file mode 100644 index 000000000..42f2a45b0 --- /dev/null +++ b/packages/coding-agent/src/core/export-html/template.html @@ -0,0 +1,54 @@ + + + + + + Session Export + + + + + +
+ +
+
+
+
+
+ +
+
+ + + + + + + + + + + + + diff --git a/packages/coding-agent/src/core/export-html/template.js b/packages/coding-agent/src/core/export-html/template.js new file mode 100644 index 000000000..b6137a9b8 --- /dev/null +++ b/packages/coding-agent/src/core/export-html/template.js @@ -0,0 +1,1185 @@ + (function() { + 'use strict'; + + // ============================================================ + // DATA LOADING + // ============================================================ + + const base64 = document.getElementById('session-data').textContent; + const binary = atob(base64); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + const data = JSON.parse(new TextDecoder('utf-8').decode(bytes)); + const { header, entries, leafId, systemPrompt, tools } = data; + + // ============================================================ + // DATA STRUCTURES + // ============================================================ + + // Entry lookup by ID + const byId = new Map(); + for (const entry of entries) { + byId.set(entry.id, entry); + } + + // Tool call lookup (toolCallId -> {name, arguments}) + const toolCallMap = new Map(); + for (const entry of entries) { + if (entry.type === 'message' && entry.message.role === 'assistant') { + const content = entry.message.content; + if (Array.isArray(content)) { + for (const block of content) { + if (block.type === 'toolCall') { + toolCallMap.set(block.id, { name: block.name, arguments: block.arguments }); + } + } + } + } + } + + // Label lookup (entryId -> label string) + // Labels are stored in 'label' entries that reference their target via parentId + const labelMap = new Map(); + for (const entry of entries) { + if (entry.type === 'label' && entry.parentId && entry.label) { + labelMap.set(entry.parentId, entry.label); + } + } + + // ============================================================ + // TREE DATA PREPARATION (no DOM, pure data) + // ============================================================ + + /** + * Build tree structure from flat entries. + * Returns array of root nodes, each with { entry, children, label }. + */ + function buildTree() { + const nodeMap = new Map(); + const roots = []; + + // Create nodes + for (const entry of entries) { + nodeMap.set(entry.id, { + entry, + children: [], + label: labelMap.get(entry.id) + }); + } + + // Build parent-child relationships + for (const entry of entries) { + const node = nodeMap.get(entry.id); + if (entry.parentId === null || entry.parentId === undefined || entry.parentId === entry.id) { + roots.push(node); + } else { + const parent = nodeMap.get(entry.parentId); + if (parent) { + parent.children.push(node); + } else { + roots.push(node); + } + } + } + + // Sort children by timestamp + function sortChildren(node) { + node.children.sort((a, b) => + new Date(a.entry.timestamp).getTime() - new Date(b.entry.timestamp).getTime() + ); + node.children.forEach(sortChildren); + } + roots.forEach(sortChildren); + + return roots; + } + + /** + * Build set of entry IDs on path from root to target. + */ + function buildActivePathIds(targetId) { + const ids = new Set(); + let current = byId.get(targetId); + while (current) { + ids.add(current.id); + // Stop if no parent or self-referencing (root) + if (!current.parentId || current.parentId === current.id) { + break; + } + current = byId.get(current.parentId); + } + return ids; + } + + /** + * Get array of entries from root to target (the conversation path). + */ + function getPath(targetId) { + const path = []; + let current = byId.get(targetId); + while (current) { + path.unshift(current); + // Stop if no parent or self-referencing (root) + if (!current.parentId || current.parentId === current.id) { + break; + } + current = byId.get(current.parentId); + } + return path; + } + + /** + * Flatten tree into list with indentation and connector info. + * Returns array of { node, indent, showConnector, isLast, gutters, isVirtualRootChild, multipleRoots }. + * Matches tree-selector.ts logic exactly. + */ + function flattenTree(roots, activePathIds) { + const result = []; + const multipleRoots = roots.length > 1; + + // Mark which subtrees contain the active leaf + const containsActive = new Map(); + function markActive(node) { + let has = activePathIds.has(node.entry.id); + for (const child of node.children) { + if (markActive(child)) has = true; + } + containsActive.set(node, has); + return has; + } + roots.forEach(markActive); + + // Stack: [node, indent, justBranched, showConnector, isLast, gutters, isVirtualRootChild] + const stack = []; + + // Add roots (prioritize branch containing active leaf) + const orderedRoots = [...roots].sort((a, b) => + Number(containsActive.get(b)) - Number(containsActive.get(a)) + ); + for (let i = orderedRoots.length - 1; i >= 0; i--) { + const isLast = i === orderedRoots.length - 1; + stack.push([orderedRoots[i], multipleRoots ? 1 : 0, multipleRoots, multipleRoots, isLast, [], multipleRoots]); + } + + while (stack.length > 0) { + const [node, indent, justBranched, showConnector, isLast, gutters, isVirtualRootChild] = stack.pop(); + + result.push({ node, indent, showConnector, isLast, gutters, isVirtualRootChild, multipleRoots }); + + const children = node.children; + const multipleChildren = children.length > 1; + + // Order children (active branch first) + const orderedChildren = [...children].sort((a, b) => + Number(containsActive.get(b)) - Number(containsActive.get(a)) + ); + + // Calculate child indent (matches tree-selector.ts) + let childIndent; + if (multipleChildren) { + // Parent branches: children get +1 + childIndent = indent + 1; + } else if (justBranched && indent > 0) { + // First generation after a branch: +1 for visual grouping + childIndent = indent + 1; + } else { + // Single-child chain: stay flat + childIndent = indent; + } + + // Build gutters for children + const connectorDisplayed = showConnector && !isVirtualRootChild; + const currentDisplayIndent = multipleRoots ? Math.max(0, indent - 1) : indent; + const connectorPosition = Math.max(0, currentDisplayIndent - 1); + const childGutters = connectorDisplayed + ? [...gutters, { position: connectorPosition, show: !isLast }] + : gutters; + + // Add children in reverse order for stack + for (let i = orderedChildren.length - 1; i >= 0; i--) { + const childIsLast = i === orderedChildren.length - 1; + stack.push([orderedChildren[i], childIndent, multipleChildren, multipleChildren, childIsLast, childGutters, false]); + } + } + + return result; + } + + /** + * Build ASCII prefix string for tree node. + */ + function buildTreePrefix(flatNode) { + const { indent, showConnector, isLast, gutters, isVirtualRootChild, multipleRoots } = flatNode; + const displayIndent = multipleRoots ? Math.max(0, indent - 1) : indent; + const connector = showConnector && !isVirtualRootChild ? (isLast ? '└─ ' : '├─ ') : ''; + const connectorPosition = connector ? displayIndent - 1 : -1; + + const totalChars = displayIndent * 3; + const prefixChars = []; + for (let i = 0; i < totalChars; i++) { + const level = Math.floor(i / 3); + const posInLevel = i % 3; + + const gutter = gutters.find(g => g.position === level); + if (gutter) { + prefixChars.push(posInLevel === 0 ? (gutter.show ? '│' : ' ') : ' '); + } else if (connector && level === connectorPosition) { + if (posInLevel === 0) { + prefixChars.push(isLast ? '└' : '├'); + } else if (posInLevel === 1) { + prefixChars.push('─'); + } else { + prefixChars.push(' '); + } + } else { + prefixChars.push(' '); + } + } + return prefixChars.join(''); + } + + // ============================================================ + // FILTERING (pure data) + // ============================================================ + + let filterMode = 'default'; + let searchQuery = ''; + + function hasTextContent(content) { + if (typeof content === 'string') return content.trim().length > 0; + if (Array.isArray(content)) { + for (const c of content) { + if (c.type === 'text' && c.text && c.text.trim().length > 0) return true; + } + } + return false; + } + + function extractContent(content) { + if (typeof content === 'string') return content; + if (Array.isArray(content)) { + return content + .filter(c => c.type === 'text' && c.text) + .map(c => c.text) + .join(''); + } + return ''; + } + + function getSearchableText(entry, label) { + const parts = []; + if (label) parts.push(label); + + switch (entry.type) { + case 'message': { + const msg = entry.message; + parts.push(msg.role); + if (msg.content) parts.push(extractContent(msg.content)); + if (msg.role === 'bashExecution' && msg.command) parts.push(msg.command); + break; + } + case 'custom_message': + parts.push(entry.customType); + parts.push(typeof entry.content === 'string' ? entry.content : extractContent(entry.content)); + break; + case 'compaction': + parts.push('compaction'); + break; + case 'branch_summary': + parts.push('branch summary', entry.summary); + break; + case 'model_change': + parts.push('model', entry.modelId); + break; + case 'thinking_level_change': + parts.push('thinking', entry.thinkingLevel); + break; + } + + return parts.join(' ').toLowerCase(); + } + + /** + * Filter flat nodes based on current filterMode and searchQuery. + */ + function filterNodes(flatNodes, currentLeafId) { + const searchTokens = searchQuery.toLowerCase().split(/\s+/).filter(Boolean); + + return flatNodes.filter(flatNode => { + const entry = flatNode.node.entry; + const label = flatNode.node.label; + const isCurrentLeaf = entry.id === currentLeafId; + + // Always show current leaf + if (isCurrentLeaf) return true; + + // Hide assistant messages with only tool calls (no text) unless error/aborted + if (entry.type === 'message' && entry.message.role === 'assistant') { + const msg = entry.message; + const hasText = hasTextContent(msg.content); + const isErrorOrAborted = msg.stopReason && msg.stopReason !== 'stop' && msg.stopReason !== 'toolUse'; + if (!hasText && !isErrorOrAborted) return false; + } + + // Apply filter mode + const isSettingsEntry = ['label', 'custom', 'model_change', 'thinking_level_change'].includes(entry.type); + let passesFilter = true; + + switch (filterMode) { + case 'user-only': + passesFilter = entry.type === 'message' && entry.message.role === 'user'; + break; + case 'no-tools': + passesFilter = !isSettingsEntry && !(entry.type === 'message' && entry.message.role === 'toolResult'); + break; + case 'labeled-only': + passesFilter = label !== undefined; + break; + case 'all': + passesFilter = true; + break; + default: // 'default' + passesFilter = !isSettingsEntry; + break; + } + + if (!passesFilter) return false; + + // Apply search filter + if (searchTokens.length > 0) { + const nodeText = getSearchableText(entry, label); + if (!searchTokens.every(t => nodeText.includes(t))) return false; + } + + return true; + }); + } + + // ============================================================ + // TREE DISPLAY TEXT (pure data -> string) + // ============================================================ + + function shortenPath(p) { + if (p.startsWith('/Users/')) { + const parts = p.split('/'); + if (parts.length > 2) return '~' + p.slice(('/Users/' + parts[2]).length); + } + if (p.startsWith('/home/')) { + const parts = p.split('/'); + if (parts.length > 2) return '~' + p.slice(('/home/' + parts[2]).length); + } + return p; + } + + function formatToolCall(name, args) { + switch (name) { + case 'read': { + const path = shortenPath(String(args.path || args.file_path || '')); + const offset = args.offset; + const limit = args.limit; + let display = path; + if (offset !== undefined || limit !== undefined) { + const start = offset ?? 1; + const end = limit !== undefined ? start + limit - 1 : ''; + display += `:${start}${end ? `-${end}` : ''}`; + } + return `[read: ${display}]`; + } + case 'write': + return `[write: ${shortenPath(String(args.path || args.file_path || ''))}]`; + case 'edit': + return `[edit: ${shortenPath(String(args.path || args.file_path || ''))}]`; + case 'bash': { + const rawCmd = String(args.command || ''); + const cmd = rawCmd.replace(/[\n\t]/g, ' ').trim().slice(0, 50); + return `[bash: ${cmd}${rawCmd.length > 50 ? '...' : ''}]`; + } + case 'grep': + return `[grep: /${args.pattern || ''}/ in ${shortenPath(String(args.path || '.'))}]`; + case 'find': + return `[find: ${args.pattern || ''} in ${shortenPath(String(args.path || '.'))}]`; + case 'ls': + return `[ls: ${shortenPath(String(args.path || '.'))}]`; + default: { + const argsStr = JSON.stringify(args).slice(0, 40); + return `[${name}: ${argsStr}${JSON.stringify(args).length > 40 ? '...' : ''}]`; + } + } + } + + function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + + /** + * Truncate string to maxLen chars, append "..." if truncated. + */ + function truncate(s, maxLen = 100) { + if (s.length <= maxLen) return s; + return s.slice(0, maxLen) + '...'; + } + + /** + * Get display text for tree node (returns HTML string). + */ + function getTreeNodeDisplayHtml(entry, label) { + const normalize = s => s.replace(/[\n\t]/g, ' ').trim(); + const labelHtml = label ? `[${escapeHtml(label)}] ` : ''; + + switch (entry.type) { + case 'message': { + const msg = entry.message; + if (msg.role === 'user') { + const content = truncate(normalize(extractContent(msg.content))); + return labelHtml + `user: ${escapeHtml(content)}`; + } + if (msg.role === 'assistant') { + const textContent = truncate(normalize(extractContent(msg.content))); + if (textContent) { + return labelHtml + `assistant: ${escapeHtml(textContent)}`; + } + if (msg.stopReason === 'aborted') { + return labelHtml + `assistant: (aborted)`; + } + if (msg.errorMessage) { + return labelHtml + `assistant: ${escapeHtml(truncate(msg.errorMessage))}`; + } + return labelHtml + `assistant: (no text)`; + } + if (msg.role === 'toolResult') { + const toolCall = msg.toolCallId ? toolCallMap.get(msg.toolCallId) : null; + if (toolCall) { + return labelHtml + `${escapeHtml(formatToolCall(toolCall.name, toolCall.arguments))}`; + } + return labelHtml + `[${msg.toolName || 'tool'}]`; + } + if (msg.role === 'bashExecution') { + const cmd = truncate(normalize(msg.command || '')); + return labelHtml + `[bash]: ${escapeHtml(cmd)}`; + } + return labelHtml + `[${msg.role}]`; + } + case 'compaction': + return labelHtml + `[compaction: ${Math.round(entry.tokensBefore/1000)}k tokens]`; + case 'branch_summary': { + const summary = truncate(normalize(entry.summary || '')); + return labelHtml + `[branch summary]: ${escapeHtml(summary)}`; + } + case 'custom_message': { + const content = typeof entry.content === 'string' ? entry.content : extractContent(entry.content); + return labelHtml + `[${escapeHtml(entry.customType)}]: ${escapeHtml(truncate(normalize(content)))}`; + } + case 'model_change': + return labelHtml + `[model: ${entry.modelId}]`; + case 'thinking_level_change': + return labelHtml + `[thinking: ${entry.thinkingLevel}]`; + default: + return labelHtml + `[${entry.type}]`; + } + } + + // ============================================================ + // TREE RENDERING (DOM manipulation) + // ============================================================ + + let currentLeafId = leafId; + let treeRendered = false; + + function renderTree() { + const tree = buildTree(); + const activePathIds = buildActivePathIds(currentLeafId); + const flatNodes = flattenTree(tree, activePathIds); + const filtered = filterNodes(flatNodes, currentLeafId); + const container = document.getElementById('tree-container'); + + // Full render only on first call or when filter/search changes + if (!treeRendered) { + container.innerHTML = ''; + + for (const flatNode of filtered) { + const entry = flatNode.node.entry; + const isOnPath = activePathIds.has(entry.id); + const isLeaf = entry.id === currentLeafId; + + const div = document.createElement('div'); + div.className = 'tree-node'; + if (isOnPath) div.classList.add('in-path'); + if (isLeaf) div.classList.add('active'); + div.dataset.id = entry.id; + + const prefix = buildTreePrefix(flatNode); + const prefixSpan = document.createElement('span'); + prefixSpan.className = 'tree-prefix'; + prefixSpan.textContent = prefix; + + const marker = document.createElement('span'); + marker.className = 'tree-marker'; + marker.textContent = isOnPath ? '•' : ' '; + + const content = document.createElement('span'); + content.className = 'tree-content'; + content.innerHTML = getTreeNodeDisplayHtml(entry, flatNode.node.label); + + div.appendChild(prefixSpan); + div.appendChild(marker); + div.appendChild(content); + div.addEventListener('click', () => navigateTo(entry.id)); + + container.appendChild(div); + } + + treeRendered = true; + } else { + // Just update markers and classes + const nodes = container.querySelectorAll('.tree-node'); + for (const node of nodes) { + const id = node.dataset.id; + const isOnPath = activePathIds.has(id); + const isLeaf = id === currentLeafId; + + node.classList.toggle('in-path', isOnPath); + node.classList.toggle('active', isLeaf); + + const marker = node.querySelector('.tree-marker'); + if (marker) { + marker.textContent = isOnPath ? '•' : ' '; + } + } + } + + document.getElementById('tree-status').textContent = `${filtered.length} / ${flatNodes.length} entries`; + + // Scroll active node into view after layout + setTimeout(() => { + const activeNode = container.querySelector('.tree-node.active'); + if (activeNode) { + activeNode.scrollIntoView({ block: 'nearest' }); + } + }, 0); + } + + function forceTreeRerender() { + treeRendered = false; + renderTree(); + } + + // ============================================================ + // MESSAGE RENDERING + // ============================================================ + + function formatTokens(count) { + if (count < 1000) return count.toString(); + if (count < 10000) return (count / 1000).toFixed(1) + 'k'; + if (count < 1000000) return Math.round(count / 1000) + 'k'; + return (count / 1000000).toFixed(1) + 'M'; + } + + function formatTimestamp(ts) { + if (!ts) return ''; + const date = new Date(ts); + return date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', second: '2-digit' }); + } + + function replaceTabs(text) { + return text.replace(/\t/g, ' '); + } + + function getLanguageFromPath(filePath) { + const ext = filePath.split('.').pop()?.toLowerCase(); + const extToLang = { + ts: 'typescript', tsx: 'typescript', js: 'javascript', jsx: 'javascript', + py: 'python', rb: 'ruby', rs: 'rust', go: 'go', java: 'java', + c: 'c', cpp: 'cpp', h: 'c', hpp: 'cpp', cs: 'csharp', + php: 'php', sh: 'bash', bash: 'bash', zsh: 'bash', + sql: 'sql', html: 'html', css: 'css', scss: 'scss', + json: 'json', yaml: 'yaml', yml: 'yaml', xml: 'xml', + md: 'markdown', dockerfile: 'dockerfile' + }; + return extToLang[ext]; + } + + function findToolResult(toolCallId) { + for (const entry of entries) { + if (entry.type === 'message' && entry.message.role === 'toolResult') { + if (entry.message.toolCallId === toolCallId) { + return entry.message; + } + } + } + return null; + } + + function formatExpandableOutput(text, maxLines, lang) { + text = replaceTabs(text); + const lines = text.split('\n'); + const displayLines = lines.slice(0, maxLines); + const remaining = lines.length - maxLines; + + if (lang) { + let highlighted; + try { + highlighted = hljs.highlight(text, { language: lang }).value; + } catch { + highlighted = escapeHtml(text); + } + + if (remaining > 0) { + const previewCode = displayLines.join('\n'); + let previewHighlighted; + try { + previewHighlighted = hljs.highlight(previewCode, { language: lang }).value; + } catch { + previewHighlighted = escapeHtml(previewCode); + } + + return ``; + } + + return `
${highlighted}
`; + } + + // Plain text output + if (remaining > 0) { + let out = ''; + return out; + } + + let out = '
'; + for (const line of displayLines) { + out += `
${escapeHtml(replaceTabs(line))}
`; + } + out += '
'; + return out; + } + + function renderToolCall(call) { + const result = findToolResult(call.id); + const isError = result?.isError || false; + const statusClass = result ? (isError ? 'error' : 'success') : 'pending'; + + const getResultText = () => { + if (!result) return ''; + const textBlocks = result.content.filter(c => c.type === 'text'); + return textBlocks.map(c => c.text).join('\n'); + }; + + const getResultImages = () => { + if (!result) return []; + return result.content.filter(c => c.type === 'image'); + }; + + const renderResultImages = () => { + const images = getResultImages(); + if (images.length === 0) return ''; + return '
' + + images.map(img => ``).join('') + + '
'; + }; + + let html = `
`; + const args = call.arguments || {}; + const name = call.name; + + switch (name) { + case 'bash': { + const command = args.command || ''; + html += `
$ ${escapeHtml(command)}
`; + if (result) { + const output = getResultText().trim(); + if (output) html += formatExpandableOutput(output, 5); + } + break; + } + case 'read': { + const filePath = args.file_path || args.path || ''; + const offset = args.offset; + const limit = args.limit; + const lang = getLanguageFromPath(filePath); + + let pathHtml = escapeHtml(shortenPath(filePath)); + if (offset !== undefined || limit !== undefined) { + const startLine = offset ?? 1; + const endLine = limit !== undefined ? startLine + limit - 1 : ''; + pathHtml += `:${startLine}${endLine ? '-' + endLine : ''}`; + } + + html += `
read ${pathHtml}
`; + if (result) { + html += renderResultImages(); + const output = getResultText(); + if (output) html += formatExpandableOutput(output, 10, lang); + } + break; + } + case 'write': { + const filePath = args.file_path || args.path || ''; + const content = args.content || ''; + const lines = content.split('\n'); + const lang = getLanguageFromPath(filePath); + + html += `
write ${escapeHtml(shortenPath(filePath))}`; + if (lines.length > 10) html += ` (${lines.length} lines)`; + html += '
'; + + if (content) html += formatExpandableOutput(content, 10, lang); + if (result) { + const output = getResultText().trim(); + if (output) html += `
${escapeHtml(output)}
`; + } + break; + } + case 'edit': { + const filePath = args.file_path || args.path || ''; + html += `
edit ${escapeHtml(shortenPath(filePath))}
`; + + if (result?.details?.diff) { + const diffLines = result.details.diff.split('\n'); + html += '
'; + for (const line of diffLines) { + const cls = line.match(/^\+/) ? 'diff-added' : line.match(/^-/) ? 'diff-removed' : 'diff-context'; + html += `
${escapeHtml(replaceTabs(line))}
`; + } + html += '
'; + } else if (result) { + const output = getResultText().trim(); + if (output) html += `
${escapeHtml(output)}
`; + } + break; + } + default: { + html += `
${escapeHtml(name)}
`; + html += `
${escapeHtml(JSON.stringify(args, null, 2))}
`; + if (result) { + const output = getResultText(); + if (output) html += formatExpandableOutput(output, 10); + } + } + } + + html += '
'; + return html; + } + + function renderEntry(entry) { + const ts = formatTimestamp(entry.timestamp); + const tsHtml = ts ? `
${ts}
` : ''; + const entryId = `entry-${entry.id}`; + + if (entry.type === 'message') { + const msg = entry.message; + + if (msg.role === 'user') { + let html = `
${tsHtml}`; + const content = msg.content; + + if (Array.isArray(content)) { + const images = content.filter(c => c.type === 'image'); + if (images.length > 0) { + html += '
'; + for (const img of images) { + html += ``; + } + html += '
'; + } + } + + const text = typeof content === 'string' ? content : + content.filter(c => c.type === 'text').map(c => c.text).join('\n'); + if (text.trim()) { + html += `
${safeMarkedParse(text)}
`; + } + html += '
'; + return html; + } + + if (msg.role === 'assistant') { + let html = `
${tsHtml}`; + + for (const block of msg.content) { + if (block.type === 'text' && block.text.trim()) { + html += `
${safeMarkedParse(block.text)}
`; + } else if (block.type === 'thinking' && block.thinking.trim()) { + html += `
+
${escapeHtml(block.thinking)}
+
Thinking ...
+
`; + } + } + + for (const block of msg.content) { + if (block.type === 'toolCall') { + html += renderToolCall(block); + } + } + + if (msg.stopReason === 'aborted') { + html += '
Aborted
'; + } else if (msg.stopReason === 'error') { + html += `
Error: ${escapeHtml(msg.errorMessage || 'Unknown error')}
`; + } + + html += '
'; + return html; + } + + if (msg.role === 'bashExecution') { + const isError = msg.cancelled || (msg.exitCode !== 0 && msg.exitCode !== null); + let html = `
${tsHtml}`; + html += `
$ ${escapeHtml(msg.command)}
`; + if (msg.output) html += formatExpandableOutput(msg.output, 10); + if (msg.cancelled) { + html += '
(cancelled)
'; + } else if (msg.exitCode !== 0 && msg.exitCode !== null) { + html += `
(exit ${msg.exitCode})
`; + } + html += '
'; + return html; + } + + if (msg.role === 'toolResult') return ''; + } + + if (entry.type === 'model_change') { + return `
${tsHtml}Switched to model: ${escapeHtml(entry.provider)}/${escapeHtml(entry.modelId)}
`; + } + + if (entry.type === 'compaction') { + return `
+
[compaction]
+
Compacted from ${entry.tokensBefore.toLocaleString()} tokens
+
Compacted from ${entry.tokensBefore.toLocaleString()} tokens\n\n${escapeHtml(entry.summary)}
+
`; + } + + if (entry.type === 'branch_summary') { + return `
${tsHtml} +
Branch Summary
+
${safeMarkedParse(entry.summary)}
+
`; + } + + if (entry.type === 'custom_message' && entry.display) { + return `
${tsHtml} +
[${escapeHtml(entry.customType)}]
+
${safeMarkedParse(typeof entry.content === 'string' ? entry.content : JSON.stringify(entry.content))}
+
`; + } + + return ''; + } + + // ============================================================ + // HEADER / STATS + // ============================================================ + + function computeStats(entryList) { + let userMessages = 0, assistantMessages = 0, toolResults = 0; + let customMessages = 0, compactions = 0, branchSummaries = 0, toolCalls = 0; + const tokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }; + const cost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }; + const models = new Set(); + + for (const entry of entryList) { + if (entry.type === 'message') { + const msg = entry.message; + if (msg.role === 'user') userMessages++; + if (msg.role === 'assistant') { + assistantMessages++; + if (msg.model) models.add(msg.provider ? `${msg.provider}/${msg.model}` : msg.model); + if (msg.usage) { + tokens.input += msg.usage.input || 0; + tokens.output += msg.usage.output || 0; + tokens.cacheRead += msg.usage.cacheRead || 0; + tokens.cacheWrite += msg.usage.cacheWrite || 0; + if (msg.usage.cost) { + cost.input += msg.usage.cost.input || 0; + cost.output += msg.usage.cost.output || 0; + cost.cacheRead += msg.usage.cost.cacheRead || 0; + cost.cacheWrite += msg.usage.cost.cacheWrite || 0; + } + } + toolCalls += msg.content.filter(c => c.type === 'toolCall').length; + } + if (msg.role === 'toolResult') toolResults++; + } else if (entry.type === 'compaction') { + compactions++; + } else if (entry.type === 'branch_summary') { + branchSummaries++; + } else if (entry.type === 'custom_message') { + customMessages++; + } + } + + return { userMessages, assistantMessages, toolResults, customMessages, compactions, branchSummaries, toolCalls, tokens, cost, models: Array.from(models) }; + } + + const globalStats = computeStats(entries); + + function renderHeader() { + const totalCost = globalStats.cost.input + globalStats.cost.output + globalStats.cost.cacheRead + globalStats.cost.cacheWrite; + + const tokenParts = []; + if (globalStats.tokens.input) tokenParts.push(`↑${formatTokens(globalStats.tokens.input)}`); + if (globalStats.tokens.output) tokenParts.push(`↓${formatTokens(globalStats.tokens.output)}`); + if (globalStats.tokens.cacheRead) tokenParts.push(`R${formatTokens(globalStats.tokens.cacheRead)}`); + if (globalStats.tokens.cacheWrite) tokenParts.push(`W${formatTokens(globalStats.tokens.cacheWrite)}`); + + const msgParts = []; + if (globalStats.userMessages) msgParts.push(`${globalStats.userMessages} user`); + if (globalStats.assistantMessages) msgParts.push(`${globalStats.assistantMessages} assistant`); + if (globalStats.toolResults) msgParts.push(`${globalStats.toolResults} tool results`); + if (globalStats.customMessages) msgParts.push(`${globalStats.customMessages} custom`); + if (globalStats.compactions) msgParts.push(`${globalStats.compactions} compactions`); + if (globalStats.branchSummaries) msgParts.push(`${globalStats.branchSummaries} branch summaries`); + + let html = ` +
+

Session: ${escapeHtml(header?.id || 'unknown')}

+
Ctrl+T toggle thinking · Ctrl+O toggle tools
+
+
Date:${header?.timestamp ? new Date(header.timestamp).toLocaleString() : 'unknown'}
+
Models:${globalStats.models.join(', ') || 'unknown'}
+
Messages:${msgParts.join(', ') || '0'}
+
Tool Calls:${globalStats.toolCalls}
+
Tokens:${tokenParts.join(' ') || '0'}
+
Cost:$${totalCost.toFixed(3)}
+
+
`; + + if (systemPrompt) { + html += `
+
System Prompt
+
${escapeHtml(systemPrompt)}
+
`; + } + + if (tools && tools.length > 0) { + html += `
+
Available Tools
+
+ ${tools.map(t => `
${escapeHtml(t.name)} - ${escapeHtml(t.description)}
`).join('')} +
+
`; + } + + return html; + } + + // ============================================================ + // NAVIGATION + // ============================================================ + + // Cache for rendered entry DOM nodes + const entryCache = new Map(); + + function renderEntryToNode(entry) { + // Check cache first + if (entryCache.has(entry.id)) { + return entryCache.get(entry.id).cloneNode(true); + } + + // Render to HTML string, then parse to node + const html = renderEntry(entry); + if (!html) return null; + + const template = document.createElement('template'); + template.innerHTML = html; + const node = template.content.firstElementChild; + + // Cache the node + if (node) { + entryCache.set(entry.id, node.cloneNode(true)); + } + return node; + } + + function navigateTo(targetId, scrollMode = 'target') { + currentLeafId = targetId; + const path = getPath(targetId); + + renderTree(); + + document.getElementById('header-container').innerHTML = renderHeader(); + + // Build messages using cached DOM nodes + const messagesEl = document.getElementById('messages'); + const fragment = document.createDocumentFragment(); + + for (const entry of path) { + const node = renderEntryToNode(entry); + if (node) { + fragment.appendChild(node); + } + } + + messagesEl.innerHTML = ''; + messagesEl.appendChild(fragment); + + // Use setTimeout(0) to ensure DOM is fully laid out before scrolling + setTimeout(() => { + const content = document.getElementById('content'); + if (scrollMode === 'bottom') { + content.scrollTop = content.scrollHeight; + } else if (scrollMode === 'target') { + const targetEl = document.getElementById(`entry-${targetId}`); + if (targetEl) { + targetEl.scrollIntoView({ block: 'center' }); + } + } + }, 0); + } + + // ============================================================ + // INITIALIZATION + // ============================================================ + + // Escape HTML tags in text (but not code blocks) + function escapeHtmlTags(text) { + return text.replace(/<(?=[a-zA-Z\/])/g, '<'); + } + + // Configure marked with syntax highlighting and HTML escaping for text + marked.use({ + breaks: true, + gfm: true, + renderer: { + // Code blocks: syntax highlight, no HTML escaping + code(token) { + const code = token.text; + const lang = token.lang; + let highlighted; + if (lang && hljs.getLanguage(lang)) { + try { + highlighted = hljs.highlight(code, { language: lang }).value; + } catch { + highlighted = escapeHtml(code); + } + } else { + // Auto-detect language if not specified + try { + highlighted = hljs.highlightAuto(code).value; + } catch { + highlighted = escapeHtml(code); + } + } + return `
${highlighted}
`; + }, + // Text content: escape HTML tags + text(token) { + return escapeHtmlTags(escapeHtml(token.text)); + }, + // Inline code: escape HTML + codespan(token) { + return `${escapeHtml(token.text)}`; + } + } + }); + + // Simple marked parse (escaping handled in renderers) + function safeMarkedParse(text) { + return marked.parse(text); + } + + // Search input + const searchInput = document.getElementById('tree-search'); + searchInput.addEventListener('input', (e) => { + searchQuery = e.target.value; + forceTreeRerender(); + }); + + // Filter buttons + document.querySelectorAll('.filter-btn').forEach(btn => { + btn.addEventListener('click', () => { + document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + filterMode = btn.dataset.filter; + forceTreeRerender(); + }); + }); + + // Sidebar toggle + const sidebar = document.getElementById('sidebar'); + const overlay = document.getElementById('sidebar-overlay'); + const hamburger = document.getElementById('hamburger'); + + hamburger.addEventListener('click', () => { + sidebar.classList.add('open'); + overlay.classList.add('open'); + hamburger.style.display = 'none'; + }); + + const closeSidebar = () => { + sidebar.classList.remove('open'); + overlay.classList.remove('open'); + hamburger.style.display = ''; + }; + + overlay.addEventListener('click', closeSidebar); + document.getElementById('sidebar-close').addEventListener('click', closeSidebar); + + // Toggle states + let thinkingExpanded = true; + let toolOutputsExpanded = false; + + const toggleThinking = () => { + thinkingExpanded = !thinkingExpanded; + document.querySelectorAll('.thinking-text').forEach(el => { + el.style.display = thinkingExpanded ? '' : 'none'; + }); + document.querySelectorAll('.thinking-collapsed').forEach(el => { + el.style.display = thinkingExpanded ? 'none' : 'block'; + }); + }; + + const toggleToolOutputs = () => { + toolOutputsExpanded = !toolOutputsExpanded; + document.querySelectorAll('.tool-output.expandable').forEach(el => { + el.classList.toggle('expanded', toolOutputsExpanded); + }); + document.querySelectorAll('.compaction').forEach(el => { + el.classList.toggle('expanded', toolOutputsExpanded); + }); + }; + + // Keyboard shortcuts + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + searchInput.value = ''; + searchQuery = ''; + navigateTo(leafId, 'bottom'); + } + if (e.ctrlKey && e.key === 't') { + e.preventDefault(); + toggleThinking(); + } + if (e.ctrlKey && e.key === 'o') { + e.preventDefault(); + toggleToolOutputs(); + } + }); + + // Initial render - don't scroll, stay at top + if (leafId) { + navigateTo(leafId, 'none'); + } else if (entries.length > 0) { + // Fallback: use last entry if no leafId + navigateTo(entries[entries.length - 1].id, 'none'); + } + })(); diff --git a/packages/coding-agent/src/core/export-html/vendor/highlight.min.js b/packages/coding-agent/src/core/export-html/vendor/highlight.min.js new file mode 100644 index 000000000..5d699ae6a --- /dev/null +++ b/packages/coding-agent/src/core/export-html/vendor/highlight.min.js @@ -0,0 +1,1213 @@ +/*! + Highlight.js v11.9.0 (git: f47103d4f1) + (c) 2006-2023 undefined and other contributors + License: BSD-3-Clause + */ +var hljs=function(){"use strict";function e(n){ +return n instanceof Map?n.clear=n.delete=n.set=()=>{ +throw Error("map is read-only")}:n instanceof Set&&(n.add=n.clear=n.delete=()=>{ +throw Error("set is read-only") +}),Object.freeze(n),Object.getOwnPropertyNames(n).forEach((t=>{ +const a=n[t],i=typeof a;"object"!==i&&"function"!==i||Object.isFrozen(a)||e(a) +})),n}class n{constructor(e){ +void 0===e.data&&(e.data={}),this.data=e.data,this.isMatchIgnored=!1} +ignoreMatch(){this.isMatchIgnored=!0}}function t(e){ +return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'") +}function a(e,...n){const t=Object.create(null);for(const n in e)t[n]=e[n] +;return n.forEach((e=>{for(const n in e)t[n]=e[n]})),t}const i=e=>!!e.scope +;class r{constructor(e,n){ +this.buffer="",this.classPrefix=n.classPrefix,e.walk(this)}addText(e){ +this.buffer+=t(e)}openNode(e){if(!i(e))return;const n=((e,{prefix:n})=>{ +if(e.startsWith("language:"))return e.replace("language:","language-") +;if(e.includes(".")){const t=e.split(".") +;return[`${n}${t.shift()}`,...t.map(((e,n)=>`${e}${"_".repeat(n+1)}`))].join(" ") +}return`${n}${e}`})(e.scope,{prefix:this.classPrefix});this.span(n)} +closeNode(e){i(e)&&(this.buffer+="")}value(){return this.buffer}span(e){ +this.buffer+=``}}const s=(e={})=>{const n={children:[]} +;return Object.assign(n,e),n};class o{constructor(){ +this.rootNode=s(),this.stack=[this.rootNode]}get top(){ +return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){ +this.top.children.push(e)}openNode(e){const n=s({scope:e}) +;this.add(n),this.stack.push(n)}closeNode(){ +if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){ +for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)} +walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,n){ +return"string"==typeof n?e.addText(n):n.children&&(e.openNode(n), +n.children.forEach((n=>this._walk(e,n))),e.closeNode(n)),e}static _collapse(e){ +"string"!=typeof e&&e.children&&(e.children.every((e=>"string"==typeof e))?e.children=[e.children.join("")]:e.children.forEach((e=>{ +o._collapse(e)})))}}class l extends o{constructor(e){super(),this.options=e} +addText(e){""!==e&&this.add(e)}startScope(e){this.openNode(e)}endScope(){ +this.closeNode()}__addSublanguage(e,n){const t=e.root +;n&&(t.scope="language:"+n),this.add(t)}toHTML(){ +return new r(this,this.options).value()}finalize(){ +return this.closeAllNodes(),!0}}function c(e){ +return e?"string"==typeof e?e:e.source:null}function d(e){return b("(?=",e,")")} +function g(e){return b("(?:",e,")*")}function u(e){return b("(?:",e,")?")} +function b(...e){return e.map((e=>c(e))).join("")}function m(...e){const n=(e=>{ +const n=e[e.length-1] +;return"object"==typeof n&&n.constructor===Object?(e.splice(e.length-1,1),n):{} +})(e);return"("+(n.capture?"":"?:")+e.map((e=>c(e))).join("|")+")"} +function p(e){return RegExp(e.toString()+"|").exec("").length-1} +const _=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./ +;function h(e,{joinWith:n}){let t=0;return e.map((e=>{t+=1;const n=t +;let a=c(e),i="";for(;a.length>0;){const e=_.exec(a);if(!e){i+=a;break} +i+=a.substring(0,e.index), +a=a.substring(e.index+e[0].length),"\\"===e[0][0]&&e[1]?i+="\\"+(Number(e[1])+n):(i+=e[0], +"("===e[0]&&t++)}return i})).map((e=>`(${e})`)).join(n)} +const f="[a-zA-Z]\\w*",E="[a-zA-Z_]\\w*",y="\\b\\d+(\\.\\d+)?",N="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",w="\\b(0b[01]+)",v={ +begin:"\\\\[\\s\\S]",relevance:0},O={scope:"string",begin:"'",end:"'", +illegal:"\\n",contains:[v]},k={scope:"string",begin:'"',end:'"',illegal:"\\n", +contains:[v]},x=(e,n,t={})=>{const i=a({scope:"comment",begin:e,end:n, +contains:[]},t);i.contains.push({scope:"doctag", +begin:"[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)", +end:/(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,excludeBegin:!0,relevance:0}) +;const r=m("I","a","is","so","us","to","at","if","in","it","on",/[A-Za-z]+['](d|ve|re|ll|t|s|n)/,/[A-Za-z]+[-][a-z]+/,/[A-Za-z][a-z]{2,}/) +;return i.contains.push({begin:b(/[ ]+/,"(",r,/[.]?[:]?([.][ ]|[ ])/,"){3}")}),i +},M=x("//","$"),S=x("/\\*","\\*/"),A=x("#","$");var C=Object.freeze({ +__proto__:null,APOS_STRING_MODE:O,BACKSLASH_ESCAPE:v,BINARY_NUMBER_MODE:{ +scope:"number",begin:w,relevance:0},BINARY_NUMBER_RE:w,COMMENT:x, +C_BLOCK_COMMENT_MODE:S,C_LINE_COMMENT_MODE:M,C_NUMBER_MODE:{scope:"number", +begin:N,relevance:0},C_NUMBER_RE:N,END_SAME_AS_BEGIN:e=>Object.assign(e,{ +"on:begin":(e,n)=>{n.data._beginMatch=e[1]},"on:end":(e,n)=>{ +n.data._beginMatch!==e[1]&&n.ignoreMatch()}}),HASH_COMMENT_MODE:A,IDENT_RE:f, +MATCH_NOTHING_RE:/\b\B/,METHOD_GUARD:{begin:"\\.\\s*"+E,relevance:0}, +NUMBER_MODE:{scope:"number",begin:y,relevance:0},NUMBER_RE:y, +PHRASAL_WORDS_MODE:{ +begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/ +},QUOTE_STRING_MODE:k,REGEXP_MODE:{scope:"regexp",begin:/\/(?=[^/\n]*\/)/, +end:/\/[gimuy]*/,contains:[v,{begin:/\[/,end:/\]/,relevance:0,contains:[v]}]}, +RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~", +SHEBANG:(e={})=>{const n=/^#![ ]*\// +;return e.binary&&(e.begin=b(n,/.*\b/,e.binary,/\b.*/)),a({scope:"meta",begin:n, +end:/$/,relevance:0,"on:begin":(e,n)=>{0!==e.index&&n.ignoreMatch()}},e)}, +TITLE_MODE:{scope:"title",begin:f,relevance:0},UNDERSCORE_IDENT_RE:E, +UNDERSCORE_TITLE_MODE:{scope:"title",begin:E,relevance:0}});function T(e,n){ +"."===e.input[e.index-1]&&n.ignoreMatch()}function R(e,n){ +void 0!==e.className&&(e.scope=e.className,delete e.className)}function D(e,n){ +n&&e.beginKeywords&&(e.begin="\\b("+e.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)", +e.__beforeBegin=T,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords, +void 0===e.relevance&&(e.relevance=0))}function I(e,n){ +Array.isArray(e.illegal)&&(e.illegal=m(...e.illegal))}function L(e,n){ +if(e.match){ +if(e.begin||e.end)throw Error("begin & end are not supported with match") +;e.begin=e.match,delete e.match}}function B(e,n){ +void 0===e.relevance&&(e.relevance=1)}const $=(e,n)=>{if(!e.beforeMatch)return +;if(e.starts)throw Error("beforeMatch cannot be used with starts") +;const t=Object.assign({},e);Object.keys(e).forEach((n=>{delete e[n] +})),e.keywords=t.keywords,e.begin=b(t.beforeMatch,d(t.begin)),e.starts={ +relevance:0,contains:[Object.assign(t,{endsParent:!0})] +},e.relevance=0,delete t.beforeMatch +},z=["of","and","for","in","not","or","if","then","parent","list","value"],F="keyword" +;function U(e,n,t=F){const a=Object.create(null) +;return"string"==typeof e?i(t,e.split(" ")):Array.isArray(e)?i(t,e):Object.keys(e).forEach((t=>{ +Object.assign(a,U(e[t],n,t))})),a;function i(e,t){ +n&&(t=t.map((e=>e.toLowerCase()))),t.forEach((n=>{const t=n.split("|") +;a[t[0]]=[e,j(t[0],t[1])]}))}}function j(e,n){ +return n?Number(n):(e=>z.includes(e.toLowerCase()))(e)?0:1}const P={},K=e=>{ +console.error(e)},H=(e,...n)=>{console.log("WARN: "+e,...n)},q=(e,n)=>{ +P[`${e}/${n}`]||(console.log(`Deprecated as of ${e}. ${n}`),P[`${e}/${n}`]=!0) +},G=Error();function Z(e,n,{key:t}){let a=0;const i=e[t],r={},s={} +;for(let e=1;e<=n.length;e++)s[e+a]=i[e],r[e+a]=!0,a+=p(n[e-1]) +;e[t]=s,e[t]._emit=r,e[t]._multi=!0}function W(e){(e=>{ +e.scope&&"object"==typeof e.scope&&null!==e.scope&&(e.beginScope=e.scope, +delete e.scope)})(e),"string"==typeof e.beginScope&&(e.beginScope={ +_wrap:e.beginScope}),"string"==typeof e.endScope&&(e.endScope={_wrap:e.endScope +}),(e=>{if(Array.isArray(e.begin)){ +if(e.skip||e.excludeBegin||e.returnBegin)throw K("skip, excludeBegin, returnBegin not compatible with beginScope: {}"), +G +;if("object"!=typeof e.beginScope||null===e.beginScope)throw K("beginScope must be object"), +G;Z(e,e.begin,{key:"beginScope"}),e.begin=h(e.begin,{joinWith:""})}})(e),(e=>{ +if(Array.isArray(e.end)){ +if(e.skip||e.excludeEnd||e.returnEnd)throw K("skip, excludeEnd, returnEnd not compatible with endScope: {}"), +G +;if("object"!=typeof e.endScope||null===e.endScope)throw K("endScope must be object"), +G;Z(e,e.end,{key:"endScope"}),e.end=h(e.end,{joinWith:""})}})(e)}function Q(e){ +function n(n,t){ +return RegExp(c(n),"m"+(e.case_insensitive?"i":"")+(e.unicodeRegex?"u":"")+(t?"g":"")) +}class t{constructor(){ +this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0} +addRule(e,n){ +n.position=this.position++,this.matchIndexes[this.matchAt]=n,this.regexes.push([n,e]), +this.matchAt+=p(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null) +;const e=this.regexes.map((e=>e[1]));this.matcherRe=n(h(e,{joinWith:"|" +}),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex +;const n=this.matcherRe.exec(e);if(!n)return null +;const t=n.findIndex(((e,n)=>n>0&&void 0!==e)),a=this.matchIndexes[t] +;return n.splice(0,t),Object.assign(n,a)}}class i{constructor(){ +this.rules=[],this.multiRegexes=[], +this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){ +if(this.multiRegexes[e])return this.multiRegexes[e];const n=new t +;return this.rules.slice(e).forEach((([e,t])=>n.addRule(e,t))), +n.compile(),this.multiRegexes[e]=n,n}resumingScanAtSamePosition(){ +return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(e,n){ +this.rules.push([e,n]),"begin"===n.type&&this.count++}exec(e){ +const n=this.getMatcher(this.regexIndex);n.lastIndex=this.lastIndex +;let t=n.exec(e) +;if(this.resumingScanAtSamePosition())if(t&&t.index===this.lastIndex);else{ +const n=this.getMatcher(0);n.lastIndex=this.lastIndex+1,t=n.exec(e)} +return t&&(this.regexIndex+=t.position+1, +this.regexIndex===this.count&&this.considerAll()),t}} +if(e.compilerExtensions||(e.compilerExtensions=[]), +e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.") +;return e.classNameAliases=a(e.classNameAliases||{}),function t(r,s){const o=r +;if(r.isCompiled)return o +;[R,L,W,$].forEach((e=>e(r,s))),e.compilerExtensions.forEach((e=>e(r,s))), +r.__beforeBegin=null,[D,I,B].forEach((e=>e(r,s))),r.isCompiled=!0;let l=null +;return"object"==typeof r.keywords&&r.keywords.$pattern&&(r.keywords=Object.assign({},r.keywords), +l=r.keywords.$pattern, +delete r.keywords.$pattern),l=l||/\w+/,r.keywords&&(r.keywords=U(r.keywords,e.case_insensitive)), +o.keywordPatternRe=n(l,!0), +s&&(r.begin||(r.begin=/\B|\b/),o.beginRe=n(o.begin),r.end||r.endsWithParent||(r.end=/\B|\b/), +r.end&&(o.endRe=n(o.end)), +o.terminatorEnd=c(o.end)||"",r.endsWithParent&&s.terminatorEnd&&(o.terminatorEnd+=(r.end?"|":"")+s.terminatorEnd)), +r.illegal&&(o.illegalRe=n(r.illegal)), +r.contains||(r.contains=[]),r.contains=[].concat(...r.contains.map((e=>(e=>(e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map((n=>a(e,{ +variants:null},n)))),e.cachedVariants?e.cachedVariants:X(e)?a(e,{ +starts:e.starts?a(e.starts):null +}):Object.isFrozen(e)?a(e):e))("self"===e?r:e)))),r.contains.forEach((e=>{t(e,o) +})),r.starts&&t(r.starts,s),o.matcher=(e=>{const n=new i +;return e.contains.forEach((e=>n.addRule(e.begin,{rule:e,type:"begin" +}))),e.terminatorEnd&&n.addRule(e.terminatorEnd,{type:"end" +}),e.illegal&&n.addRule(e.illegal,{type:"illegal"}),n})(o),o}(e)}function X(e){ +return!!e&&(e.endsWithParent||X(e.starts))}class V extends Error{ +constructor(e,n){super(e),this.name="HTMLInjectionError",this.html=n}} +const J=t,Y=a,ee=Symbol("nomatch"),ne=t=>{ +const a=Object.create(null),i=Object.create(null),r=[];let s=!0 +;const o="Could not find the language '{}', did you forget to load/include a language module?",c={ +disableAutodetect:!0,name:"Plain text",contains:[]};let p={ +ignoreUnescapedHTML:!1,throwUnescapedHTML:!1,noHighlightRe:/^(no-?highlight)$/i, +languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-", +cssSelector:"pre code",languages:null,__emitter:l};function _(e){ +return p.noHighlightRe.test(e)}function h(e,n,t){let a="",i="" +;"object"==typeof n?(a=e, +t=n.ignoreIllegals,i=n.language):(q("10.7.0","highlight(lang, code, ...args) has been deprecated."), +q("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"), +i=e,a=n),void 0===t&&(t=!0);const r={code:a,language:i};x("before:highlight",r) +;const s=r.result?r.result:f(r.language,r.code,t) +;return s.code=r.code,x("after:highlight",s),s}function f(e,t,i,r){ +const l=Object.create(null);function c(){if(!x.keywords)return void S.addText(A) +;let e=0;x.keywordPatternRe.lastIndex=0;let n=x.keywordPatternRe.exec(A),t="" +;for(;n;){t+=A.substring(e,n.index) +;const i=w.case_insensitive?n[0].toLowerCase():n[0],r=(a=i,x.keywords[a]);if(r){ +const[e,a]=r +;if(S.addText(t),t="",l[i]=(l[i]||0)+1,l[i]<=7&&(C+=a),e.startsWith("_"))t+=n[0];else{ +const t=w.classNameAliases[e]||e;g(n[0],t)}}else t+=n[0] +;e=x.keywordPatternRe.lastIndex,n=x.keywordPatternRe.exec(A)}var a +;t+=A.substring(e),S.addText(t)}function d(){null!=x.subLanguage?(()=>{ +if(""===A)return;let e=null;if("string"==typeof x.subLanguage){ +if(!a[x.subLanguage])return void S.addText(A) +;e=f(x.subLanguage,A,!0,M[x.subLanguage]),M[x.subLanguage]=e._top +}else e=E(A,x.subLanguage.length?x.subLanguage:null) +;x.relevance>0&&(C+=e.relevance),S.__addSublanguage(e._emitter,e.language) +})():c(),A=""}function g(e,n){ +""!==e&&(S.startScope(n),S.addText(e),S.endScope())}function u(e,n){let t=1 +;const a=n.length-1;for(;t<=a;){if(!e._emit[t]){t++;continue} +const a=w.classNameAliases[e[t]]||e[t],i=n[t];a?g(i,a):(A=i,c(),A=""),t++}} +function b(e,n){ +return e.scope&&"string"==typeof e.scope&&S.openNode(w.classNameAliases[e.scope]||e.scope), +e.beginScope&&(e.beginScope._wrap?(g(A,w.classNameAliases[e.beginScope._wrap]||e.beginScope._wrap), +A=""):e.beginScope._multi&&(u(e.beginScope,n),A="")),x=Object.create(e,{parent:{ +value:x}}),x}function m(e,t,a){let i=((e,n)=>{const t=e&&e.exec(n) +;return t&&0===t.index})(e.endRe,a);if(i){if(e["on:end"]){const a=new n(e) +;e["on:end"](t,a),a.isMatchIgnored&&(i=!1)}if(i){ +for(;e.endsParent&&e.parent;)e=e.parent;return e}} +if(e.endsWithParent)return m(e.parent,t,a)}function _(e){ +return 0===x.matcher.regexIndex?(A+=e[0],1):(D=!0,0)}function h(e){ +const n=e[0],a=t.substring(e.index),i=m(x,e,a);if(!i)return ee;const r=x +;x.endScope&&x.endScope._wrap?(d(), +g(n,x.endScope._wrap)):x.endScope&&x.endScope._multi?(d(), +u(x.endScope,e)):r.skip?A+=n:(r.returnEnd||r.excludeEnd||(A+=n), +d(),r.excludeEnd&&(A=n));do{ +x.scope&&S.closeNode(),x.skip||x.subLanguage||(C+=x.relevance),x=x.parent +}while(x!==i.parent);return i.starts&&b(i.starts,e),r.returnEnd?0:n.length} +let y={};function N(a,r){const o=r&&r[0];if(A+=a,null==o)return d(),0 +;if("begin"===y.type&&"end"===r.type&&y.index===r.index&&""===o){ +if(A+=t.slice(r.index,r.index+1),!s){const n=Error(`0 width match regex (${e})`) +;throw n.languageName=e,n.badRule=y.rule,n}return 1} +if(y=r,"begin"===r.type)return(e=>{ +const t=e[0],a=e.rule,i=new n(a),r=[a.__beforeBegin,a["on:begin"]] +;for(const n of r)if(n&&(n(e,i),i.isMatchIgnored))return _(t) +;return a.skip?A+=t:(a.excludeBegin&&(A+=t), +d(),a.returnBegin||a.excludeBegin||(A=t)),b(a,e),a.returnBegin?0:t.length})(r) +;if("illegal"===r.type&&!i){ +const e=Error('Illegal lexeme "'+o+'" for mode "'+(x.scope||"")+'"') +;throw e.mode=x,e}if("end"===r.type){const e=h(r);if(e!==ee)return e} +if("illegal"===r.type&&""===o)return 1 +;if(R>1e5&&R>3*r.index)throw Error("potential infinite loop, way more iterations than matches") +;return A+=o,o.length}const w=v(e) +;if(!w)throw K(o.replace("{}",e)),Error('Unknown language: "'+e+'"') +;const O=Q(w);let k="",x=r||O;const M={},S=new p.__emitter(p);(()=>{const e=[] +;for(let n=x;n!==w;n=n.parent)n.scope&&e.unshift(n.scope) +;e.forEach((e=>S.openNode(e)))})();let A="",C=0,T=0,R=0,D=!1;try{ +if(w.__emitTokens)w.__emitTokens(t,S);else{for(x.matcher.considerAll();;){ +R++,D?D=!1:x.matcher.considerAll(),x.matcher.lastIndex=T +;const e=x.matcher.exec(t);if(!e)break;const n=N(t.substring(T,e.index),e) +;T=e.index+n}N(t.substring(T))}return S.finalize(),k=S.toHTML(),{language:e, +value:k,relevance:C,illegal:!1,_emitter:S,_top:x}}catch(n){ +if(n.message&&n.message.includes("Illegal"))return{language:e,value:J(t), +illegal:!0,relevance:0,_illegalBy:{message:n.message,index:T, +context:t.slice(T-100,T+100),mode:n.mode,resultSoFar:k},_emitter:S};if(s)return{ +language:e,value:J(t),illegal:!1,relevance:0,errorRaised:n,_emitter:S,_top:x} +;throw n}}function E(e,n){n=n||p.languages||Object.keys(a);const t=(e=>{ +const n={value:J(e),illegal:!1,relevance:0,_top:c,_emitter:new p.__emitter(p)} +;return n._emitter.addText(e),n})(e),i=n.filter(v).filter(k).map((n=>f(n,e,!1))) +;i.unshift(t);const r=i.sort(((e,n)=>{ +if(e.relevance!==n.relevance)return n.relevance-e.relevance +;if(e.language&&n.language){if(v(e.language).supersetOf===n.language)return 1 +;if(v(n.language).supersetOf===e.language)return-1}return 0})),[s,o]=r,l=s +;return l.secondBest=o,l}function y(e){let n=null;const t=(e=>{ +let n=e.className+" ";n+=e.parentNode?e.parentNode.className:"" +;const t=p.languageDetectRe.exec(n);if(t){const n=v(t[1]) +;return n||(H(o.replace("{}",t[1])), +H("Falling back to no-highlight mode for this block.",e)),n?t[1]:"no-highlight"} +return n.split(/\s+/).find((e=>_(e)||v(e)))})(e);if(_(t))return +;if(x("before:highlightElement",{el:e,language:t +}),e.dataset.highlighted)return void console.log("Element previously highlighted. To highlight again, first unset `dataset.highlighted`.",e) +;if(e.children.length>0&&(p.ignoreUnescapedHTML||(console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."), +console.warn("https://github.com/highlightjs/highlight.js/wiki/security"), +console.warn("The element with unescaped HTML:"), +console.warn(e)),p.throwUnescapedHTML))throw new V("One of your code blocks includes unescaped HTML.",e.innerHTML) +;n=e;const a=n.textContent,r=t?h(a,{language:t,ignoreIllegals:!0}):E(a) +;e.innerHTML=r.value,e.dataset.highlighted="yes",((e,n,t)=>{const a=n&&i[n]||t +;e.classList.add("hljs"),e.classList.add("language-"+a) +})(e,t,r.language),e.result={language:r.language,re:r.relevance, +relevance:r.relevance},r.secondBest&&(e.secondBest={ +language:r.secondBest.language,relevance:r.secondBest.relevance +}),x("after:highlightElement",{el:e,result:r,text:a})}let N=!1;function w(){ +"loading"!==document.readyState?document.querySelectorAll(p.cssSelector).forEach(y):N=!0 +}function v(e){return e=(e||"").toLowerCase(),a[e]||a[i[e]]} +function O(e,{languageName:n}){"string"==typeof e&&(e=[e]),e.forEach((e=>{ +i[e.toLowerCase()]=n}))}function k(e){const n=v(e) +;return n&&!n.disableAutodetect}function x(e,n){const t=e;r.forEach((e=>{ +e[t]&&e[t](n)}))} +"undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",(()=>{ +N&&w()}),!1),Object.assign(t,{highlight:h,highlightAuto:E,highlightAll:w, +highlightElement:y, +highlightBlock:e=>(q("10.7.0","highlightBlock will be removed entirely in v12.0"), +q("10.7.0","Please use highlightElement now."),y(e)),configure:e=>{p=Y(p,e)}, +initHighlighting:()=>{ +w(),q("10.6.0","initHighlighting() deprecated. Use highlightAll() now.")}, +initHighlightingOnLoad:()=>{ +w(),q("10.6.0","initHighlightingOnLoad() deprecated. Use highlightAll() now.") +},registerLanguage:(e,n)=>{let i=null;try{i=n(t)}catch(n){ +if(K("Language definition for '{}' could not be registered.".replace("{}",e)), +!s)throw n;K(n),i=c} +i.name||(i.name=e),a[e]=i,i.rawDefinition=n.bind(null,t),i.aliases&&O(i.aliases,{ +languageName:e})},unregisterLanguage:e=>{delete a[e] +;for(const n of Object.keys(i))i[n]===e&&delete i[n]}, +listLanguages:()=>Object.keys(a),getLanguage:v,registerAliases:O, +autoDetection:k,inherit:Y,addPlugin:e=>{(e=>{ +e["before:highlightBlock"]&&!e["before:highlightElement"]&&(e["before:highlightElement"]=n=>{ +e["before:highlightBlock"](Object.assign({block:n.el},n)) +}),e["after:highlightBlock"]&&!e["after:highlightElement"]&&(e["after:highlightElement"]=n=>{ +e["after:highlightBlock"](Object.assign({block:n.el},n))})})(e),r.push(e)}, +removePlugin:e=>{const n=r.indexOf(e);-1!==n&&r.splice(n,1)}}),t.debugMode=()=>{ +s=!1},t.safeMode=()=>{s=!0},t.versionString="11.9.0",t.regex={concat:b, +lookahead:d,either:m,optional:u,anyNumberOfTimes:g} +;for(const n in C)"object"==typeof C[n]&&e(C[n]);return Object.assign(t,C),t +},te=ne({});te.newInstance=()=>ne({});var ae=te;const ie=e=>({IMPORTANT:{ +scope:"meta",begin:"!important"},BLOCK_COMMENT:e.C_BLOCK_COMMENT_MODE,HEXCOLOR:{ +scope:"number",begin:/#(([0-9a-fA-F]{3,4})|(([0-9a-fA-F]{2}){3,4}))\b/}, +FUNCTION_DISPATCH:{className:"built_in",begin:/[\w-]+(?=\()/}, +ATTRIBUTE_SELECTOR_MODE:{scope:"selector-attr",begin:/\[/,end:/\]/,illegal:"$", +contains:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},CSS_NUMBER_MODE:{ +scope:"number", +begin:e.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?", +relevance:0},CSS_VARIABLE:{className:"attr",begin:/--[A-Za-z_][A-Za-z0-9_-]*/} +}),re=["a","abbr","address","article","aside","audio","b","blockquote","body","button","canvas","caption","cite","code","dd","del","details","dfn","div","dl","dt","em","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","html","i","iframe","img","input","ins","kbd","label","legend","li","main","mark","menu","nav","object","ol","p","q","quote","samp","section","span","strong","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","ul","var","video"],se=["any-hover","any-pointer","aspect-ratio","color","color-gamut","color-index","device-aspect-ratio","device-height","device-width","display-mode","forced-colors","grid","height","hover","inverted-colors","monochrome","orientation","overflow-block","overflow-inline","pointer","prefers-color-scheme","prefers-contrast","prefers-reduced-motion","prefers-reduced-transparency","resolution","scan","scripting","update","width","min-width","max-width","min-height","max-height"],oe=["active","any-link","blank","checked","current","default","defined","dir","disabled","drop","empty","enabled","first","first-child","first-of-type","fullscreen","future","focus","focus-visible","focus-within","has","host","host-context","hover","indeterminate","in-range","invalid","is","lang","last-child","last-of-type","left","link","local-link","not","nth-child","nth-col","nth-last-child","nth-last-col","nth-last-of-type","nth-of-type","only-child","only-of-type","optional","out-of-range","past","placeholder-shown","read-only","read-write","required","right","root","scope","target","target-within","user-invalid","valid","visited","where"],le=["after","backdrop","before","cue","cue-region","first-letter","first-line","grammar-error","marker","part","placeholder","selection","slotted","spelling-error"],ce=["align-content","align-items","align-self","all","animation","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-timing-function","backface-visibility","background","background-attachment","background-blend-mode","background-clip","background-color","background-image","background-origin","background-position","background-repeat","background-size","block-size","border","border-block","border-block-color","border-block-end","border-block-end-color","border-block-end-style","border-block-end-width","border-block-start","border-block-start-color","border-block-start-style","border-block-start-width","border-block-style","border-block-width","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-inline","border-inline-color","border-inline-end","border-inline-end-color","border-inline-end-style","border-inline-end-width","border-inline-start","border-inline-start-color","border-inline-start-style","border-inline-start-width","border-inline-style","border-inline-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-style","border-top","border-top-color","border-top-left-radius","border-top-right-radius","border-top-style","border-top-width","border-width","bottom","box-decoration-break","box-shadow","box-sizing","break-after","break-before","break-inside","caption-side","caret-color","clear","clip","clip-path","clip-rule","color","column-count","column-fill","column-gap","column-rule","column-rule-color","column-rule-style","column-rule-width","column-span","column-width","columns","contain","content","content-visibility","counter-increment","counter-reset","cue","cue-after","cue-before","cursor","direction","display","empty-cells","filter","flex","flex-basis","flex-direction","flex-flow","flex-grow","flex-shrink","flex-wrap","float","flow","font","font-display","font-family","font-feature-settings","font-kerning","font-language-override","font-size","font-size-adjust","font-smoothing","font-stretch","font-style","font-synthesis","font-variant","font-variant-caps","font-variant-east-asian","font-variant-ligatures","font-variant-numeric","font-variant-position","font-variation-settings","font-weight","gap","glyph-orientation-vertical","grid","grid-area","grid-auto-columns","grid-auto-flow","grid-auto-rows","grid-column","grid-column-end","grid-column-start","grid-gap","grid-row","grid-row-end","grid-row-start","grid-template","grid-template-areas","grid-template-columns","grid-template-rows","hanging-punctuation","height","hyphens","icon","image-orientation","image-rendering","image-resolution","ime-mode","inline-size","isolation","justify-content","left","letter-spacing","line-break","line-height","list-style","list-style-image","list-style-position","list-style-type","margin","margin-block","margin-block-end","margin-block-start","margin-bottom","margin-inline","margin-inline-end","margin-inline-start","margin-left","margin-right","margin-top","marks","mask","mask-border","mask-border-mode","mask-border-outset","mask-border-repeat","mask-border-slice","mask-border-source","mask-border-width","mask-clip","mask-composite","mask-image","mask-mode","mask-origin","mask-position","mask-repeat","mask-size","mask-type","max-block-size","max-height","max-inline-size","max-width","min-block-size","min-height","min-inline-size","min-width","mix-blend-mode","nav-down","nav-index","nav-left","nav-right","nav-up","none","normal","object-fit","object-position","opacity","order","orphans","outline","outline-color","outline-offset","outline-style","outline-width","overflow","overflow-wrap","overflow-x","overflow-y","padding","padding-block","padding-block-end","padding-block-start","padding-bottom","padding-inline","padding-inline-end","padding-inline-start","padding-left","padding-right","padding-top","page-break-after","page-break-before","page-break-inside","pause","pause-after","pause-before","perspective","perspective-origin","pointer-events","position","quotes","resize","rest","rest-after","rest-before","right","row-gap","scroll-margin","scroll-margin-block","scroll-margin-block-end","scroll-margin-block-start","scroll-margin-bottom","scroll-margin-inline","scroll-margin-inline-end","scroll-margin-inline-start","scroll-margin-left","scroll-margin-right","scroll-margin-top","scroll-padding","scroll-padding-block","scroll-padding-block-end","scroll-padding-block-start","scroll-padding-bottom","scroll-padding-inline","scroll-padding-inline-end","scroll-padding-inline-start","scroll-padding-left","scroll-padding-right","scroll-padding-top","scroll-snap-align","scroll-snap-stop","scroll-snap-type","scrollbar-color","scrollbar-gutter","scrollbar-width","shape-image-threshold","shape-margin","shape-outside","speak","speak-as","src","tab-size","table-layout","text-align","text-align-all","text-align-last","text-combine-upright","text-decoration","text-decoration-color","text-decoration-line","text-decoration-style","text-emphasis","text-emphasis-color","text-emphasis-position","text-emphasis-style","text-indent","text-justify","text-orientation","text-overflow","text-rendering","text-shadow","text-transform","text-underline-position","top","transform","transform-box","transform-origin","transform-style","transition","transition-delay","transition-duration","transition-property","transition-timing-function","unicode-bidi","vertical-align","visibility","voice-balance","voice-duration","voice-family","voice-pitch","voice-range","voice-rate","voice-stress","voice-volume","white-space","widows","width","will-change","word-break","word-spacing","word-wrap","writing-mode","z-index"].reverse(),de=oe.concat(le) +;var ge="[0-9](_*[0-9])*",ue=`\\.(${ge})`,be="[0-9a-fA-F](_*[0-9a-fA-F])*",me={ +className:"number",variants:[{ +begin:`(\\b(${ge})((${ue})|\\.)?|(${ue}))[eE][+-]?(${ge})[fFdD]?\\b`},{ +begin:`\\b(${ge})((${ue})[fFdD]?\\b|\\.([fFdD]\\b)?)`},{ +begin:`(${ue})[fFdD]?\\b`},{begin:`\\b(${ge})[fFdD]\\b`},{ +begin:`\\b0[xX]((${be})\\.?|(${be})?\\.(${be}))[pP][+-]?(${ge})[fFdD]?\\b`},{ +begin:"\\b(0|[1-9](_*[0-9])*)[lL]?\\b"},{begin:`\\b0[xX](${be})[lL]?\\b`},{ +begin:"\\b0(_*[0-7])*[lL]?\\b"},{begin:"\\b0[bB][01](_*[01])*[lL]?\\b"}], +relevance:0};function pe(e,n,t){return-1===t?"":e.replace(n,(a=>pe(e,n,t-1)))} +const _e="[A-Za-z$_][0-9A-Za-z$_]*",he=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],fe=["true","false","null","undefined","NaN","Infinity"],Ee=["Object","Function","Boolean","Symbol","Math","Date","Number","BigInt","String","RegExp","Array","Float32Array","Float64Array","Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Int32Array","Uint16Array","Uint32Array","BigInt64Array","BigUint64Array","Set","Map","WeakSet","WeakMap","ArrayBuffer","SharedArrayBuffer","Atomics","DataView","JSON","Promise","Generator","GeneratorFunction","AsyncFunction","Reflect","Proxy","Intl","WebAssembly"],ye=["Error","EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"],Ne=["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],we=["arguments","this","super","console","window","document","localStorage","sessionStorage","module","global"],ve=[].concat(Ne,Ee,ye) +;function Oe(e){const n=e.regex,t=_e,a={begin:/<[A-Za-z0-9\\._:-]+/, +end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(e,n)=>{ +const t=e[0].length+e.index,a=e.input[t] +;if("<"===a||","===a)return void n.ignoreMatch();let i +;">"===a&&(((e,{after:n})=>{const t="",M={ +match:[/const|var|let/,/\s+/,t,/\s*/,/=\s*/,/(async\s*)?/,n.lookahead(x)], +keywords:"async",className:{1:"keyword",3:"title.function"},contains:[f]} +;return{name:"JavaScript",aliases:["js","jsx","mjs","cjs"],keywords:i,exports:{ +PARAMS_CONTAINS:h,CLASS_REFERENCE:y},illegal:/#(?![$_A-z])/, +contains:[e.SHEBANG({label:"shebang",binary:"node",relevance:5}),{ +label:"use_strict",className:"meta",relevance:10, +begin:/^\s*['"]use (strict|asm)['"]/ +},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,d,g,u,b,m,{match:/\$\d+/},l,y,{ +className:"attr",begin:t+n.lookahead(":"),relevance:0},M,{ +begin:"("+e.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*", +keywords:"return throw case",relevance:0,contains:[m,e.REGEXP_MODE,{ +className:"function",begin:x,returnBegin:!0,end:"\\s*=>",contains:[{ +className:"params",variants:[{begin:e.UNDERSCORE_IDENT_RE,relevance:0},{ +className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0, +excludeEnd:!0,keywords:i,contains:h}]}]},{begin:/,/,relevance:0},{match:/\s+/, +relevance:0},{variants:[{begin:"<>",end:""},{ +match:/<[A-Za-z0-9\\._:-]+\s*\/>/},{begin:a.begin, +"on:begin":a.isTrulyOpeningTag,end:a.end}],subLanguage:"xml",contains:[{ +begin:a.begin,end:a.end,skip:!0,contains:["self"]}]}]},N,{ +beginKeywords:"while if switch catch for"},{ +begin:"\\b(?!function)"+e.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{", +returnBegin:!0,label:"func.def",contains:[f,e.inherit(e.TITLE_MODE,{begin:t, +className:"title.function"})]},{match:/\.\.\./,relevance:0},O,{match:"\\$"+t, +relevance:0},{match:[/\bconstructor(?=\s*\()/],className:{1:"title.function"}, +contains:[f]},w,{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/, +className:"variable.constant"},E,k,{match:/\$[(.]/}]}} +const ke=e=>b(/\b/,e,/\w$/.test(e)?/\b/:/\B/),xe=["Protocol","Type"].map(ke),Me=["init","self"].map(ke),Se=["Any","Self"],Ae=["actor","any","associatedtype","async","await",/as\?/,/as!/,"as","borrowing","break","case","catch","class","consume","consuming","continue","convenience","copy","default","defer","deinit","didSet","distributed","do","dynamic","each","else","enum","extension","fallthrough",/fileprivate\(set\)/,"fileprivate","final","for","func","get","guard","if","import","indirect","infix",/init\?/,/init!/,"inout",/internal\(set\)/,"internal","in","is","isolated","nonisolated","lazy","let","macro","mutating","nonmutating",/open\(set\)/,"open","operator","optional","override","postfix","precedencegroup","prefix",/private\(set\)/,"private","protocol",/public\(set\)/,"public","repeat","required","rethrows","return","set","some","static","struct","subscript","super","switch","throws","throw",/try\?/,/try!/,"try","typealias",/unowned\(safe\)/,/unowned\(unsafe\)/,"unowned","var","weak","where","while","willSet"],Ce=["false","nil","true"],Te=["assignment","associativity","higherThan","left","lowerThan","none","right"],Re=["#colorLiteral","#column","#dsohandle","#else","#elseif","#endif","#error","#file","#fileID","#fileLiteral","#filePath","#function","#if","#imageLiteral","#keyPath","#line","#selector","#sourceLocation","#warning"],De=["abs","all","any","assert","assertionFailure","debugPrint","dump","fatalError","getVaList","isKnownUniquelyReferenced","max","min","numericCast","pointwiseMax","pointwiseMin","precondition","preconditionFailure","print","readLine","repeatElement","sequence","stride","swap","swift_unboxFromSwiftValueWithType","transcode","type","unsafeBitCast","unsafeDowncast","withExtendedLifetime","withUnsafeMutablePointer","withUnsafePointer","withVaList","withoutActuallyEscaping","zip"],Ie=m(/[/=\-+!*%<>&|^~?]/,/[\u00A1-\u00A7]/,/[\u00A9\u00AB]/,/[\u00AC\u00AE]/,/[\u00B0\u00B1]/,/[\u00B6\u00BB\u00BF\u00D7\u00F7]/,/[\u2016-\u2017]/,/[\u2020-\u2027]/,/[\u2030-\u203E]/,/[\u2041-\u2053]/,/[\u2055-\u205E]/,/[\u2190-\u23FF]/,/[\u2500-\u2775]/,/[\u2794-\u2BFF]/,/[\u2E00-\u2E7F]/,/[\u3001-\u3003]/,/[\u3008-\u3020]/,/[\u3030]/),Le=m(Ie,/[\u0300-\u036F]/,/[\u1DC0-\u1DFF]/,/[\u20D0-\u20FF]/,/[\uFE00-\uFE0F]/,/[\uFE20-\uFE2F]/),Be=b(Ie,Le,"*"),$e=m(/[a-zA-Z_]/,/[\u00A8\u00AA\u00AD\u00AF\u00B2-\u00B5\u00B7-\u00BA]/,/[\u00BC-\u00BE\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF]/,/[\u0100-\u02FF\u0370-\u167F\u1681-\u180D\u180F-\u1DBF]/,/[\u1E00-\u1FFF]/,/[\u200B-\u200D\u202A-\u202E\u203F-\u2040\u2054\u2060-\u206F]/,/[\u2070-\u20CF\u2100-\u218F\u2460-\u24FF\u2776-\u2793]/,/[\u2C00-\u2DFF\u2E80-\u2FFF]/,/[\u3004-\u3007\u3021-\u302F\u3031-\u303F\u3040-\uD7FF]/,/[\uF900-\uFD3D\uFD40-\uFDCF\uFDF0-\uFE1F\uFE30-\uFE44]/,/[\uFE47-\uFEFE\uFF00-\uFFFD]/),ze=m($e,/\d/,/[\u0300-\u036F\u1DC0-\u1DFF\u20D0-\u20FF\uFE20-\uFE2F]/),Fe=b($e,ze,"*"),Ue=b(/[A-Z]/,ze,"*"),je=["attached","autoclosure",b(/convention\(/,m("swift","block","c"),/\)/),"discardableResult","dynamicCallable","dynamicMemberLookup","escaping","freestanding","frozen","GKInspectable","IBAction","IBDesignable","IBInspectable","IBOutlet","IBSegueAction","inlinable","main","nonobjc","NSApplicationMain","NSCopying","NSManaged",b(/objc\(/,Fe,/\)/),"objc","objcMembers","propertyWrapper","requires_stored_property_inits","resultBuilder","Sendable","testable","UIApplicationMain","unchecked","unknown","usableFromInline","warn_unqualified_access"],Pe=["iOS","iOSApplicationExtension","macOS","macOSApplicationExtension","macCatalyst","macCatalystApplicationExtension","watchOS","watchOSApplicationExtension","tvOS","tvOSApplicationExtension","swift"] +;var Ke=Object.freeze({__proto__:null,grmr_bash:e=>{const n=e.regex,t={},a={ +begin:/\$\{/,end:/\}/,contains:["self",{begin:/:-/,contains:[t]}]} +;Object.assign(t,{className:"variable",variants:[{ +begin:n.concat(/\$[\w\d#@][\w\d_]*/,"(?![\\w\\d])(?![$])")},a]});const i={ +className:"subst",begin:/\$\(/,end:/\)/,contains:[e.BACKSLASH_ESCAPE]},r={ +begin:/<<-?\s*(?=\w+)/,starts:{contains:[e.END_SAME_AS_BEGIN({begin:/(\w+)/, +end:/(\w+)/,className:"string"})]}},s={className:"string",begin:/"/,end:/"/, +contains:[e.BACKSLASH_ESCAPE,t,i]};i.contains.push(s);const o={begin:/\$?\(\(/, +end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},e.NUMBER_MODE,t] +},l=e.SHEBANG({binary:"(fish|bash|zsh|sh|csh|ksh|tcsh|dash|scsh)",relevance:10 +}),c={className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0, +contains:[e.inherit(e.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0};return{ +name:"Bash",aliases:["sh"],keywords:{$pattern:/\b[a-z][a-z0-9._-]+\b/, +keyword:["if","then","else","elif","fi","for","while","until","in","do","done","case","esac","function","select"], +literal:["true","false"], +built_in:["break","cd","continue","eval","exec","exit","export","getopts","hash","pwd","readonly","return","shift","test","times","trap","umask","unset","alias","bind","builtin","caller","command","declare","echo","enable","help","let","local","logout","mapfile","printf","read","readarray","source","type","typeset","ulimit","unalias","set","shopt","autoload","bg","bindkey","bye","cap","chdir","clone","comparguments","compcall","compctl","compdescribe","compfiles","compgroups","compquote","comptags","comptry","compvalues","dirs","disable","disown","echotc","echoti","emulate","fc","fg","float","functions","getcap","getln","history","integer","jobs","kill","limit","log","noglob","popd","print","pushd","pushln","rehash","sched","setcap","setopt","stat","suspend","ttyctl","unfunction","unhash","unlimit","unsetopt","vared","wait","whence","where","which","zcompile","zformat","zftp","zle","zmodload","zparseopts","zprof","zpty","zregexparse","zsocket","zstyle","ztcp","chcon","chgrp","chown","chmod","cp","dd","df","dir","dircolors","ln","ls","mkdir","mkfifo","mknod","mktemp","mv","realpath","rm","rmdir","shred","sync","touch","truncate","vdir","b2sum","base32","base64","cat","cksum","comm","csplit","cut","expand","fmt","fold","head","join","md5sum","nl","numfmt","od","paste","ptx","pr","sha1sum","sha224sum","sha256sum","sha384sum","sha512sum","shuf","sort","split","sum","tac","tail","tr","tsort","unexpand","uniq","wc","arch","basename","chroot","date","dirname","du","echo","env","expr","factor","groups","hostid","id","link","logname","nice","nohup","nproc","pathchk","pinky","printenv","printf","pwd","readlink","runcon","seq","sleep","stat","stdbuf","stty","tee","test","timeout","tty","uname","unlink","uptime","users","who","whoami","yes"] +},contains:[l,e.SHEBANG(),c,o,e.HASH_COMMENT_MODE,r,{match:/(\/[a-z._-]+)+/},s,{ +match:/\\"/},{className:"string",begin:/'/,end:/'/},{match:/\\'/},t]}}, +grmr_c:e=>{const n=e.regex,t=e.COMMENT("//","$",{contains:[{begin:/\\\n/}] +}),a="decltype\\(auto\\)",i="[a-zA-Z_]\\w*::",r="("+a+"|"+n.optional(i)+"[a-zA-Z_]\\w*"+n.optional("<[^<>]+>")+")",s={ +className:"type",variants:[{begin:"\\b[a-z\\d_]*_t\\b"},{ +match:/\batomic_[a-z]{3,6}\b/}]},o={className:"string",variants:[{ +begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{ +begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)", +end:"'",illegal:"."},e.END_SAME_AS_BEGIN({ +begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},l={ +className:"number",variants:[{begin:"\\b(0b[01']+)"},{ +begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)((ll|LL|l|L)(u|U)?|(u|U)(ll|LL|l|L)?|f|F|b|B)" +},{ +begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)" +}],relevance:0},c={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{ +keyword:"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include" +},contains:[{begin:/\\\n/,relevance:0},e.inherit(o,{className:"string"}),{ +className:"string",begin:/<.*?>/},t,e.C_BLOCK_COMMENT_MODE]},d={ +className:"title",begin:n.optional(i)+e.IDENT_RE,relevance:0 +},g=n.optional(i)+e.IDENT_RE+"\\s*\\(",u={ +keyword:["asm","auto","break","case","continue","default","do","else","enum","extern","for","fortran","goto","if","inline","register","restrict","return","sizeof","struct","switch","typedef","union","volatile","while","_Alignas","_Alignof","_Atomic","_Generic","_Noreturn","_Static_assert","_Thread_local","alignas","alignof","noreturn","static_assert","thread_local","_Pragma"], +type:["float","double","signed","unsigned","int","short","long","char","void","_Bool","_Complex","_Imaginary","_Decimal32","_Decimal64","_Decimal128","const","static","complex","bool","imaginary"], +literal:"true false NULL", +built_in:"std string wstring cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set pair bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap priority_queue make_pair array shared_ptr abort terminate abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf future isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr" +},b=[c,s,t,e.C_BLOCK_COMMENT_MODE,l,o],m={variants:[{begin:/=/,end:/;/},{ +begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",end:/;/}], +keywords:u,contains:b.concat([{begin:/\(/,end:/\)/,keywords:u, +contains:b.concat(["self"]),relevance:0}]),relevance:0},p={ +begin:"("+r+"[\\*&\\s]+)+"+g,returnBegin:!0,end:/[{;=]/,excludeEnd:!0, +keywords:u,illegal:/[^\w\s\*&:<>.]/,contains:[{begin:a,keywords:u,relevance:0},{ +begin:g,returnBegin:!0,contains:[e.inherit(d,{className:"title.function"})], +relevance:0},{relevance:0,match:/,/},{className:"params",begin:/\(/,end:/\)/, +keywords:u,relevance:0,contains:[t,e.C_BLOCK_COMMENT_MODE,o,l,s,{begin:/\(/, +end:/\)/,keywords:u,relevance:0,contains:["self",t,e.C_BLOCK_COMMENT_MODE,o,l,s] +}]},s,t,e.C_BLOCK_COMMENT_MODE,c]};return{name:"C",aliases:["h"],keywords:u, +disableAutodetect:!0,illegal:"=]/,contains:[{ +beginKeywords:"final class struct"},e.TITLE_MODE]}]),exports:{preprocessor:c, +strings:o,keywords:u}}},grmr_cpp:e=>{const n=e.regex,t=e.COMMENT("//","$",{ +contains:[{begin:/\\\n/}] +}),a="decltype\\(auto\\)",i="[a-zA-Z_]\\w*::",r="(?!struct)("+a+"|"+n.optional(i)+"[a-zA-Z_]\\w*"+n.optional("<[^<>]+>")+")",s={ +className:"type",begin:"\\b[a-z\\d_]*_t\\b"},o={className:"string",variants:[{ +begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{ +begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)", +end:"'",illegal:"."},e.END_SAME_AS_BEGIN({ +begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},l={ +className:"number",variants:[{begin:"\\b(0b[01']+)"},{ +begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)((ll|LL|l|L)(u|U)?|(u|U)(ll|LL|l|L)?|f|F|b|B)" +},{ +begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)" +}],relevance:0},c={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{ +keyword:"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include" +},contains:[{begin:/\\\n/,relevance:0},e.inherit(o,{className:"string"}),{ +className:"string",begin:/<.*?>/},t,e.C_BLOCK_COMMENT_MODE]},d={ +className:"title",begin:n.optional(i)+e.IDENT_RE,relevance:0 +},g=n.optional(i)+e.IDENT_RE+"\\s*\\(",u={ +type:["bool","char","char16_t","char32_t","char8_t","double","float","int","long","short","void","wchar_t","unsigned","signed","const","static"], +keyword:["alignas","alignof","and","and_eq","asm","atomic_cancel","atomic_commit","atomic_noexcept","auto","bitand","bitor","break","case","catch","class","co_await","co_return","co_yield","compl","concept","const_cast|10","consteval","constexpr","constinit","continue","decltype","default","delete","do","dynamic_cast|10","else","enum","explicit","export","extern","false","final","for","friend","goto","if","import","inline","module","mutable","namespace","new","noexcept","not","not_eq","nullptr","operator","or","or_eq","override","private","protected","public","reflexpr","register","reinterpret_cast|10","requires","return","sizeof","static_assert","static_cast|10","struct","switch","synchronized","template","this","thread_local","throw","transaction_safe","transaction_safe_dynamic","true","try","typedef","typeid","typename","union","using","virtual","volatile","while","xor","xor_eq"], +literal:["NULL","false","nullopt","nullptr","true"],built_in:["_Pragma"], +_type_hints:["any","auto_ptr","barrier","binary_semaphore","bitset","complex","condition_variable","condition_variable_any","counting_semaphore","deque","false_type","future","imaginary","initializer_list","istringstream","jthread","latch","lock_guard","multimap","multiset","mutex","optional","ostringstream","packaged_task","pair","promise","priority_queue","queue","recursive_mutex","recursive_timed_mutex","scoped_lock","set","shared_future","shared_lock","shared_mutex","shared_timed_mutex","shared_ptr","stack","string_view","stringstream","timed_mutex","thread","true_type","tuple","unique_lock","unique_ptr","unordered_map","unordered_multimap","unordered_multiset","unordered_set","variant","vector","weak_ptr","wstring","wstring_view"] +},b={className:"function.dispatch",relevance:0,keywords:{ +_hint:["abort","abs","acos","apply","as_const","asin","atan","atan2","calloc","ceil","cerr","cin","clog","cos","cosh","cout","declval","endl","exchange","exit","exp","fabs","floor","fmod","forward","fprintf","fputs","free","frexp","fscanf","future","invoke","isalnum","isalpha","iscntrl","isdigit","isgraph","islower","isprint","ispunct","isspace","isupper","isxdigit","labs","launder","ldexp","log","log10","make_pair","make_shared","make_shared_for_overwrite","make_tuple","make_unique","malloc","memchr","memcmp","memcpy","memset","modf","move","pow","printf","putchar","puts","realloc","scanf","sin","sinh","snprintf","sprintf","sqrt","sscanf","std","stderr","stdin","stdout","strcat","strchr","strcmp","strcpy","strcspn","strlen","strncat","strncmp","strncpy","strpbrk","strrchr","strspn","strstr","swap","tan","tanh","terminate","to_underlying","tolower","toupper","vfprintf","visit","vprintf","vsprintf"] +}, +begin:n.concat(/\b/,/(?!decltype)/,/(?!if)/,/(?!for)/,/(?!switch)/,/(?!while)/,e.IDENT_RE,n.lookahead(/(<[^<>]+>|)\s*\(/)) +},m=[b,c,s,t,e.C_BLOCK_COMMENT_MODE,l,o],p={variants:[{begin:/=/,end:/;/},{ +begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",end:/;/}], +keywords:u,contains:m.concat([{begin:/\(/,end:/\)/,keywords:u, +contains:m.concat(["self"]),relevance:0}]),relevance:0},_={className:"function", +begin:"("+r+"[\\*&\\s]+)+"+g,returnBegin:!0,end:/[{;=]/,excludeEnd:!0, +keywords:u,illegal:/[^\w\s\*&:<>.]/,contains:[{begin:a,keywords:u,relevance:0},{ +begin:g,returnBegin:!0,contains:[d],relevance:0},{begin:/::/,relevance:0},{ +begin:/:/,endsWithParent:!0,contains:[o,l]},{relevance:0,match:/,/},{ +className:"params",begin:/\(/,end:/\)/,keywords:u,relevance:0, +contains:[t,e.C_BLOCK_COMMENT_MODE,o,l,s,{begin:/\(/,end:/\)/,keywords:u, +relevance:0,contains:["self",t,e.C_BLOCK_COMMENT_MODE,o,l,s]}] +},s,t,e.C_BLOCK_COMMENT_MODE,c]};return{name:"C++", +aliases:["cc","c++","h++","hpp","hh","hxx","cxx"],keywords:u,illegal:"",keywords:u,contains:["self",s]},{begin:e.IDENT_RE+"::",keywords:u},{ +match:[/\b(?:enum(?:\s+(?:class|struct))?|class|struct|union)/,/\s+/,/\w+/], +className:{1:"keyword",3:"title.class"}}])}},grmr_csharp:e=>{const n={ +keyword:["abstract","as","base","break","case","catch","class","const","continue","do","else","event","explicit","extern","finally","fixed","for","foreach","goto","if","implicit","in","interface","internal","is","lock","namespace","new","operator","out","override","params","private","protected","public","readonly","record","ref","return","scoped","sealed","sizeof","stackalloc","static","struct","switch","this","throw","try","typeof","unchecked","unsafe","using","virtual","void","volatile","while"].concat(["add","alias","and","ascending","async","await","by","descending","equals","from","get","global","group","init","into","join","let","nameof","not","notnull","on","or","orderby","partial","remove","select","set","unmanaged","value|0","var","when","where","with","yield"]), +built_in:["bool","byte","char","decimal","delegate","double","dynamic","enum","float","int","long","nint","nuint","object","sbyte","short","string","ulong","uint","ushort"], +literal:["default","false","null","true"]},t=e.inherit(e.TITLE_MODE,{ +begin:"[a-zA-Z](\\.?\\w)*"}),a={className:"number",variants:[{ +begin:"\\b(0b[01']+)"},{ +begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{ +begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)" +}],relevance:0},i={className:"string",begin:'@"',end:'"',contains:[{begin:'""'}] +},r=e.inherit(i,{illegal:/\n/}),s={className:"subst",begin:/\{/,end:/\}/, +keywords:n},o=e.inherit(s,{illegal:/\n/}),l={className:"string",begin:/\$"/, +end:'"',illegal:/\n/,contains:[{begin:/\{\{/},{begin:/\}\}/ +},e.BACKSLASH_ESCAPE,o]},c={className:"string",begin:/\$@"/,end:'"',contains:[{ +begin:/\{\{/},{begin:/\}\}/},{begin:'""'},s]},d=e.inherit(c,{illegal:/\n/, +contains:[{begin:/\{\{/},{begin:/\}\}/},{begin:'""'},o]}) +;s.contains=[c,l,i,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,a,e.C_BLOCK_COMMENT_MODE], +o.contains=[d,l,r,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,a,e.inherit(e.C_BLOCK_COMMENT_MODE,{ +illegal:/\n/})];const g={variants:[c,l,i,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE] +},u={begin:"<",end:">",contains:[{beginKeywords:"in out"},t] +},b=e.IDENT_RE+"(<"+e.IDENT_RE+"(\\s*,\\s*"+e.IDENT_RE+")*>)?(\\[\\])?",m={ +begin:"@"+e.IDENT_RE,relevance:0};return{name:"C#",aliases:["cs","c#"], +keywords:n,illegal:/::/,contains:[e.COMMENT("///","$",{returnBegin:!0, +contains:[{className:"doctag",variants:[{begin:"///",relevance:0},{ +begin:"\x3c!--|--\x3e"},{begin:""}]}] +}),e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{className:"meta",begin:"#", +end:"$",keywords:{ +keyword:"if else elif endif define undef warning error line region endregion pragma checksum" +}},g,a,{beginKeywords:"class interface",relevance:0,end:/[{;=]/, +illegal:/[^\s:,]/,contains:[{beginKeywords:"where class" +},t,u,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{beginKeywords:"namespace", +relevance:0,end:/[{;=]/,illegal:/[^\s:]/, +contains:[t,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{ +beginKeywords:"record",relevance:0,end:/[{;=]/,illegal:/[^\s:]/, +contains:[t,u,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"meta", +begin:"^\\s*\\[(?=[\\w])",excludeBegin:!0,end:"\\]",excludeEnd:!0,contains:[{ +className:"string",begin:/"/,end:/"/}]},{ +beginKeywords:"new return throw await else",relevance:0},{className:"function", +begin:"("+b+"\\s+)+"+e.IDENT_RE+"\\s*(<[^=]+>\\s*)?\\(",returnBegin:!0, +end:/\s*[{;=]/,excludeEnd:!0,keywords:n,contains:[{ +beginKeywords:"public private protected static internal protected abstract async extern override unsafe virtual new sealed partial", +relevance:0},{begin:e.IDENT_RE+"\\s*(<[^=]+>\\s*)?\\(",returnBegin:!0, +contains:[e.TITLE_MODE,u],relevance:0},{match:/\(\)/},{className:"params", +begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:n,relevance:0, +contains:[g,a,e.C_BLOCK_COMMENT_MODE] +},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},m]}},grmr_css:e=>{ +const n=e.regex,t=ie(e),a=[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE];return{ +name:"CSS",case_insensitive:!0,illegal:/[=|'\$]/,keywords:{ +keyframePosition:"from to"},classNameAliases:{keyframePosition:"selector-tag"}, +contains:[t.BLOCK_COMMENT,{begin:/-(webkit|moz|ms|o)-(?=[a-z])/ +},t.CSS_NUMBER_MODE,{className:"selector-id",begin:/#[A-Za-z0-9_-]+/,relevance:0 +},{className:"selector-class",begin:"\\.[a-zA-Z-][a-zA-Z0-9_-]*",relevance:0 +},t.ATTRIBUTE_SELECTOR_MODE,{className:"selector-pseudo",variants:[{ +begin:":("+oe.join("|")+")"},{begin:":(:)?("+le.join("|")+")"}] +},t.CSS_VARIABLE,{className:"attribute",begin:"\\b("+ce.join("|")+")\\b"},{ +begin:/:/,end:/[;}{]/, +contains:[t.BLOCK_COMMENT,t.HEXCOLOR,t.IMPORTANT,t.CSS_NUMBER_MODE,...a,{ +begin:/(url|data-uri)\(/,end:/\)/,relevance:0,keywords:{built_in:"url data-uri" +},contains:[...a,{className:"string",begin:/[^)]/,endsWithParent:!0, +excludeEnd:!0}]},t.FUNCTION_DISPATCH]},{begin:n.lookahead(/@/),end:"[{;]", +relevance:0,illegal:/:/,contains:[{className:"keyword",begin:/@-?\w[\w]*(-\w+)*/ +},{begin:/\s/,endsWithParent:!0,excludeEnd:!0,relevance:0,keywords:{ +$pattern:/[a-z-]+/,keyword:"and or not only",attribute:se.join(" ")},contains:[{ +begin:/[a-z-]+(?=:)/,className:"attribute"},...a,t.CSS_NUMBER_MODE]}]},{ +className:"selector-tag",begin:"\\b("+re.join("|")+")\\b"}]}},grmr_diff:e=>{ +const n=e.regex;return{name:"Diff",aliases:["patch"],contains:[{ +className:"meta",relevance:10, +match:n.either(/^@@ +-\d+,\d+ +\+\d+,\d+ +@@/,/^\*\*\* +\d+,\d+ +\*\*\*\*$/,/^--- +\d+,\d+ +----$/) +},{className:"comment",variants:[{ +begin:n.either(/Index: /,/^index/,/={3,}/,/^-{3}/,/^\*{3} /,/^\+{3}/,/^diff --git/), +end:/$/},{match:/^\*{15}$/}]},{className:"addition",begin:/^\+/,end:/$/},{ +className:"deletion",begin:/^-/,end:/$/},{className:"addition",begin:/^!/, +end:/$/}]}},grmr_go:e=>{const n={ +keyword:["break","case","chan","const","continue","default","defer","else","fallthrough","for","func","go","goto","if","import","interface","map","package","range","return","select","struct","switch","type","var"], +type:["bool","byte","complex64","complex128","error","float32","float64","int8","int16","int32","int64","string","uint8","uint16","uint32","uint64","int","uint","uintptr","rune"], +literal:["true","false","iota","nil"], +built_in:["append","cap","close","complex","copy","imag","len","make","new","panic","print","println","real","recover","delete"] +};return{name:"Go",aliases:["golang"],keywords:n,illegal:"{const n=e.regex;return{name:"GraphQL",aliases:["gql"], +case_insensitive:!0,disableAutodetect:!1,keywords:{ +keyword:["query","mutation","subscription","type","input","schema","directive","interface","union","scalar","fragment","enum","on"], +literal:["true","false","null"]}, +contains:[e.HASH_COMMENT_MODE,e.QUOTE_STRING_MODE,e.NUMBER_MODE,{ +scope:"punctuation",match:/[.]{3}/,relevance:0},{scope:"punctuation", +begin:/[\!\(\)\:\=\[\]\{\|\}]{1}/,relevance:0},{scope:"variable",begin:/\$/, +end:/\W/,excludeEnd:!0,relevance:0},{scope:"meta",match:/@\w+/,excludeEnd:!0},{ +scope:"symbol",begin:n.concat(/[_A-Za-z][_0-9A-Za-z]*/,n.lookahead(/\s*:/)), +relevance:0}],illegal:[/[;<']/,/BEGIN/]}},grmr_ini:e=>{const n=e.regex,t={ +className:"number",relevance:0,variants:[{begin:/([+-]+)?[\d]+_[\d_]+/},{ +begin:e.NUMBER_RE}]},a=e.COMMENT();a.variants=[{begin:/;/,end:/$/},{begin:/#/, +end:/$/}];const i={className:"variable",variants:[{begin:/\$[\w\d"][\w\d_]*/},{ +begin:/\$\{(.*?)\}/}]},r={className:"literal", +begin:/\bon|off|true|false|yes|no\b/},s={className:"string", +contains:[e.BACKSLASH_ESCAPE],variants:[{begin:"'''",end:"'''",relevance:10},{ +begin:'"""',end:'"""',relevance:10},{begin:'"',end:'"'},{begin:"'",end:"'"}] +},o={begin:/\[/,end:/\]/,contains:[a,r,i,s,t,"self"],relevance:0 +},l=n.either(/[A-Za-z0-9_-]+/,/"(\\"|[^"])*"/,/'[^']*'/);return{ +name:"TOML, also INI",aliases:["toml"],case_insensitive:!0,illegal:/\S/, +contains:[a,{className:"section",begin:/\[+/,end:/\]+/},{ +begin:n.concat(l,"(\\s*\\.\\s*",l,")*",n.lookahead(/\s*=\s*[^#\s]/)), +className:"attr",starts:{end:/$/,contains:[a,o,r,i,s,t]}}]}},grmr_java:e=>{ +const n=e.regex,t="[\xc0-\u02b8a-zA-Z_$][\xc0-\u02b8a-zA-Z_$0-9]*",a=t+pe("(?:<"+t+"~~~(?:\\s*,\\s*"+t+"~~~)*>)?",/~~~/g,2),i={ +keyword:["synchronized","abstract","private","var","static","if","const ","for","while","strictfp","finally","protected","import","native","final","void","enum","else","break","transient","catch","instanceof","volatile","case","assert","package","default","public","try","switch","continue","throws","protected","public","private","module","requires","exports","do","sealed","yield","permits"], +literal:["false","true","null"], +type:["char","boolean","long","float","int","byte","short","double"], +built_in:["super","this"]},r={className:"meta",begin:"@"+t,contains:[{ +begin:/\(/,end:/\)/,contains:["self"]}]},s={className:"params",begin:/\(/, +end:/\)/,keywords:i,relevance:0,contains:[e.C_BLOCK_COMMENT_MODE],endsParent:!0} +;return{name:"Java",aliases:["jsp"],keywords:i,illegal:/<\/|#/, +contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{begin:/\w+@/, +relevance:0},{className:"doctag",begin:"@[A-Za-z]+"}]}),{ +begin:/import java\.[a-z]+\./,keywords:"import",relevance:2 +},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{begin:/"""/,end:/"""/, +className:"string",contains:[e.BACKSLASH_ESCAPE] +},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{ +match:[/\b(?:class|interface|enum|extends|implements|new)/,/\s+/,t],className:{ +1:"keyword",3:"title.class"}},{match:/non-sealed/,scope:"keyword"},{ +begin:[n.concat(/(?!else)/,t),/\s+/,t,/\s+/,/=(?!=)/],className:{1:"type", +3:"variable",5:"operator"}},{begin:[/record/,/\s+/,t],className:{1:"keyword", +3:"title.class"},contains:[s,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{ +beginKeywords:"new throw return else",relevance:0},{ +begin:["(?:"+a+"\\s+)",e.UNDERSCORE_IDENT_RE,/\s*(?=\()/],className:{ +2:"title.function"},keywords:i,contains:[{className:"params",begin:/\(/, +end:/\)/,keywords:i,relevance:0, +contains:[r,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,me,e.C_BLOCK_COMMENT_MODE] +},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},me,r]}},grmr_javascript:Oe, +grmr_json:e=>{const n=["true","false","null"],t={scope:"literal", +beginKeywords:n.join(" ")};return{name:"JSON",keywords:{literal:n},contains:[{ +className:"attr",begin:/"(\\.|[^\\"\r\n])*"(?=\s*:)/,relevance:1.01},{ +match:/[{}[\],:]/,className:"punctuation",relevance:0 +},e.QUOTE_STRING_MODE,t,e.C_NUMBER_MODE,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE], +illegal:"\\S"}},grmr_kotlin:e=>{const n={ +keyword:"abstract as val var vararg get set class object open private protected public noinline crossinline dynamic final enum if else do while for when throw try catch finally import package is in fun override companion reified inline lateinit init interface annotation data sealed internal infix operator out by constructor super tailrec where const inner suspend typealias external expect actual", +built_in:"Byte Short Char Int Long Boolean Float Double Void Unit Nothing", +literal:"true false null"},t={className:"symbol",begin:e.UNDERSCORE_IDENT_RE+"@" +},a={className:"subst",begin:/\$\{/,end:/\}/,contains:[e.C_NUMBER_MODE]},i={ +className:"variable",begin:"\\$"+e.UNDERSCORE_IDENT_RE},r={className:"string", +variants:[{begin:'"""',end:'"""(?=[^"])',contains:[i,a]},{begin:"'",end:"'", +illegal:/\n/,contains:[e.BACKSLASH_ESCAPE]},{begin:'"',end:'"',illegal:/\n/, +contains:[e.BACKSLASH_ESCAPE,i,a]}]};a.contains.push(r);const s={ +className:"meta", +begin:"@(?:file|property|field|get|set|receiver|param|setparam|delegate)\\s*:(?:\\s*"+e.UNDERSCORE_IDENT_RE+")?" +},o={className:"meta",begin:"@"+e.UNDERSCORE_IDENT_RE,contains:[{begin:/\(/, +end:/\)/,contains:[e.inherit(r,{className:"string"}),"self"]}] +},l=me,c=e.COMMENT("/\\*","\\*/",{contains:[e.C_BLOCK_COMMENT_MODE]}),d={ +variants:[{className:"type",begin:e.UNDERSCORE_IDENT_RE},{begin:/\(/,end:/\)/, +contains:[]}]},g=d;return g.variants[1].contains=[d],d.variants[1].contains=[g], +{name:"Kotlin",aliases:["kt","kts"],keywords:n, +contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{className:"doctag", +begin:"@[A-Za-z]+"}]}),e.C_LINE_COMMENT_MODE,c,{className:"keyword", +begin:/\b(break|continue|return|this)\b/,starts:{contains:[{className:"symbol", +begin:/@\w+/}]}},t,s,o,{className:"function",beginKeywords:"fun",end:"[(]|$", +returnBegin:!0,excludeEnd:!0,keywords:n,relevance:5,contains:[{ +begin:e.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,relevance:0, +contains:[e.UNDERSCORE_TITLE_MODE]},{className:"type",begin://, +keywords:"reified",relevance:0},{className:"params",begin:/\(/,end:/\)/, +endsParent:!0,keywords:n,relevance:0,contains:[{begin:/:/,end:/[=,\/]/, +endsWithParent:!0,contains:[d,e.C_LINE_COMMENT_MODE,c],relevance:0 +},e.C_LINE_COMMENT_MODE,c,s,o,r,e.C_NUMBER_MODE]},c]},{ +begin:[/class|interface|trait/,/\s+/,e.UNDERSCORE_IDENT_RE],beginScope:{ +3:"title.class"},keywords:"class interface trait",end:/[:\{(]|$/,excludeEnd:!0, +illegal:"extends implements",contains:[{ +beginKeywords:"public protected internal private constructor" +},e.UNDERSCORE_TITLE_MODE,{className:"type",begin://,excludeBegin:!0, +excludeEnd:!0,relevance:0},{className:"type",begin:/[,:]\s*/,end:/[<\(,){\s]|$/, +excludeBegin:!0,returnEnd:!0},s,o]},r,{className:"meta",begin:"^#!/usr/bin/env", +end:"$",illegal:"\n"},l]}},grmr_less:e=>{ +const n=ie(e),t=de,a="[\\w-]+",i="("+a+"|@\\{"+a+"\\})",r=[],s=[],o=e=>({ +className:"string",begin:"~?"+e+".*?"+e}),l=(e,n,t)=>({className:e,begin:n, +relevance:t}),c={$pattern:/[a-z-]+/,keyword:"and or not only", +attribute:se.join(" ")},d={begin:"\\(",end:"\\)",contains:s,keywords:c, +relevance:0} +;s.push(e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,o("'"),o('"'),n.CSS_NUMBER_MODE,{ +begin:"(url|data-uri)\\(",starts:{className:"string",end:"[\\)\\n]", +excludeEnd:!0} +},n.HEXCOLOR,d,l("variable","@@?"+a,10),l("variable","@\\{"+a+"\\}"),l("built_in","~?`[^`]*?`"),{ +className:"attribute",begin:a+"\\s*:",end:":",returnBegin:!0,excludeEnd:!0 +},n.IMPORTANT,{beginKeywords:"and not"},n.FUNCTION_DISPATCH);const g=s.concat({ +begin:/\{/,end:/\}/,contains:r}),u={beginKeywords:"when",endsWithParent:!0, +contains:[{beginKeywords:"and not"}].concat(s)},b={begin:i+"\\s*:", +returnBegin:!0,end:/[;}]/,relevance:0,contains:[{begin:/-(webkit|moz|ms|o)-/ +},n.CSS_VARIABLE,{className:"attribute",begin:"\\b("+ce.join("|")+")\\b", +end:/(?=:)/,starts:{endsWithParent:!0,illegal:"[<=$]",relevance:0,contains:s}}] +},m={className:"keyword", +begin:"@(import|media|charset|font-face|(-[a-z]+-)?keyframes|supports|document|namespace|page|viewport|host)\\b", +starts:{end:"[;{}]",keywords:c,returnEnd:!0,contains:s,relevance:0}},p={ +className:"variable",variants:[{begin:"@"+a+"\\s*:",relevance:15},{begin:"@"+a +}],starts:{end:"[;}]",returnEnd:!0,contains:g}},_={variants:[{ +begin:"[\\.#:&\\[>]",end:"[;{}]"},{begin:i,end:/\{/}],returnBegin:!0, +returnEnd:!0,illegal:"[<='$\"]",relevance:0, +contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,u,l("keyword","all\\b"),l("variable","@\\{"+a+"\\}"),{ +begin:"\\b("+re.join("|")+")\\b",className:"selector-tag" +},n.CSS_NUMBER_MODE,l("selector-tag",i,0),l("selector-id","#"+i),l("selector-class","\\."+i,0),l("selector-tag","&",0),n.ATTRIBUTE_SELECTOR_MODE,{ +className:"selector-pseudo",begin:":("+oe.join("|")+")"},{ +className:"selector-pseudo",begin:":(:)?("+le.join("|")+")"},{begin:/\(/, +end:/\)/,relevance:0,contains:g},{begin:"!important"},n.FUNCTION_DISPATCH]},h={ +begin:a+":(:)?"+`(${t.join("|")})`,returnBegin:!0,contains:[_]} +;return r.push(e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,m,p,h,b,_,u,n.FUNCTION_DISPATCH), +{name:"Less",case_insensitive:!0,illegal:"[=>'/<($\"]",contains:r}}, +grmr_lua:e=>{const n="\\[=*\\[",t="\\]=*\\]",a={begin:n,end:t,contains:["self"] +},i=[e.COMMENT("--(?!"+n+")","$"),e.COMMENT("--"+n,t,{contains:[a],relevance:10 +})];return{name:"Lua",keywords:{$pattern:e.UNDERSCORE_IDENT_RE, +literal:"true false nil", +keyword:"and break do else elseif end for goto if in local not or repeat return then until while", +built_in:"_G _ENV _VERSION __index __newindex __mode __call __metatable __tostring __len __gc __add __sub __mul __div __mod __pow __concat __unm __eq __lt __le assert collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstring module next pairs pcall print rawequal rawget rawset require select setfenv setmetatable tonumber tostring type unpack xpcall arg self coroutine resume yield status wrap create running debug getupvalue debug sethook getmetatable gethook setmetatable setlocal traceback setfenv getinfo setupvalue getlocal getregistry getfenv io lines write close flush open output type read stderr stdin input stdout popen tmpfile math log max acos huge ldexp pi cos tanh pow deg tan cosh sinh random randomseed frexp ceil floor rad abs sqrt modf asin min mod fmod log10 atan2 exp sin atan os exit setlocale date getenv difftime remove time clock tmpname rename execute package preload loadlib loaded loaders cpath config path seeall string sub upper len gfind rep find match char dump gmatch reverse byte format gsub lower table setn insert getn foreachi maxn foreach concat sort remove" +},contains:i.concat([{className:"function",beginKeywords:"function",end:"\\)", +contains:[e.inherit(e.TITLE_MODE,{ +begin:"([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*"}),{className:"params", +begin:"\\(",endsWithParent:!0,contains:i}].concat(i) +},e.C_NUMBER_MODE,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{className:"string", +begin:n,end:t,contains:[a],relevance:5}])}},grmr_makefile:e=>{const n={ +className:"variable",variants:[{begin:"\\$\\("+e.UNDERSCORE_IDENT_RE+"\\)", +contains:[e.BACKSLASH_ESCAPE]},{begin:/\$[@%{ +const n={begin:/<\/?[A-Za-z_]/,end:">",subLanguage:"xml",relevance:0},t={ +variants:[{begin:/\[.+?\]\[.*?\]/,relevance:0},{ +begin:/\[.+?\]\(((data|javascript|mailto):|(?:http|ftp)s?:\/\/).*?\)/, +relevance:2},{ +begin:e.regex.concat(/\[.+?\]\(/,/[A-Za-z][A-Za-z0-9+.-]*/,/:\/\/.*?\)/), +relevance:2},{begin:/\[.+?\]\([./?&#].*?\)/,relevance:1},{ +begin:/\[.*?\]\(.*?\)/,relevance:0}],returnBegin:!0,contains:[{match:/\[(?=\])/ +},{className:"string",relevance:0,begin:"\\[",end:"\\]",excludeBegin:!0, +returnEnd:!0},{className:"link",relevance:0,begin:"\\]\\(",end:"\\)", +excludeBegin:!0,excludeEnd:!0},{className:"symbol",relevance:0,begin:"\\]\\[", +end:"\\]",excludeBegin:!0,excludeEnd:!0}]},a={className:"strong",contains:[], +variants:[{begin:/_{2}(?!\s)/,end:/_{2}/},{begin:/\*{2}(?!\s)/,end:/\*{2}/}] +},i={className:"emphasis",contains:[],variants:[{begin:/\*(?![*\s])/,end:/\*/},{ +begin:/_(?![_\s])/,end:/_/,relevance:0}]},r=e.inherit(a,{contains:[] +}),s=e.inherit(i,{contains:[]});a.contains.push(s),i.contains.push(r) +;let o=[n,t];return[a,i,r,s].forEach((e=>{e.contains=e.contains.concat(o) +})),o=o.concat(a,i),{name:"Markdown",aliases:["md","mkdown","mkd"],contains:[{ +className:"section",variants:[{begin:"^#{1,6}",end:"$",contains:o},{ +begin:"(?=^.+?\\n[=-]{2,}$)",contains:[{begin:"^[=-]*$"},{begin:"^",end:"\\n", +contains:o}]}]},n,{className:"bullet",begin:"^[ \t]*([*+-]|(\\d+\\.))(?=\\s+)", +end:"\\s+",excludeEnd:!0},a,i,{className:"quote",begin:"^>\\s+",contains:o, +end:"$"},{className:"code",variants:[{begin:"(`{3,})[^`](.|\\n)*?\\1`*[ ]*"},{ +begin:"(~{3,})[^~](.|\\n)*?\\1~*[ ]*"},{begin:"```",end:"```+[ ]*$"},{ +begin:"~~~",end:"~~~+[ ]*$"},{begin:"`.+?`"},{begin:"(?=^( {4}|\\t))", +contains:[{begin:"^( {4}|\\t)",end:"(\\n)$"}],relevance:0}]},{ +begin:"^[-\\*]{3,}",end:"$"},t,{begin:/^\[[^\n]+\]:/,returnBegin:!0,contains:[{ +className:"symbol",begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0},{ +className:"link",begin:/:\s*/,end:/$/,excludeBegin:!0}]}]}},grmr_objectivec:e=>{ +const n=/[a-zA-Z@][a-zA-Z0-9_]*/,t={$pattern:n, +keyword:["@interface","@class","@protocol","@implementation"]};return{ +name:"Objective-C",aliases:["mm","objc","obj-c","obj-c++","objective-c++"], +keywords:{"variable.language":["this","super"],$pattern:n, +keyword:["while","export","sizeof","typedef","const","struct","for","union","volatile","static","mutable","if","do","return","goto","enum","else","break","extern","asm","case","default","register","explicit","typename","switch","continue","inline","readonly","assign","readwrite","self","@synchronized","id","typeof","nonatomic","IBOutlet","IBAction","strong","weak","copy","in","out","inout","bycopy","byref","oneway","__strong","__weak","__block","__autoreleasing","@private","@protected","@public","@try","@property","@end","@throw","@catch","@finally","@autoreleasepool","@synthesize","@dynamic","@selector","@optional","@required","@encode","@package","@import","@defs","@compatibility_alias","__bridge","__bridge_transfer","__bridge_retained","__bridge_retain","__covariant","__contravariant","__kindof","_Nonnull","_Nullable","_Null_unspecified","__FUNCTION__","__PRETTY_FUNCTION__","__attribute__","getter","setter","retain","unsafe_unretained","nonnull","nullable","null_unspecified","null_resettable","class","instancetype","NS_DESIGNATED_INITIALIZER","NS_UNAVAILABLE","NS_REQUIRES_SUPER","NS_RETURNS_INNER_POINTER","NS_INLINE","NS_AVAILABLE","NS_DEPRECATED","NS_ENUM","NS_OPTIONS","NS_SWIFT_UNAVAILABLE","NS_ASSUME_NONNULL_BEGIN","NS_ASSUME_NONNULL_END","NS_REFINED_FOR_SWIFT","NS_SWIFT_NAME","NS_SWIFT_NOTHROW","NS_DURING","NS_HANDLER","NS_ENDHANDLER","NS_VALUERETURN","NS_VOIDRETURN"], +literal:["false","true","FALSE","TRUE","nil","YES","NO","NULL"], +built_in:["dispatch_once_t","dispatch_queue_t","dispatch_sync","dispatch_async","dispatch_once"], +type:["int","float","char","unsigned","signed","short","long","double","wchar_t","unichar","void","bool","BOOL","id|0","_Bool"] +},illegal:"/,end:/$/,illegal:"\\n" +},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"class", +begin:"("+t.keyword.join("|")+")\\b",end:/(\{|$)/,excludeEnd:!0,keywords:t, +contains:[e.UNDERSCORE_TITLE_MODE]},{begin:"\\."+e.UNDERSCORE_IDENT_RE, +relevance:0}]}},grmr_perl:e=>{const n=e.regex,t=/[dualxmsipngr]{0,12}/,a={ +$pattern:/[\w.]+/, +keyword:"abs accept alarm and atan2 bind binmode bless break caller chdir chmod chomp chop chown chr chroot close closedir connect continue cos crypt dbmclose dbmopen defined delete die do dump each else elsif endgrent endhostent endnetent endprotoent endpwent endservent eof eval exec exists exit exp fcntl fileno flock for foreach fork format formline getc getgrent getgrgid getgrnam gethostbyaddr gethostbyname gethostent getlogin getnetbyaddr getnetbyname getnetent getpeername getpgrp getpriority getprotobyname getprotobynumber getprotoent getpwent getpwnam getpwuid getservbyname getservbyport getservent getsockname getsockopt given glob gmtime goto grep gt hex if index int ioctl join keys kill last lc lcfirst length link listen local localtime log lstat lt ma map mkdir msgctl msgget msgrcv msgsnd my ne next no not oct open opendir or ord our pack package pipe pop pos print printf prototype push q|0 qq quotemeta qw qx rand read readdir readline readlink readpipe recv redo ref rename require reset return reverse rewinddir rindex rmdir say scalar seek seekdir select semctl semget semop send setgrent sethostent setnetent setpgrp setpriority setprotoent setpwent setservent setsockopt shift shmctl shmget shmread shmwrite shutdown sin sleep socket socketpair sort splice split sprintf sqrt srand stat state study sub substr symlink syscall sysopen sysread sysseek system syswrite tell telldir tie tied time times tr truncate uc ucfirst umask undef unless unlink unpack unshift untie until use utime values vec wait waitpid wantarray warn when while write x|0 xor y|0" +},i={className:"subst",begin:"[$@]\\{",end:"\\}",keywords:a},r={begin:/->\{/, +end:/\}/},s={variants:[{begin:/\$\d/},{ +begin:n.concat(/[$%@](\^\w\b|#\w+(::\w+)*|\{\w+\}|\w+(::\w*)*)/,"(?![A-Za-z])(?![@$%])") +},{begin:/[$%@][^\s\w{]/,relevance:0}] +},o=[e.BACKSLASH_ESCAPE,i,s],l=[/!/,/\//,/\|/,/\?/,/'/,/"/,/#/],c=(e,a,i="\\1")=>{ +const r="\\1"===i?i:n.concat(i,a) +;return n.concat(n.concat("(?:",e,")"),a,/(?:\\.|[^\\\/])*?/,r,/(?:\\.|[^\\\/])*?/,i,t) +},d=(e,a,i)=>n.concat(n.concat("(?:",e,")"),a,/(?:\\.|[^\\\/])*?/,i,t),g=[s,e.HASH_COMMENT_MODE,e.COMMENT(/^=\w/,/=cut/,{ +endsWithParent:!0}),r,{className:"string",contains:o,variants:[{ +begin:"q[qwxr]?\\s*\\(",end:"\\)",relevance:5},{begin:"q[qwxr]?\\s*\\[", +end:"\\]",relevance:5},{begin:"q[qwxr]?\\s*\\{",end:"\\}",relevance:5},{ +begin:"q[qwxr]?\\s*\\|",end:"\\|",relevance:5},{begin:"q[qwxr]?\\s*<",end:">", +relevance:5},{begin:"qw\\s+q",end:"q",relevance:5},{begin:"'",end:"'", +contains:[e.BACKSLASH_ESCAPE]},{begin:'"',end:'"'},{begin:"`",end:"`", +contains:[e.BACKSLASH_ESCAPE]},{begin:/\{\w+\}/,relevance:0},{ +begin:"-?\\w+\\s*=>",relevance:0}]},{className:"number", +begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b", +relevance:0},{ +begin:"(\\/\\/|"+e.RE_STARTERS_RE+"|\\b(split|return|print|reverse|grep)\\b)\\s*", +keywords:"split return print reverse grep",relevance:0, +contains:[e.HASH_COMMENT_MODE,{className:"regexp",variants:[{ +begin:c("s|tr|y",n.either(...l,{capture:!0}))},{begin:c("s|tr|y","\\(","\\)")},{ +begin:c("s|tr|y","\\[","\\]")},{begin:c("s|tr|y","\\{","\\}")}],relevance:2},{ +className:"regexp",variants:[{begin:/(m|qr)\/\//,relevance:0},{ +begin:d("(?:m|qr)?",/\//,/\//)},{begin:d("m|qr",n.either(...l,{capture:!0 +}),/\1/)},{begin:d("m|qr",/\(/,/\)/)},{begin:d("m|qr",/\[/,/\]/)},{ +begin:d("m|qr",/\{/,/\}/)}]}]},{className:"function",beginKeywords:"sub", +end:"(\\s*\\(.*?\\))?[;{]",excludeEnd:!0,relevance:5,contains:[e.TITLE_MODE]},{ +begin:"-\\w\\b",relevance:0},{begin:"^__DATA__$",end:"^__END__$", +subLanguage:"mojolicious",contains:[{begin:"^@@.*",end:"$",className:"comment"}] +}];return i.contains=g,r.contains=g,{name:"Perl",aliases:["pl","pm"],keywords:a, +contains:g}},grmr_php:e=>{ +const n=e.regex,t=/(?![A-Za-z0-9])(?![$])/,a=n.concat(/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/,t),i=n.concat(/(\\?[A-Z][a-z0-9_\x7f-\xff]+|\\?[A-Z]+(?=[A-Z][a-z0-9_\x7f-\xff])){1,}/,t),r={ +scope:"variable",match:"\\$+"+a},s={scope:"subst",variants:[{begin:/\$\w+/},{ +begin:/\{\$/,end:/\}/}]},o=e.inherit(e.APOS_STRING_MODE,{illegal:null +}),l="[ \t\n]",c={scope:"string",variants:[e.inherit(e.QUOTE_STRING_MODE,{ +illegal:null,contains:e.QUOTE_STRING_MODE.contains.concat(s)}),o,{ +begin:/<<<[ \t]*(?:(\w+)|"(\w+)")\n/,end:/[ \t]*(\w+)\b/, +contains:e.QUOTE_STRING_MODE.contains.concat(s),"on:begin":(e,n)=>{ +n.data._beginMatch=e[1]||e[2]},"on:end":(e,n)=>{ +n.data._beginMatch!==e[1]&&n.ignoreMatch()}},e.END_SAME_AS_BEGIN({ +begin:/<<<[ \t]*'(\w+)'\n/,end:/[ \t]*(\w+)\b/})]},d={scope:"number",variants:[{ +begin:"\\b0[bB][01]+(?:_[01]+)*\\b"},{begin:"\\b0[oO][0-7]+(?:_[0-7]+)*\\b"},{ +begin:"\\b0[xX][\\da-fA-F]+(?:_[\\da-fA-F]+)*\\b"},{ +begin:"(?:\\b\\d+(?:_\\d+)*(\\.(?:\\d+(?:_\\d+)*))?|\\B\\.\\d+)(?:[eE][+-]?\\d+)?" +}],relevance:0 +},g=["false","null","true"],u=["__CLASS__","__DIR__","__FILE__","__FUNCTION__","__COMPILER_HALT_OFFSET__","__LINE__","__METHOD__","__NAMESPACE__","__TRAIT__","die","echo","exit","include","include_once","print","require","require_once","array","abstract","and","as","binary","bool","boolean","break","callable","case","catch","class","clone","const","continue","declare","default","do","double","else","elseif","empty","enddeclare","endfor","endforeach","endif","endswitch","endwhile","enum","eval","extends","final","finally","float","for","foreach","from","global","goto","if","implements","instanceof","insteadof","int","integer","interface","isset","iterable","list","match|0","mixed","new","never","object","or","private","protected","public","readonly","real","return","string","switch","throw","trait","try","unset","use","var","void","while","xor","yield"],b=["Error|0","AppendIterator","ArgumentCountError","ArithmeticError","ArrayIterator","ArrayObject","AssertionError","BadFunctionCallException","BadMethodCallException","CachingIterator","CallbackFilterIterator","CompileError","Countable","DirectoryIterator","DivisionByZeroError","DomainException","EmptyIterator","ErrorException","Exception","FilesystemIterator","FilterIterator","GlobIterator","InfiniteIterator","InvalidArgumentException","IteratorIterator","LengthException","LimitIterator","LogicException","MultipleIterator","NoRewindIterator","OutOfBoundsException","OutOfRangeException","OuterIterator","OverflowException","ParentIterator","ParseError","RangeException","RecursiveArrayIterator","RecursiveCachingIterator","RecursiveCallbackFilterIterator","RecursiveDirectoryIterator","RecursiveFilterIterator","RecursiveIterator","RecursiveIteratorIterator","RecursiveRegexIterator","RecursiveTreeIterator","RegexIterator","RuntimeException","SeekableIterator","SplDoublyLinkedList","SplFileInfo","SplFileObject","SplFixedArray","SplHeap","SplMaxHeap","SplMinHeap","SplObjectStorage","SplObserver","SplPriorityQueue","SplQueue","SplStack","SplSubject","SplTempFileObject","TypeError","UnderflowException","UnexpectedValueException","UnhandledMatchError","ArrayAccess","BackedEnum","Closure","Fiber","Generator","Iterator","IteratorAggregate","Serializable","Stringable","Throwable","Traversable","UnitEnum","WeakReference","WeakMap","Directory","__PHP_Incomplete_Class","parent","php_user_filter","self","static","stdClass"],m={ +keyword:u,literal:(e=>{const n=[];return e.forEach((e=>{ +n.push(e),e.toLowerCase()===e?n.push(e.toUpperCase()):n.push(e.toLowerCase()) +})),n})(g),built_in:b},p=e=>e.map((e=>e.replace(/\|\d+$/,""))),_={variants:[{ +match:[/new/,n.concat(l,"+"),n.concat("(?!",p(b).join("\\b|"),"\\b)"),i],scope:{ +1:"keyword",4:"title.class"}}]},h=n.concat(a,"\\b(?!\\()"),f={variants:[{ +match:[n.concat(/::/,n.lookahead(/(?!class\b)/)),h],scope:{2:"variable.constant" +}},{match:[/::/,/class/],scope:{2:"variable.language"}},{ +match:[i,n.concat(/::/,n.lookahead(/(?!class\b)/)),h],scope:{1:"title.class", +3:"variable.constant"}},{match:[i,n.concat("::",n.lookahead(/(?!class\b)/))], +scope:{1:"title.class"}},{match:[i,/::/,/class/],scope:{1:"title.class", +3:"variable.language"}}]},E={scope:"attr", +match:n.concat(a,n.lookahead(":"),n.lookahead(/(?!::)/))},y={relevance:0, +begin:/\(/,end:/\)/,keywords:m,contains:[E,r,f,e.C_BLOCK_COMMENT_MODE,c,d,_] +},N={relevance:0, +match:[/\b/,n.concat("(?!fn\\b|function\\b|",p(u).join("\\b|"),"|",p(b).join("\\b|"),"\\b)"),a,n.concat(l,"*"),n.lookahead(/(?=\()/)], +scope:{3:"title.function.invoke"},contains:[y]};y.contains.push(N) +;const w=[E,f,e.C_BLOCK_COMMENT_MODE,c,d,_];return{case_insensitive:!1, +keywords:m,contains:[{begin:n.concat(/#\[\s*/,i),beginScope:"meta",end:/]/, +endScope:"meta",keywords:{literal:g,keyword:["new","array"]},contains:[{ +begin:/\[/,end:/]/,keywords:{literal:g,keyword:["new","array"]}, +contains:["self",...w]},...w,{scope:"meta",match:i}] +},e.HASH_COMMENT_MODE,e.COMMENT("//","$"),e.COMMENT("/\\*","\\*/",{contains:[{ +scope:"doctag",match:"@[A-Za-z]+"}]}),{match:/__halt_compiler\(\);/, +keywords:"__halt_compiler",starts:{scope:"comment",end:e.MATCH_NOTHING_RE, +contains:[{match:/\?>/,scope:"meta",endsParent:!0}]}},{scope:"meta",variants:[{ +begin:/<\?php/,relevance:10},{begin:/<\?=/},{begin:/<\?/,relevance:.1},{ +begin:/\?>/}]},{scope:"variable.language",match:/\$this\b/},r,N,f,{ +match:[/const/,/\s/,a],scope:{1:"keyword",3:"variable.constant"}},_,{ +scope:"function",relevance:0,beginKeywords:"fn function",end:/[;{]/, +excludeEnd:!0,illegal:"[$%\\[]",contains:[{beginKeywords:"use" +},e.UNDERSCORE_TITLE_MODE,{begin:"=>",endsParent:!0},{scope:"params", +begin:"\\(",end:"\\)",excludeBegin:!0,excludeEnd:!0,keywords:m, +contains:["self",r,f,e.C_BLOCK_COMMENT_MODE,c,d]}]},{scope:"class",variants:[{ +beginKeywords:"enum",illegal:/[($"]/},{beginKeywords:"class interface trait", +illegal:/[:($"]/}],relevance:0,end:/\{/,excludeEnd:!0,contains:[{ +beginKeywords:"extends implements"},e.UNDERSCORE_TITLE_MODE]},{ +beginKeywords:"namespace",relevance:0,end:";",illegal:/[.']/, +contains:[e.inherit(e.UNDERSCORE_TITLE_MODE,{scope:"title.class"})]},{ +beginKeywords:"use",relevance:0,end:";",contains:[{ +match:/\b(as|const|function)\b/,scope:"keyword"},e.UNDERSCORE_TITLE_MODE]},c,d]} +},grmr_php_template:e=>({name:"PHP template",subLanguage:"xml",contains:[{ +begin:/<\?(php|=)?/,end:/\?>/,subLanguage:"php",contains:[{begin:"/\\*", +end:"\\*/",skip:!0},{begin:'b"',end:'"',skip:!0},{begin:"b'",end:"'",skip:!0 +},e.inherit(e.APOS_STRING_MODE,{illegal:null,className:null,contains:null, +skip:!0}),e.inherit(e.QUOTE_STRING_MODE,{illegal:null,className:null, +contains:null,skip:!0})]}]}),grmr_plaintext:e=>({name:"Plain text", +aliases:["text","txt"],disableAutodetect:!0}),grmr_python:e=>{ +const n=e.regex,t=/[\p{XID_Start}_]\p{XID_Continue}*/u,a=["and","as","assert","async","await","break","case","class","continue","def","del","elif","else","except","finally","for","from","global","if","import","in","is","lambda","match","nonlocal|10","not","or","pass","raise","return","try","while","with","yield"],i={ +$pattern:/[A-Za-z]\w+|__\w+__/,keyword:a, +built_in:["__import__","abs","all","any","ascii","bin","bool","breakpoint","bytearray","bytes","callable","chr","classmethod","compile","complex","delattr","dict","dir","divmod","enumerate","eval","exec","filter","float","format","frozenset","getattr","globals","hasattr","hash","help","hex","id","input","int","isinstance","issubclass","iter","len","list","locals","map","max","memoryview","min","next","object","oct","open","ord","pow","print","property","range","repr","reversed","round","set","setattr","slice","sorted","staticmethod","str","sum","super","tuple","type","vars","zip"], +literal:["__debug__","Ellipsis","False","None","NotImplemented","True"], +type:["Any","Callable","Coroutine","Dict","List","Literal","Generic","Optional","Sequence","Set","Tuple","Type","Union"] +},r={className:"meta",begin:/^(>>>|\.\.\.) /},s={className:"subst",begin:/\{/, +end:/\}/,keywords:i,illegal:/#/},o={begin:/\{\{/,relevance:0},l={ +className:"string",contains:[e.BACKSLASH_ESCAPE],variants:[{ +begin:/([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?'''/,end:/'''/, +contains:[e.BACKSLASH_ESCAPE,r],relevance:10},{ +begin:/([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?"""/,end:/"""/, +contains:[e.BACKSLASH_ESCAPE,r],relevance:10},{ +begin:/([fF][rR]|[rR][fF]|[fF])'''/,end:/'''/, +contains:[e.BACKSLASH_ESCAPE,r,o,s]},{begin:/([fF][rR]|[rR][fF]|[fF])"""/, +end:/"""/,contains:[e.BACKSLASH_ESCAPE,r,o,s]},{begin:/([uU]|[rR])'/,end:/'/, +relevance:10},{begin:/([uU]|[rR])"/,end:/"/,relevance:10},{ +begin:/([bB]|[bB][rR]|[rR][bB])'/,end:/'/},{begin:/([bB]|[bB][rR]|[rR][bB])"/, +end:/"/},{begin:/([fF][rR]|[rR][fF]|[fF])'/,end:/'/, +contains:[e.BACKSLASH_ESCAPE,o,s]},{begin:/([fF][rR]|[rR][fF]|[fF])"/,end:/"/, +contains:[e.BACKSLASH_ESCAPE,o,s]},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE] +},c="[0-9](_?[0-9])*",d=`(\\b(${c}))?\\.(${c})|\\b(${c})\\.`,g="\\b|"+a.join("|"),u={ +className:"number",relevance:0,variants:[{ +begin:`(\\b(${c})|(${d}))[eE][+-]?(${c})[jJ]?(?=${g})`},{begin:`(${d})[jJ]?`},{ +begin:`\\b([1-9](_?[0-9])*|0+(_?0)*)[lLjJ]?(?=${g})`},{ +begin:`\\b0[bB](_?[01])+[lL]?(?=${g})`},{begin:`\\b0[oO](_?[0-7])+[lL]?(?=${g})` +},{begin:`\\b0[xX](_?[0-9a-fA-F])+[lL]?(?=${g})`},{begin:`\\b(${c})[jJ](?=${g})` +}]},b={className:"comment",begin:n.lookahead(/# type:/),end:/$/,keywords:i, +contains:[{begin:/# type:/},{begin:/#/,end:/\b\B/,endsWithParent:!0}]},m={ +className:"params",variants:[{className:"",begin:/\(\s*\)/,skip:!0},{begin:/\(/, +end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:i, +contains:["self",r,u,l,e.HASH_COMMENT_MODE]}]};return s.contains=[l,u,r],{ +name:"Python",aliases:["py","gyp","ipython"],unicodeRegex:!0,keywords:i, +illegal:/(<\/|\?)|=>/,contains:[r,u,{begin:/\bself\b/},{beginKeywords:"if", +relevance:0},l,b,e.HASH_COMMENT_MODE,{match:[/\bdef/,/\s+/,t],scope:{ +1:"keyword",3:"title.function"},contains:[m]},{variants:[{ +match:[/\bclass/,/\s+/,t,/\s*/,/\(\s*/,t,/\s*\)/]},{match:[/\bclass/,/\s+/,t]}], +scope:{1:"keyword",3:"title.class",6:"title.class.inherited"}},{ +className:"meta",begin:/^[\t ]*@/,end:/(?=#)|$/,contains:[u,m,l]}]}}, +grmr_python_repl:e=>({aliases:["pycon"],contains:[{className:"meta.prompt", +starts:{end:/ |$/,starts:{end:"$",subLanguage:"python"}},variants:[{ +begin:/^>>>(?=[ ]|$)/},{begin:/^\.\.\.(?=[ ]|$)/}]}]}),grmr_r:e=>{ +const n=e.regex,t=/(?:(?:[a-zA-Z]|\.[._a-zA-Z])[._a-zA-Z0-9]*)|\.(?!\d)/,a=n.either(/0[xX][0-9a-fA-F]+\.[0-9a-fA-F]*[pP][+-]?\d+i?/,/0[xX][0-9a-fA-F]+(?:[pP][+-]?\d+)?[Li]?/,/(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?[Li]?/),i=/[=!<>:]=|\|\||&&|:::?|<-|<<-|->>|->|\|>|[-+*\/?!$&|:<=>@^~]|\*\*/,r=n.either(/[()]/,/[{}]/,/\[\[/,/[[\]]/,/\\/,/,/) +;return{name:"R",keywords:{$pattern:t, +keyword:"function if in break next repeat else for while", +literal:"NULL NA TRUE FALSE Inf NaN NA_integer_|10 NA_real_|10 NA_character_|10 NA_complex_|10", +built_in:"LETTERS letters month.abb month.name pi T F abs acos acosh all any anyNA Arg as.call as.character as.complex as.double as.environment as.integer as.logical as.null.default as.numeric as.raw asin asinh atan atanh attr attributes baseenv browser c call ceiling class Conj cos cosh cospi cummax cummin cumprod cumsum digamma dim dimnames emptyenv exp expression floor forceAndCall gamma gc.time globalenv Im interactive invisible is.array is.atomic is.call is.character is.complex is.double is.environment is.expression is.finite is.function is.infinite is.integer is.language is.list is.logical is.matrix is.na is.name is.nan is.null is.numeric is.object is.pairlist is.raw is.recursive is.single is.symbol lazyLoadDBfetch length lgamma list log max min missing Mod names nargs nzchar oldClass on.exit pos.to.env proc.time prod quote range Re rep retracemem return round seq_along seq_len seq.int sign signif sin sinh sinpi sqrt standardGeneric substitute sum switch tan tanh tanpi tracemem trigamma trunc unclass untracemem UseMethod xtfrm" +},contains:[e.COMMENT(/#'/,/$/,{contains:[{scope:"doctag",match:/@examples/, +starts:{end:n.lookahead(n.either(/\n^#'\s*(?=@[a-zA-Z]+)/,/\n^(?!#')/)), +endsParent:!0}},{scope:"doctag",begin:"@param",end:/$/,contains:[{ +scope:"variable",variants:[{match:t},{match:/`(?:\\.|[^`\\])+`/}],endsParent:!0 +}]},{scope:"doctag",match:/@[a-zA-Z]+/},{scope:"keyword",match:/\\[a-zA-Z]+/}] +}),e.HASH_COMMENT_MODE,{scope:"string",contains:[e.BACKSLASH_ESCAPE], +variants:[e.END_SAME_AS_BEGIN({begin:/[rR]"(-*)\(/,end:/\)(-*)"/ +}),e.END_SAME_AS_BEGIN({begin:/[rR]"(-*)\{/,end:/\}(-*)"/ +}),e.END_SAME_AS_BEGIN({begin:/[rR]"(-*)\[/,end:/\](-*)"/ +}),e.END_SAME_AS_BEGIN({begin:/[rR]'(-*)\(/,end:/\)(-*)'/ +}),e.END_SAME_AS_BEGIN({begin:/[rR]'(-*)\{/,end:/\}(-*)'/ +}),e.END_SAME_AS_BEGIN({begin:/[rR]'(-*)\[/,end:/\](-*)'/}),{begin:'"',end:'"', +relevance:0},{begin:"'",end:"'",relevance:0}]},{relevance:0,variants:[{scope:{ +1:"operator",2:"number"},match:[i,a]},{scope:{1:"operator",2:"number"}, +match:[/%[^%]*%/,a]},{scope:{1:"punctuation",2:"number"},match:[r,a]},{scope:{ +2:"number"},match:[/[^a-zA-Z0-9._]|^/,a]}]},{scope:{3:"operator"}, +match:[t,/\s+/,/<-/,/\s+/]},{scope:"operator",relevance:0,variants:[{match:i},{ +match:/%[^%]*%/}]},{scope:"punctuation",relevance:0,match:r},{begin:"`",end:"`", +contains:[{begin:/\\./}]}]}},grmr_ruby:e=>{ +const n=e.regex,t="([a-zA-Z_]\\w*[!?=]?|[-+~]@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?)",a=n.either(/\b([A-Z]+[a-z0-9]+)+/,/\b([A-Z]+[a-z0-9]+)+[A-Z]+/),i=n.concat(a,/(::\w+)*/),r={ +"variable.constant":["__FILE__","__LINE__","__ENCODING__"], +"variable.language":["self","super"], +keyword:["alias","and","begin","BEGIN","break","case","class","defined","do","else","elsif","end","END","ensure","for","if","in","module","next","not","or","redo","require","rescue","retry","return","then","undef","unless","until","when","while","yield","include","extend","prepend","public","private","protected","raise","throw"], +built_in:["proc","lambda","attr_accessor","attr_reader","attr_writer","define_method","private_constant","module_function"], +literal:["true","false","nil"]},s={className:"doctag",begin:"@[A-Za-z]+"},o={ +begin:"#<",end:">"},l=[e.COMMENT("#","$",{contains:[s] +}),e.COMMENT("^=begin","^=end",{contains:[s],relevance:10 +}),e.COMMENT("^__END__",e.MATCH_NOTHING_RE)],c={className:"subst",begin:/#\{/, +end:/\}/,keywords:r},d={className:"string",contains:[e.BACKSLASH_ESCAPE,c], +variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/`/,end:/`/},{ +begin:/%[qQwWx]?\(/,end:/\)/},{begin:/%[qQwWx]?\[/,end:/\]/},{ +begin:/%[qQwWx]?\{/,end:/\}/},{begin:/%[qQwWx]?/},{begin:/%[qQwWx]?\//, +end:/\//},{begin:/%[qQwWx]?%/,end:/%/},{begin:/%[qQwWx]?-/,end:/-/},{ +begin:/%[qQwWx]?\|/,end:/\|/},{begin:/\B\?(\\\d{1,3})/},{ +begin:/\B\?(\\x[A-Fa-f0-9]{1,2})/},{begin:/\B\?(\\u\{?[A-Fa-f0-9]{1,6}\}?)/},{ +begin:/\B\?(\\M-\\C-|\\M-\\c|\\c\\M-|\\M-|\\C-\\M-)[\x20-\x7e]/},{ +begin:/\B\?\\(c|C-)[\x20-\x7e]/},{begin:/\B\?\\?\S/},{ +begin:n.concat(/<<[-~]?'?/,n.lookahead(/(\w+)(?=\W)[^\n]*\n(?:[^\n]*\n)*?\s*\1\b/)), +contains:[e.END_SAME_AS_BEGIN({begin:/(\w+)/,end:/(\w+)/, +contains:[e.BACKSLASH_ESCAPE,c]})]}]},g="[0-9](_?[0-9])*",u={className:"number", +relevance:0,variants:[{ +begin:`\\b([1-9](_?[0-9])*|0)(\\.(${g}))?([eE][+-]?(${g})|r)?i?\\b`},{ +begin:"\\b0[dD][0-9](_?[0-9])*r?i?\\b"},{begin:"\\b0[bB][0-1](_?[0-1])*r?i?\\b" +},{begin:"\\b0[oO][0-7](_?[0-7])*r?i?\\b"},{ +begin:"\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*r?i?\\b"},{ +begin:"\\b0(_?[0-7])+r?i?\\b"}]},b={variants:[{match:/\(\)/},{ +className:"params",begin:/\(/,end:/(?=\))/,excludeBegin:!0,endsParent:!0, +keywords:r}]},m=[d,{variants:[{match:[/class\s+/,i,/\s+<\s+/,i]},{ +match:[/\b(class|module)\s+/,i]}],scope:{2:"title.class", +4:"title.class.inherited"},keywords:r},{match:[/(include|extend)\s+/,i],scope:{ +2:"title.class"},keywords:r},{relevance:0,match:[i,/\.new[. (]/],scope:{ +1:"title.class"}},{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/, +className:"variable.constant"},{relevance:0,match:a,scope:"title.class"},{ +match:[/def/,/\s+/,t],scope:{1:"keyword",3:"title.function"},contains:[b]},{ +begin:e.IDENT_RE+"::"},{className:"symbol", +begin:e.UNDERSCORE_IDENT_RE+"(!|\\?)?:",relevance:0},{className:"symbol", +begin:":(?!\\s)",contains:[d,{begin:t}],relevance:0},u,{className:"variable", +begin:"(\\$\\W)|((\\$|@@?)(\\w+))(?=[^@$?])(?![A-Za-z])(?![@$?'])"},{ +className:"params",begin:/\|/,end:/\|/,excludeBegin:!0,excludeEnd:!0, +relevance:0,keywords:r},{begin:"("+e.RE_STARTERS_RE+"|unless)\\s*", +keywords:"unless",contains:[{className:"regexp",contains:[e.BACKSLASH_ESCAPE,c], +illegal:/\n/,variants:[{begin:"/",end:"/[a-z]*"},{begin:/%r\{/,end:/\}[a-z]*/},{ +begin:"%r\\(",end:"\\)[a-z]*"},{begin:"%r!",end:"![a-z]*"},{begin:"%r\\[", +end:"\\][a-z]*"}]}].concat(o,l),relevance:0}].concat(o,l) +;c.contains=m,b.contains=m;const p=[{begin:/^\s*=>/,starts:{end:"$",contains:m} +},{className:"meta.prompt", +begin:"^([>?]>|[\\w#]+\\(\\w+\\):\\d+:\\d+[>*]|(\\w+-)?\\d+\\.\\d+\\.\\d+(p\\d+)?[^\\d][^>]+>)(?=[ ])", +starts:{end:"$",keywords:r,contains:m}}];return l.unshift(o),{name:"Ruby", +aliases:["rb","gemspec","podspec","thor","irb"],keywords:r,illegal:/\/\*/, +contains:[e.SHEBANG({binary:"ruby"})].concat(p).concat(l).concat(m)}}, +grmr_rust:e=>{const n=e.regex,t={className:"title.function.invoke",relevance:0, +begin:n.concat(/\b/,/(?!let|for|while|if|else|match\b)/,e.IDENT_RE,n.lookahead(/\s*\(/)) +},a="([ui](8|16|32|64|128|size)|f(32|64))?",i=["drop ","Copy","Send","Sized","Sync","Drop","Fn","FnMut","FnOnce","ToOwned","Clone","Debug","PartialEq","PartialOrd","Eq","Ord","AsRef","AsMut","Into","From","Default","Iterator","Extend","IntoIterator","DoubleEndedIterator","ExactSizeIterator","SliceConcatExt","ToString","assert!","assert_eq!","bitflags!","bytes!","cfg!","col!","concat!","concat_idents!","debug_assert!","debug_assert_eq!","env!","eprintln!","panic!","file!","format!","format_args!","include_bytes!","include_str!","line!","local_data_key!","module_path!","option_env!","print!","println!","select!","stringify!","try!","unimplemented!","unreachable!","vec!","write!","writeln!","macro_rules!","assert_ne!","debug_assert_ne!"],r=["i8","i16","i32","i64","i128","isize","u8","u16","u32","u64","u128","usize","f32","f64","str","char","bool","Box","Option","Result","String","Vec"] +;return{name:"Rust",aliases:["rs"],keywords:{$pattern:e.IDENT_RE+"!?",type:r, +keyword:["abstract","as","async","await","become","box","break","const","continue","crate","do","dyn","else","enum","extern","false","final","fn","for","if","impl","in","let","loop","macro","match","mod","move","mut","override","priv","pub","ref","return","self","Self","static","struct","super","trait","true","try","type","typeof","unsafe","unsized","use","virtual","where","while","yield"], +literal:["true","false","Some","None","Ok","Err"],built_in:i},illegal:""},t]}}, +grmr_scss:e=>{const n=ie(e),t=le,a=oe,i="@[a-z-]+",r={className:"variable", +begin:"(\\$[a-zA-Z-][a-zA-Z0-9_-]*)\\b",relevance:0};return{name:"SCSS", +case_insensitive:!0,illegal:"[=/|']", +contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,n.CSS_NUMBER_MODE,{ +className:"selector-id",begin:"#[A-Za-z0-9_-]+",relevance:0},{ +className:"selector-class",begin:"\\.[A-Za-z0-9_-]+",relevance:0 +},n.ATTRIBUTE_SELECTOR_MODE,{className:"selector-tag", +begin:"\\b("+re.join("|")+")\\b",relevance:0},{className:"selector-pseudo", +begin:":("+a.join("|")+")"},{className:"selector-pseudo", +begin:":(:)?("+t.join("|")+")"},r,{begin:/\(/,end:/\)/, +contains:[n.CSS_NUMBER_MODE]},n.CSS_VARIABLE,{className:"attribute", +begin:"\\b("+ce.join("|")+")\\b"},{ +begin:"\\b(whitespace|wait|w-resize|visible|vertical-text|vertical-ideographic|uppercase|upper-roman|upper-alpha|underline|transparent|top|thin|thick|text|text-top|text-bottom|tb-rl|table-header-group|table-footer-group|sw-resize|super|strict|static|square|solid|small-caps|separate|se-resize|scroll|s-resize|rtl|row-resize|ridge|right|repeat|repeat-y|repeat-x|relative|progress|pointer|overline|outside|outset|oblique|nowrap|not-allowed|normal|none|nw-resize|no-repeat|no-drop|newspaper|ne-resize|n-resize|move|middle|medium|ltr|lr-tb|lowercase|lower-roman|lower-alpha|loose|list-item|line|line-through|line-edge|lighter|left|keep-all|justify|italic|inter-word|inter-ideograph|inside|inset|inline|inline-block|inherit|inactive|ideograph-space|ideograph-parenthesis|ideograph-numeric|ideograph-alpha|horizontal|hidden|help|hand|groove|fixed|ellipsis|e-resize|double|dotted|distribute|distribute-space|distribute-letter|distribute-all-lines|disc|disabled|default|decimal|dashed|crosshair|collapse|col-resize|circle|char|center|capitalize|break-word|break-all|bottom|both|bolder|bold|block|bidi-override|below|baseline|auto|always|all-scroll|absolute|table|table-cell)\\b" +},{begin:/:/,end:/[;}{]/,relevance:0, +contains:[n.BLOCK_COMMENT,r,n.HEXCOLOR,n.CSS_NUMBER_MODE,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,n.IMPORTANT,n.FUNCTION_DISPATCH] +},{begin:"@(page|font-face)",keywords:{$pattern:i,keyword:"@page @font-face"}},{ +begin:"@",end:"[{;]",returnBegin:!0,keywords:{$pattern:/[a-z-]+/, +keyword:"and or not only",attribute:se.join(" ")},contains:[{begin:i, +className:"keyword"},{begin:/[a-z-]+(?=:)/,className:"attribute" +},r,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,n.HEXCOLOR,n.CSS_NUMBER_MODE] +},n.FUNCTION_DISPATCH]}},grmr_shell:e=>({name:"Shell Session", +aliases:["console","shellsession"],contains:[{className:"meta.prompt", +begin:/^\s{0,3}[/~\w\d[\]()@-]*[>%$#][ ]?/,starts:{end:/[^\\](?=\s*$)/, +subLanguage:"bash"}}]}),grmr_sql:e=>{ +const n=e.regex,t=e.COMMENT("--","$"),a=["true","false","unknown"],i=["bigint","binary","blob","boolean","char","character","clob","date","dec","decfloat","decimal","float","int","integer","interval","nchar","nclob","national","numeric","real","row","smallint","time","timestamp","varchar","varying","varbinary"],r=["abs","acos","array_agg","asin","atan","avg","cast","ceil","ceiling","coalesce","corr","cos","cosh","count","covar_pop","covar_samp","cume_dist","dense_rank","deref","element","exp","extract","first_value","floor","json_array","json_arrayagg","json_exists","json_object","json_objectagg","json_query","json_table","json_table_primitive","json_value","lag","last_value","lead","listagg","ln","log","log10","lower","max","min","mod","nth_value","ntile","nullif","percent_rank","percentile_cont","percentile_disc","position","position_regex","power","rank","regr_avgx","regr_avgy","regr_count","regr_intercept","regr_r2","regr_slope","regr_sxx","regr_sxy","regr_syy","row_number","sin","sinh","sqrt","stddev_pop","stddev_samp","substring","substring_regex","sum","tan","tanh","translate","translate_regex","treat","trim","trim_array","unnest","upper","value_of","var_pop","var_samp","width_bucket"],s=["create table","insert into","primary key","foreign key","not null","alter table","add constraint","grouping sets","on overflow","character set","respect nulls","ignore nulls","nulls first","nulls last","depth first","breadth first"],o=r,l=["abs","acos","all","allocate","alter","and","any","are","array","array_agg","array_max_cardinality","as","asensitive","asin","asymmetric","at","atan","atomic","authorization","avg","begin","begin_frame","begin_partition","between","bigint","binary","blob","boolean","both","by","call","called","cardinality","cascaded","case","cast","ceil","ceiling","char","char_length","character","character_length","check","classifier","clob","close","coalesce","collate","collect","column","commit","condition","connect","constraint","contains","convert","copy","corr","corresponding","cos","cosh","count","covar_pop","covar_samp","create","cross","cube","cume_dist","current","current_catalog","current_date","current_default_transform_group","current_path","current_role","current_row","current_schema","current_time","current_timestamp","current_path","current_role","current_transform_group_for_type","current_user","cursor","cycle","date","day","deallocate","dec","decimal","decfloat","declare","default","define","delete","dense_rank","deref","describe","deterministic","disconnect","distinct","double","drop","dynamic","each","element","else","empty","end","end_frame","end_partition","end-exec","equals","escape","every","except","exec","execute","exists","exp","external","extract","false","fetch","filter","first_value","float","floor","for","foreign","frame_row","free","from","full","function","fusion","get","global","grant","group","grouping","groups","having","hold","hour","identity","in","indicator","initial","inner","inout","insensitive","insert","int","integer","intersect","intersection","interval","into","is","join","json_array","json_arrayagg","json_exists","json_object","json_objectagg","json_query","json_table","json_table_primitive","json_value","lag","language","large","last_value","lateral","lead","leading","left","like","like_regex","listagg","ln","local","localtime","localtimestamp","log","log10","lower","match","match_number","match_recognize","matches","max","member","merge","method","min","minute","mod","modifies","module","month","multiset","national","natural","nchar","nclob","new","no","none","normalize","not","nth_value","ntile","null","nullif","numeric","octet_length","occurrences_regex","of","offset","old","omit","on","one","only","open","or","order","out","outer","over","overlaps","overlay","parameter","partition","pattern","per","percent","percent_rank","percentile_cont","percentile_disc","period","portion","position","position_regex","power","precedes","precision","prepare","primary","procedure","ptf","range","rank","reads","real","recursive","ref","references","referencing","regr_avgx","regr_avgy","regr_count","regr_intercept","regr_r2","regr_slope","regr_sxx","regr_sxy","regr_syy","release","result","return","returns","revoke","right","rollback","rollup","row","row_number","rows","running","savepoint","scope","scroll","search","second","seek","select","sensitive","session_user","set","show","similar","sin","sinh","skip","smallint","some","specific","specifictype","sql","sqlexception","sqlstate","sqlwarning","sqrt","start","static","stddev_pop","stddev_samp","submultiset","subset","substring","substring_regex","succeeds","sum","symmetric","system","system_time","system_user","table","tablesample","tan","tanh","then","time","timestamp","timezone_hour","timezone_minute","to","trailing","translate","translate_regex","translation","treat","trigger","trim","trim_array","true","truncate","uescape","union","unique","unknown","unnest","update","upper","user","using","value","values","value_of","var_pop","var_samp","varbinary","varchar","varying","versioning","when","whenever","where","width_bucket","window","with","within","without","year","add","asc","collation","desc","final","first","last","view"].filter((e=>!r.includes(e))),c={ +begin:n.concat(/\b/,n.either(...o),/\s*\(/),relevance:0,keywords:{built_in:o}} +;return{name:"SQL",case_insensitive:!0,illegal:/[{}]|<\//,keywords:{ +$pattern:/\b[\w\.]+/,keyword:((e,{exceptions:n,when:t}={})=>{const a=t +;return n=n||[],e.map((e=>e.match(/\|\d+$/)||n.includes(e)?e:a(e)?e+"|0":e)) +})(l,{when:e=>e.length<3}),literal:a,type:i, +built_in:["current_catalog","current_date","current_default_transform_group","current_path","current_role","current_schema","current_transform_group_for_type","current_user","session_user","system_time","system_user","current_time","localtime","current_timestamp","localtimestamp"] +},contains:[{begin:n.either(...s),relevance:0,keywords:{$pattern:/[\w\.]+/, +keyword:l.concat(s),literal:a,type:i}},{className:"type", +begin:n.either("double precision","large object","with timezone","without timezone") +},c,{className:"variable",begin:/@[a-z0-9][a-z0-9_]*/},{className:"string", +variants:[{begin:/'/,end:/'/,contains:[{begin:/''/}]}]},{begin:/"/,end:/"/, +contains:[{begin:/""/}]},e.C_NUMBER_MODE,e.C_BLOCK_COMMENT_MODE,t,{ +className:"operator",begin:/[-+*/=%^~]|&&?|\|\|?|!=?|<(?:=>?|<|>)?|>[>=]?/, +relevance:0}]}},grmr_swift:e=>{const n={match:/\s+/,relevance:0 +},t=e.COMMENT("/\\*","\\*/",{contains:["self"]}),a=[e.C_LINE_COMMENT_MODE,t],i={ +match:[/\./,m(...xe,...Me)],className:{2:"keyword"}},r={match:b(/\./,m(...Ae)), +relevance:0},s=Ae.filter((e=>"string"==typeof e)).concat(["_|0"]),o={variants:[{ +className:"keyword", +match:m(...Ae.filter((e=>"string"!=typeof e)).concat(Se).map(ke),...Me)}]},l={ +$pattern:m(/\b\w+/,/#\w+/),keyword:s.concat(Re),literal:Ce},c=[i,r,o],g=[{ +match:b(/\./,m(...De)),relevance:0},{className:"built_in", +match:b(/\b/,m(...De),/(?=\()/)}],u={match:/->/,relevance:0},p=[u,{ +className:"operator",relevance:0,variants:[{match:Be},{match:`\\.(\\.|${Le})+`}] +}],_="([0-9]_*)+",h="([0-9a-fA-F]_*)+",f={className:"number",relevance:0, +variants:[{match:`\\b(${_})(\\.(${_}))?([eE][+-]?(${_}))?\\b`},{ +match:`\\b0x(${h})(\\.(${h}))?([pP][+-]?(${_}))?\\b`},{match:/\b0o([0-7]_*)+\b/ +},{match:/\b0b([01]_*)+\b/}]},E=(e="")=>({className:"subst",variants:[{ +match:b(/\\/,e,/[0\\tnr"']/)},{match:b(/\\/,e,/u\{[0-9a-fA-F]{1,8}\}/)}] +}),y=(e="")=>({className:"subst",match:b(/\\/,e,/[\t ]*(?:[\r\n]|\r\n)/) +}),N=(e="")=>({className:"subst",label:"interpol",begin:b(/\\/,e,/\(/),end:/\)/ +}),w=(e="")=>({begin:b(e,/"""/),end:b(/"""/,e),contains:[E(e),y(e),N(e)] +}),v=(e="")=>({begin:b(e,/"/),end:b(/"/,e),contains:[E(e),N(e)]}),O={ +className:"string", +variants:[w(),w("#"),w("##"),w("###"),v(),v("#"),v("##"),v("###")] +},k=[e.BACKSLASH_ESCAPE,{begin:/\[/,end:/\]/,relevance:0, +contains:[e.BACKSLASH_ESCAPE]}],x={begin:/\/[^\s](?=[^/\n]*\/)/,end:/\//, +contains:k},M=e=>{const n=b(e,/\//),t=b(/\//,e);return{begin:n,end:t, +contains:[...k,{scope:"comment",begin:`#(?!.*${t})`,end:/$/}]}},S={ +scope:"regexp",variants:[M("###"),M("##"),M("#"),x]},A={match:b(/`/,Fe,/`/) +},C=[A,{className:"variable",match:/\$\d+/},{className:"variable", +match:`\\$${ze}+`}],T=[{match:/(@|#(un)?)available/,scope:"keyword",starts:{ +contains:[{begin:/\(/,end:/\)/,keywords:Pe,contains:[...p,f,O]}]}},{ +scope:"keyword",match:b(/@/,m(...je))},{scope:"meta",match:b(/@/,Fe)}],R={ +match:d(/\b[A-Z]/),relevance:0,contains:[{className:"type", +match:b(/(AV|CA|CF|CG|CI|CL|CM|CN|CT|MK|MP|MTK|MTL|NS|SCN|SK|UI|WK|XC)/,ze,"+") +},{className:"type",match:Ue,relevance:0},{match:/[?!]+/,relevance:0},{ +match:/\.\.\./,relevance:0},{match:b(/\s+&\s+/,d(Ue)),relevance:0}]},D={ +begin://,keywords:l,contains:[...a,...c,...T,u,R]};R.contains.push(D) +;const I={begin:/\(/,end:/\)/,relevance:0,keywords:l,contains:["self",{ +match:b(Fe,/\s*:/),keywords:"_|0",relevance:0 +},...a,S,...c,...g,...p,f,O,...C,...T,R]},L={begin://, +keywords:"repeat each",contains:[...a,R]},B={begin:/\(/,end:/\)/,keywords:l, +contains:[{begin:m(d(b(Fe,/\s*:/)),d(b(Fe,/\s+/,Fe,/\s*:/))),end:/:/, +relevance:0,contains:[{className:"keyword",match:/\b_\b/},{className:"params", +match:Fe}]},...a,...c,...p,f,O,...T,R,I],endsParent:!0,illegal:/["']/},$={ +match:[/(func|macro)/,/\s+/,m(A.match,Fe,Be)],className:{1:"keyword", +3:"title.function"},contains:[L,B,n],illegal:[/\[/,/%/]},z={ +match:[/\b(?:subscript|init[?!]?)/,/\s*(?=[<(])/],className:{1:"keyword"}, +contains:[L,B,n],illegal:/\[|%/},F={match:[/operator/,/\s+/,Be],className:{ +1:"keyword",3:"title"}},U={begin:[/precedencegroup/,/\s+/,Ue],className:{ +1:"keyword",3:"title"},contains:[R],keywords:[...Te,...Ce],end:/}/} +;for(const e of O.variants){const n=e.contains.find((e=>"interpol"===e.label)) +;n.keywords=l;const t=[...c,...g,...p,f,O,...C];n.contains=[...t,{begin:/\(/, +end:/\)/,contains:["self",...t]}]}return{name:"Swift",keywords:l, +contains:[...a,$,z,{beginKeywords:"struct protocol class extension enum actor", +end:"\\{",excludeEnd:!0,keywords:l,contains:[e.inherit(e.TITLE_MODE,{ +className:"title.class",begin:/[A-Za-z$_][\u00C0-\u02B80-9A-Za-z$_]*/}),...c] +},F,U,{beginKeywords:"import",end:/$/,contains:[...a],relevance:0 +},S,...c,...g,...p,f,O,...C,...T,R,I]}},grmr_typescript:e=>{ +const n=Oe(e),t=_e,a=["any","void","number","boolean","string","object","never","symbol","bigint","unknown"],i={ +beginKeywords:"namespace",end:/\{/,excludeEnd:!0, +contains:[n.exports.CLASS_REFERENCE]},r={beginKeywords:"interface",end:/\{/, +excludeEnd:!0,keywords:{keyword:"interface extends",built_in:a}, +contains:[n.exports.CLASS_REFERENCE]},s={$pattern:_e, +keyword:he.concat(["type","namespace","interface","public","private","protected","implements","declare","abstract","readonly","enum","override"]), +literal:fe,built_in:ve.concat(a),"variable.language":we},o={className:"meta", +begin:"@"+t},l=(e,n,t)=>{const a=e.contains.findIndex((e=>e.label===n)) +;if(-1===a)throw Error("can not find mode to replace");e.contains.splice(a,1,t)} +;return Object.assign(n.keywords,s), +n.exports.PARAMS_CONTAINS.push(o),n.contains=n.contains.concat([o,i,r]), +l(n,"shebang",e.SHEBANG()),l(n,"use_strict",{className:"meta",relevance:10, +begin:/^\s*['"]use strict['"]/ +}),n.contains.find((e=>"func.def"===e.label)).relevance=0,Object.assign(n,{ +name:"TypeScript",aliases:["ts","tsx","mts","cts"]}),n},grmr_vbnet:e=>{ +const n=e.regex,t=/\d{1,2}\/\d{1,2}\/\d{4}/,a=/\d{4}-\d{1,2}-\d{1,2}/,i=/(\d|1[012])(:\d+){0,2} *(AM|PM)/,r=/\d{1,2}(:\d{1,2}){1,2}/,s={ +className:"literal",variants:[{begin:n.concat(/# */,n.either(a,t),/ *#/)},{ +begin:n.concat(/# */,r,/ *#/)},{begin:n.concat(/# */,i,/ *#/)},{ +begin:n.concat(/# */,n.either(a,t),/ +/,n.either(i,r),/ *#/)}] +},o=e.COMMENT(/'''/,/$/,{contains:[{className:"doctag",begin:/<\/?/,end:/>/}] +}),l=e.COMMENT(null,/$/,{variants:[{begin:/'/},{begin:/([\t ]|^)REM(?=\s)/}]}) +;return{name:"Visual Basic .NET",aliases:["vb"],case_insensitive:!0, +classNameAliases:{label:"symbol"},keywords:{ +keyword:"addhandler alias aggregate ansi as async assembly auto binary by byref byval call case catch class compare const continue custom declare default delegate dim distinct do each equals else elseif end enum erase error event exit explicit finally for friend from function get global goto group handles if implements imports in inherits interface into iterator join key let lib loop me mid module mustinherit mustoverride mybase myclass namespace narrowing new next notinheritable notoverridable of off on operator option optional order overloads overridable overrides paramarray partial preserve private property protected public raiseevent readonly redim removehandler resume return select set shadows shared skip static step stop structure strict sub synclock take text then throw to try unicode until using when where while widening with withevents writeonly yield", +built_in:"addressof and andalso await directcast gettype getxmlnamespace is isfalse isnot istrue like mod nameof new not or orelse trycast typeof xor cbool cbyte cchar cdate cdbl cdec cint clng cobj csbyte cshort csng cstr cuint culng cushort", +type:"boolean byte char date decimal double integer long object sbyte short single string uinteger ulong ushort", +literal:"true false nothing"}, +illegal:"//|\\{|\\}|endif|gosub|variant|wend|^\\$ ",contains:[{ +className:"string",begin:/"(""|[^/n])"C\b/},{className:"string",begin:/"/, +end:/"/,illegal:/\n/,contains:[{begin:/""/}]},s,{className:"number",relevance:0, +variants:[{begin:/\b\d[\d_]*((\.[\d_]+(E[+-]?[\d_]+)?)|(E[+-]?[\d_]+))[RFD@!#]?/ +},{begin:/\b\d[\d_]*((U?[SIL])|[%&])?/},{begin:/&H[\dA-F_]+((U?[SIL])|[%&])?/},{ +begin:/&O[0-7_]+((U?[SIL])|[%&])?/},{begin:/&B[01_]+((U?[SIL])|[%&])?/}]},{ +className:"label",begin:/^\w+:/},o,l,{className:"meta", +begin:/[\t ]*#(const|disable|else|elseif|enable|end|externalsource|if|region)\b/, +end:/$/,keywords:{ +keyword:"const disable else elseif enable end externalsource if region then"}, +contains:[l]}]}},grmr_wasm:e=>{e.regex;const n=e.COMMENT(/\(;/,/;\)/) +;return n.contains.push("self"),{name:"WebAssembly",keywords:{$pattern:/[\w.]+/, +keyword:["anyfunc","block","br","br_if","br_table","call","call_indirect","data","drop","elem","else","end","export","func","global.get","global.set","local.get","local.set","local.tee","get_global","get_local","global","if","import","local","loop","memory","memory.grow","memory.size","module","mut","nop","offset","param","result","return","select","set_global","set_local","start","table","tee_local","then","type","unreachable"] +},contains:[e.COMMENT(/;;/,/$/),n,{match:[/(?:offset|align)/,/\s*/,/=/], +className:{1:"keyword",3:"operator"}},{className:"variable",begin:/\$[\w_]+/},{ +match:/(\((?!;)|\))+/,className:"punctuation",relevance:0},{ +begin:[/(?:func|call|call_indirect)/,/\s+/,/\$[^\s)]+/],className:{1:"keyword", +3:"title.function"}},e.QUOTE_STRING_MODE,{match:/(i32|i64|f32|f64)(?!\.)/, +className:"type"},{className:"keyword", +match:/\b(f32|f64|i32|i64)(?:\.(?:abs|add|and|ceil|clz|const|convert_[su]\/i(?:32|64)|copysign|ctz|demote\/f64|div(?:_[su])?|eqz?|extend_[su]\/i32|floor|ge(?:_[su])?|gt(?:_[su])?|le(?:_[su])?|load(?:(?:8|16|32)_[su])?|lt(?:_[su])?|max|min|mul|nearest|neg?|or|popcnt|promote\/f32|reinterpret\/[fi](?:32|64)|rem_[su]|rot[lr]|shl|shr_[su]|store(?:8|16|32)?|sqrt|sub|trunc(?:_[su]\/f(?:32|64))?|wrap\/i64|xor))\b/ +},{className:"number",relevance:0, +match:/[+-]?\b(?:\d(?:_?\d)*(?:\.\d(?:_?\d)*)?(?:[eE][+-]?\d(?:_?\d)*)?|0x[\da-fA-F](?:_?[\da-fA-F])*(?:\.[\da-fA-F](?:_?[\da-fA-D])*)?(?:[pP][+-]?\d(?:_?\d)*)?)\b|\binf\b|\bnan(?::0x[\da-fA-F](?:_?[\da-fA-D])*)?\b/ +}]}},grmr_xml:e=>{ +const n=e.regex,t=n.concat(/[\p{L}_]/u,n.optional(/[\p{L}0-9_.-]*:/u),/[\p{L}0-9_.-]*/u),a={ +className:"symbol",begin:/&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/},i={begin:/\s/, +contains:[{className:"keyword",begin:/#?[a-z_][a-z1-9_-]+/,illegal:/\n/}] +},r=e.inherit(i,{begin:/\(/,end:/\)/}),s=e.inherit(e.APOS_STRING_MODE,{ +className:"string"}),o=e.inherit(e.QUOTE_STRING_MODE,{className:"string"}),l={ +endsWithParent:!0,illegal:/`]+/}]}]}]};return{ +name:"HTML, XML", +aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"], +case_insensitive:!0,unicodeRegex:!0,contains:[{className:"meta",begin://,relevance:10,contains:[i,o,s,r,{begin:/\[/,end:/\]/,contains:[{ +className:"meta",begin://,contains:[i,r,o,s]}]}] +},e.COMMENT(//,{relevance:10}),{begin://, +relevance:10},a,{className:"meta",end:/\?>/,variants:[{begin:/<\?xml/, +relevance:10,contains:[o]},{begin:/<\?[a-z][a-z0-9]+/}]},{className:"tag", +begin:/)/,end:/>/,keywords:{name:"style"},contains:[l],starts:{ +end:/<\/style>/,returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag", +begin:/)/,end:/>/,keywords:{name:"script"},contains:[l],starts:{ +end:/<\/script>/,returnEnd:!0,subLanguage:["javascript","handlebars","xml"]}},{ +className:"tag",begin:/<>|<\/>/},{className:"tag", +begin:n.concat(//,/>/,/\s/)))), +end:/\/?>/,contains:[{className:"name",begin:t,relevance:0,starts:l}]},{ +className:"tag",begin:n.concat(/<\//,n.lookahead(n.concat(t,/>/))),contains:[{ +className:"name",begin:t,relevance:0},{begin:/>/,relevance:0,endsParent:!0}]}]} +},grmr_yaml:e=>{ +const n="true false yes no null",t="[\\w#;/?:@&=+$,.~*'()[\\]]+",a={ +className:"string",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/ +},{begin:/\S+/}],contains:[e.BACKSLASH_ESCAPE,{className:"template-variable", +variants:[{begin:/\{\{/,end:/\}\}/},{begin:/%\{/,end:/\}/}]}]},i=e.inherit(a,{ +variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/[^\s,{}[\]]+/}]}),r={ +end:",",endsWithParent:!0,excludeEnd:!0,keywords:n,relevance:0},s={begin:/\{/, +end:/\}/,contains:[r],illegal:"\\n",relevance:0},o={begin:"\\[",end:"\\]", +contains:[r],illegal:"\\n",relevance:0},l=[{className:"attr",variants:[{ +begin:"\\w[\\w :\\/.-]*:(?=[ \t]|$)"},{begin:'"\\w[\\w :\\/.-]*":(?=[ \t]|$)'},{ +begin:"'\\w[\\w :\\/.-]*':(?=[ \t]|$)"}]},{className:"meta",begin:"^---\\s*$", +relevance:10},{className:"string", +begin:"[\\|>]([1-9]?[+-])?[ ]*\\n( +)[^ ][^\\n]*\\n(\\2[^\\n]+\\n?)*"},{ +begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0, +relevance:0},{className:"type",begin:"!\\w+!"+t},{className:"type", +begin:"!<"+t+">"},{className:"type",begin:"!"+t},{className:"type",begin:"!!"+t +},{className:"meta",begin:"&"+e.UNDERSCORE_IDENT_RE+"$"},{className:"meta", +begin:"\\*"+e.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"-(?=[ ]|$)", +relevance:0},e.HASH_COMMENT_MODE,{beginKeywords:n,keywords:{literal:n}},{ +className:"number", +begin:"\\b[0-9]{4}(-[0-9][0-9]){0,2}([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?(\\.[0-9]*)?([ \\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?\\b" +},{className:"number",begin:e.C_NUMBER_RE+"\\b",relevance:0},s,o,a],c=[...l] +;return c.pop(),c.push(i),r.contains=c,{name:"YAML",case_insensitive:!0, +aliases:["yml"],contains:l}}});const He=ae;for(const e of Object.keys(Ke)){ +const n=e.replace("grmr_","").replace("_","-");He.registerLanguage(n,Ke[e])} +return He}() +;"object"==typeof exports&&"undefined"!=typeof module&&(module.exports=hljs); \ No newline at end of file diff --git a/packages/coding-agent/src/core/export-html/vendor/marked.min.js b/packages/coding-agent/src/core/export-html/vendor/marked.min.js new file mode 100644 index 000000000..79394fd8f --- /dev/null +++ b/packages/coding-agent/src/core/export-html/vendor/marked.min.js @@ -0,0 +1,6 @@ +/** + * marked v15.0.4 - a markdown parser + * Copyright (c) 2011-2024, Christopher Jeffrey. (MIT Licensed) + * https://github.com/markedjs/marked + */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).marked={})}(this,(function(e){"use strict";function t(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}function n(t){e.defaults=t}e.defaults={async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null};const s={exec:()=>null};function r(e,t=""){let n="string"==typeof e?e:e.source;const s={replace:(e,t)=>{let r="string"==typeof t?t:t.source;return r=r.replace(i.caret,"$1"),n=n.replace(e,r),s},getRegex:()=>new RegExp(n,t)};return s}const i={codeRemoveIndent:/^(?: {1,4}| {0,3}\t)/gm,outputLinkReplace:/\\([\[\]])/g,indentCodeCompensation:/^(\s+)(?:```)/,beginningSpace:/^\s+/,endingHash:/#$/,startingSpaceChar:/^ /,endingSpaceChar:/ $/,nonSpaceChar:/[^ ]/,newLineCharGlobal:/\n/g,tabCharGlobal:/\t/g,multipleSpaceGlobal:/\s+/g,blankLine:/^[ \t]*$/,doubleBlankLine:/\n[ \t]*\n[ \t]*$/,blockquoteStart:/^ {0,3}>/,blockquoteSetextReplace:/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,blockquoteSetextReplace2:/^ {0,3}>[ \t]?/gm,listReplaceTabs:/^\t+/,listReplaceNesting:/^ {1,4}(?=( {4})*[^ ])/g,listIsTask:/^\[[ xX]\] /,listReplaceTask:/^\[[ xX]\] +/,anyLine:/\n.*\n/,hrefBrackets:/^<(.*)>$/,tableDelimiter:/[:|]/,tableAlignChars:/^\||\| *$/g,tableRowBlankLine:/\n[ \t]*$/,tableAlignRight:/^ *-+: *$/,tableAlignCenter:/^ *:-+: *$/,tableAlignLeft:/^ *:-+ *$/,startATag:/^/i,startPreScriptTag:/^<(pre|code|kbd|script)(\s|>)/i,endPreScriptTag:/^<\/(pre|code|kbd|script)(\s|>)/i,startAngleBracket:/^$/,pedanticHrefTitle:/^([^'"]*[^\s])\s+(['"])(.*)\2/,unicodeAlphaNumeric:/[\p{L}\p{N}]/u,escapeTest:/[&<>"']/,escapeReplace:/[&<>"']/g,escapeTestNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,escapeReplaceNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g,unescapeTest:/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi,caret:/(^|[^\[])\^/g,percentDecode:/%25/g,findPipe:/\|/g,splitPipe:/ \|/,slashPipe:/\\\|/g,carriageReturn:/\r\n|\r/g,spaceLine:/^ +$/gm,notSpaceStart:/^\S*/,endingNewline:/\n$/,listItemRegex:e=>new RegExp(`^( {0,3}${e})((?:[\t ][^\\n]*)?(?:\\n|$))`),nextBulletRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`),hrRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),fencesBeginRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}(?:\`\`\`|~~~)`),headingBeginRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}#`),htmlBeginRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}<(?:[a-z].*>|!--)`,"i")},l=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,o=/(?:[*+-]|\d{1,9}[.)])/,a=r(/^(?!bull |blockCode|fences|blockquote|heading|html)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html))+?)\n {0,3}(=+|-+) *(?:\n+|$)/).replace(/bull/g,o).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).getRegex(),c=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,h=/(?!\s*\])(?:\\.|[^\[\]\\])+/,p=r(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/).replace("label",h).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),u=r(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,o).getRegex(),g="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",k=/|$))/,f=r("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$))","i").replace("comment",k).replace("tag",g).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),d=r(c).replace("hr",l).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",g).getRegex(),x={blockquote:r(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",d).getRegex(),code:/^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/,def:p,fences:/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,hr:l,html:f,lheading:a,list:u,newline:/^(?:[ \t]*(?:\n|$))+/,paragraph:d,table:s,text:/^[^\n]+/},b=r("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",l).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code","(?: {4}| {0,3}\t)[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",g).getRegex(),w={...x,table:b,paragraph:r(c).replace("hr",l).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",b).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",g).getRegex()},m={...x,html:r("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",k).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:s,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:r(c).replace("hr",l).replace("heading"," *#{1,6} *[^\n]").replace("lheading",a).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},y=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,$=/^( {2,}|\\)\n(?!\s*$)/,R=/[\p{P}\p{S}]/u,S=/[\s\p{P}\p{S}]/u,T=/[^\s\p{P}\p{S}]/u,z=r(/^((?![*_])punctSpace)/,"u").replace(/punctSpace/g,S).getRegex(),A=r(/^(?:\*+(?:((?!\*)punct)|[^\s*]))|^_+(?:((?!_)punct)|([^\s_]))/,"u").replace(/punct/g,R).getRegex(),_=r("^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)punct(\\*+)(?=[\\s]|$)|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)|[\\s](\\*+)(?!\\*)(?=punct)|(?!\\*)punct(\\*+)(?!\\*)(?=punct)|notPunctSpace(\\*+)(?=notPunctSpace)","gu").replace(/notPunctSpace/g,T).replace(/punctSpace/g,S).replace(/punct/g,R).getRegex(),P=r("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)punct(_+)(?=[\\s]|$)|notPunctSpace(_+)(?!_)(?=punctSpace|$)|(?!_)punctSpace(_+)(?=notPunctSpace)|[\\s](_+)(?!_)(?=punct)|(?!_)punct(_+)(?!_)(?=punct)","gu").replace(/notPunctSpace/g,T).replace(/punctSpace/g,S).replace(/punct/g,R).getRegex(),I=r(/\\(punct)/,"gu").replace(/punct/g,R).getRegex(),L=r(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),B=r(k).replace("(?:--\x3e|$)","--\x3e").getRegex(),C=r("^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",B).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),E=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,q=r(/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/).replace("label",E).replace("href",/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),Z=r(/^!?\[(label)\]\[(ref)\]/).replace("label",E).replace("ref",h).getRegex(),v=r(/^!?\[(ref)\](?:\[\])?/).replace("ref",h).getRegex(),D={_backpedal:s,anyPunctuation:I,autolink:L,blockSkip:/\[[^[\]]*?\]\((?:\\.|[^\\\(\)]|\((?:\\.|[^\\\(\)])*\))*\)|`[^`]*?`|<[^<>]*?>/g,br:$,code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,del:s,emStrongLDelim:A,emStrongRDelimAst:_,emStrongRDelimUnd:P,escape:y,link:q,nolink:v,punctuation:z,reflink:Z,reflinkSearch:r("reflink|nolink(?!\\()","g").replace("reflink",Z).replace("nolink",v).getRegex(),tag:C,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\":">",'"':""","'":"'"},H=e=>G[e];function X(e,t){if(t){if(i.escapeTest.test(e))return e.replace(i.escapeReplace,H)}else if(i.escapeTestNoEncode.test(e))return e.replace(i.escapeReplaceNoEncode,H);return e}function F(e){try{e=encodeURI(e).replace(i.percentDecode,"%")}catch{return null}return e}function U(e,t){const n=e.replace(i.findPipe,((e,t,n)=>{let s=!1,r=t;for(;--r>=0&&"\\"===n[r];)s=!s;return s?"|":" |"})).split(i.splitPipe);let s=0;if(n[0].trim()||n.shift(),n.length>0&&!n.at(-1)?.trim()&&n.pop(),t)if(n.length>t)n.splice(t);else for(;n.length0)return{type:"space",raw:t[0]}}code(e){const t=this.rules.block.code.exec(e);if(t){const e=t[0].replace(this.rules.other.codeRemoveIndent,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?e:J(e,"\n")}}}fences(e){const t=this.rules.block.fences.exec(e);if(t){const e=t[0],n=function(e,t,n){const s=e.match(n.other.indentCodeCompensation);if(null===s)return t;const r=s[1];return t.split("\n").map((e=>{const t=e.match(n.other.beginningSpace);if(null===t)return e;const[s]=t;return s.length>=r.length?e.slice(r.length):e})).join("\n")}(e,t[3]||"",this.rules);return{type:"code",raw:e,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:n}}}heading(e){const t=this.rules.block.heading.exec(e);if(t){let e=t[2].trim();if(this.rules.other.endingHash.test(e)){const t=J(e,"#");this.options.pedantic?e=t.trim():t&&!this.rules.other.endingSpaceChar.test(t)||(e=t.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:e,tokens:this.lexer.inline(e)}}}hr(e){const t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:J(t[0],"\n")}}blockquote(e){const t=this.rules.block.blockquote.exec(e);if(t){let e=J(t[0],"\n").split("\n"),n="",s="";const r=[];for(;e.length>0;){let t=!1;const i=[];let l;for(l=0;l1,r={type:"list",raw:"",ordered:s,start:s?+n.slice(0,-1):"",loose:!1,items:[]};n=s?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=s?n:"[*+-]");const i=this.rules.other.listItemRegex(n);let l=!1;for(;e;){let n=!1,s="",o="";if(!(t=i.exec(e)))break;if(this.rules.block.hr.test(e))break;s=t[0],e=e.substring(s.length);let a=t[2].split("\n",1)[0].replace(this.rules.other.listReplaceTabs,(e=>" ".repeat(3*e.length))),c=e.split("\n",1)[0],h=!a.trim(),p=0;if(this.options.pedantic?(p=2,o=a.trimStart()):h?p=t[1].length+1:(p=t[2].search(this.rules.other.nonSpaceChar),p=p>4?1:p,o=a.slice(p),p+=t[1].length),h&&this.rules.other.blankLine.test(c)&&(s+=c+"\n",e=e.substring(c.length+1),n=!0),!n){const t=this.rules.other.nextBulletRegex(p),n=this.rules.other.hrRegex(p),r=this.rules.other.fencesBeginRegex(p),i=this.rules.other.headingBeginRegex(p),l=this.rules.other.htmlBeginRegex(p);for(;e;){const u=e.split("\n",1)[0];let g;if(c=u,this.options.pedantic?(c=c.replace(this.rules.other.listReplaceNesting," "),g=c):g=c.replace(this.rules.other.tabCharGlobal," "),r.test(c))break;if(i.test(c))break;if(l.test(c))break;if(t.test(c))break;if(n.test(c))break;if(g.search(this.rules.other.nonSpaceChar)>=p||!c.trim())o+="\n"+g.slice(p);else{if(h)break;if(a.replace(this.rules.other.tabCharGlobal," ").search(this.rules.other.nonSpaceChar)>=4)break;if(r.test(a))break;if(i.test(a))break;if(n.test(a))break;o+="\n"+c}h||c.trim()||(h=!0),s+=u+"\n",e=e.substring(u.length+1),a=g.slice(p)}}r.loose||(l?r.loose=!0:this.rules.other.doubleBlankLine.test(s)&&(l=!0));let u,g=null;this.options.gfm&&(g=this.rules.other.listIsTask.exec(o),g&&(u="[ ] "!==g[0],o=o.replace(this.rules.other.listReplaceTask,""))),r.items.push({type:"list_item",raw:s,task:!!g,checked:u,loose:!1,text:o,tokens:[]}),r.raw+=s}const o=r.items.at(-1);if(!o)return;o.raw=o.raw.trimEnd(),o.text=o.text.trimEnd(),r.raw=r.raw.trimEnd();for(let e=0;e"space"===e.type)),n=t.length>0&&t.some((e=>this.rules.other.anyLine.test(e.raw)));r.loose=n}if(r.loose)for(let e=0;e({text:e,tokens:this.lexer.inline(e),header:!1,align:i.align[t]}))));return i}}lheading(e){const t=this.rules.block.lheading.exec(e);if(t)return{type:"heading",raw:t[0],depth:"="===t[2].charAt(0)?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){const t=this.rules.block.paragraph.exec(e);if(t){const e="\n"===t[1].charAt(t[1].length-1)?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:e,tokens:this.lexer.inline(e)}}}text(e){const t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){const t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:t[1]}}tag(e){const t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&this.rules.other.startATag.test(t[0])?this.lexer.state.inLink=!0:this.lexer.state.inLink&&this.rules.other.endATag.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&this.rules.other.startPreScriptTag.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&this.rules.other.endPreScriptTag.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){const t=this.rules.inline.link.exec(e);if(t){const e=t[2].trim();if(!this.options.pedantic&&this.rules.other.startAngleBracket.test(e)){if(!this.rules.other.endAngleBracket.test(e))return;const t=J(e.slice(0,-1),"\\");if((e.length-t.length)%2==0)return}else{const e=function(e,t){if(-1===e.indexOf(t[1]))return-1;let n=0;for(let s=0;s-1){const n=(0===t[0].indexOf("!")?5:4)+t[1].length+e;t[2]=t[2].substring(0,e),t[0]=t[0].substring(0,n).trim(),t[3]=""}}let n=t[2],s="";if(this.options.pedantic){const e=this.rules.other.pedanticHrefTitle.exec(n);e&&(n=e[1],s=e[3])}else s=t[3]?t[3].slice(1,-1):"";return n=n.trim(),this.rules.other.startAngleBracket.test(n)&&(n=this.options.pedantic&&!this.rules.other.endAngleBracket.test(e)?n.slice(1):n.slice(1,-1)),K(t,{href:n?n.replace(this.rules.inline.anyPunctuation,"$1"):n,title:s?s.replace(this.rules.inline.anyPunctuation,"$1"):s},t[0],this.lexer,this.rules)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){const e=t[(n[2]||n[1]).replace(this.rules.other.multipleSpaceGlobal," ").toLowerCase()];if(!e){const e=n[0].charAt(0);return{type:"text",raw:e,text:e}}return K(n,e,n[0],this.lexer,this.rules)}}emStrong(e,t,n=""){let s=this.rules.inline.emStrongLDelim.exec(e);if(!s)return;if(s[3]&&n.match(this.rules.other.unicodeAlphaNumeric))return;if(!(s[1]||s[2]||"")||!n||this.rules.inline.punctuation.exec(n)){const n=[...s[0]].length-1;let r,i,l=n,o=0;const a="*"===s[0][0]?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(a.lastIndex=0,t=t.slice(-1*e.length+n);null!=(s=a.exec(t));){if(r=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!r)continue;if(i=[...r].length,s[3]||s[4]){l+=i;continue}if((s[5]||s[6])&&n%3&&!((n+i)%3)){o+=i;continue}if(l-=i,l>0)continue;i=Math.min(i,i+l+o);const t=[...s[0]][0].length,a=e.slice(0,n+s.index+t+i);if(Math.min(n,i)%2){const e=a.slice(1,-1);return{type:"em",raw:a,text:e,tokens:this.lexer.inlineTokens(e)}}const c=a.slice(2,-2);return{type:"strong",raw:a,text:c,tokens:this.lexer.inlineTokens(c)}}}}codespan(e){const t=this.rules.inline.code.exec(e);if(t){let e=t[2].replace(this.rules.other.newLineCharGlobal," ");const n=this.rules.other.nonSpaceChar.test(e),s=this.rules.other.startingSpaceChar.test(e)&&this.rules.other.endingSpaceChar.test(e);return n&&s&&(e=e.substring(1,e.length-1)),{type:"codespan",raw:t[0],text:e}}}br(e){const t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){const t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e){const t=this.rules.inline.autolink.exec(e);if(t){let e,n;return"@"===t[2]?(e=t[1],n="mailto:"+e):(e=t[1],n=e),{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let e,n;if("@"===t[2])e=t[0],n="mailto:"+e;else{let s;do{s=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??""}while(s!==t[0]);e=t[0],n="www."===t[1]?"http://"+t[0]:t[0]}return{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}inlineText(e){const t=this.rules.inline.text.exec(e);if(t){const e=this.lexer.state.inRawBlock;return{type:"text",raw:t[0],text:t[0],escaped:e}}}}class W{tokens;options;state;tokenizer;inlineQueue;constructor(t){this.tokens=[],this.tokens.links=Object.create(null),this.options=t||e.defaults,this.options.tokenizer=this.options.tokenizer||new V,this.tokenizer=this.options.tokenizer,this.tokenizer.options=this.options,this.tokenizer.lexer=this,this.inlineQueue=[],this.state={inLink:!1,inRawBlock:!1,top:!0};const n={other:i,block:j.normal,inline:N.normal};this.options.pedantic?(n.block=j.pedantic,n.inline=N.pedantic):this.options.gfm&&(n.block=j.gfm,this.options.breaks?n.inline=N.breaks:n.inline=N.gfm),this.tokenizer.rules=n}static get rules(){return{block:j,inline:N}}static lex(e,t){return new W(t).lex(e)}static lexInline(e,t){return new W(t).inlineTokens(e)}lex(e){e=e.replace(i.carriageReturn,"\n"),this.blockTokens(e,this.tokens);for(let e=0;e!!(s=n.call({lexer:this},e,t))&&(e=e.substring(s.raw.length),t.push(s),!0))))continue;if(s=this.tokenizer.space(e)){e=e.substring(s.raw.length);const n=t.at(-1);1===s.raw.length&&void 0!==n?n.raw+="\n":t.push(s);continue}if(s=this.tokenizer.code(e)){e=e.substring(s.raw.length);const n=t.at(-1);"paragraph"===n?.type||"text"===n?.type?(n.raw+="\n"+s.raw,n.text+="\n"+s.text,this.inlineQueue.at(-1).src=n.text):t.push(s);continue}if(s=this.tokenizer.fences(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.heading(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.hr(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.blockquote(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.list(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.html(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.def(e)){e=e.substring(s.raw.length);const n=t.at(-1);"paragraph"===n?.type||"text"===n?.type?(n.raw+="\n"+s.raw,n.text+="\n"+s.raw,this.inlineQueue.at(-1).src=n.text):this.tokens.links[s.tag]||(this.tokens.links[s.tag]={href:s.href,title:s.title});continue}if(s=this.tokenizer.table(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.lheading(e)){e=e.substring(s.raw.length),t.push(s);continue}let r=e;if(this.options.extensions?.startBlock){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startBlock.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(r=e.substring(0,t+1))}if(this.state.top&&(s=this.tokenizer.paragraph(r))){const i=t.at(-1);n&&"paragraph"===i?.type?(i.raw+="\n"+s.raw,i.text+="\n"+s.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=i.text):t.push(s),n=r.length!==e.length,e=e.substring(s.raw.length)}else if(s=this.tokenizer.text(e)){e=e.substring(s.raw.length);const n=t.at(-1);"text"===n?.type?(n.raw+="\n"+s.raw,n.text+="\n"+s.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=n.text):t.push(s)}else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let n=e,s=null;if(this.tokens.links){const e=Object.keys(this.tokens.links);if(e.length>0)for(;null!=(s=this.tokenizer.rules.inline.reflinkSearch.exec(n));)e.includes(s[0].slice(s[0].lastIndexOf("[")+1,-1))&&(n=n.slice(0,s.index)+"["+"a".repeat(s[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;null!=(s=this.tokenizer.rules.inline.blockSkip.exec(n));)n=n.slice(0,s.index)+"["+"a".repeat(s[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);for(;null!=(s=this.tokenizer.rules.inline.anyPunctuation.exec(n));)n=n.slice(0,s.index)+"++"+n.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);let r=!1,i="";for(;e;){let s;if(r||(i=""),r=!1,this.options.extensions?.inline?.some((n=>!!(s=n.call({lexer:this},e,t))&&(e=e.substring(s.raw.length),t.push(s),!0))))continue;if(s=this.tokenizer.escape(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.tag(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.link(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.reflink(e,this.tokens.links)){e=e.substring(s.raw.length);const n=t.at(-1);"text"===s.type&&"text"===n?.type?(n.raw+=s.raw,n.text+=s.text):t.push(s);continue}if(s=this.tokenizer.emStrong(e,n,i)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.codespan(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.br(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.del(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.autolink(e)){e=e.substring(s.raw.length),t.push(s);continue}if(!this.state.inLink&&(s=this.tokenizer.url(e))){e=e.substring(s.raw.length),t.push(s);continue}let l=e;if(this.options.extensions?.startInline){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startInline.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(l=e.substring(0,t+1))}if(s=this.tokenizer.inlineText(l)){e=e.substring(s.raw.length),"_"!==s.raw.slice(-1)&&(i=s.raw.slice(-1)),r=!0;const n=t.at(-1);"text"===n?.type?(n.raw+=s.raw,n.text+=s.text):t.push(s)}else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}return t}}class Y{options;parser;constructor(t){this.options=t||e.defaults}space(e){return""}code({text:e,lang:t,escaped:n}){const s=(t||"").match(i.notSpaceStart)?.[0],r=e.replace(i.endingNewline,"")+"\n";return s?'
'+(n?r:X(r,!0))+"
\n":"
"+(n?r:X(r,!0))+"
\n"}blockquote({tokens:e}){return`
\n${this.parser.parse(e)}
\n`}html({text:e}){return e}heading({tokens:e,depth:t}){return`${this.parser.parseInline(e)}\n`}hr(e){return"
\n"}list(e){const t=e.ordered,n=e.start;let s="";for(let t=0;t\n"+s+"\n"}listitem(e){let t="";if(e.task){const n=this.checkbox({checked:!!e.checked});e.loose?"paragraph"===e.tokens[0]?.type?(e.tokens[0].text=n+" "+e.tokens[0].text,e.tokens[0].tokens&&e.tokens[0].tokens.length>0&&"text"===e.tokens[0].tokens[0].type&&(e.tokens[0].tokens[0].text=n+" "+X(e.tokens[0].tokens[0].text),e.tokens[0].tokens[0].escaped=!0)):e.tokens.unshift({type:"text",raw:n+" ",text:n+" ",escaped:!0}):t+=n+" "}return t+=this.parser.parse(e.tokens,!!e.loose),`
  • ${t}
  • \n`}checkbox({checked:e}){return"'}paragraph({tokens:e}){return`

    ${this.parser.parseInline(e)}

    \n`}table(e){let t="",n="";for(let t=0;t${s}`),"\n\n"+t+"\n"+s+"
    \n"}tablerow({text:e}){return`\n${e}\n`}tablecell(e){const t=this.parser.parseInline(e.tokens),n=e.header?"th":"td";return(e.align?`<${n} align="${e.align}">`:`<${n}>`)+t+`\n`}strong({tokens:e}){return`${this.parser.parseInline(e)}`}em({tokens:e}){return`${this.parser.parseInline(e)}`}codespan({text:e}){return`${X(e,!0)}`}br(e){return"
    "}del({tokens:e}){return`${this.parser.parseInline(e)}`}link({href:e,title:t,tokens:n}){const s=this.parser.parseInline(n),r=F(e);if(null===r)return s;let i='
    ",i}image({href:e,title:t,text:n}){const s=F(e);if(null===s)return X(n);let r=`${n}{const r=e[s].flat(1/0);n=n.concat(this.walkTokens(r,t))})):e.tokens&&(n=n.concat(this.walkTokens(e.tokens,t)))}}return n}use(...e){const t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach((e=>{const n={...e};if(n.async=this.defaults.async||n.async||!1,e.extensions&&(e.extensions.forEach((e=>{if(!e.name)throw new Error("extension name required");if("renderer"in e){const n=t.renderers[e.name];t.renderers[e.name]=n?function(...t){let s=e.renderer.apply(this,t);return!1===s&&(s=n.apply(this,t)),s}:e.renderer}if("tokenizer"in e){if(!e.level||"block"!==e.level&&"inline"!==e.level)throw new Error("extension level must be 'block' or 'inline'");const n=t[e.level];n?n.unshift(e.tokenizer):t[e.level]=[e.tokenizer],e.start&&("block"===e.level?t.startBlock?t.startBlock.push(e.start):t.startBlock=[e.start]:"inline"===e.level&&(t.startInline?t.startInline.push(e.start):t.startInline=[e.start]))}"childTokens"in e&&e.childTokens&&(t.childTokens[e.name]=e.childTokens)})),n.extensions=t),e.renderer){const t=this.defaults.renderer||new Y(this.defaults);for(const n in e.renderer){if(!(n in t))throw new Error(`renderer '${n}' does not exist`);if(["options","parser"].includes(n))continue;const s=n,r=e.renderer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n||""}}n.renderer=t}if(e.tokenizer){const t=this.defaults.tokenizer||new V(this.defaults);for(const n in e.tokenizer){if(!(n in t))throw new Error(`tokenizer '${n}' does not exist`);if(["options","rules","lexer"].includes(n))continue;const s=n,r=e.tokenizer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.tokenizer=t}if(e.hooks){const t=this.defaults.hooks||new ne;for(const n in e.hooks){if(!(n in t))throw new Error(`hook '${n}' does not exist`);if(["options","block"].includes(n))continue;const s=n,r=e.hooks[s],i=t[s];ne.passThroughHooks.has(n)?t[s]=e=>{if(this.defaults.async)return Promise.resolve(r.call(t,e)).then((e=>i.call(t,e)));const n=r.call(t,e);return i.call(t,n)}:t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.hooks=t}if(e.walkTokens){const t=this.defaults.walkTokens,s=e.walkTokens;n.walkTokens=function(e){let n=[];return n.push(s.call(this,e)),t&&(n=n.concat(t.call(this,e))),n}}this.defaults={...this.defaults,...n}})),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return W.lex(e,t??this.defaults)}parser(e,t){return te.parse(e,t??this.defaults)}parseMarkdown(e){return(t,n)=>{const s={...n},r={...this.defaults,...s},i=this.onError(!!r.silent,!!r.async);if(!0===this.defaults.async&&!1===s.async)return i(new Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise."));if(null==t)return i(new Error("marked(): input parameter is undefined or null"));if("string"!=typeof t)return i(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(t)+", string expected"));r.hooks&&(r.hooks.options=r,r.hooks.block=e);const l=r.hooks?r.hooks.provideLexer():e?W.lex:W.lexInline,o=r.hooks?r.hooks.provideParser():e?te.parse:te.parseInline;if(r.async)return Promise.resolve(r.hooks?r.hooks.preprocess(t):t).then((e=>l(e,r))).then((e=>r.hooks?r.hooks.processAllTokens(e):e)).then((e=>r.walkTokens?Promise.all(this.walkTokens(e,r.walkTokens)).then((()=>e)):e)).then((e=>o(e,r))).then((e=>r.hooks?r.hooks.postprocess(e):e)).catch(i);try{r.hooks&&(t=r.hooks.preprocess(t));let e=l(t,r);r.hooks&&(e=r.hooks.processAllTokens(e)),r.walkTokens&&this.walkTokens(e,r.walkTokens);let n=o(e,r);return r.hooks&&(n=r.hooks.postprocess(n)),n}catch(e){return i(e)}}}onError(e,t){return n=>{if(n.message+="\nPlease report this to https://github.com/markedjs/marked.",e){const e="

    An error occurred:

    "+X(n.message+"",!0)+"
    ";return t?Promise.resolve(e):e}if(t)return Promise.reject(n);throw n}}}const re=new se;function ie(e,t){return re.parse(e,t)}ie.options=ie.setOptions=function(e){return re.setOptions(e),ie.defaults=re.defaults,n(ie.defaults),ie},ie.getDefaults=t,ie.defaults=e.defaults,ie.use=function(...e){return re.use(...e),ie.defaults=re.defaults,n(ie.defaults),ie},ie.walkTokens=function(e,t){return re.walkTokens(e,t)},ie.parseInline=re.parseInline,ie.Parser=te,ie.parser=te.parse,ie.Renderer=Y,ie.TextRenderer=ee,ie.Lexer=W,ie.lexer=W.lex,ie.Tokenizer=V,ie.Hooks=ne,ie.parse=ie;const le=ie.options,oe=ie.setOptions,ae=ie.use,ce=ie.walkTokens,he=ie.parseInline,pe=ie,ue=te.parse,ge=W.lex;e.Hooks=ne,e.Lexer=W,e.Marked=se,e.Parser=te,e.Renderer=Y,e.TextRenderer=ee,e.Tokenizer=V,e.getDefaults=t,e.lexer=ge,e.marked=ie,e.options=le,e.parse=pe,e.parseInline=he,e.parser=ue,e.setOptions=oe,e.use=ae,e.walkTokens=ce})); diff --git a/packages/coding-agent/src/core/hooks/index.ts b/packages/coding-agent/src/core/hooks/index.ts index 7a6f23c90..9729ecbc3 100644 --- a/packages/coding-agent/src/core/hooks/index.ts +++ b/packages/coding-agent/src/core/hooks/index.ts @@ -3,20 +3,14 @@ export { discoverAndLoadHooks, loadHooks, type AppendEntryHandler, + type BranchHandler, type LoadedHook, type LoadHooksResult, + type NavigateTreeHandler, + type NewSessionHandler, type SendMessageHandler, } from "./loader.js"; export { execCommand, HookRunner, type HookErrorListener } from "./runner.js"; export { wrapToolsWithHooks, wrapToolWithHooks } from "./tool-wrapper.js"; -export type * from "./types.js"; -export { - isBashToolResult, - isReadToolResult, - isEditToolResult, - isWriteToolResult, - isGrepToolResult, - isFindToolResult, - isLsToolResult, -} from "./types.js"; +export * from "./types.js"; export type { ReadonlySessionManager } from "../session-manager.js"; diff --git a/packages/coding-agent/src/core/hooks/loader.ts b/packages/coding-agent/src/core/hooks/loader.ts index 3ac44b27f..f876aed9a 100644 --- a/packages/coding-agent/src/core/hooks/loader.ts +++ b/packages/coding-agent/src/core/hooks/loader.ts @@ -10,6 +10,7 @@ import { fileURLToPath } from "node:url"; import { createJiti } from "jiti"; import { getAgentDir } from "../../config.js"; import type { HookMessage } from "../messages.js"; +import type { SessionManager } from "../session-manager.js"; import { execCommand } from "./runner.js"; import type { ExecOptions, HookAPI, HookFactory, HookMessageRenderer, RegisteredCommand } from "./types.js"; @@ -60,6 +61,27 @@ export type SendMessageHandler = ( */ export type AppendEntryHandler = (customType: string, data?: T) => void; +/** + * New session handler type for ctx.newSession() in HookCommandContext. + */ +export type NewSessionHandler = (options?: { + parentSession?: string; + setup?: (sessionManager: SessionManager) => Promise; +}) => Promise<{ cancelled: boolean }>; + +/** + * Branch handler type for ctx.branch() in HookCommandContext. + */ +export type BranchHandler = (entryId: string) => Promise<{ cancelled: boolean }>; + +/** + * Navigate tree handler type for ctx.navigateTree() in HookCommandContext. + */ +export type NavigateTreeHandler = ( + targetId: string, + options?: { summarize?: boolean }, +) => Promise<{ cancelled: boolean }>; + /** * Registered handlers for a loaded hook. */ @@ -126,7 +148,7 @@ function resolveHookPath(hookPath: string, cwd: string): string { /** * Create a HookAPI instance that collects handlers, renderers, and commands. - * Returns the API, maps, and a function to set the send message handler later. + * Returns the API, maps, and functions to set handlers later. */ function createHookAPI( handlers: Map, diff --git a/packages/coding-agent/src/core/hooks/runner.ts b/packages/coding-agent/src/core/hooks/runner.ts index 6be8b7596..56bb53a24 100644 --- a/packages/coding-agent/src/core/hooks/runner.ts +++ b/packages/coding-agent/src/core/hooks/runner.ts @@ -4,14 +4,23 @@ import type { AgentMessage } from "@mariozechner/pi-agent-core"; import type { Model } from "@mariozechner/pi-ai"; +import { theme } from "../../modes/interactive/theme/theme.js"; import type { ModelRegistry } from "../model-registry.js"; import type { SessionManager } from "../session-manager.js"; -import type { AppendEntryHandler, LoadedHook, SendMessageHandler } from "./loader.js"; +import type { + AppendEntryHandler, + BranchHandler, + LoadedHook, + NavigateTreeHandler, + NewSessionHandler, + SendMessageHandler, +} from "./loader.js"; import type { BeforeAgentStartEvent, BeforeAgentStartEventResult, ContextEvent, ContextEventResult, + HookCommandContext, HookContext, HookError, HookEvent, @@ -39,9 +48,14 @@ const noOpUIContext: HookUIContext = { confirm: async () => false, input: async () => undefined, notify: () => {}, + setStatus: () => {}, custom: async () => undefined as never, setEditorText: () => {}, getEditorText: () => "", + editor: async () => undefined, + get theme() { + return theme; + }, }; /** @@ -56,6 +70,13 @@ export class HookRunner { private modelRegistry: ModelRegistry; private errorListeners: Set = new Set(); private getModel: () => Model | undefined = () => undefined; + private isIdleFn: () => boolean = () => true; + private waitForIdleFn: () => Promise = async () => {}; + private abortFn: () => void = () => {}; + private hasQueuedMessagesFn: () => boolean = () => false; + private newSessionHandler: NewSessionHandler = async () => ({ cancelled: false }); + private branchHandler: BranchHandler = async () => ({ cancelled: false }); + private navigateTreeHandler: NavigateTreeHandler = async () => ({ cancelled: false }); constructor(hooks: LoadedHook[], cwd: string, sessionManager: SessionManager, modelRegistry: ModelRegistry) { this.hooks = hooks; @@ -77,12 +98,41 @@ export class HookRunner { sendMessageHandler: SendMessageHandler; /** Handler for hooks to append entries */ appendEntryHandler: AppendEntryHandler; + /** Handler for creating new sessions (for HookCommandContext) */ + newSessionHandler?: NewSessionHandler; + /** Handler for branching sessions (for HookCommandContext) */ + branchHandler?: BranchHandler; + /** Handler for navigating session tree (for HookCommandContext) */ + navigateTreeHandler?: NavigateTreeHandler; + /** Function to check if agent is idle */ + isIdle?: () => boolean; + /** Function to wait for agent to be idle */ + waitForIdle?: () => Promise; + /** Function to abort current operation (fire-and-forget) */ + abort?: () => void; + /** Function to check if there are queued messages */ + hasQueuedMessages?: () => boolean; /** UI context for interactive prompts */ uiContext?: HookUIContext; /** Whether UI is available */ hasUI?: boolean; }): void { this.getModel = options.getModel; + this.isIdleFn = options.isIdle ?? (() => true); + this.waitForIdleFn = options.waitForIdle ?? (async () => {}); + this.abortFn = options.abort ?? (() => {}); + this.hasQueuedMessagesFn = options.hasQueuedMessages ?? (() => false); + // Store session handlers for HookCommandContext + if (options.newSessionHandler) { + this.newSessionHandler = options.newSessionHandler; + } + if (options.branchHandler) { + this.branchHandler = options.branchHandler; + } + if (options.navigateTreeHandler) { + this.navigateTreeHandler = options.navigateTreeHandler; + } + // Set per-hook handlers for pi.sendMessage() and pi.appendEntry() for (const hook of this.hooks) { hook.setSendMessageHandler(options.sendMessageHandler); hook.setAppendEntryHandler(options.appendEntryHandler); @@ -198,6 +248,23 @@ export class HookRunner { sessionManager: this.sessionManager, modelRegistry: this.modelRegistry, model: this.getModel(), + isIdle: () => this.isIdleFn(), + abort: () => this.abortFn(), + hasQueuedMessages: () => this.hasQueuedMessagesFn(), + }; + } + + /** + * Create the command context for slash command handlers. + * Extends HookContext with session control methods that are only safe in commands. + */ + createCommandContext(): HookCommandContext { + return { + ...this.createContext(), + waitForIdle: () => this.waitForIdleFn(), + newSession: (options) => this.newSessionHandler(options), + branch: (entryId) => this.branchHandler(entryId), + navigateTree: (targetId, options) => this.navigateTreeHandler(targetId, options), }; } @@ -206,15 +273,9 @@ export class HookRunner { */ private isSessionBeforeEvent( type: string, - ): type is - | "session_before_switch" - | "session_before_new" - | "session_before_branch" - | "session_before_compact" - | "session_before_tree" { + ): type is "session_before_switch" | "session_before_branch" | "session_before_compact" | "session_before_tree" { return ( type === "session_before_switch" || - type === "session_before_new" || type === "session_before_branch" || type === "session_before_compact" || type === "session_before_tree" diff --git a/packages/coding-agent/src/core/hooks/types.ts b/packages/coding-agent/src/core/hooks/types.ts index 65ab9c0d5..e2317d2d7 100644 --- a/packages/coding-agent/src/core/hooks/types.ts +++ b/packages/coding-agent/src/core/hooks/types.ts @@ -13,7 +13,13 @@ import type { CompactionPreparation, CompactionResult } from "../compaction/inde import type { ExecOptions, ExecResult } from "../exec.js"; import type { HookMessage } from "../messages.js"; import type { ModelRegistry } from "../model-registry.js"; -import type { BranchSummaryEntry, CompactionEntry, ReadonlySessionManager, SessionEntry } from "../session-manager.js"; +import type { + BranchSummaryEntry, + CompactionEntry, + ReadonlySessionManager, + SessionEntry, + SessionManager, +} from "../session-manager.js"; import type { EditToolDetails } from "../tools/edit.js"; import type { @@ -57,6 +63,17 @@ export interface HookUIContext { */ notify(message: string, type?: "info" | "warning" | "error"): void; + /** + * Set status text in the footer/status bar. + * Pass undefined as text to clear the status for this key. + * Text can include ANSI escape codes for styling. + * Note: Newlines, tabs, and carriage returns are replaced with spaces. + * The combined status line is truncated to terminal width. + * @param key - Unique key to identify this status (e.g., hook name) + * @param text - Status text to display, or undefined to clear + */ + setStatus(key: string, text: string | undefined): void; + /** * Show a custom component with keyboard focus. * The factory receives TUI, theme, and a done() callback to close the component. @@ -101,10 +118,30 @@ export interface HookUIContext { * @returns Current editor text */ getEditorText(): string; + + /** + * Show a multi-line editor for text editing. + * Supports Ctrl+G to open external editor ($VISUAL or $EDITOR). + * @param title - Title describing what is being edited + * @param prefill - Optional initial text + * @returns Edited text, or undefined if cancelled (Escape) + */ + editor(title: string, prefill?: string): Promise; + + /** + * Get the current theme for styling text with ANSI codes. + * Use theme.fg() and theme.bg() to style status text. + * + * @example + * const theme = ctx.ui.theme; + * ctx.ui.setStatus("my-hook", theme.fg("success", "✓") + " Ready"); + */ + readonly theme: Theme; } /** - * Context passed to hook event and command handlers. + * Context passed to hook event handlers. + * For command handlers, see HookCommandContext which extends this with session control methods. */ export interface HookContext { /** UI methods for user interaction */ @@ -119,6 +156,63 @@ export interface HookContext { modelRegistry: ModelRegistry; /** Current model (may be undefined if no model is selected yet) */ model: Model | undefined; + /** Whether the agent is idle (not streaming) */ + isIdle(): boolean; + /** Abort the current agent operation (fire-and-forget, does not wait) */ + abort(): void; + /** Whether there are queued messages waiting to be processed */ + hasQueuedMessages(): boolean; +} + +/** + * Extended context for slash command handlers. + * Includes session control methods that are only safe in user-initiated commands. + * + * These methods are not available in event handlers because they can cause + * deadlocks when called from within the agent loop (e.g., tool_call, context events). + */ +export interface HookCommandContext extends HookContext { + /** Wait for the agent to finish streaming */ + waitForIdle(): Promise; + + /** + * Start a new session, optionally with a setup callback to initialize it. + * The setup callback receives a writable SessionManager for the new session. + * + * @param options.parentSession - Path to parent session for lineage tracking + * @param options.setup - Async callback to initialize the new session (e.g., append messages) + * @returns Object with `cancelled: true` if a hook cancelled the new session + * + * @example + * // Handoff: summarize current session and start fresh with context + * await ctx.newSession({ + * parentSession: ctx.sessionManager.getSessionFile(), + * setup: async (sm) => { + * sm.appendMessage({ role: "user", content: [{ type: "text", text: summary }] }); + * } + * }); + */ + newSession(options?: { + parentSession?: string; + setup?: (sessionManager: SessionManager) => Promise; + }): Promise<{ cancelled: boolean }>; + + /** + * Branch from a specific entry, creating a new session file. + * + * @param entryId - ID of the entry to branch from + * @returns Object with `cancelled: true` if a hook cancelled the branch + */ + branch(entryId: string): Promise<{ cancelled: boolean }>; + + /** + * Navigate to a different point in the session tree (in-place). + * + * @param targetId - ID of the entry to navigate to + * @param options.summarize - Whether to summarize the abandoned branch + * @returns Object with `cancelled: true` if a hook cancelled the navigation + */ + navigateTree(targetId: string, options?: { summarize?: boolean }): Promise<{ cancelled: boolean }>; } // ============================================================================ @@ -133,27 +227,21 @@ export interface SessionStartEvent { /** Fired before switching to another session (can be cancelled) */ export interface SessionBeforeSwitchEvent { type: "session_before_switch"; - /** Session file we're switching to */ - targetSessionFile: string; + /** Reason for the switch */ + reason: "new" | "resume"; + /** Session file we're switching to (only for "resume") */ + targetSessionFile?: string; } /** Fired after switching to another session */ export interface SessionSwitchEvent { type: "session_switch"; + /** Reason for the switch */ + reason: "new" | "resume"; /** Session file we came from */ previousSessionFile: string | undefined; } -/** Fired before creating a new session (can be cancelled) */ -export interface SessionBeforeNewEvent { - type: "session_before_new"; -} - -/** Fired after creating a new session */ -export interface SessionNewEvent { - type: "session_new"; -} - /** Fired before branching a session (can be cancelled) */ export interface SessionBeforeBranchEvent { type: "session_before_branch"; @@ -234,8 +322,6 @@ export type SessionEvent = | SessionStartEvent | SessionBeforeSwitchEvent | SessionSwitchEvent - | SessionBeforeNewEvent - | SessionNewEvent | SessionBeforeBranchEvent | SessionBranchEvent | SessionBeforeCompactEvent @@ -484,12 +570,6 @@ export interface SessionBeforeSwitchResult { cancel?: boolean; } -/** Return type for session_before_new handlers */ -export interface SessionBeforeNewResult { - /** If true, cancel the new session */ - cancel?: boolean; -} - /** Return type for session_before_branch handlers */ export interface SessionBeforeBranchResult { /** @@ -567,7 +647,7 @@ export interface RegisteredCommand { name: string; description?: string; - handler: (args: string, ctx: HookContext) => Promise; + handler: (args: string, ctx: HookCommandContext) => Promise; } /** @@ -579,8 +659,6 @@ export interface HookAPI { on(event: "session_start", handler: HookHandler): void; on(event: "session_before_switch", handler: HookHandler): void; on(event: "session_switch", handler: HookHandler): void; - on(event: "session_before_new", handler: HookHandler): void; - on(event: "session_new", handler: HookHandler): void; on(event: "session_before_branch", handler: HookHandler): void; on(event: "session_branch", handler: HookHandler): void; on( @@ -659,7 +737,7 @@ export interface HookAPI { /** * Register a custom slash command. - * Handler receives HookCommandContext. + * Handler receives HookCommandContext with session control methods. */ registerCommand(name: string, options: { description?: string; handler: RegisteredCommand["handler"] }): void; diff --git a/packages/coding-agent/src/core/sdk.ts b/packages/coding-agent/src/core/sdk.ts index e24cd6e7d..7f945bcc9 100644 --- a/packages/coding-agent/src/core/sdk.ts +++ b/packages/coding-agent/src/core/sdk.ts @@ -140,7 +140,7 @@ export interface CreateAgentSessionResult { // Re-exports export type { CustomTool } from "./custom-tools/types.js"; -export type { HookAPI, HookContext, HookFactory } from "./hooks/types.js"; +export type { HookAPI, HookCommandContext, HookContext, HookFactory } from "./hooks/types.js"; export type { Settings, SkillsSettings } from "./settings-manager.js"; export type { Skill } from "./skills.js"; export type { FileSlashCommand } from "./slash-commands.js"; @@ -345,6 +345,11 @@ function createLoadedHooksFromDefinitions(definitions: Array<{ path?: string; fa const commands = new Map(); let sendMessageHandler: (message: any, triggerTurn?: boolean) => void = () => {}; let appendEntryHandler: (customType: string, data?: any) => void = () => {}; + let newSessionHandler: (options?: any) => Promise<{ cancelled: boolean }> = async () => ({ cancelled: false }); + let branchHandler: (entryId: string) => Promise<{ cancelled: boolean }> = async () => ({ cancelled: false }); + let navigateTreeHandler: (targetId: string, options?: any) => Promise<{ cancelled: boolean }> = async () => ({ + cancelled: false, + }); const api = { on: (event: string, handler: (...args: unknown[]) => Promise) => { @@ -364,6 +369,9 @@ function createLoadedHooksFromDefinitions(definitions: Array<{ path?: string; fa registerCommand: (name: string, options: any) => { commands.set(name, { name, ...options }); }, + newSession: (options?: any) => newSessionHandler(options), + branch: (entryId: string) => branchHandler(entryId), + navigateTree: (targetId: string, options?: any) => navigateTreeHandler(targetId, options), }; def.factory(api as any); @@ -380,6 +388,15 @@ function createLoadedHooksFromDefinitions(definitions: Array<{ path?: string; fa setAppendEntryHandler: (handler: (customType: string, data?: any) => void) => { appendEntryHandler = handler; }, + setNewSessionHandler: (handler: (options?: any) => Promise<{ cancelled: boolean }>) => { + newSessionHandler = handler; + }, + setBranchHandler: (handler: (entryId: string) => Promise<{ cancelled: boolean }>) => { + branchHandler = handler; + }, + setNavigateTreeHandler: (handler: (targetId: string, options?: any) => Promise<{ cancelled: boolean }>) => { + navigateTreeHandler = handler; + }, }; }); } @@ -550,12 +567,18 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} } } - // Wrap custom tools with context getter (agent is assigned below, accessed at execute time) + // Wrap custom tools with context getter (agent/session assigned below, accessed at execute time) let agent: Agent; + let session: AgentSession; const wrappedCustomTools = wrapCustomTools(customToolsResult.tools, () => ({ sessionManager, modelRegistry, model: agent.state.model, + isIdle: () => !session.isStreaming, + hasQueuedMessages: () => session.queuedMessageCount > 0, + abort: () => { + session.abort(); + }, })); let allToolsArray: Tool[] = [...builtInTools, ...wrappedCustomTools]; @@ -629,7 +652,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} sessionManager.appendThinkingLevelChange(thinkingLevel); } - const session = new AgentSession({ + session = new AgentSession({ agent, sessionManager, settingsManager, diff --git a/packages/coding-agent/src/core/session-manager.ts b/packages/coding-agent/src/core/session-manager.ts index dc47f749e..2a35e0886 100644 --- a/packages/coding-agent/src/core/session-manager.ts +++ b/packages/coding-agent/src/core/session-manager.ts @@ -1,6 +1,5 @@ import type { AgentMessage } from "@mariozechner/pi-agent-core"; import type { ImageContent, Message, TextContent } from "@mariozechner/pi-ai"; -import { randomUUID } from "crypto"; import { appendFileSync, closeSync, @@ -31,7 +30,11 @@ export interface SessionHeader { id: string; timestamp: string; cwd: string; - branchedFrom?: string; + parentSession?: string; +} + +export interface NewSessionOptions { + parentSession?: string; } export interface SessionEntryBase { @@ -178,11 +181,11 @@ export type ReadonlySessionManager = Pick< /** Generate a unique short ID (8 hex chars, collision-checked) */ function generateId(byId: { has(id: string): boolean }): string { for (let i = 0; i < 100; i++) { - const id = randomUUID().slice(0, 8); + const id = crypto.randomUUID().slice(0, 8); if (!byId.has(id)) return id; } // Fallback to full UUID if somehow we have collisions - return randomUUID(); + return crypto.randomUUID(); } /** Migrate v1 → v2: add id/parentId tree structure. Mutates in place. */ @@ -495,7 +498,7 @@ export class SessionManager { if (existsSync(this.sessionFile)) { this.fileEntries = loadEntriesFromFile(this.sessionFile); const header = this.fileEntries.find((e) => e.type === "session") as SessionHeader | undefined; - this.sessionId = header?.id ?? randomUUID(); + this.sessionId = header?.id ?? crypto.randomUUID(); if (migrateToCurrentVersion(this.fileEntries)) { this._rewriteFile(); @@ -508,8 +511,8 @@ export class SessionManager { } } - newSession(): string | undefined { - this.sessionId = randomUUID(); + newSession(options?: NewSessionOptions): string | undefined { + this.sessionId = crypto.randomUUID(); const timestamp = new Date().toISOString(); const header: SessionHeader = { type: "session", @@ -517,11 +520,13 @@ export class SessionManager { id: this.sessionId, timestamp, cwd: this.cwd, + parentSession: options?.parentSession, }; this.fileEntries = [header]; this.byId.clear(); this.leafId = null; this.flushed = false; + // Only generate filename if persisting and not already set (e.g., via --session flag) if (this.persist && !this.sessionFile) { const fileTimestamp = timestamp.replace(/[:.]/g, "-"); @@ -918,7 +923,7 @@ export class SessionManager { // Filter out LabelEntry from path - we'll recreate them from the resolved map const pathWithoutLabels = path.filter((e) => e.type !== "label"); - const newSessionId = randomUUID(); + const newSessionId = crypto.randomUUID(); const timestamp = new Date().toISOString(); const fileTimestamp = timestamp.replace(/[:.]/g, "-"); const newSessionFile = join(this.getSessionDir(), `${fileTimestamp}_${newSessionId}.jsonl`); @@ -929,7 +934,7 @@ export class SessionManager { id: newSessionId, timestamp, cwd: this.cwd, - branchedFrom: this.persist ? this.sessionFile : undefined, + parentSession: this.persist ? this.sessionFile : undefined, }; // Collect labels for entries in the path diff --git a/packages/coding-agent/src/core/tools/bash.ts b/packages/coding-agent/src/core/tools/bash.ts index 8061e2201..01b15c32d 100644 --- a/packages/coding-agent/src/core/tools/bash.ts +++ b/packages/coding-agent/src/core/tools/bash.ts @@ -3,6 +3,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import type { AgentTool } from "@mariozechner/pi-agent-core"; import { Type } from "@sinclair/typebox"; +import type { Subprocess } from "bun"; import { getShellConfig, killProcessTree } from "../../utils/shell.js"; import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateTail } from "./truncate.js"; @@ -37,174 +38,169 @@ export function createBashTool(cwd: string): AgentTool { signal?: AbortSignal, onUpdate?, ) => { - return new Promise((resolve, reject) => { - const { shell, args } = getShellConfig(); - const child = spawn(shell, [...args, command], { - cwd, - - stdin: "ignore", - stdout: "pipe", - stderr: "pipe", - }); - - // We'll stream to a temp file if output gets large - let tempFilePath: string | undefined; - let tempFileStream: ReturnType | undefined; - let totalBytes = 0; - - // Keep a rolling buffer of the last chunk for tail truncation - const chunks: Buffer[] = []; - let chunksBytes = 0; - // Keep more than we need so we have enough for truncation - const maxChunksBytes = DEFAULT_MAX_BYTES * 2; - - let timedOut = false; - - // Set timeout if provided - let timeoutHandle: NodeJS.Timeout | undefined; - if (timeout !== undefined && timeout > 0) { - timeoutHandle = setTimeout(() => { - timedOut = true; - onAbort(); - }, timeout * 1000); - } - - const handleData = (data: Buffer) => { - totalBytes += data.length; - - // Start writing to temp file once we exceed the threshold - if (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) { - tempFilePath = getTempFilePath(); - tempFileStream = createWriteStream(tempFilePath); - // Write all buffered chunks to the file - for (const chunk of chunks) { - tempFileStream.write(chunk); - } - } - - // Write to temp file if we have one - if (tempFileStream) { - tempFileStream.write(data); - } - - // Keep rolling buffer of recent data - chunks.push(data); - chunksBytes += data.length; - - // Trim old chunks if buffer is too large - while (chunksBytes > maxChunksBytes && chunks.length > 1) { - const removed = chunks.shift()!; - chunksBytes -= removed.length; - } - - // Stream partial output to callback (truncated rolling buffer) - if (onUpdate) { - const fullBuffer = Buffer.concat(chunks); - const fullText = fullBuffer.toString("utf-8"); - const truncation = truncateTail(fullText); - onUpdate({ - content: [{ type: "text", text: truncation.content || "" }], - details: { - truncation: truncation.truncated ? truncation : undefined, - fullOutputPath: tempFilePath, - }, - }); - } - }; - - // Collect stdout and stderr together - if (child.stdout) { - child.stdout.on("data", handleData); - } - if (child.stderr) { - child.stderr.on("data", handleData); - } - - // Handle process exit - child.on("close", (code) => { - if (timeoutHandle) { - clearTimeout(timeoutHandle); - } - if (signal) { - signal.removeEventListener("abort", onAbort); - } - - // Close temp file stream - if (tempFileStream) { - tempFileStream.end(); - } - - // Combine all buffered chunks - const fullBuffer = Buffer.concat(chunks); - const fullOutput = fullBuffer.toString("utf-8"); - - if (signal?.aborted) { - let output = fullOutput; - if (output) output += "\n\n"; - output += "Command aborted"; - reject(new Error(output)); - return; - } - - if (timedOut) { - let output = fullOutput; - if (output) output += "\n\n"; - output += `Command timed out after ${timeout} seconds`; - reject(new Error(output)); - return; - } - - // Apply tail truncation - const truncation = truncateTail(fullOutput); - let outputText = truncation.content || "(no output)"; - - // Build details with truncation info - let details: BashToolDetails | undefined; - - if (truncation.truncated) { - details = { - truncation, - fullOutputPath: tempFilePath, - }; - - // Build actionable notice - const startLine = truncation.totalLines - truncation.outputLines + 1; - const endLine = truncation.totalLines; - - if (truncation.lastLinePartial) { - // Edge case: last line alone > 30KB - const lastLineSize = formatSize(Buffer.byteLength(fullOutput.split("\n").pop() || "", "utf-8")); - outputText += `\n\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${tempFilePath}]`; - } else if (truncation.truncatedBy === "lines") { - outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${tempFilePath}]`; - } else { - outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${tempFilePath}]`; - } - } - - if (code !== 0 && code !== null) { - outputText += `\n\nCommand exited with code ${code}`; - reject(new Error(outputText)); - } else { - resolve({ content: [{ type: "text", text: outputText }], details }); - } - }); - - // Handle abort signal - kill entire process tree - const onAbort = () => { - if (child.pid) { - killProcessTree(child.pid); - } - }; - - if (signal) { - if (signal.aborted) { - onAbort(); - } else { - signal.addEventListener("abort", onAbort, { once: true }); - } - } + const { shell, args } = getShellConfig(); + const child: Subprocess = Bun.spawn([shell, ...args, command], { + cwd, + stdin: "ignore", + stdout: "pipe", + stderr: "pipe", }); + + // We'll stream to a temp file if output gets large + let tempFilePath: string | undefined; + let tempFileStream: ReturnType | undefined; + let totalBytes = 0; + + // Keep a rolling buffer of the last chunks for tail truncation + const chunks: Buffer[] = []; + let chunksBytes = 0; + const maxChunksBytes = DEFAULT_MAX_BYTES * 2; + + let timedOut = false; + let aborted = false; + + // Handle abort signal + const onAbort = () => { + aborted = true; + if (child.pid) { + killProcessTree(child.pid); + } + }; + + if (signal) { + if (signal.aborted) { + child.kill(); + throw new Error("Command aborted"); + } + signal.addEventListener("abort", onAbort, { once: true }); + } + + // Set timeout if provided + let timeoutHandle: Timer | undefined; + if (timeout !== undefined && timeout > 0) { + timeoutHandle = setTimeout(() => { + timedOut = true; + onAbort(); + }, timeout * 1000); + } + + const handleData = (data: Buffer) => { + totalBytes += data.length; + + // Start writing to temp file once we exceed the threshold + if (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) { + tempFilePath = getTempFilePath(); + tempFileStream = createWriteStream(tempFilePath); + for (const chunk of chunks) { + tempFileStream.write(chunk); + } + } + + if (tempFileStream) { + tempFileStream.write(data); + } + + // Keep rolling buffer of recent data + chunks.push(data); + chunksBytes += data.length; + + while (chunksBytes > maxChunksBytes && chunks.length > 1) { + const removed = chunks.shift()!; + chunksBytes -= removed.length; + } + + // Stream partial output to callback + if (onUpdate) { + const fullBuffer = Buffer.concat(chunks); + const fullText = fullBuffer.toString("utf-8"); + const truncation = truncateTail(fullText); + onUpdate({ + content: [{ type: "text", text: truncation.content || "" }], + details: { + truncation: truncation.truncated ? truncation : undefined, + fullOutputPath: tempFilePath, + }, + }); + } + }; + + // Read streams using Bun's ReadableStream API + const stdoutReader = (child.stdout as ReadableStream).getReader(); + const stderrReader = (child.stderr as ReadableStream).getReader(); + + await Promise.all([ + (async () => { + while (true) { + const { done, value } = await stdoutReader.read(); + if (done) break; + handleData(Buffer.from(value)); + } + })(), + (async () => { + while (true) { + const { done, value } = await stderrReader.read(); + if (done) break; + handleData(Buffer.from(value)); + } + })(), + ]); + + const exitCode = await child.exited; + + // Cleanup + if (timeoutHandle) clearTimeout(timeoutHandle); + if (signal) signal.removeEventListener("abort", onAbort); + if (tempFileStream) tempFileStream.end(); + + // Combine all buffered chunks + const fullBuffer = Buffer.concat(chunks); + const fullOutput = fullBuffer.toString("utf-8"); + + if (aborted && !timedOut) { + let output = fullOutput; + if (output) output += "\n\n"; + output += "Command aborted"; + throw new Error(output); + } + + if (timedOut) { + let output = fullOutput; + if (output) output += "\n\n"; + output += `Command timed out after ${timeout} seconds`; + throw new Error(output); + } + + // Apply tail truncation + const truncation = truncateTail(fullOutput); + let outputText = truncation.content || "(no output)"; + + let details: BashToolDetails | undefined; + + if (truncation.truncated) { + details = { + truncation, + fullOutputPath: tempFilePath, + }; + + const startLine = truncation.totalLines - truncation.outputLines + 1; + const endLine = truncation.totalLines; + + if (truncation.lastLinePartial) { + const lastLineSize = formatSize(Buffer.byteLength(fullOutput.split("\n").pop() || "", "utf-8")); + outputText += `\n\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${tempFilePath}]`; + } else if (truncation.truncatedBy === "lines") { + outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${tempFilePath}]`; + } else { + outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${tempFilePath}]`; + } + } + + if (exitCode !== 0 && exitCode !== null) { + outputText += `\n\nCommand exited with code ${exitCode}`; + throw new Error(outputText); + } + + return { content: [{ type: "text", text: outputText }], details }; }, }; } diff --git a/packages/coding-agent/src/core/tools/edit-diff.ts b/packages/coding-agent/src/core/tools/edit-diff.ts new file mode 100644 index 000000000..a29710c72 --- /dev/null +++ b/packages/coding-agent/src/core/tools/edit-diff.ts @@ -0,0 +1,211 @@ +/** + * Shared diff computation utilities for the edit tool. + * Used by both edit.ts (for execution) and tool-execution.ts (for preview rendering). + */ + +import * as Diff from "diff"; +import { constants } from "fs"; +import { access, readFile } from "fs/promises"; +import { resolveToCwd } from "./path-utils.js"; + +export function detectLineEnding(content: string): "\r\n" | "\n" { + const crlfIdx = content.indexOf("\r\n"); + const lfIdx = content.indexOf("\n"); + if (lfIdx === -1) return "\n"; + if (crlfIdx === -1) return "\n"; + return crlfIdx < lfIdx ? "\r\n" : "\n"; +} + +export function normalizeToLF(text: string): string { + return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); +} + +export function restoreLineEndings(text: string, ending: "\r\n" | "\n"): string { + return ending === "\r\n" ? text.replace(/\n/g, "\r\n") : text; +} + +/** Strip UTF-8 BOM if present, return both the BOM (if any) and the text without it */ +export function stripBom(content: string): { bom: string; text: string } { + return content.startsWith("\uFEFF") ? { bom: "\uFEFF", text: content.slice(1) } : { bom: "", text: content }; +} + +/** + * Generate a unified diff string with line numbers and context. + * Returns both the diff string and the first changed line number (in the new file). + */ +export function generateDiffString( + oldContent: string, + newContent: string, + contextLines = 4, +): { diff: string; firstChangedLine: number | undefined } { + const parts = Diff.diffLines(oldContent, newContent); + const output: string[] = []; + + const oldLines = oldContent.split("\n"); + const newLines = newContent.split("\n"); + const maxLineNum = Math.max(oldLines.length, newLines.length); + const lineNumWidth = String(maxLineNum).length; + + let oldLineNum = 1; + let newLineNum = 1; + let lastWasChange = false; + let firstChangedLine: number | undefined; + + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + const raw = part.value.split("\n"); + if (raw[raw.length - 1] === "") { + raw.pop(); + } + + if (part.added || part.removed) { + // Capture the first changed line (in the new file) + if (firstChangedLine === undefined) { + firstChangedLine = newLineNum; + } + + // Show the change + for (const line of raw) { + if (part.added) { + const lineNum = String(newLineNum).padStart(lineNumWidth, " "); + output.push(`+${lineNum} ${line}`); + newLineNum++; + } else { + // removed + const lineNum = String(oldLineNum).padStart(lineNumWidth, " "); + output.push(`-${lineNum} ${line}`); + oldLineNum++; + } + } + lastWasChange = true; + } else { + // Context lines - only show a few before/after changes + const nextPartIsChange = i < parts.length - 1 && (parts[i + 1].added || parts[i + 1].removed); + + if (lastWasChange || nextPartIsChange) { + // Show context + let linesToShow = raw; + let skipStart = 0; + let skipEnd = 0; + + if (!lastWasChange) { + // Show only last N lines as leading context + skipStart = Math.max(0, raw.length - contextLines); + linesToShow = raw.slice(skipStart); + } + + if (!nextPartIsChange && linesToShow.length > contextLines) { + // Show only first N lines as trailing context + skipEnd = linesToShow.length - contextLines; + linesToShow = linesToShow.slice(0, contextLines); + } + + // Add ellipsis if we skipped lines at start + if (skipStart > 0) { + output.push(` ${"".padStart(lineNumWidth, " ")} ...`); + // Update line numbers for the skipped leading context + oldLineNum += skipStart; + newLineNum += skipStart; + } + + for (const line of linesToShow) { + const lineNum = String(oldLineNum).padStart(lineNumWidth, " "); + output.push(` ${lineNum} ${line}`); + oldLineNum++; + newLineNum++; + } + + // Add ellipsis if we skipped lines at end + if (skipEnd > 0) { + output.push(` ${"".padStart(lineNumWidth, " ")} ...`); + // Update line numbers for the skipped trailing context + oldLineNum += skipEnd; + newLineNum += skipEnd; + } + } else { + // Skip these context lines entirely + oldLineNum += raw.length; + newLineNum += raw.length; + } + + lastWasChange = false; + } + } + + return { diff: output.join("\n"), firstChangedLine }; +} + +export interface EditDiffResult { + diff: string; + firstChangedLine: number | undefined; +} + +export interface EditDiffError { + error: string; +} + +/** + * Compute the diff for an edit operation without applying it. + * Used for preview rendering in the TUI before the tool executes. + */ +export async function computeEditDiff( + path: string, + oldText: string, + newText: string, + cwd: string, +): Promise { + const absolutePath = resolveToCwd(path, cwd); + + try { + // Check if file exists and is readable + try { + await access(absolutePath, constants.R_OK); + } catch { + return { error: `File not found: ${path}` }; + } + + // Read the file + const rawContent = await readFile(absolutePath, "utf-8"); + + // Strip BOM before matching (LLM won't include invisible BOM in oldText) + const { text: content } = stripBom(rawContent); + + const normalizedContent = normalizeToLF(content); + const normalizedOldText = normalizeToLF(oldText); + const normalizedNewText = normalizeToLF(newText); + + // Check if old text exists + if (!normalizedContent.includes(normalizedOldText)) { + return { + error: `Could not find the exact text in ${path}. The old text must match exactly including all whitespace and newlines.`, + }; + } + + // Count occurrences + const occurrences = normalizedContent.split(normalizedOldText).length - 1; + if (occurrences > 1) { + return { + error: `Found ${occurrences} occurrences of the text in ${path}. The text must be unique. Please provide more context to make it unique.`, + }; + } + + // Compute the new content + const index = normalizedContent.indexOf(normalizedOldText); + const normalizedNewContent = + normalizedContent.substring(0, index) + + normalizedNewText + + normalizedContent.substring(index + normalizedOldText.length); + + // Check if it would actually change anything + if (normalizedContent === normalizedNewContent) { + return { + error: `No changes would be made to ${path}. The replacement produces identical content.`, + }; + } + + // Generate the diff + return generateDiffString(normalizedContent, normalizedNewContent); + } catch (err) { + return { error: err instanceof Error ? err.message : String(err) }; + } +} diff --git a/packages/coding-agent/src/core/tools/edit.ts b/packages/coding-agent/src/core/tools/edit.ts index a46a209d8..3360308c4 100644 --- a/packages/coding-agent/src/core/tools/edit.ts +++ b/packages/coding-agent/src/core/tools/edit.ts @@ -1,132 +1,10 @@ import type { AgentTool } from "@mariozechner/pi-agent-core"; import { Type } from "@sinclair/typebox"; -import * as Diff from "diff"; import { constants } from "fs"; import { access, readFile, writeFile } from "fs/promises"; +import { detectLineEnding, generateDiffString, normalizeToLF, restoreLineEndings, stripBom } from "./edit-diff.js"; import { resolveToCwd } from "./path-utils.js"; -function detectLineEnding(content: string): "\r\n" | "\n" { - const crlfIdx = content.indexOf("\r\n"); - const lfIdx = content.indexOf("\n"); - if (lfIdx === -1) return "\n"; - if (crlfIdx === -1) return "\n"; - return crlfIdx < lfIdx ? "\r\n" : "\n"; -} - -function normalizeToLF(text: string): string { - return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); -} - -function restoreLineEndings(text: string, ending: "\r\n" | "\n"): string { - return ending === "\r\n" ? text.replace(/\n/g, "\r\n") : text; -} - -/** - * Generate a unified diff string with line numbers and context - * Returns both the diff string and the first changed line number (in the new file) - */ -function generateDiffString( - oldContent: string, - newContent: string, - contextLines = 4, -): { diff: string; firstChangedLine: number | undefined } { - const parts = Diff.diffLines(oldContent, newContent); - const output: string[] = []; - - const oldLines = oldContent.split("\n"); - const newLines = newContent.split("\n"); - const maxLineNum = Math.max(oldLines.length, newLines.length); - const lineNumWidth = String(maxLineNum).length; - - let oldLineNum = 1; - let newLineNum = 1; - let lastWasChange = false; - let firstChangedLine: number | undefined; - - for (let i = 0; i < parts.length; i++) { - const part = parts[i]; - const raw = part.value.split("\n"); - if (raw[raw.length - 1] === "") { - raw.pop(); - } - - if (part.added || part.removed) { - // Capture the first changed line (in the new file) - if (firstChangedLine === undefined) { - firstChangedLine = newLineNum; - } - - // Show the change - for (const line of raw) { - if (part.added) { - const lineNum = String(newLineNum).padStart(lineNumWidth, " "); - output.push(`+${lineNum} ${line}`); - newLineNum++; - } else { - // removed - const lineNum = String(oldLineNum).padStart(lineNumWidth, " "); - output.push(`-${lineNum} ${line}`); - oldLineNum++; - } - } - lastWasChange = true; - } else { - // Context lines - only show a few before/after changes - const nextPartIsChange = i < parts.length - 1 && (parts[i + 1].added || parts[i + 1].removed); - - if (lastWasChange || nextPartIsChange) { - // Show context - let linesToShow = raw; - let skipStart = 0; - let skipEnd = 0; - - if (!lastWasChange) { - // Show only last N lines as leading context - skipStart = Math.max(0, raw.length - contextLines); - linesToShow = raw.slice(skipStart); - } - - if (!nextPartIsChange && linesToShow.length > contextLines) { - // Show only first N lines as trailing context - skipEnd = linesToShow.length - contextLines; - linesToShow = linesToShow.slice(0, contextLines); - } - - // Add ellipsis if we skipped lines at start - if (skipStart > 0) { - output.push(` ${"".padStart(lineNumWidth, " ")} ...`); - // Update line numbers for the skipped leading context - oldLineNum += skipStart; - newLineNum += skipStart; - } - - for (const line of linesToShow) { - const lineNum = String(oldLineNum).padStart(lineNumWidth, " "); - output.push(` ${lineNum} ${line}`); - oldLineNum++; - newLineNum++; - } - - // Add ellipsis if we skipped lines at end - if (skipEnd > 0) { - output.push(` ${"".padStart(lineNumWidth, " ")} ...`); - // Update line numbers for the skipped trailing context - oldLineNum += skipEnd; - newLineNum += skipEnd; - } - } else { - // Skip these context lines entirely - oldLineNum += raw.length; - newLineNum += raw.length; - } - - lastWasChange = false; - } - } - - return { diff: output.join("\n"), firstChangedLine }; -} - const editSchema = Type.Object({ path: Type.String({ description: "Path to the file to edit (relative or absolute)" }), oldText: Type.String({ description: "Exact text to find and replace (must match exactly)" }), @@ -196,13 +74,16 @@ export function createEditTool(cwd: string): AgentTool { } // Read the file - const content = await readFile(absolutePath, "utf-8"); + const rawContent = await readFile(absolutePath, "utf-8"); // Check if aborted after reading if (aborted) { return; } + // Strip BOM before matching (LLM won't include invisible BOM in oldText) + const { bom, text: content } = stripBom(rawContent); + const originalEnding = detectLineEnding(content); const normalizedContent = normalizeToLF(content); const normalizedOldText = normalizeToLF(oldText); @@ -262,7 +143,7 @@ export function createEditTool(cwd: string): AgentTool { return; } - const finalContent = restoreLineEndings(normalizedNewContent, originalEnding); + const finalContent = bom + restoreLineEndings(normalizedNewContent, originalEnding); await writeFile(absolutePath, finalContent, "utf-8"); // Check if aborted after writing diff --git a/packages/coding-agent/src/core/tools/grep.ts b/packages/coding-agent/src/core/tools/grep.ts index fb0839718..3ac33e36e 100644 --- a/packages/coding-agent/src/core/tools/grep.ts +++ b/packages/coding-agent/src/core/tools/grep.ts @@ -1,7 +1,8 @@ import { readFileSync, type Stats, statSync } from "node:fs"; -import path from "node:path"; +import nodePath from "node:path"; import type { AgentTool } from "@mariozechner/pi-agent-core"; import { Type } from "@sinclair/typebox"; +import type { Subprocess } from "bun"; import { ensureTool } from "../../utils/tools-manager.js"; import { resolveToCwd } from "./path-utils.js"; import { @@ -62,246 +63,247 @@ export function createGrepTool(cwd: string): AgentTool { }, signal?: AbortSignal, ) => { - return new Promise((resolve, reject) => { - if (signal?.aborted) { - reject(new Error("Operation aborted")); + if (signal?.aborted) { + throw new Error("Operation aborted"); + } + + const rgPath = await ensureTool("rg", true); + if (!rgPath) { + throw new Error("ripgrep (rg) is not available and could not be downloaded"); + } + + const searchPath = resolveToCwd(searchDir || ".", cwd); + let searchStat: Stats; + try { + searchStat = statSync(searchPath); + } catch (_err) { + throw new Error(`Path not found: ${searchPath}`); + } + + const isDirectory = searchStat.isDirectory(); + const contextValue = context && context > 0 ? context : 0; + const effectiveLimit = Math.max(1, limit ?? DEFAULT_LIMIT); + + const formatPath = (filePath: string): string => { + if (isDirectory) { + const relative = nodePath.relative(searchPath, filePath); + if (relative && !relative.startsWith("..")) { + return relative.replace(/\\/g, "/"); + } + } + return nodePath.basename(filePath); + }; + + const fileCache = new Map(); + const getFileLines = (filePath: string): string[] => { + let lines = fileCache.get(filePath); + if (!lines) { + try { + const content = readFileSync(filePath, "utf-8"); + lines = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n"); + } catch { + lines = []; + } + fileCache.set(filePath, lines); + } + return lines; + }; + + const args: string[] = ["--json", "--line-number", "--color=never", "--hidden"]; + + if (ignoreCase) { + args.push("--ignore-case"); + } + + if (literal) { + args.push("--fixed-strings"); + } + + if (glob) { + args.push("--glob", glob); + } + + args.push(pattern, searchPath); + + const child: Subprocess = Bun.spawn([rgPath, ...args], { + stdin: "ignore", + stdout: "pipe", + stderr: "pipe", + }); + + let stderr = ""; + let matchCount = 0; + let matchLimitReached = false; + let linesTruncated = false; + let aborted = false; + let killedDueToLimit = false; + const outputLines: string[] = []; + + const stopChild = (dueToLimit: boolean = false) => { + killedDueToLimit = dueToLimit; + child.kill(); + }; + + const onAbort = () => { + aborted = true; + stopChild(); + }; + + if (signal) { + signal.addEventListener("abort", onAbort, { once: true }); + } + + const formatBlock = (filePath: string, lineNumber: number): string[] => { + const relativePath = formatPath(filePath); + const lines = getFileLines(filePath); + if (!lines.length) { + return [`${relativePath}:${lineNumber}: (unable to read file)`]; + } + + const block: string[] = []; + const start = contextValue > 0 ? Math.max(1, lineNumber - contextValue) : lineNumber; + const end = contextValue > 0 ? Math.min(lines.length, lineNumber + contextValue) : lineNumber; + + for (let current = start; current <= end; current++) { + const lineText = lines[current - 1] ?? ""; + const sanitized = lineText.replace(/\r/g, ""); + const isMatchLine = current === lineNumber; + + const { text: truncatedText, wasTruncated } = truncateLine(sanitized); + if (wasTruncated) { + linesTruncated = true; + } + + if (isMatchLine) { + block.push(`${relativePath}:${current}: ${truncatedText}`); + } else { + block.push(`${relativePath}-${current}- ${truncatedText}`); + } + } + + return block; + }; + + const processLine = (line: string) => { + if (!line.trim() || matchCount >= effectiveLimit) { return; } - let settled = false; - const settle = (fn: () => void) => { - if (!settled) { - settled = true; - fn(); - } - }; + let event: { type: string; data?: { path?: { text?: string }; line_number?: number } }; + try { + event = JSON.parse(line); + } catch { + return; + } + if (event.type === "match") { + matchCount++; + const filePath = event.data?.path?.text; + const lineNumber = event.data?.line_number; + + if (filePath && typeof lineNumber === "number") { + outputLines.push(...formatBlock(filePath, lineNumber)); + } + + if (matchCount >= effectiveLimit) { + matchLimitReached = true; + stopChild(true); + } + } + }; + + // Read streams using Bun's ReadableStream API + const stdoutReader = (child.stdout as ReadableStream).getReader(); + const stderrReader = (child.stderr as ReadableStream).getReader(); + const decoder = new TextDecoder(); + let stdoutBuffer = ""; + + await Promise.all([ + // Process stdout line by line (async () => { - try { - const rgPath = await ensureTool("rg", true); - if (!rgPath) { - settle(() => reject(new Error("ripgrep (rg) is not available and could not be downloaded"))); - return; + while (true) { + const { done, value } = await stdoutReader.read(); + if (done) break; + + stdoutBuffer += decoder.decode(value, { stream: true }); + const lines = stdoutBuffer.split("\n"); + // Keep the last incomplete line in the buffer + stdoutBuffer = lines.pop() ?? ""; + + for (const line of lines) { + processLine(line); } - - const searchPath = resolveToCwd(searchDir || ".", cwd); - let searchStat: Stats; - try { - searchStat = statSync(searchPath); - } catch (_err) { - settle(() => reject(new Error(`Path not found: ${searchPath}`))); - return; - } - - const isDirectory = searchStat.isDirectory(); - const contextValue = context && context > 0 ? context : 0; - const effectiveLimit = Math.max(1, limit ?? DEFAULT_LIMIT); - - const formatPath = (filePath: string): string => { - if (isDirectory) { - const relative = path.relative(searchPath, filePath); - if (relative && !relative.startsWith("..")) { - return relative.replace(/\\/g, "/"); - } - } - return path.basename(filePath); - }; - - const fileCache = new Map(); - const getFileLines = (filePath: string): string[] => { - let lines = fileCache.get(filePath); - if (!lines) { - try { - const content = readFileSync(filePath, "utf-8"); - lines = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n"); - } catch { - lines = []; - } - fileCache.set(filePath, lines); - } - return lines; - }; - - const args: string[] = ["--json", "--line-number", "--color=never", "--hidden"]; - - if (ignoreCase) { - args.push("--ignore-case"); - } - - if (literal) { - args.push("--fixed-strings"); - } - - if (glob) { - args.push("--glob", glob); - } - - args.push(pattern, searchPath); - - const child = spawn(rgPath, args, { stdio: ["ignore", "pipe", "pipe"] }); - const rl = createInterface({ input: child.stdout }); - let stderr = ""; - let matchCount = 0; - let matchLimitReached = false; - let linesTruncated = false; - let aborted = false; - let killedDueToLimit = false; - const outputLines: string[] = []; - - const cleanup = () => { - rl.close(); - signal?.removeEventListener("abort", onAbort); - }; - - const stopChild = (dueToLimit: boolean = false) => { - if (!child.killed) { - killedDueToLimit = dueToLimit; - child.kill(); - } - }; - - const onAbort = () => { - aborted = true; - stopChild(); - }; - - signal?.addEventListener("abort", onAbort, { once: true }); - - child.stderr?.on("data", (chunk) => { - stderr += chunk.toString(); - }); - - const formatBlock = (filePath: string, lineNumber: number): string[] => { - const relativePath = formatPath(filePath); - const lines = getFileLines(filePath); - if (!lines.length) { - return [`${relativePath}:${lineNumber}: (unable to read file)`]; - } - - const block: string[] = []; - const start = contextValue > 0 ? Math.max(1, lineNumber - contextValue) : lineNumber; - const end = contextValue > 0 ? Math.min(lines.length, lineNumber + contextValue) : lineNumber; - - for (let current = start; current <= end; current++) { - const lineText = lines[current - 1] ?? ""; - const sanitized = lineText.replace(/\r/g, ""); - const isMatchLine = current === lineNumber; - - // Truncate long lines - const { text: truncatedText, wasTruncated } = truncateLine(sanitized); - if (wasTruncated) { - linesTruncated = true; - } - - if (isMatchLine) { - block.push(`${relativePath}:${current}: ${truncatedText}`); - } else { - block.push(`${relativePath}-${current}- ${truncatedText}`); - } - } - - return block; - }; - - rl.on("line", (line) => { - if (!line.trim() || matchCount >= effectiveLimit) { - return; - } - - let event: any; - try { - event = JSON.parse(line); - } catch { - return; - } - - if (event.type === "match") { - matchCount++; - const filePath = event.data?.path?.text; - const lineNumber = event.data?.line_number; - - if (filePath && typeof lineNumber === "number") { - outputLines.push(...formatBlock(filePath, lineNumber)); - } - - if (matchCount >= effectiveLimit) { - matchLimitReached = true; - stopChild(true); - } - } - }); - - child.on("error", (error) => { - cleanup(); - settle(() => reject(new Error(`Failed to run ripgrep: ${error.message}`))); - }); - - child.on("close", (code) => { - cleanup(); - - if (aborted) { - settle(() => reject(new Error("Operation aborted"))); - return; - } - - if (!killedDueToLimit && code !== 0 && code !== 1) { - const errorMsg = stderr.trim() || `ripgrep exited with code ${code}`; - settle(() => reject(new Error(errorMsg))); - return; - } - - if (matchCount === 0) { - settle(() => - resolve({ content: [{ type: "text", text: "No matches found" }], details: undefined }), - ); - return; - } - - // Apply byte truncation (no line limit since we already have match limit) - const rawOutput = outputLines.join("\n"); - const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER }); - - let output = truncation.content; - const details: GrepToolDetails = {}; - - // Build notices - const notices: string[] = []; - - if (matchLimitReached) { - notices.push( - `${effectiveLimit} matches limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`, - ); - details.matchLimitReached = effectiveLimit; - } - - if (truncation.truncated) { - notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`); - details.truncation = truncation; - } - - if (linesTruncated) { - notices.push( - `Some lines truncated to ${GREP_MAX_LINE_LENGTH} chars. Use read tool to see full lines`, - ); - details.linesTruncated = true; - } - - if (notices.length > 0) { - output += `\n\n[${notices.join(". ")}]`; - } - - settle(() => - resolve({ - content: [{ type: "text", text: output }], - details: Object.keys(details).length > 0 ? details : undefined, - }), - ); - }); - } catch (err) { - settle(() => reject(err as Error)); } - })(); - }); + // Process any remaining content + if (stdoutBuffer.trim()) { + processLine(stdoutBuffer); + } + })(), + // Collect stderr + (async () => { + while (true) { + const { done, value } = await stderrReader.read(); + if (done) break; + stderr += decoder.decode(value, { stream: true }); + } + })(), + ]); + + const exitCode = await child.exited; + + // Cleanup + if (signal) { + signal.removeEventListener("abort", onAbort); + } + + if (aborted) { + throw new Error("Operation aborted"); + } + + if (!killedDueToLimit && exitCode !== 0 && exitCode !== 1) { + const errorMsg = stderr.trim() || `ripgrep exited with code ${exitCode}`; + throw new Error(errorMsg); + } + + if (matchCount === 0) { + return { content: [{ type: "text", text: "No matches found" }], details: undefined }; + } + + // Apply byte truncation (no line limit since we already have match limit) + const rawOutput = outputLines.join("\n"); + const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER }); + + let output = truncation.content; + const details: GrepToolDetails = {}; + + // Build notices + const notices: string[] = []; + + if (matchLimitReached) { + notices.push( + `${effectiveLimit} matches limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`, + ); + details.matchLimitReached = effectiveLimit; + } + + if (truncation.truncated) { + notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`); + details.truncation = truncation; + } + + if (linesTruncated) { + notices.push(`Some lines truncated to ${GREP_MAX_LINE_LENGTH} chars. Use read tool to see full lines`); + details.linesTruncated = true; + } + + if (notices.length > 0) { + output += `\n\n[${notices.join(". ")}]`; + } + + return { + content: [{ type: "text", text: output }], + details: Object.keys(details).length > 0 ? details : undefined, + }; }, }; } diff --git a/packages/coding-agent/src/index.ts b/packages/coding-agent/src/index.ts index f92654408..ba2e1a522 100644 --- a/packages/coding-agent/src/index.ts +++ b/packages/coding-agent/src/index.ts @@ -107,6 +107,7 @@ export { getLatestCompactionEntry, type ModelChangeEntry, migrateSessionEntries, + type NewSessionOptions, parseSessionEntries, type SessionContext, type SessionEntry, diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 9b67d7481..a81afeccd 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -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"; diff --git a/packages/coding-agent/src/modes/interactive/components/bordered-loader.ts b/packages/coding-agent/src/modes/interactive/components/bordered-loader.ts index 811ef9f71..e83f32a4a 100644 --- a/packages/coding-agent/src/modes/interactive/components/bordered-loader.ts +++ b/packages/coding-agent/src/modes/interactive/components/bordered-loader.ts @@ -8,8 +8,8 @@ export class BorderedLoader extends Container { constructor(tui: TUI, theme: Theme, message: string) { super(); - this.addChild(new DynamicBorder()); - this.addChild(new Spacer(1)); + const borderColor = (s: string) => theme.fg("border", s); + this.addChild(new DynamicBorder(borderColor)); this.loader = new CancellableLoader( tui, (s) => theme.fg("accent", s), @@ -20,7 +20,7 @@ export class BorderedLoader extends Container { this.addChild(new Spacer(1)); this.addChild(new Text(theme.fg("muted", "esc cancel"), 1, 0)); this.addChild(new Spacer(1)); - this.addChild(new DynamicBorder()); + this.addChild(new DynamicBorder(borderColor)); } get signal(): AbortSignal { diff --git a/packages/coding-agent/src/modes/interactive/components/dynamic-border.ts b/packages/coding-agent/src/modes/interactive/components/dynamic-border.ts index b835ae0ad..2ee05ccbe 100644 --- a/packages/coding-agent/src/modes/interactive/components/dynamic-border.ts +++ b/packages/coding-agent/src/modes/interactive/components/dynamic-border.ts @@ -2,7 +2,11 @@ import type { Component } from "@mariozechner/pi-tui"; import { theme } from "../theme/theme.js"; /** - * Dynamic border component that adjusts to viewport width + * Dynamic border component that adjusts to viewport width. + * + * Note: When used from hooks loaded via jiti, the global `theme` may be undefined + * because jiti creates a separate module cache. Always pass an explicit color + * function when using DynamicBorder in components exported for hook use. */ export class DynamicBorder implements Component { private color: (str: string) => string; diff --git a/packages/coding-agent/src/modes/interactive/components/footer.ts b/packages/coding-agent/src/modes/interactive/components/footer.ts index 347404dc3..78db717c4 100644 --- a/packages/coding-agent/src/modes/interactive/components/footer.ts +++ b/packages/coding-agent/src/modes/interactive/components/footer.ts @@ -1,10 +1,22 @@ import type { AssistantMessage } from "@mariozechner/pi-ai"; -import { type Component, visibleWidth } from "@mariozechner/pi-tui"; +import { type Component, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui"; import { existsSync, type FSWatcher, readFileSync, watch } from "fs"; import { dirname, join } from "path"; import type { AgentSession } from "../../../core/agent-session.js"; import { theme } from "../theme/theme.js"; +/** + * Sanitize text for display in a single-line status. + * Removes newlines, tabs, carriage returns, and other control characters. + */ +function sanitizeStatusText(text: string): string { + // Replace newlines, tabs, carriage returns with space, then collapse multiple spaces + return text + .replace(/[\r\n\t]/g, " ") + .replace(/ +/g, " ") + .trim(); +} + /** * Find the git root directory by walking up from cwd. * Returns the path to .git/HEAD if found, null otherwise. @@ -34,6 +46,7 @@ export class FooterComponent implements Component { private gitWatcher: FSWatcher | null = null; private onBranchChange: (() => void) | null = null; private autoCompactEnabled: boolean = true; + private hookStatuses: Map = new Map(); constructor(session: AgentSession) { this.session = session; @@ -43,6 +56,21 @@ export class FooterComponent implements Component { this.autoCompactEnabled = enabled; } + /** + * Set hook status text to display in the footer. + * Text is sanitized (newlines/tabs replaced with spaces) and truncated to terminal width. + * ANSI escape codes for styling are preserved. + * @param key - Unique key to identify this status + * @param text - Status text, or undefined to clear + */ + setHookStatus(key: string, text: string | undefined): void { + if (text === undefined) { + this.hookStatuses.delete(key); + } else { + this.hookStatuses.set(key, text); + } + } + /** * Set up a file watcher on .git/HEAD to detect branch changes. * Call the provided callback when branch changes. @@ -279,6 +307,18 @@ export class FooterComponent implements Component { const remainder = statsLine.slice(statsLeft.length); // padding + rightSide const dimRemainder = theme.fg("dim", remainder); - return [theme.fg("dim", pwd), dimStatsLeft + dimRemainder]; + const lines = [theme.fg("dim", pwd), dimStatsLeft + dimRemainder]; + + // Add hook statuses on a single line, sorted by key alphabetically + if (this.hookStatuses.size > 0) { + const sortedStatuses = Array.from(this.hookStatuses.entries()) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([, text]) => sanitizeStatusText(text)); + const statusLine = sortedStatuses.join(" "); + // Truncate to terminal width with dim ellipsis for consistency with footer style + lines.push(truncateToWidth(statusLine, width, theme.fg("dim", "..."))); + } + + return lines; } } diff --git a/packages/coding-agent/src/modes/interactive/components/hook-editor.ts b/packages/coding-agent/src/modes/interactive/components/hook-editor.ts new file mode 100644 index 000000000..e54bb3ad5 --- /dev/null +++ b/packages/coding-agent/src/modes/interactive/components/hook-editor.ts @@ -0,0 +1,117 @@ +/** + * Multi-line editor component for hooks. + * Supports Ctrl+G for external editor. + */ + +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { Container, Editor, isCtrlG, isEscape, Spacer, Text, type TUI } from "@mariozechner/pi-tui"; +import { getEditorTheme, theme } from "../theme/theme.js"; +import { DynamicBorder } from "./dynamic-border.js"; + +export class HookEditorComponent extends Container { + private editor: Editor; + private onSubmitCallback: (value: string) => void; + private onCancelCallback: () => void; + private tui: TUI; + + constructor( + tui: TUI, + title: string, + prefill: string | undefined, + onSubmit: (value: string) => void, + onCancel: () => void, + ) { + super(); + + this.tui = tui; + this.onSubmitCallback = onSubmit; + this.onCancelCallback = onCancel; + + // Add top border + this.addChild(new DynamicBorder()); + this.addChild(new Spacer(1)); + + // Add title + this.addChild(new Text(theme.fg("accent", title), 1, 0)); + this.addChild(new Spacer(1)); + + // Create editor + this.editor = new Editor(getEditorTheme()); + if (prefill) { + this.editor.setText(prefill); + } + this.addChild(this.editor); + + this.addChild(new Spacer(1)); + + // Add hint + const hasExternalEditor = !!(process.env.VISUAL || process.env.EDITOR); + const hint = hasExternalEditor + ? "ctrl+enter submit esc cancel ctrl+g external editor" + : "ctrl+enter submit esc cancel"; + this.addChild(new Text(theme.fg("dim", hint), 1, 0)); + + this.addChild(new Spacer(1)); + + // Add bottom border + this.addChild(new DynamicBorder()); + } + + handleInput(keyData: string): void { + // Ctrl+Enter to submit + if (keyData === "\x1b[13;5u" || keyData === "\x1b[27;5;13~") { + this.onSubmitCallback(this.editor.getText()); + return; + } + + // Escape to cancel + if (isEscape(keyData)) { + this.onCancelCallback(); + return; + } + + // Ctrl+G for external editor + if (isCtrlG(keyData)) { + this.openExternalEditor(); + return; + } + + // Forward to editor + this.editor.handleInput(keyData); + } + + private openExternalEditor(): void { + const editorCmd = process.env.VISUAL || process.env.EDITOR; + if (!editorCmd) { + return; + } + + const currentText = this.editor.getText(); + const tmpFile = path.join(os.tmpdir(), `pi-hook-editor-${Date.now()}.md`); + + try { + fs.writeFileSync(tmpFile, currentText, "utf-8"); + this.tui.stop(); + + const [editor, ...editorArgs] = editorCmd.split(" "); + const result = Bun.spawnSync([editor, ...editorArgs, tmpFile], { + stdio: ["inherit", "inherit", "inherit"], + }); + + if (result.exitCode === 0) { + const newContent = fs.readFileSync(tmpFile, "utf-8").replace(/\n$/, ""); + this.editor.setText(newContent); + } + } finally { + try { + fs.unlinkSync(tmpFile); + } catch { + // Ignore cleanup errors + } + this.tui.start(); + this.tui.requestRender(); + } + } +} diff --git a/packages/coding-agent/src/modes/interactive/components/model-selector.ts b/packages/coding-agent/src/modes/interactive/components/model-selector.ts index bbf77f2a8..0d1ad239e 100644 --- a/packages/coding-agent/src/modes/interactive/components/model-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/model-selector.ts @@ -152,6 +152,7 @@ export class ModelSelectorComponent extends Container { this.allModels = models; this.filteredModels = models; + this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, models.length - 1)); } private filterModels(query: string): void { @@ -216,11 +217,13 @@ export class ModelSelectorComponent extends Container { handleInput(keyData: string): void { // Up arrow - wrap to bottom when at top if (isArrowUp(keyData)) { + if (this.filteredModels.length === 0) return; this.selectedIndex = this.selectedIndex === 0 ? this.filteredModels.length - 1 : this.selectedIndex - 1; this.updateList(); } // Down arrow - wrap to top when at bottom else if (isArrowDown(keyData)) { + if (this.filteredModels.length === 0) return; this.selectedIndex = this.selectedIndex === this.filteredModels.length - 1 ? 0 : this.selectedIndex + 1; this.updateList(); } diff --git a/packages/coding-agent/src/modes/interactive/components/tool-execution.ts b/packages/coding-agent/src/modes/interactive/components/tool-execution.ts index 837f94fba..5e77e9747 100644 --- a/packages/coding-agent/src/modes/interactive/components/tool-execution.ts +++ b/packages/coding-agent/src/modes/interactive/components/tool-execution.ts @@ -12,6 +12,7 @@ import { } from "@mariozechner/pi-tui"; import stripAnsi from "strip-ansi"; import type { CustomTool } from "../../../core/custom-tools/types.js"; +import { computeEditDiff, type EditDiffError, type EditDiffResult } from "../../../core/tools/edit-diff.js"; import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize } from "../../../core/tools/truncate.js"; import { sanitizeBinaryOutput } from "../../../utils/shell.js"; import { getLanguageFromPath, highlightCode, theme } from "../theme/theme.js"; @@ -58,11 +59,15 @@ export class ToolExecutionComponent extends Container { private isPartial = true; private customTool?: CustomTool; private ui: TUI; + private cwd: string; private result?: { content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>; isError: boolean; details?: any; }; + // Cached edit diff preview (computed when args arrive, before tool executes) + private editDiffPreview?: EditDiffResult | EditDiffError; + private editDiffArgsKey?: string; // Track which args the preview is for constructor( toolName: string, @@ -70,6 +75,7 @@ export class ToolExecutionComponent extends Container { options: ToolExecutionOptions = {}, customTool: CustomTool | undefined, ui: TUI, + cwd: string = process.cwd(), ) { super(); this.toolName = toolName; @@ -77,6 +83,7 @@ export class ToolExecutionComponent extends Container { this.showImages = options.showImages ?? true; this.customTool = customTool; this.ui = ui; + this.cwd = cwd; this.addChild(new Spacer(1)); @@ -98,6 +105,47 @@ export class ToolExecutionComponent extends Container { this.updateDisplay(); } + /** + * Signal that args are complete (tool is about to execute). + * This triggers diff computation for edit tool. + */ + setArgsComplete(): void { + this.maybeComputeEditDiff(); + } + + /** + * Compute edit diff preview when we have complete args. + * This runs async and updates display when done. + */ + private maybeComputeEditDiff(): void { + if (this.toolName !== "edit") return; + + const path = this.args?.path; + const oldText = this.args?.oldText; + const newText = this.args?.newText; + + // Need all three params to compute diff + if (!path || oldText === undefined || newText === undefined) return; + + // Create a key to track which args this computation is for + const argsKey = JSON.stringify({ path, oldText, newText }); + + // Skip if we already computed for these exact args + if (this.editDiffArgsKey === argsKey) return; + + this.editDiffArgsKey = argsKey; + + // Compute diff async + computeEditDiff(path, oldText, newText, this.cwd).then((result) => { + // Only update if args haven't changed since we started + if (this.editDiffArgsKey === argsKey) { + this.editDiffPreview = result; + this.updateDisplay(); + this.ui.requestRender(); + } + }); + } + updateResult( result: { content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>; @@ -393,9 +441,6 @@ export class ToolExecutionComponent extends Container { theme.fg("toolTitle", theme.bold("write")) + " " + (path ? theme.fg("accent", path) : theme.fg("toolOutput", "...")); - if (totalLines > 10) { - text += ` (${totalLines} lines)`; - } if (fileContent) { const maxLines = this.expanded ? lines.length : 10; @@ -408,29 +453,38 @@ export class ToolExecutionComponent extends Container { .map((line: string) => (lang ? replaceTabs(line) : theme.fg("toolOutput", replaceTabs(line)))) .join("\n"); if (remaining > 0) { - text += theme.fg("toolOutput", `\n... (${remaining} more lines)`); + text += theme.fg("toolOutput", `\n... (${remaining} more lines, ${totalLines} total)`); } } } else if (this.toolName === "edit") { const rawPath = this.args?.file_path || this.args?.path || ""; const path = shortenPath(rawPath); - // Build path display, appending :line if we have a successful result with line info + // Build path display, appending :line if we have diff info let pathDisplay = path ? theme.fg("accent", path) : theme.fg("toolOutput", "..."); - if (this.result && !this.result.isError && this.result.details?.firstChangedLine) { - pathDisplay += theme.fg("warning", `:${this.result.details.firstChangedLine}`); + const firstChangedLine = + (this.editDiffPreview && "firstChangedLine" in this.editDiffPreview + ? this.editDiffPreview.firstChangedLine + : undefined) || + (this.result && !this.result.isError ? this.result.details?.firstChangedLine : undefined); + if (firstChangedLine) { + pathDisplay += theme.fg("warning", `:${firstChangedLine}`); } text = `${theme.fg("toolTitle", theme.bold("edit"))} ${pathDisplay}`; - if (this.result) { - if (this.result.isError) { - const errorText = this.getTextOutput(); - if (errorText) { - text += `\n\n${theme.fg("error", errorText)}`; - } - } else if (this.result.details?.diff) { - text += `\n\n${renderDiff(this.result.details.diff, { filePath: rawPath })}`; + if (this.result?.isError) { + // Show error from result + const errorText = this.getTextOutput(); + if (errorText) { + text += `\n\n${theme.fg("error", errorText)}`; + } + } else if (this.editDiffPreview) { + // Use cached diff preview (works both before and after execution) + if ("error" in this.editDiffPreview) { + text += `\n\n${theme.fg("error", this.editDiffPreview.error)}`; + } else if (this.editDiffPreview.diff) { + text += `\n\n${renderDiff(this.editDiffPreview.diff, { filePath: rawPath })}`; } } } else if (this.toolName === "ls") { diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index d06baa4b9..03e4fc50a 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -37,11 +37,13 @@ import { copyToClipboard } from "../../utils/clipboard.js"; import { ArminComponent } from "./components/armin.js"; import { AssistantMessageComponent } from "./components/assistant-message.js"; import { BashExecutionComponent } from "./components/bash-execution.js"; +import { BorderedLoader } from "./components/bordered-loader.js"; import { BranchSummaryMessageComponent } from "./components/branch-summary-message.js"; import { CompactionSummaryMessageComponent } from "./components/compaction-summary-message.js"; import { CustomEditor } from "./components/custom-editor.js"; import { DynamicBorder } from "./components/dynamic-border.js"; import { FooterComponent } from "./components/footer.js"; +import { HookEditorComponent } from "./components/hook-editor.js"; import { HookInputComponent } from "./components/hook-input.js"; import { HookMessageComponent } from "./components/hook-message.js"; import { HookSelectorComponent } from "./components/hook-selector.js"; @@ -130,6 +132,7 @@ export class InteractiveMode { // Hook UI state private hookSelector: HookSelectorComponent | undefined = undefined; private hookInput: HookInputComponent | undefined = undefined; + private hookEditor: HookEditorComponent | undefined = undefined; // Custom tools for custom rendering private customTools: Map; @@ -172,6 +175,7 @@ export class InteractiveMode { { name: "settings", description: "Open settings menu" }, { name: "model", description: "Select model (opens selector UI)" }, { name: "export", description: "Export session to HTML file" }, + { name: "share", description: "Share session as a secret GitHub gist" }, { name: "copy", description: "Copy last agent message to clipboard" }, { name: "session", description: "Show session info and stats" }, { name: "changelog", description: "Show changelog entries" }, @@ -368,9 +372,14 @@ export class InteractiveMode { confirm: (title, message) => this.showHookConfirm(title, message), input: (title, placeholder) => this.showHookInput(title, placeholder), notify: (message, type) => this.showHookNotify(message, type), + setStatus: (key, text) => this.setHookStatus(key, text), custom: (factory) => this.showHookCustom(factory), setEditorText: (text) => this.editor.setText(text), getEditorText: () => this.editor.getText(), + editor: (title, prefill) => this.showHookEditor(title, prefill), + get theme() { + return theme; + }, }; this.setToolUIContext(uiContext, true); @@ -405,6 +414,74 @@ export class InteractiveMode { appendEntryHandler: (customType, data) => { this.sessionManager.appendCustomEntry(customType, data); }, + newSessionHandler: async (options) => { + // Stop any loading animation + if (this.loadingAnimation) { + this.loadingAnimation.stop(); + this.loadingAnimation = undefined; + } + this.statusContainer.clear(); + + // Create new session + const success = await this.session.newSession({ parentSession: options?.parentSession }); + if (!success) { + return { cancelled: true }; + } + + // Call setup callback if provided + if (options?.setup) { + await options.setup(this.sessionManager); + } + + // Clear UI state + this.chatContainer.clear(); + this.pendingMessagesContainer.clear(); + this.streamingComponent = undefined; + this.streamingMessage = undefined; + this.pendingTools.clear(); + + this.chatContainer.addChild(new Spacer(1)); + this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1)); + this.ui.requestRender(); + + return { cancelled: false }; + }, + branchHandler: async (entryId) => { + const result = await this.session.branch(entryId); + if (result.cancelled) { + return { cancelled: true }; + } + + // Update UI + this.chatContainer.clear(); + this.renderInitialMessages(); + this.editor.setText(result.selectedText); + this.showStatus("Branched to new session"); + + return { cancelled: false }; + }, + navigateTreeHandler: async (targetId, options) => { + const result = await this.session.navigateTree(targetId, { summarize: options?.summarize }); + if (result.cancelled) { + return { cancelled: true }; + } + + // Update UI + this.chatContainer.clear(); + this.renderInitialMessages(); + if (result.editorText) { + this.editor.setText(result.editorText); + } + this.showStatus("Navigated to selected point"); + + return { cancelled: false }; + }, + isIdle: () => !this.session.isStreaming, + waitForIdle: () => this.session.agent.waitForIdle(), + abort: () => { + this.session.abort(); + }, + hasQueuedMessages: () => this.session.queuedMessageCount > 0, uiContext, hasUI: true, }); @@ -439,6 +516,11 @@ export class InteractiveMode { sessionManager: this.session.sessionManager, modelRegistry: this.session.modelRegistry, model: this.session.model, + isIdle: () => !this.session.isStreaming, + hasQueuedMessages: () => this.session.queuedMessageCount > 0, + abort: () => { + this.session.abort(); + }, }); } catch (err) { this.showToolError(tool.name, err instanceof Error ? err.message : String(err)); @@ -456,6 +538,14 @@ export class InteractiveMode { this.ui.requestRender(); } + /** + * Set hook status text in the footer. + */ + private setHookStatus(key: string, text: string | undefined): void { + this.footer.setHookStatus(key, text); + this.ui.requestRender(); + } + /** * Show a selector for hooks. */ @@ -536,6 +626,43 @@ export class InteractiveMode { this.ui.requestRender(); } + /** + * Show a multi-line editor for hooks (with Ctrl+G support). + */ + private showHookEditor(title: string, prefill?: string): Promise { + return new Promise((resolve) => { + this.hookEditor = new HookEditorComponent( + this.ui, + title, + prefill, + (value) => { + this.hideHookEditor(); + resolve(value); + }, + () => { + this.hideHookEditor(); + resolve(undefined); + }, + ); + + this.editorContainer.clear(); + this.editorContainer.addChild(this.hookEditor); + this.ui.setFocus(this.hookEditor); + this.ui.requestRender(); + }); + } + + /** + * Hide the hook editor. + */ + private hideHookEditor(): void { + this.editorContainer.clear(); + this.editorContainer.addChild(this.editor); + this.hookEditor = undefined; + this.ui.setFocus(this.editor); + this.ui.requestRender(); + } + /** * Show a notification for hooks. */ @@ -674,8 +801,13 @@ export class InteractiveMode { this.editor.setText(""); return; } + if (text === "/share") { + await this.handleShareCommand(); + this.editor.setText(""); + return; + } if (text === "/copy") { - this.handleCopyCommand(); + await this.handleCopyCommand(); this.editor.setText(""); return; } @@ -901,6 +1033,11 @@ export class InteractiveMode { }); } this.pendingTools.clear(); + } else { + // Args are now complete - trigger diff computation for edit tools + for (const [, component] of this.pendingTools.entries()) { + component.setArgsComplete(); + } } this.streamingComponent = undefined; this.streamingMessage = undefined; @@ -1399,8 +1536,8 @@ export class InteractiveMode { stderr: "inherit", }); - // On successful exit (status 0), replace editor content - if (result.status === 0) { + // On successful exit (exitCode 0), replace editor content + if (result.exitCode === 0) { const newContent = fs.readFileSync(tmpFile, "utf-8").replace(/\n$/, ""); this.editor.setText(newContent); } @@ -1910,7 +2047,123 @@ export class InteractiveMode { } } - private handleCopyCommand(): void { + private async handleShareCommand(): Promise { + // Check if gh is available and logged in + try { + const authResult = Bun.spawnSync(["gh", "auth", "status"]); + if (authResult.exitCode !== 0) { + this.showError("GitHub CLI is not logged in. Run 'gh auth login' first."); + return; + } + } catch { + this.showError("GitHub CLI (gh) is not installed. Install it from https://cli.github.com/"); + return; + } + + // Export to a temp file + const tmpFile = path.join(os.tmpdir(), "session.html"); + try { + this.session.exportToHtml(tmpFile); + } catch (error: unknown) { + this.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`); + return; + } + + // Show cancellable loader, replacing the editor + const loader = new BorderedLoader(this.ui, theme, "Creating gist..."); + this.editorContainer.clear(); + this.editorContainer.addChild(loader); + this.ui.setFocus(loader); + this.ui.requestRender(); + + const restoreEditor = () => { + loader.dispose(); + this.editorContainer.clear(); + this.editorContainer.addChild(this.editor); + this.ui.setFocus(this.editor); + try { + fs.unlinkSync(tmpFile); + } catch { + // Ignore cleanup errors + } + }; + + // Create a secret gist asynchronously + let proc: ReturnType | null = null; + + loader.onAbort = () => { + proc?.kill(); + restoreEditor(); + this.showStatus("Share cancelled"); + }; + + try { + const result = await new Promise<{ stdout: string; stderr: string; code: number | null }>((resolve) => { + proc = Bun.spawn(["gh", "gist", "create", "--public=false", tmpFile], { + stdout: "pipe", + stderr: "pipe", + }); + let stdout = ""; + let stderr = ""; + + const stdoutReader = (proc.stdout as ReadableStream).getReader(); + const stderrReader = (proc.stderr as ReadableStream).getReader(); + const decoder = new TextDecoder(); + + (async () => { + try { + while (true) { + const { done, value } = await stdoutReader.read(); + if (done) break; + stdout += decoder.decode(value); + } + } catch {} + })(); + + (async () => { + try { + while (true) { + const { done, value } = await stderrReader.read(); + if (done) break; + stderr += decoder.decode(value); + } + } catch {} + })(); + + proc.exited.then((code) => resolve({ stdout, stderr, code })); + }); + + if (loader.signal.aborted) return; + + restoreEditor(); + + if (result.code !== 0) { + const errorMsg = result.stderr?.trim() || "Unknown error"; + this.showError(`Failed to create gist: ${errorMsg}`); + return; + } + + // Extract gist ID from the URL returned by gh + // gh returns something like: https://gist.github.com/username/GIST_ID + const gistUrl = result.stdout?.trim(); + const gistId = gistUrl?.split("/").pop(); + if (!gistId) { + this.showError("Failed to parse gist ID from gh output"); + return; + } + + // Create the preview URL + const previewUrl = `https://shittycodingagent.ai/session?${gistId}`; + this.showStatus(`Share URL: ${previewUrl}\nGist: ${gistUrl}`); + } catch (error: unknown) { + if (!loader.signal.aborted) { + restoreEditor(); + this.showError(`Failed to create gist: ${error instanceof Error ? error.message : "Unknown error"}`); + } + } + } + + private async handleCopyCommand(): Promise { const text = this.session.getLastAssistantText(); if (!text) { this.showError("No agent messages to copy yet."); @@ -1918,7 +2171,7 @@ export class InteractiveMode { } try { - copyToClipboard(text); + await copyToClipboard(text); this.showStatus("Copied last agent message to clipboard"); } catch (error) { this.showError(error instanceof Error ? error.message : String(error)); @@ -2031,8 +2284,8 @@ export class InteractiveMode { } this.statusContainer.clear(); - // Reset via session (emits hook and tool session events) - await this.session.reset(); + // New session via session (emits hook and tool session events) + await this.session.newSession(); // Clear UI state this.chatContainer.clear(); diff --git a/packages/coding-agent/src/modes/interactive/theme/dark.json b/packages/coding-agent/src/modes/interactive/theme/dark.json index 069e32fd6..ba2bd8937 100644 --- a/packages/coding-agent/src/modes/interactive/theme/dark.json +++ b/packages/coding-agent/src/modes/interactive/theme/dark.json @@ -2,21 +2,21 @@ "$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/theme-schema.json", "name": "dark", "vars": { - "cyan": "#00d7ff", - "blue": "#5f87ff", - "green": "#b5bd68", - "red": "#cc6666", - "yellow": "#ffff00", - "gray": "#808080", - "dimGray": "#666666", - "darkGray": "#505050", - "accent": "#8abeb7", - "selectedBg": "#3a3a4a", - "userMsgBg": "#343541", - "toolPendingBg": "#282832", - "toolSuccessBg": "#283228", - "toolErrorBg": "#3c2828", - "customMsgBg": "#2d2838" + "cyan": "#0088fa", + "blue": "#178fb9", + "green": "#89d281", + "red": "#fc3a4b", + "yellow": "#e4c00f", + "gray": "#777d88", + "dimGray": "#5f6673", + "darkGray": "#3d424a", + "accent": "#febc38", + "selectedBg": "#31363f", + "userMsgBg": "#221d1a", + "toolPendingBg": "#1d2129", + "toolSuccessBg": "#161a1f", + "toolErrorBg": "#291d1d", + "customMsgBg": "#2a2530" }, "colors": { "accent": "accent", @@ -36,28 +36,30 @@ "userMessageText": "", "customMessageBg": "customMsgBg", "customMessageText": "", - "customMessageLabel": "#9575cd", + "customMessageLabel": "#b281d6", "toolPendingBg": "toolPendingBg", "toolSuccessBg": "toolSuccessBg", "toolErrorBg": "toolErrorBg", "toolTitle": "", "toolOutput": "gray", - "mdHeading": "#f0c674", - "mdLink": "#81a2be", + "mdHeading": "#febc38", + "mdLink": "#0088fa", "mdLinkUrl": "dimGray", - "mdCode": "accent", - "mdCodeBlock": "green", - "mdCodeBlockBorder": "gray", + "mdCode": "#e5c1ff", + "mdCodeBlock": "#9CDCFE", + "mdCodeBlockBorder": "darkGray", "mdQuote": "gray", - "mdQuoteBorder": "gray", - "mdHr": "gray", + "mdQuoteBorder": "darkGray", + "mdHr": "darkGray", "mdListBullet": "accent", "toolDiffAdded": "green", "toolDiffRemoved": "red", "toolDiffContext": "gray", + "link": "#0088fa", + "syntaxComment": "#6A9955", "syntaxKeyword": "#569CD6", "syntaxFunction": "#DCDCAA", @@ -69,12 +71,17 @@ "syntaxPunctuation": "#D4D4D4", "thinkingOff": "darkGray", - "thinkingMinimal": "#6e6e6e", - "thinkingLow": "#5f87af", - "thinkingMedium": "#81a2be", - "thinkingHigh": "#b294bb", - "thinkingXhigh": "#d183e8", + "thinkingMinimal": "dimGray", + "thinkingLow": "#178fb9", + "thinkingMedium": "#0088fa", + "thinkingHigh": "#b281d6", + "thinkingXhigh": "#e5c1ff", - "bashMode": "green" + "bashMode": "cyan" + }, + "export": { + "pageBg": "#18181e", + "cardBg": "#1e1e24", + "infoBg": "#3c3728" } } diff --git a/packages/coding-agent/src/modes/interactive/theme/light.json b/packages/coding-agent/src/modes/interactive/theme/light.json index 138af303b..9154b161f 100644 --- a/packages/coding-agent/src/modes/interactive/theme/light.json +++ b/packages/coding-agent/src/modes/interactive/theme/light.json @@ -75,5 +75,10 @@ "thinkingXhigh": "#8b008b", "bashMode": "green" + }, + "export": { + "pageBg": "#f8f8f8", + "cardBg": "#ffffff", + "infoBg": "#fffae6" } } diff --git a/packages/coding-agent/src/modes/interactive/theme/theme-schema.json b/packages/coding-agent/src/modes/interactive/theme/theme-schema.json index f561ecb1f..820813e03 100644 --- a/packages/coding-agent/src/modes/interactive/theme/theme-schema.json +++ b/packages/coding-agent/src/modes/interactive/theme/theme-schema.json @@ -267,6 +267,25 @@ } }, "additionalProperties": false + }, + "export": { + "type": "object", + "description": "Optional colors for HTML export (defaults derived from userMessageBg if not specified)", + "properties": { + "pageBg": { + "$ref": "#/$defs/colorValue", + "description": "Page background color" + }, + "cardBg": { + "$ref": "#/$defs/colorValue", + "description": "Card/container background color" + }, + "infoBg": { + "$ref": "#/$defs/colorValue", + "description": "Info sections background (system prompt, notices)" + } + }, + "additionalProperties": false } }, "additionalProperties": false, diff --git a/packages/coding-agent/src/modes/interactive/theme/theme.ts b/packages/coding-agent/src/modes/interactive/theme/theme.ts index 0121e199e..524dc13d6 100644 --- a/packages/coding-agent/src/modes/interactive/theme/theme.ts +++ b/packages/coding-agent/src/modes/interactive/theme/theme.ts @@ -82,10 +82,18 @@ const ThemeJsonSchema = Type.Object({ // Bash Mode (1 color) bashMode: ColorValueSchema, }), + export: Type.Optional( + Type.Object({ + pageBg: Type.Optional(ColorValueSchema), + cardBg: Type.Optional(ColorValueSchema), + infoBg: Type.Optional(ColorValueSchema), + }), + ), }); type ThemeJson = Static; +// @ts-expect-error TypeBox CJS/ESM type mismatch const validateThemeJson = TypeCompiler.Compile(ThemeJsonSchema); export type ThemeColor = @@ -652,6 +660,129 @@ 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 { + 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 = {}; + 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"; +} + +/** + * Get explicit export colors from theme JSON, if specified. + * Returns undefined for each color that isn't explicitly set. + */ +export function getThemeExportColors(themeName?: string): { + pageBg?: string; + cardBg?: string; + infoBg?: string; +} { + const name = themeName ?? getDefaultTheme(); + try { + const themeJson = loadThemeJson(name); + const exportSection = themeJson.export; + if (!exportSection) return {}; + + const vars = themeJson.vars ?? {}; + const resolve = (value: string | number | undefined): string | undefined => { + if (value === undefined) return undefined; + if (typeof value === "number") return ansi256ToHex(value); + if (value.startsWith("$")) { + const resolved = vars[value]; + if (resolved === undefined) return undefined; + if (typeof resolved === "number") return ansi256ToHex(resolved); + return resolved; + } + return value; + }; + + return { + pageBg: resolve(exportSection.pageBg), + cardBg: resolve(exportSection.cardBg), + infoBg: resolve(exportSection.infoBg), + }; + } catch { + return {}; + } +} + // ============================================================================ // TUI Helpers // ============================================================================ diff --git a/packages/coding-agent/src/modes/print-mode.ts b/packages/coding-agent/src/modes/print-mode.ts index b0aec7fad..04ff4f363 100644 --- a/packages/coding-agent/src/modes/print-mode.ts +++ b/packages/coding-agent/src/modes/print-mode.ts @@ -63,6 +63,11 @@ export async function runPrintMode( sessionManager: session.sessionManager, modelRegistry: session.modelRegistry, model: session.model, + isIdle: () => !session.isStreaming, + hasQueuedMessages: () => session.queuedMessageCount > 0, + abort: () => { + session.abort(); + }, }, ); } catch (_err) { diff --git a/packages/coding-agent/src/modes/rpc/rpc-client.ts b/packages/coding-agent/src/modes/rpc/rpc-client.ts index 65468779c..a2f9f2aec 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-client.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-client.ts @@ -92,7 +92,7 @@ export class RpcClient { // Collect stderr for debugging (async () => { - const reader = this.process!.stderr.getReader(); + const reader = (this.process!.stderr as ReadableStream).getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); @@ -102,10 +102,10 @@ export class RpcClient { })(); // Set up line reader for stdout - const textStream = this.process.stdout.pipeThrough(new TextDecoderStream()); + const textStream = (this.process.stdout as ReadableStream).pipeThrough(new TextDecoderStream()); this.lineReader = textStream .pipeThrough( - new TransformStream({ + new TransformStream({ transform(chunk, controller) { const lines = chunk.split("\n"); for (const line of lines) { @@ -116,7 +116,7 @@ export class RpcClient { }, }), ) - .getReader(); + .getReader() as ReadableStreamDefaultReader; // Process lines in background (async () => { @@ -224,11 +224,12 @@ export class RpcClient { } /** - * Reset session (clear all messages). - * @returns Object with `cancelled: true` if a hook cancelled the reset + * Start a new session, optionally with parent tracking. + * @param parentSession - Optional parent session path for lineage tracking + * @returns Object with `cancelled: true` if a hook cancelled the new session */ - async reset(): Promise<{ cancelled: boolean }> { - const response = await this.send({ type: "reset" }); + async newSession(parentSession?: string): Promise<{ cancelled: boolean }> { + const response = await this.send({ type: "new_session", parentSession }); return this.getData(response); } @@ -498,7 +499,7 @@ export class RpcClient { }, }); - const writer = this.process!.stdin.getWriter(); + const writer = (this.process!.stdin as unknown as WritableStream).getWriter(); writer.write(new TextEncoder().encode(`${JSON.stringify(fullCommand)}\n`)); writer.releaseLock(); }); diff --git a/packages/coding-agent/src/modes/rpc/rpc-mode.ts b/packages/coding-agent/src/modes/rpc/rpc-mode.ts index e398e2a67..a8f061636 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-mode.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-mode.ts @@ -13,6 +13,7 @@ import type { AgentSession } from "../../core/agent-session.js"; import type { HookUIContext } from "../../core/hooks/index.js"; +import { theme } from "../interactive/theme/theme.js"; import type { RpcCommand, RpcHookUIRequest, RpcHookUIResponse, RpcResponse, RpcSessionState } from "./rpc-types.js"; // Re-export types for consumers @@ -117,6 +118,17 @@ export async function runRpcMode(session: AgentSession): Promise { } as RpcHookUIRequest); }, + setStatus(key: string, text: string | undefined): void { + // Fire and forget - no response needed + output({ + type: "hook_ui_request", + id: crypto.randomUUID(), + method: "setStatus", + statusKey: key, + statusText: text, + } as RpcHookUIRequest); + }, + async custom() { // Custom UI not supported in RPC mode return undefined as never; @@ -137,6 +149,29 @@ export async function runRpcMode(session: AgentSession): Promise { // Host should track editor state locally if needed return ""; }, + + async editor(title: string, prefill?: string): Promise { + const id = crypto.randomUUID(); + return new Promise((resolve, reject) => { + pendingHookRequests.set(id, { + resolve: (response: RpcHookUIResponse) => { + if ("cancelled" in response && response.cancelled) { + resolve(undefined); + } else if ("value" in response) { + resolve(response.value); + } else { + resolve(undefined); + } + }, + reject, + }); + output({ type: "hook_ui_request", id, method: "editor", title, prefill } as RpcHookUIRequest); + }); + }, + + get theme() { + return theme; + }, }); // Set up hooks with RPC-based UI context @@ -178,6 +213,11 @@ export async function runRpcMode(session: AgentSession): Promise { sessionManager: session.sessionManager, modelRegistry: session.modelRegistry, model: session.model, + isIdle: () => !session.isStreaming, + hasQueuedMessages: () => session.queuedMessageCount > 0, + abort: () => { + session.abort(); + }, }, ); } catch (_err) { @@ -221,9 +261,10 @@ export async function runRpcMode(session: AgentSession): Promise { return success(id, "abort"); } - case "reset": { - const cancelled = !(await session.reset()); - return success(id, "reset", { cancelled }); + case "new_session": { + const options = command.parentSession ? { parentSession: command.parentSession } : undefined; + const cancelled = !(await session.newSession(options)); + return success(id, "new_session", { cancelled }); } // ================================================================= @@ -391,7 +432,7 @@ export async function runRpcMode(session: AgentSession): Promise { }; // Listen for JSON input - use Bun's ReadableStream - const stdinReader = process.stdin + const stdinReader = (Bun.stdin.stream() as ReadableStream) .pipeThrough(new TextDecoderStream()) .pipeThrough( new TransformStream({ diff --git a/packages/coding-agent/src/modes/rpc/rpc-types.ts b/packages/coding-agent/src/modes/rpc/rpc-types.ts index 8ab212473..a58b86c96 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-types.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-types.ts @@ -20,7 +20,7 @@ export type RpcCommand = | { id?: string; type: "prompt"; message: string; images?: ImageContent[] } | { id?: string; type: "queue_message"; message: string } | { id?: string; type: "abort" } - | { id?: string; type: "reset" } + | { id?: string; type: "new_session"; parentSession?: string } // State | { id?: string; type: "get_state" } @@ -87,7 +87,7 @@ export type RpcResponse = | { id?: string; type: "response"; command: "prompt"; success: true } | { id?: string; type: "response"; command: "queue_message"; success: true } | { id?: string; type: "response"; command: "abort"; success: true } - | { id?: string; type: "response"; command: "reset"; success: true; data: { cancelled: boolean } } + | { id?: string; type: "response"; command: "new_session"; success: true; data: { cancelled: boolean } } // State | { id?: string; type: "response"; command: "get_state"; success: true; data: RpcSessionState } @@ -175,6 +175,7 @@ export type RpcHookUIRequest = | { type: "hook_ui_request"; id: string; method: "select"; title: string; options: string[] } | { type: "hook_ui_request"; id: string; method: "confirm"; title: string; message: string } | { type: "hook_ui_request"; id: string; method: "input"; title: string; placeholder?: string } + | { type: "hook_ui_request"; id: string; method: "editor"; title: string; prefill?: string } | { type: "hook_ui_request"; id: string; @@ -182,6 +183,7 @@ export type RpcHookUIRequest = message: string; notifyType?: "info" | "warning" | "error"; } + | { type: "hook_ui_request"; id: string; method: "setStatus"; statusKey: string; statusText: string | undefined } | { type: "hook_ui_request"; id: string; method: "set_editor_text"; text: string }; // ============================================================================ diff --git a/packages/coding-agent/src/utils/clipboard.ts b/packages/coding-agent/src/utils/clipboard.ts index 5baf954ae..7a3389dc4 100644 --- a/packages/coding-agent/src/utils/clipboard.ts +++ b/packages/coding-agent/src/utils/clipboard.ts @@ -1,21 +1,40 @@ -import { execSync } from "child_process"; import { platform } from "os"; -export function copyToClipboard(text: string): void { +async function spawnWithTimeout(cmd: string[], input: string, timeoutMs: number): Promise { + const proc = Bun.spawn(cmd, { stdin: "pipe" }); + + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => reject(new Error("Clipboard operation timed out")), timeoutMs); + }); + + try { + proc.stdin.write(input); + proc.stdin.end(); + await Promise.race([proc.exited, timeoutPromise]); + + if (proc.exitCode !== 0) { + throw new Error(`Command failed with exit code ${proc.exitCode}`); + } + } finally { + proc.kill(); + } +} + +export async function copyToClipboard(text: string): Promise { const p = platform(); - const options = { input: text, timeout: 5000 }; + const timeout = 5000; try { if (p === "darwin") { - execSync("pbcopy", options); + await spawnWithTimeout(["pbcopy"], text, timeout); } else if (p === "win32") { - execSync("clip", options); + await spawnWithTimeout(["clip"], text, timeout); } else { // Linux - try xclip first, fall back to xsel try { - execSync("xclip -selection clipboard", options); + await spawnWithTimeout(["xclip", "-selection", "clipboard"], text, timeout); } catch { - execSync("xsel --clipboard --input", options); + await spawnWithTimeout(["xsel", "--clipboard", "--input"], text, timeout); } } } catch (error) { diff --git a/packages/coding-agent/src/utils/tools-manager.ts b/packages/coding-agent/src/utils/tools-manager.ts index 18f5b5c54..c9b0f68f4 100644 --- a/packages/coding-agent/src/utils/tools-manager.ts +++ b/packages/coding-agent/src/utils/tools-manager.ts @@ -120,7 +120,7 @@ async function downloadFile(url: string, dest: string): Promise { fileStream.write(Buffer.from(value)); } fileStream.end(); - await new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { fileStream.on("finish", resolve); fileStream.on("error", reject); }); diff --git a/packages/coding-agent/test/compaction-hooks.test.ts b/packages/coding-agent/test/compaction-hooks.test.ts index a4fd1eea3..5d5a51309 100644 --- a/packages/coding-agent/test/compaction-hooks.test.ts +++ b/packages/coding-agent/test/compaction-hooks.test.ts @@ -21,6 +21,7 @@ import { ModelRegistry } from "../src/core/model-registry.js"; import { SessionManager } from "../src/core/session-manager.js"; import { SettingsManager } from "../src/core/settings-manager.js"; import { codingTools } from "../src/core/tools/index.js"; +import { theme } from "../src/modes/interactive/theme/theme.js"; const API_KEY = process.env.ANTHROPIC_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY; @@ -108,9 +109,14 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => { confirm: async () => false, input: async () => undefined, notify: () => {}, + setStatus: () => {}, custom: async () => undefined as never, setEditorText: () => {}, getEditorText: () => "", + editor: async () => undefined, + get theme() { + return theme; + }, }, hasUI: false, }); diff --git a/packages/coding-agent/test/rpc-example.ts b/packages/coding-agent/test/rpc-example.ts index 79d33320e..bb26ffe13 100644 --- a/packages/coding-agent/test/rpc-example.ts +++ b/packages/coding-agent/test/rpc-example.ts @@ -1,9 +1,6 @@ -import { dirname, join } from "node:path"; +import { join } from "node:path"; import * as readline from "node:readline"; -import { fileURLToPath } from "node:url"; -import { RpcClient } from "../src/modes/rpc/rpc-client.ts"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); +import { RpcClient } from "../src/modes/rpc/rpc-client.js"; /** * Interactive example of using coding-agent via RpcClient. @@ -12,7 +9,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); async function main() { const client = new RpcClient({ - cliPath: join(__dirname, "../src/cli.ts"), + cliPath: join(import.meta.dir, "../src/cli.ts"), provider: "anthropic", model: "claude-sonnet-4-20250514", args: ["--no-session"], diff --git a/packages/coding-agent/test/rpc.test.ts b/packages/coding-agent/test/rpc.test.ts index e0d6edac0..c8e032ae3 100644 --- a/packages/coding-agent/test/rpc.test.ts +++ b/packages/coding-agent/test/rpc.test.ts @@ -1,13 +1,10 @@ import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; +import { join } from "node:path"; import type { AgentEvent } from "@mariozechner/pi-agent-core"; import { afterEach, beforeEach, describe, expect, test } from "vitest"; import { RpcClient } from "../src/modes/rpc/rpc-client.js"; -const __dirname = dirname(fileURLToPath(import.meta.url)); - /** * RPC mode tests. */ @@ -18,8 +15,8 @@ describe.skipIf(!process.env.ANTHROPIC_API_KEY && !process.env.ANTHROPIC_OAUTH_T beforeEach(() => { sessionDir = join(tmpdir(), `pi-rpc-test-${Date.now()}`); client = new RpcClient({ - cliPath: join(__dirname, "..", "dist", "cli.js"), - cwd: join(__dirname, ".."), + cliPath: join(import.meta.dir, "..", "dist", "cli.js"), + cwd: join(import.meta.dir, ".."), env: { PI_CODING_AGENT_DIR: sessionDir }, provider: "anthropic", model: "claude-sonnet-4-5", @@ -237,7 +234,7 @@ describe.skipIf(!process.env.ANTHROPIC_API_KEY && !process.env.ANTHROPIC_OAUTH_T expect(stats.assistantMessages).toBeGreaterThanOrEqual(1); }, 90000); - test("should reset session", async () => { + test("should create new session", async () => { await client.start(); // Send a prompt @@ -247,8 +244,8 @@ describe.skipIf(!process.env.ANTHROPIC_API_KEY && !process.env.ANTHROPIC_OAUTH_T let state = await client.getState(); expect(state.messageCount).toBeGreaterThan(0); - // Reset - await client.reset(); + // New session + await client.newSession(); // Verify messages cleared state = await client.getState(); diff --git a/packages/coding-agent/test/streaming-render-debug.ts b/packages/coding-agent/test/streaming-render-debug.ts index 05f72e5c1..ad81edfa5 100644 --- a/packages/coding-agent/test/streaming-render-debug.ts +++ b/packages/coding-agent/test/streaming-render-debug.ts @@ -7,21 +7,17 @@ import type { AssistantMessage } from "@mariozechner/pi-ai"; import { ProcessTerminal, TUI } from "@mariozechner/pi-tui"; import { readFileSync } from "fs"; -import { dirname, join } from "path"; -import { fileURLToPath } from "url"; +import { join } from "path"; import { AssistantMessageComponent } from "../src/modes/interactive/components/assistant-message.js"; import { initTheme } from "../src/modes/interactive/theme/theme.js"; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - // Initialize dark theme with full color support process.env.COLORTERM = "truecolor"; initTheme("dark"); // Load the real fixture that caused the bug const fixtureMessage: AssistantMessage = JSON.parse( - readFileSync(join(__dirname, "fixtures/assistant-message-with-thinking-code.json"), "utf-8"), + readFileSync(join(import.meta.dir, "fixtures/assistant-message-with-thinking-code.json"), "utf-8"), ); // Extract thinking and text content diff --git a/packages/coding-agent/test/tools.test.ts b/packages/coding-agent/test/tools.test.ts index 8f18b9aa9..d4601eb9d 100644 --- a/packages/coding-agent/test/tools.test.ts +++ b/packages/coding-agent/test/tools.test.ts @@ -433,4 +433,18 @@ describe("edit tool CRLF handling", () => { }), ).rejects.toThrow(/Found 2 occurrences/); }); + + it("should preserve UTF-8 BOM after edit", async () => { + const testFile = join(testDir, "bom-test.txt"); + writeFileSync(testFile, "\uFEFFfirst\r\nsecond\r\nthird\r\n"); + + await editTool.execute("test-bom", { + path: testFile, + oldText: "second\n", + newText: "REPLACED\n", + }); + + const content = readFileSync(testFile, "utf-8"); + expect(content).toBe("\uFEFFfirst\r\nREPLACED\r\nthird\r\n"); + }); }); diff --git a/packages/mom/CHANGELOG.md b/packages/mom/CHANGELOG.md index 996de8e64..7378f900e 100644 --- a/packages/mom/CHANGELOG.md +++ b/packages/mom/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +## [0.31.1] - 2026-01-02 + +## [0.31.0] - 2026-01-02 + ### Breaking Changes - `AgentTool` import moved from `@mariozechner/pi-ai` to `@mariozechner/pi-agent-core` diff --git a/packages/mom/src/context.ts b/packages/mom/src/context.ts index 69018d996..9f7f09a3b 100644 --- a/packages/mom/src/context.ts +++ b/packages/mom/src/context.ts @@ -10,7 +10,6 @@ * - MomSettingsManager: Simple settings for mom (compaction, retry, model preferences) */ -import { randomBytes } from "node:crypto"; import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import type { AgentMessage } from "@mariozechner/pi-agent-core"; @@ -27,10 +26,10 @@ import { } from "@mariozechner/pi-coding-agent"; function uuidv4(): string { - const bytes = randomBytes(16); + const bytes = crypto.getRandomValues(new Uint8Array(16)); bytes[6] = (bytes[6] & 0x0f) | 0x40; bytes[8] = (bytes[8] & 0x3f) | 0x80; - const hex = bytes.toString("hex"); + const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(""); return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`; } diff --git a/packages/mom/src/sandbox.ts b/packages/mom/src/sandbox.ts index e3466a574..944291ca9 100644 --- a/packages/mom/src/sandbox.ts +++ b/packages/mom/src/sandbox.ts @@ -145,7 +145,10 @@ class HostExecutor implements Executor { return result; }; - const [stdout, stderr] = await Promise.all([readStream(proc.stdout), readStream(proc.stderr)]); + const [stdout, stderr] = await Promise.all([ + readStream(proc.stdout as ReadableStream), + readStream(proc.stderr as ReadableStream), + ]); const code = await proc.exited; diff --git a/packages/mom/src/tools/bash.ts b/packages/mom/src/tools/bash.ts index b98658ed9..91115a4ae 100644 --- a/packages/mom/src/tools/bash.ts +++ b/packages/mom/src/tools/bash.ts @@ -1,4 +1,3 @@ -import { randomBytes } from "node:crypto"; import { tmpdir } from "node:os"; import { join } from "node:path"; import type { AgentTool } from "@mariozechner/pi-agent-core"; @@ -10,7 +9,8 @@ import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult * Generate a unique temp file path for bash output */ function getTempFilePath(): string { - const id = randomBytes(8).toString("hex"); + const bytes = crypto.getRandomValues(new Uint8Array(8)); + const id = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(""); return join(tmpdir(), `mom-bash-${id}.log`); } diff --git a/packages/pods/src/commands/models.ts b/packages/pods/src/commands/models.ts index 828e0a164..6bf34f1e1 100644 --- a/packages/pods/src/commands/models.ts +++ b/packages/pods/src/commands/models.ts @@ -83,7 +83,7 @@ export const startModel = async ( gpus?: number; }, ) => { - const { name: podName, pod } = getPod(options.pod); + const { name: podName, pod } = await getPod(options.pod); // Validation if (!pod.modelsPath) { @@ -334,8 +334,12 @@ WRAPPER // Read stdout and stderr streams const decoder = new TextDecoder(); - const stdoutReader = logProcess.stdout.getReader(); - const stderrReader = logProcess.stderr.getReader(); + const stdoutReader = ( + logProcess.stdout as ReadableStream + ).getReader() as ReadableStreamDefaultReader; + const stderrReader = ( + logProcess.stderr as ReadableStream + ).getReader() as ReadableStreamDefaultReader; const readStream = async (reader: ReadableStreamDefaultReader) => { while (true) { diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 3e066be97..a3fe3bf07 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -2,6 +2,14 @@ ## [Unreleased] +## [0.31.1] - 2026-01-02 + +### Fixed + +- `visibleWidth()` now strips OSC 8 hyperlink sequences, fixing text wrapping for clickable links ([#396](https://github.com/badlogic/pi-mono/pull/396) by [@Cursivez](https://github.com/Cursivez)) + +## [0.31.0] - 2026-01-02 + ### Added - `isShiftCtrlO()` key detection function for Shift+Ctrl+O (Kitty protocol) @@ -12,11 +20,13 @@ ### Changed - README.md completely rewritten with accurate component documentation, theme interfaces, and examples +- `visibleWidth()` reimplemented with grapheme-based width calculation, 10x faster on Bun and ~15% faster on Node ([#369](https://github.com/badlogic/pi-mono/pull/369) by [@nathyong](https://github.com/nathyong)) ### Fixed - Markdown component now renders HTML tags as plain text instead of silently dropping them ([#359](https://github.com/badlogic/pi-mono/issues/359)) - Crash in `visibleWidth()` and grapheme iteration when encountering undefined code points ([#372](https://github.com/badlogic/pi-mono/pull/372) by [@HACKE-RC](https://github.com/HACKE-RC)) +- ZWJ emoji sequences (rainbow flag, family, etc.) now render with correct width instead of being split into multiple characters ([#369](https://github.com/badlogic/pi-mono/pull/369) by [@nathyong](https://github.com/nathyong)) ## [0.29.0] - 2025-12-25 diff --git a/packages/tui/package.json b/packages/tui/package.json index 9666565f2..7a4e886a6 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -34,9 +34,9 @@ "dependencies": { "@types/mime-types": "^2.1.4", "chalk": "^5.5.0", + "get-east-asian-width": "^1.3.0", "marked": "^15.0.12", - "mime-types": "^3.0.1", - "string-width": "^8.1.0" + "mime-types": "^3.0.1" }, "devDependencies": { "@xterm/headless": "^5.5.0", diff --git a/packages/tui/src/utils.ts b/packages/tui/src/utils.ts index 44f1c0764..1a7b77679 100644 --- a/packages/tui/src/utils.ts +++ b/packages/tui/src/utils.ts @@ -1,16 +1,135 @@ -import stringWidth from "string-width"; +import { eastAsianWidth } from "get-east-asian-width"; -// Format characters that crash string-width (Format but not Default_Ignorable_Code_Point) -const CRASHING_FORMAT_CHARS = - /[\u0600-\u0605\u06DD\u070F\u0890\u0891\u08E2\uFFF9-\uFFFB\u{110BD}\u{110CD}\u{13430}-\u{1343F}\u{1BCA0}-\u{1BCA3}]/gu; +// Grapheme segmenter (shared instance) +const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" }); + +/** + * Get the shared grapheme segmenter instance. + */ +export function getSegmenter(): Intl.Segmenter { + return segmenter; +} + +/** + * Check if a grapheme cluster (after segmentation) could possibly be an RGI emoji. + * This is a fast heuristic to avoid the expensive rgiEmojiRegex test. + * The tested Unicode blocks are deliberately broad to account for future + * Unicode additions. + */ +function couldBeEmoji(segment: string): boolean { + const cp = segment.codePointAt(0)!; + return ( + (cp >= 0x1f000 && cp <= 0x1fbff) || // Emoji and Pictograph + (cp >= 0x2300 && cp <= 0x23ff) || // Misc technical + (cp >= 0x2600 && cp <= 0x27bf) || // Misc symbols, dingbats + (cp >= 0x2b50 && cp <= 0x2b55) || // Specific stars/circles + segment.includes("\uFE0F") || // Contains VS16 (emoji presentation selector) + segment.length > 2 // Multi-codepoint sequences (ZWJ, skin tones, etc.) + ); +} + +// Regexes for character classification (same as string-width library) +const zeroWidthRegex = /^(?:\p{Default_Ignorable_Code_Point}|\p{Control}|\p{Mark}|\p{Surrogate})+$/v; +const leadingNonPrintingRegex = /^[\p{Default_Ignorable_Code_Point}\p{Control}\p{Format}\p{Mark}\p{Surrogate}]+/v; +const rgiEmojiRegex = /^\p{RGI_Emoji}$/v; + +// Cache for non-ASCII strings +const WIDTH_CACHE_SIZE = 512; +const widthCache = new Map(); + +/** + * Calculate the terminal width of a single grapheme cluster. + * Based on code from the string-width library, but includes a possible-emoji + * check to avoid running the RGI_Emoji regex unnecessarily. + */ +function graphemeWidth(segment: string): number { + // Zero-width clusters + if (zeroWidthRegex.test(segment)) { + return 0; + } + + // Emoji check with pre-filter + if (couldBeEmoji(segment) && rgiEmojiRegex.test(segment)) { + return 2; + } + + // Get base visible codepoint + const base = segment.replace(leadingNonPrintingRegex, ""); + const cp = base.codePointAt(0); + if (cp === undefined) { + return 0; + } + + let width = eastAsianWidth(cp); + + // Trailing halfwidth/fullwidth forms + if (segment.length > 1) { + for (const char of segment.slice(1)) { + const c = char.codePointAt(0)!; + if (c >= 0xff00 && c <= 0xffef) { + width += eastAsianWidth(c); + } + } + } + + return width; +} /** * Calculate the visible width of a string in terminal columns. */ export function visibleWidth(str: string): number { - if (!str) return 0; - const normalized = str.replace(/\t/g, " ").replace(CRASHING_FORMAT_CHARS, ""); - return stringWidth(normalized); + if (str.length === 0) { + return 0; + } + + // Fast path: pure ASCII printable + let isPureAscii = true; + for (let i = 0; i < str.length; i++) { + const code = str.charCodeAt(i); + if (code < 0x20 || code > 0x7e) { + isPureAscii = false; + break; + } + } + if (isPureAscii) { + return str.length; + } + + // Check cache + const cached = widthCache.get(str); + if (cached !== undefined) { + return cached; + } + + // Normalize: tabs to 3 spaces, strip ANSI escape codes + let clean = str; + if (str.includes("\t")) { + clean = clean.replace(/\t/g, " "); + } + if (clean.includes("\x1b")) { + // Strip SGR codes (\x1b[...m) and cursor codes (\x1b[...G/K/H/J) + clean = clean.replace(/\x1b\[[0-9;]*[mGKHJ]/g, ""); + // Strip OSC 8 hyperlinks: \x1b]8;;URL\x07 and \x1b]8;;\x07 + clean = clean.replace(/\x1b\]8;;[^\x07]*\x07/g, ""); + } + + // Calculate width + let width = 0; + for (const { segment } of segmenter.segment(clean)) { + width += graphemeWidth(segment); + } + + // Cache result + if (widthCache.size >= WIDTH_CACHE_SIZE) { + const firstKey = widthCache.keys().next().value; + if (firstKey !== undefined) { + widthCache.delete(firstKey); + } + } + widthCache.set(str, width); + + return width; } /** @@ -411,15 +530,6 @@ function wrapSingleLine(line: string, width: number): string[] { return wrapped.length > 0 ? wrapped : [""]; } -const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" }); - -/** - * Get the shared grapheme segmenter instance. - */ -export function getSegmenter(): Intl.Segmenter { - return segmenter; -} - const PUNCTUATION_REGEX = /[(){}[\]<>.,;:'"!?+\-=*/\\|&%^$#@~`]/; /** diff --git a/packages/tui/test/editor.test.ts b/packages/tui/test/editor.test.ts index 5910ee39b..5db555c2a 100644 --- a/packages/tui/test/editor.test.ts +++ b/packages/tui/test/editor.test.ts @@ -487,7 +487,7 @@ describe("Editor component", () => { // All content lines (between borders) should fit within width for (let i = 1; i < lines.length - 1; i++) { const lineWidth = visibleWidth(lines[i]!); - expect(lineWidth).toBe(width, `Line ${i} has width ${lineWidth}, expected ${width}`); + expect(lineWidth).toBe(width); } }); @@ -504,7 +504,7 @@ describe("Editor component", () => { // First line: 5 emojis (10 cols), second line: 1 emoji (2 cols) + padding for (let i = 1; i < lines.length - 1; i++) { const lineWidth = visibleWidth(lines[i]!); - expect(lineWidth).toBe(width, `Line ${i} has width ${lineWidth}, expected ${width}`); + expect(lineWidth).toBe(width); } }); @@ -518,7 +518,7 @@ describe("Editor component", () => { for (let i = 1; i < lines.length - 1; i++) { const lineWidth = visibleWidth(lines[i]!); - expect(lineWidth).toBe(width, `Line ${i} has width ${lineWidth}, expected ${width}`); + expect(lineWidth).toBe(width); } // Verify content split correctly diff --git a/packages/tui/test/markdown.test.ts b/packages/tui/test/markdown.test.ts index e8eac9882..1df0cdd55 100644 --- a/packages/tui/test/markdown.test.ts +++ b/packages/tui/test/markdown.test.ts @@ -243,7 +243,7 @@ describe("Markdown component", () => { const tableLines = plainLines.filter((line) => line.startsWith("│")); for (const line of tableLines) { const borderCount = line.split("│").length - 1; - expect(borderCount).toBe(2, `Expected 2 borders, got ${borderCount}: "${line}"`); + expect(borderCount).toBe(2); } // Strip box drawing characters + whitespace so we can assert the URL is preserved @@ -279,7 +279,7 @@ describe("Markdown component", () => { const tableLines = plainLines.filter((line) => line.startsWith("│")); for (const line of tableLines) { const borderCount = line.split("│").length - 1; - expect(borderCount).toBe(2, `Expected 2 borders, got ${borderCount}: "${line}"`); + expect(borderCount).toBe(2); } }); @@ -468,11 +468,7 @@ again, hello world`, const afterBackticks = plainLines.slice(closingBackticksIndex + 1); const emptyLineCount = afterBackticks.findIndex((line) => line !== ""); - expect( - emptyLineCount, - 1, - `Expected 1 empty line after code block, but found ${emptyLineCount}. Lines after backticks: ${JSON.stringify(afterBackticks.slice(0, 5))}`, - ); + expect(emptyLineCount).toBe(1); }); }); @@ -498,11 +494,7 @@ again, hello world`, const afterDivider = plainLines.slice(dividerIndex + 1); const emptyLineCount = afterDivider.findIndex((line) => line !== ""); - expect( - emptyLineCount, - 1, - `Expected 1 empty line after divider, but found ${emptyLineCount}. Lines after divider: ${JSON.stringify(afterDivider.slice(0, 5))}`, - ); + expect(emptyLineCount).toBe(1); }); }); @@ -526,11 +518,7 @@ This is a paragraph`, const afterHeading = plainLines.slice(headingIndex + 1); const emptyLineCount = afterHeading.findIndex((line) => line !== ""); - expect( - emptyLineCount, - 1, - `Expected 1 empty line after heading, but found ${emptyLineCount}. Lines after heading: ${JSON.stringify(afterHeading.slice(0, 5))}`, - ); + expect(emptyLineCount).toBe(1); }); }); @@ -556,11 +544,7 @@ again, hello world`, const afterQuote = plainLines.slice(quoteIndex + 1); const emptyLineCount = afterQuote.findIndex((line) => line !== ""); - expect( - emptyLineCount, - 1, - `Expected 1 empty line after blockquote, but found ${emptyLineCount}. Lines after quote: ${JSON.stringify(afterQuote.slice(0, 5))}`, - ); + expect(emptyLineCount).toBe(1); }); }); diff --git a/packages/web-ui/CHANGELOG.md b/packages/web-ui/CHANGELOG.md index 8a1c26e49..b80f5ec33 100644 --- a/packages/web-ui/CHANGELOG.md +++ b/packages/web-ui/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +## [0.31.1] - 2026-01-02 + +## [0.31.0] - 2026-01-02 + ### Breaking Changes - **Agent class moved to `@mariozechner/pi-agent-core`**: The `Agent` class, `AgentState`, and related types are no longer exported from this package. Import them from `@mariozechner/pi-agent-core` instead. diff --git a/packages/web-ui/example/package.json b/packages/web-ui/example/package.json index 6b734a9be..c0206c5eb 100644 --- a/packages/web-ui/example/package.json +++ b/packages/web-ui/example/package.json @@ -1,6 +1,6 @@ { "name": "pi-web-ui-example", - "version": "1.18.2", + "version": "1.19.1", "private": true, "type": "module", "scripts": { diff --git a/todo.md b/todo.md new file mode 100644 index 000000000..3ae3823f3 --- /dev/null +++ b/todo.md @@ -0,0 +1,6 @@ +# Export HTML TODOs + +- [x] "Ctrl+T toggle thinking · Ctrl+O toggle tools" font size is not the same as all the other font sizes +- [ ] System prompt doesn't show included AGENTS.md, skills. See `packages/coding-agent/src/core/system-prompt.ts`. Can only be done if we export live from a session, not via `--export` CLI flag +- [x] "Available Tools" has no newline after it +- [x] `read` tool has too much vertical spacing between tool call header and tool result, and also with tool bottom border (was white-space: pre-wrap on .tool-output preserving template literal whitespace) diff --git a/tsconfig.base.json b/tsconfig.base.json index 1e5aee7d0..6dbd2c14d 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1,8 +1,8 @@ { "compilerOptions": { - "target": "ES2022", - "module": "Node16", - "lib": ["ES2022"], + "target": "ES2024", + "module": "ESNext", + "lib": ["ES2024"], "strict": true, "esModuleInterop": true, "skipLibCheck": true, @@ -12,7 +12,7 @@ "sourceMap": true, "inlineSources": true, "inlineSourceMap": false, - "moduleResolution": "Node16", + "moduleResolution": "Bundler", "resolveJsonModule": true, "allowImportingTsExtensions": false, "experimentalDecorators": true,