From cec3a91c029e453c4bebdd02d84335a0f52503d7 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 22 Aug 2026 22:50:12 +0200 Subject: [PATCH] feat(coding-agent): defer uncommon syntax grammars --- packages/coding-agent/CHANGELOG.md | 1 + .../src/modes/interactive/interactive-mode.ts | 9 +++ .../src/utils/highlight-js-lib-index.d.ts | 19 ----- .../coding-agent/src/utils/highlight-js.d.ts | 36 +++++++++ .../src/utils/syntax-highlight.ts | 79 ++++++++++++++++++- .../test/syntax-highlight.test.ts | 39 ++++++++- 6 files changed, 162 insertions(+), 21 deletions(-) delete mode 100644 packages/coding-agent/src/utils/highlight-js-lib-index.d.ts create mode 100644 packages/coding-agent/src/utils/highlight-js.d.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index d53007714..969b2e807 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -8,6 +8,7 @@ ### Changed +- Changed syntax highlighting to initialize only twenty common languages eagerly and defer the remaining grammars until after the initial TUI render, reducing CLI startup time. - Changed the Node.js CLI and RPC entrypoints to load a bundled runtime, reducing startup filesystem reads while keeping the public library and legacy module paths on the modular runtime for normal dependency identity. - Changed session sharing to render clickable terminal links and Radius shares to display only the artifact's canonical URL. diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 3035ad4c5..10dbb7935 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -111,6 +111,7 @@ import { openBrowser } from "../../utils/open-browser.ts"; import { getCwdRelativePath } from "../../utils/paths.ts"; import { getPiUserAgent } from "../../utils/pi-user-agent.ts"; import { killTrackedDetachedChildren } from "../../utils/shell.ts"; +import { loadAllHighlightLanguages } from "../../utils/syntax-highlight.ts"; import { ensureTool, type ToolStatus } from "../../utils/tools-manager.ts"; import { checkForNewPiVersion, type LatestPiRelease } from "../../utils/version-check.ts"; import { ArminComponent } from "./components/armin.ts"; @@ -1051,6 +1052,14 @@ export class InteractiveMode { // Initialize available provider count for footer display await this.updateAvailableProviderCount(); + + // Flush the completed startup state before loading the remaining syntax grammars. + this.ui.renderNow(); + void loadAllHighlightLanguages().then(() => { + if (!this.isInitialized) return; + this.ui.invalidate(); + this.ui.requestRender(); + }); } /** diff --git a/packages/coding-agent/src/utils/highlight-js-lib-index.d.ts b/packages/coding-agent/src/utils/highlight-js-lib-index.d.ts deleted file mode 100644 index 75e31da28..000000000 --- a/packages/coding-agent/src/utils/highlight-js-lib-index.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -declare module "highlight.js/lib/index.js" { - interface HighlightResult { - value: string; - } - - interface HighlightOptions { - language: string; - ignoreIllegals?: boolean; - } - - interface HighlightJs { - highlight(code: string, options: HighlightOptions): HighlightResult; - highlightAuto(code: string, languageSubset?: string[]): HighlightResult; - getLanguage(name: string): unknown; - } - - const hljs: HighlightJs; - export default hljs; -} diff --git a/packages/coding-agent/src/utils/highlight-js.d.ts b/packages/coding-agent/src/utils/highlight-js.d.ts new file mode 100644 index 000000000..d4b71174e --- /dev/null +++ b/packages/coding-agent/src/utils/highlight-js.d.ts @@ -0,0 +1,36 @@ +interface HighlightJsResult { + value: string; +} + +interface HighlightJsOptions { + language: string; + ignoreIllegals?: boolean; +} + +interface HighlightJsLanguageDefinition { + readonly name?: string; +} + +type HighlightJsLanguageFactory = (hljs: HighlightJsApi) => HighlightJsLanguageDefinition; + +interface HighlightJsApi { + highlight(code: string, options: HighlightJsOptions): HighlightJsResult; + highlightAuto(code: string, languageSubset?: string[]): HighlightJsResult; + getLanguage(name: string): HighlightJsLanguageDefinition | undefined; + registerLanguage(name: string, language: HighlightJsLanguageFactory): void; +} + +declare module "highlight.js/lib/core.js" { + const hljs: HighlightJsApi; + export default hljs; +} + +declare module "highlight.js/lib/index.js" { + const hljs: HighlightJsApi; + export default hljs; +} + +declare module "highlight.js/lib/languages/*.js" { + const language: HighlightJsLanguageFactory; + export default language; +} diff --git a/packages/coding-agent/src/utils/syntax-highlight.ts b/packages/coding-agent/src/utils/syntax-highlight.ts index bcc4add1c..e08c4d894 100644 --- a/packages/coding-agent/src/utils/syntax-highlight.ts +++ b/packages/coding-agent/src/utils/syntax-highlight.ts @@ -1,6 +1,83 @@ -import hljs from "highlight.js/lib/index.js"; +import { createRequire } from "node:module"; +import hljs from "highlight.js/lib/core.js"; +import bash from "highlight.js/lib/languages/bash.js"; +import c from "highlight.js/lib/languages/c.js"; +import cpp from "highlight.js/lib/languages/cpp.js"; +import csharp from "highlight.js/lib/languages/csharp.js"; +import dart from "highlight.js/lib/languages/dart.js"; +import go from "highlight.js/lib/languages/go.js"; +import groovy from "highlight.js/lib/languages/groovy.js"; +import java from "highlight.js/lib/languages/java.js"; +import javascript from "highlight.js/lib/languages/javascript.js"; +import kotlin from "highlight.js/lib/languages/kotlin.js"; +import lua from "highlight.js/lib/languages/lua.js"; +import nix from "highlight.js/lib/languages/nix.js"; +import perl from "highlight.js/lib/languages/perl.js"; +import php from "highlight.js/lib/languages/php.js"; +import python from "highlight.js/lib/languages/python.js"; +import ruby from "highlight.js/lib/languages/ruby.js"; +import rust from "highlight.js/lib/languages/rust.js"; +import scala from "highlight.js/lib/languages/scala.js"; +import swift from "highlight.js/lib/languages/swift.js"; +import typescript from "highlight.js/lib/languages/typescript.js"; import { decodeHtmlEntityAt } from "./html.ts"; +const moduleRequire = createRequire(import.meta.url); +declare const PI_BUNDLED_NODE: boolean; +declare const Bun: unknown; +declare const require: (specifier: string) => unknown; + +const eagerLanguages = { + python, + java, + go, + javascript, + cpp, + typescript, + php, + ruby, + c, + csharp, + nix, + bash, + rust, + scala, + kotlin, + swift, + dart, + groovy, + perl, + lua, +}; + +for (const [name, language] of Object.entries(eagerLanguages)) { + hljs.registerLanguage(name, language); +} + +let allLanguagesPromise: Promise | undefined; + +export function loadAllHighlightLanguages(): Promise { + if (!allLanguagesPromise) { + allLanguagesPromise = new Promise((resolve) => { + setImmediate(() => { + try { + // Static require lets esbuild and Bun include this deferred module in + // standalone bundles. Source and unbundled Node runtimes need createRequire. + if ((typeof PI_BUNDLED_NODE !== "undefined" && PI_BUNDLED_NODE) || typeof Bun !== "undefined") { + require("highlight.js/lib/index.js"); + } else { + moduleRequire("highlight.js/lib/index.js"); + } + } catch { + // Eager languages and plaintext fallback remain available. + } + resolve(); + }); + }); + } + return allLanguagesPromise; +} + export type HighlightFormatter = (text: string) => string; export type HighlightTheme = Partial>; diff --git a/packages/coding-agent/test/syntax-highlight.test.ts b/packages/coding-agent/test/syntax-highlight.test.ts index 6f311e494..62fe5f025 100644 --- a/packages/coding-agent/test/syntax-highlight.test.ts +++ b/packages/coding-agent/test/syntax-highlight.test.ts @@ -1,9 +1,46 @@ import { resetCapabilitiesCache, setCapabilities } from "@earendil-works/pi-tui"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { highlightCode, initTheme } from "../src/modes/interactive/theme/theme.ts"; -import { highlight, renderHighlightedHtml, supportsLanguage } from "../src/utils/syntax-highlight.ts"; +import { + highlight, + loadAllHighlightLanguages, + renderHighlightedHtml, + supportsLanguage, +} from "../src/utils/syntax-highlight.ts"; + +const eagerLanguages = [ + "python", + "java", + "go", + "javascript", + "cpp", + "typescript", + "php", + "ruby", + "c", + "csharp", + "nix", + "bash", + "rust", + "scala", + "kotlin", + "swift", + "dart", + "groovy", + "perl", + "lua", +]; +const eagerLanguagesLoadedAtStartup = eagerLanguages.every(supportsLanguage); +const uncommonLanguageLoadedAtStartup = supportsLanguage("ada"); describe("syntax highlight renderer", () => { + it("loads the twenty most common languages at startup and defers the rest", async () => { + expect(eagerLanguagesLoadedAtStartup).toBe(true); + expect(uncommonLanguageLoadedAtStartup).toBe(false); + await loadAllHighlightLanguages(); + expect(supportsLanguage("ada")).toBe(true); + }); + it("renders highlighted spans with the provided theme", () => { const rendered = renderHighlightedHtml('const value', { keyword: (text) => `[keyword:${text}]`,