From 1a944f50f935c28a967ac83f77f31c46e3cc05ae Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 18 Dec 2025 16:01:01 +0100 Subject: [PATCH] Syntax highlighting improvements - Fix cli-highlight import (use static import instead of dynamic require) - Add syntax highlighting to read/write tool output - Update dark/light themes with VS Code default syntax colors - Add highlightCode and getLanguageFromPath exports - Upgrade @google/genai to 1.34.0 for IMAGE_RECITATION/IMAGE_OTHER FinishReason - Add AGENTS.md rule: never downgrade code to fix type errors from outdated deps --- AGENTS.md | 2 + package-lock.json | 10 +- packages/ai/package.json | 2 +- .../interactive/components/tool-execution.ts | 28 ++++-- .../src/modes/interactive/theme/dark.json | 18 ++-- .../src/modes/interactive/theme/light.json | 18 ++-- .../src/modes/interactive/theme/theme.ts | 94 +++++++++++++++++-- 7 files changed, 133 insertions(+), 39 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 3e35c109..1191b958 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,6 +15,8 @@ read README.md, then ask which module(s) to work on. Based on the answer, read t - No `any` types unless absolutely necessary - Check node_modules for external API type definitions instead of guessing - No inline imports like `await import("./foo.js")` +- 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 ## Commands - After code changes: `npm run check` (get full output, no tail) diff --git a/package-lock.json b/package-lock.json index 6fa531f0..3ee012b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -675,9 +675,9 @@ } }, "node_modules/@google/genai": { - "version": "1.31.0", - "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.31.0.tgz", - "integrity": "sha512-rK0RKXxNkbK35eDl+G651SxtxwHNEOogjyeZJUJe+Ed4yxu3xy5ufCiU0+QLT7xo4M9Spey8OAYfD8LPRlYBKw==", + "version": "1.34.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.34.0.tgz", + "integrity": "sha512-vu53UMPvjmb7PGzlYu6Tzxso8Dfhn+a7eQFaS2uNemVtDZKwzSpJ5+ikqBbXplF7RGB1STcVDqCkPvquiwb2sw==", "license": "Apache-2.0", "dependencies": { "google-auth-library": "^10.3.0", @@ -687,7 +687,7 @@ "node": ">=20.0.0" }, "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.20.1" + "@modelcontextprotocol/sdk": "^1.24.0" }, "peerDependenciesMeta": { "@modelcontextprotocol/sdk": { @@ -6424,7 +6424,7 @@ "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.71.2", - "@google/genai": "1.31.0", + "@google/genai": "^1.34.0", "@mistralai/mistralai": "1.10.0", "@sinclair/typebox": "^0.34.41", "ajv": "^8.17.1", diff --git a/packages/ai/package.json b/packages/ai/package.json index 4a7559e5..e466e06a 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -21,7 +21,7 @@ }, "dependencies": { "@anthropic-ai/sdk": "0.71.2", - "@google/genai": "1.31.0", + "@google/genai": "^1.34.0", "@mistralai/mistralai": "1.10.0", "@sinclair/typebox": "^0.34.41", "ajv": "^8.17.1", 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 6d2d7d2d..61e0113d 100644 --- a/packages/coding-agent/src/modes/interactive/components/tool-execution.ts +++ b/packages/coding-agent/src/modes/interactive/components/tool-execution.ts @@ -12,7 +12,7 @@ import { import stripAnsi from "strip-ansi"; import type { CustomAgentTool } from "../../../core/custom-tools/types.js"; import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize } from "../../../core/tools/truncate.js"; -import { theme } from "../theme/theme.js"; +import { getLanguageFromPath, highlightCode, theme } from "../theme/theme.js"; /** * Convert absolute path to tilde notation if it's in home directory @@ -280,13 +280,19 @@ export class ToolExecutionComponent extends Container { if (this.result) { const output = this.getTextOutput(); - const lines = output.split("\n"); + const rawPath = this.args?.file_path || this.args?.path || ""; + const lang = getLanguageFromPath(rawPath); + const lines = lang ? highlightCode(replaceTabs(output), lang) : output.split("\n"); const maxLines = this.expanded ? lines.length : 10; const displayLines = lines.slice(0, maxLines); const remaining = lines.length - maxLines; - text += "\n\n" + displayLines.map((line: string) => theme.fg("toolOutput", replaceTabs(line))).join("\n"); + text += + "\n\n" + + displayLines + .map((line: string) => (lang ? replaceTabs(line) : theme.fg("toolOutput", replaceTabs(line)))) + .join("\n"); if (remaining > 0) { text += theme.fg("toolOutput", `\n... (${remaining} more lines)`); } @@ -318,9 +324,15 @@ export class ToolExecutionComponent extends Container { } } } else if (this.toolName === "write") { - const path = shortenPath(this.args?.file_path || this.args?.path || ""); + const rawPath = this.args?.file_path || this.args?.path || ""; + const path = shortenPath(rawPath); const fileContent = this.args?.content || ""; - const lines = fileContent ? fileContent.split("\n") : []; + const lang = getLanguageFromPath(rawPath); + const lines = fileContent + ? lang + ? highlightCode(replaceTabs(fileContent), lang) + : fileContent.split("\n") + : []; const totalLines = lines.length; text = @@ -336,7 +348,11 @@ export class ToolExecutionComponent extends Container { const displayLines = lines.slice(0, maxLines); const remaining = lines.length - maxLines; - text += "\n\n" + displayLines.map((line: string) => theme.fg("toolOutput", replaceTabs(line))).join("\n"); + text += + "\n\n" + + displayLines + .map((line: string) => (lang ? replaceTabs(line) : theme.fg("toolOutput", replaceTabs(line)))) + .join("\n"); if (remaining > 0) { text += theme.fg("toolOutput", `\n... (${remaining} more lines)`); } diff --git a/packages/coding-agent/src/modes/interactive/theme/dark.json b/packages/coding-agent/src/modes/interactive/theme/dark.json index 28b84c4d..51ad7749 100644 --- a/packages/coding-agent/src/modes/interactive/theme/dark.json +++ b/packages/coding-agent/src/modes/interactive/theme/dark.json @@ -51,15 +51,15 @@ "toolDiffRemoved": "red", "toolDiffContext": "gray", - "syntaxComment": "gray", - "syntaxKeyword": "cyan", - "syntaxFunction": "blue", - "syntaxVariable": "", - "syntaxString": "green", - "syntaxNumber": "yellow", - "syntaxType": "cyan", - "syntaxOperator": "", - "syntaxPunctuation": "gray", + "syntaxComment": "#6A9955", + "syntaxKeyword": "#569CD6", + "syntaxFunction": "#DCDCAA", + "syntaxVariable": "#9CDCFE", + "syntaxString": "#CE9178", + "syntaxNumber": "#B5CEA8", + "syntaxType": "#4EC9B0", + "syntaxOperator": "#D4D4D4", + "syntaxPunctuation": "#D4D4D4", "thinkingOff": "darkGray", "thinkingMinimal": "#6e6e6e", diff --git a/packages/coding-agent/src/modes/interactive/theme/light.json b/packages/coding-agent/src/modes/interactive/theme/light.json index 09405d14..57eb2643 100644 --- a/packages/coding-agent/src/modes/interactive/theme/light.json +++ b/packages/coding-agent/src/modes/interactive/theme/light.json @@ -50,15 +50,15 @@ "toolDiffRemoved": "red", "toolDiffContext": "mediumGray", - "syntaxComment": "mediumGray", - "syntaxKeyword": "teal", - "syntaxFunction": "blue", - "syntaxVariable": "", - "syntaxString": "green", - "syntaxNumber": "yellow", - "syntaxType": "teal", - "syntaxOperator": "", - "syntaxPunctuation": "mediumGray", + "syntaxComment": "#008000", + "syntaxKeyword": "#0000FF", + "syntaxFunction": "#795E26", + "syntaxVariable": "#001080", + "syntaxString": "#A31515", + "syntaxNumber": "#098658", + "syntaxType": "#267F99", + "syntaxOperator": "#000000", + "syntaxPunctuation": "#000000", "thinkingOff": "lightGray", "thinkingMinimal": "#9e9e9e", diff --git a/packages/coding-agent/src/modes/interactive/theme/theme.ts b/packages/coding-agent/src/modes/interactive/theme/theme.ts index db7a6bcc..f02018a1 100644 --- a/packages/coding-agent/src/modes/interactive/theme/theme.ts +++ b/packages/coding-agent/src/modes/interactive/theme/theme.ts @@ -1,10 +1,10 @@ import * as fs from "node:fs"; -import { createRequire } from "node:module"; import * as path from "node:path"; import type { EditorTheme, MarkdownTheme, SelectListTheme } from "@mariozechner/pi-tui"; import { type Static, Type } from "@sinclair/typebox"; import { TypeCompiler } from "@sinclair/typebox/compiler"; import chalk from "chalk"; +import { highlight } from "cli-highlight"; import { getCustomThemesDir, getThemesDir } from "../../../config.js"; // ============================================================================ @@ -664,17 +664,94 @@ function getCliHighlightTheme(t: Theme): CliHighlightTheme { return cachedCliHighlightTheme; } -function requireCliHighlight(): { highlight: (code: string, opts?: any) => string } { +/** + * Highlight code with syntax coloring based on file extension or language. + * Returns array of highlighted lines. + */ +export function highlightCode(code: string, lang?: string): string[] { + const opts = { + language: lang, + ignoreIllegals: true, + theme: getCliHighlightTheme(theme), + }; try { - const require = createRequire(import.meta.url); - return require("cli-highlight"); + return highlight(code, opts).split("\n"); } catch { - return { - highlight: (code: string) => code, - }; + return code.split("\n"); } } +/** + * Get language identifier from file path extension. + */ +export function getLanguageFromPath(filePath: string): string | undefined { + const ext = filePath.split(".").pop()?.toLowerCase(); + if (!ext) return undefined; + + const extToLang: Record = { + ts: "typescript", + tsx: "typescript", + js: "javascript", + jsx: "javascript", + mjs: "javascript", + cjs: "javascript", + py: "python", + rb: "ruby", + rs: "rust", + go: "go", + java: "java", + kt: "kotlin", + swift: "swift", + c: "c", + h: "c", + cpp: "cpp", + cc: "cpp", + cxx: "cpp", + hpp: "cpp", + cs: "csharp", + php: "php", + sh: "bash", + bash: "bash", + zsh: "bash", + fish: "fish", + ps1: "powershell", + sql: "sql", + html: "html", + htm: "html", + css: "css", + scss: "scss", + sass: "sass", + less: "less", + json: "json", + yaml: "yaml", + yml: "yaml", + toml: "toml", + xml: "xml", + md: "markdown", + markdown: "markdown", + dockerfile: "dockerfile", + makefile: "makefile", + cmake: "cmake", + lua: "lua", + perl: "perl", + r: "r", + scala: "scala", + clj: "clojure", + ex: "elixir", + exs: "elixir", + erl: "erlang", + hs: "haskell", + ml: "ocaml", + vim: "vim", + graphql: "graphql", + proto: "protobuf", + tf: "hcl", + hcl: "hcl", + }; + + return extToLang[ext]; +} + export function getMarkdownTheme(): MarkdownTheme { return { heading: (text: string) => theme.fg("mdHeading", text), @@ -692,8 +769,7 @@ export function getMarkdownTheme(): MarkdownTheme { underline: (text: string) => theme.underline(text), strikethrough: (text: string) => chalk.strikethrough(text), highlightCode: (code: string, lang?: string): string[] => { - const { highlight } = requireCliHighlight(); - const opts: any = { + const opts = { language: lang, ignoreIllegals: true, theme: getCliHighlightTheme(theme),