code highlighting WIP (#2885)

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Dax <mail@thdxr.com>
This commit is contained in:
Sebastian 2025-10-02 11:30:08 +02:00 committed by GitHub
parent cc5cfc6b7d
commit 5cfec2ef9d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 234 additions and 128 deletions

View file

@ -156,8 +156,8 @@
"@openauthjs/openauth": "catalog:",
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/sdk": "workspace:*",
"@opentui/core": "0.0.0-20251001-886e38c1",
"@opentui/solid": "0.0.0-20251001-886e38c1",
"@opentui/core": "0.0.0-20251001-d57654da",
"@opentui/solid": "0.0.0-20251001-d57654da",
"@standard-schema/spec": "1.0.0",
"@zip.js/zip.js": "2.7.62",
"ai": "catalog:",
@ -183,7 +183,7 @@
"turndown": "7.2.0",
"ulid": "catalog:",
"vscode-jsonrpc": "8.2.1",
"web-tree-sitter": "0.22.6",
"web-tree-sitter": "0.26.0",
"xdg-basedir": "5.1.0",
"yargs": "18.0.0",
"zod": "catalog:",
@ -835,21 +835,21 @@
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
"@opentui/core": ["@opentui/core@0.0.0-20251001-886e38c1", "", { "dependencies": { "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.0.0-20251001-886e38c1", "@opentui/core-darwin-x64": "0.0.0-20251001-886e38c1", "@opentui/core-linux-arm64": "0.0.0-20251001-886e38c1", "@opentui/core-linux-x64": "0.0.0-20251001-886e38c1", "@opentui/core-win32-arm64": "0.0.0-20251001-886e38c1", "@opentui/core-win32-x64": "0.0.0-20251001-886e38c1", "bun-webgpu": "0.1.3", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": ">=0.25.0" } }, "sha512-mbElYuirjjWynIpc4pkNTtIYxK9zmEmEjgaV5Z1wyrSy/e5Ld5nKs39wBLKPFzx/u9XcIIz31YRLNW3hHXlxEQ=="],
"@opentui/core": ["@opentui/core@0.0.0-20251001-d57654da", "", { "dependencies": { "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.0.0-20251001-d57654da", "@opentui/core-darwin-x64": "0.0.0-20251001-d57654da", "@opentui/core-linux-arm64": "0.0.0-20251001-d57654da", "@opentui/core-linux-x64": "0.0.0-20251001-d57654da", "@opentui/core-win32-arm64": "0.0.0-20251001-d57654da", "@opentui/core-win32-x64": "0.0.0-20251001-d57654da", "bun-webgpu": "0.1.3", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": ">=0.26.0" } }, "sha512-xDERJfK0+xn3atLPPyjliCPTZOGMZ3Is5NOKuCMZnpICm3zNirJMa02/8FGmxz0fbPL+ll1MTO1qdpl2DZBb6g=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.0.0-20251001-886e38c1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-b5+wxaIaEnT0qdCxeOXgW5q0DP/y1r1dTVEVwT0JUK/NXkTa+MPIkHfPg8Ns82v/Bpv2pdvgZkcgBb8f8F0Zvg=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.0.0-20251001-d57654da", "", { "os": "darwin", "cpu": "arm64" }, "sha512-1Q2F9VYRQO/nZioOQmbJf3gpouBUDYE8fC5oeiw9p0fnOmcxPSN0LiS7YJbQcD+x5cK5N5Sv7jxe9ar44Wjivg=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.0.0-20251001-886e38c1", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ct2HbxYp9RTXbESilA+ypLG/1mZXn8fAnMUzrxJ77T10yEIzQx5AQPkMJkIwx1hMDmVVgK5M+Eryuk9/IWagAQ=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.0.0-20251001-d57654da", "", { "os": "darwin", "cpu": "x64" }, "sha512-d1iXRP2QdQe2k4Dwwq9QGDay1b2//YIua7N8DOjWztVHDGDKO/doBWfM6f4+HYEEiXegmZX+1hX/VXYEEKTxWA=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.0.0-20251001-886e38c1", "", { "os": "linux", "cpu": "arm64" }, "sha512-DVimZeRZNmMXdLmziiKGlT04J9NBk1VhkjUcWNz1xKNJz3e0VAJHGE9+B4aUCT7iT06bj6bnmfZuZDlCtmLEyg=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.0.0-20251001-d57654da", "", { "os": "linux", "cpu": "arm64" }, "sha512-dc2C1yZlE5o0NfNzV7rqYCqqRBp5TRIGje3u2QEY2xDY4qlti4dS8cSOrLTWrYWrIoCmuN7W+eGrOH4NuUF+fw=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.0.0-20251001-886e38c1", "", { "os": "linux", "cpu": "x64" }, "sha512-obw90FMU9FXnSgL+4DYBaiEPW6PNz7TIFu7Vc39yXRFfLU9eM6Jrj+KRgmEzbsLAqWB246rmAAHnAX8y0T3qpQ=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.0.0-20251001-d57654da", "", { "os": "linux", "cpu": "x64" }, "sha512-sjzj1Jos7VRIl0I7JjZcOu7T4jmMdJgoRTV+U1q9wBMSVQpV9Prb50N/egGeX8D0bEF0V1T6bWkph9xgCMNKsg=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.0.0-20251001-886e38c1", "", { "os": "win32", "cpu": "arm64" }, "sha512-me60jLhjwmGrKS18ny0quBk3eatCR5uKTTzkxnqxgqdHXaM/tbC2S9SoIOkOLWwySLjj69n8xLzuZrZSOojssQ=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.0.0-20251001-d57654da", "", { "os": "win32", "cpu": "arm64" }, "sha512-F3ySKR284VLS1UUDAtcya7RJRuknghlANwlrYxiJkVOjOa7swAaxGZNYdMXmRRvOmMraeOgHWHVK3y0qdPFV9A=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.0.0-20251001-886e38c1", "", { "os": "win32", "cpu": "x64" }, "sha512-WeoojsODqLH5nX9P9UF7QjFAKDq9Z0SLBDzvcRo7NyU3Wd9PhcQf1v9Cjc42lYgF/EOX24UGv2pDLqT0mhIANA=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.0.0-20251001-d57654da", "", { "os": "win32", "cpu": "x64" }, "sha512-aVpSsEqke8ryVh2oChQrwcz4+zYPh77radM+lks5A2bo926r3dcFWNuIBFHwGTJ0tll5iAXw2MdfHo+2r8srxQ=="],
"@opentui/solid": ["@opentui/solid@0.0.0-20251001-886e38c1", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.0.0-20251001-886e38c1", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-mEydaU1JrFL7sSz0lAVIS80IprmhTpqUFabOyLJSReSQ3SSA3tLssdJCWNdqbLXAMaP5NgpmQu6iOwvVxZdJFA=="],
"@opentui/solid": ["@opentui/solid@0.0.0-20251001-d57654da", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.0.0-20251001-d57654da", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-8rJDFe8b2icFs7kTNT4ypDCIKU0RTgE2bION197RtYh9Mu6ViIagqrfGZloRdIRLcRYroYqXdl21IVh5ji2BsQ=="],
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
@ -3107,7 +3107,7 @@
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
"web-tree-sitter": ["web-tree-sitter@0.22.6", "", {}, "sha512-hS87TH71Zd6mGAmYCvlgxeGDjqd9GTeqXNqTT+u0Gs51uIozNIaaq/kUAbV/Zf56jb2ZOyG8BxZs2GG9wbLi6Q=="],
"web-tree-sitter": ["web-tree-sitter@0.26.0", "", {}, "sha512-wGGAMnJEMF8wy33iEGxSvnyEOfVLzSaa3x6g66aEHsL/hsgFb6IVPrpacIordAMz198pE9qReCEqFUuM0pnfwg=="],
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],

View file

@ -38,8 +38,8 @@
"@openauthjs/openauth": "catalog:",
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/sdk": "workspace:*",
"@opentui/core": "0.0.0-20251001-886e38c1",
"@opentui/solid": "0.0.0-20251001-886e38c1",
"@opentui/core": "0.0.0-20251001-d57654da",
"@opentui/solid": "0.0.0-20251001-d57654da",
"@standard-schema/spec": "1.0.0",
"@zip.js/zip.js": "2.7.62",
"ai": "catalog:",
@ -65,7 +65,7 @@
"turndown": "7.2.0",
"ulid": "catalog:",
"vscode-jsonrpc": "8.2.1",
"web-tree-sitter": "0.22.6",
"web-tree-sitter": "0.26.0",
"xdg-basedir": "5.1.0",
"yargs": "18.0.0",
"zod": "catalog:"

View file

@ -40,6 +40,7 @@ for (const [os, arch] of targets) {
await $`mkdir -p ../../node_modules/${opentui}`
await $`npm pack npm pack ${opentui}`.cwd(path.join(dir, "../../node_modules")).quiet()
await $`tar -xf ../../node_modules/${opentui.replace("@opentui/", "opentui-")}-*.tgz -C ../../node_modules/${opentui} --strip-components=1`
await Bun.build({
conditions: ["browser"],
tsconfig: "./tsconfig.json",
@ -50,12 +51,14 @@ for (const [os, arch] of targets) {
execArgv: [`--user-agent=opencode/${version}`, `--env-file=""`, `--`],
windows: {},
},
entrypoints: ["./src/index.ts", "./src/cli/cmd/tui/worker.ts"],
entrypoints: ["./src/index.ts", path.resolve(dir, "../../node_modules/@opentui/core/parser.worker.js")],
define: {
OPENCODE_VERSION: `'${version}'`,
OPENCODE_TUI_PATH: `'../../../dist/${name}/bin/tui'`,
OTUI_TREE_SITTER_WORKER_PATH: "/$bunfs/root/../../node_modules/@opentui/core/parser.worker.js",
},
})
await $`rm -rf ./dist/${name}/bin/tui`
await Bun.file(`dist/${name}/package.json`).write(
JSON.stringify(

View file

@ -1,3 +1,5 @@
import { SyntaxStyle } from "@opentui/core"
const OPENCODE_THEME = {
primary: {
dark: "#fab283",
@ -243,18 +245,191 @@ type Theme = {
markdownImage: string
markdownImageText: string
markdownCodeBlock: string
syntaxComment: string
syntaxKeyword: string
syntaxFunction: string
syntaxVariable: string
syntaxString: string
syntaxNumber: string
syntaxType: string
syntaxOperator: string
syntaxPunctuation: string
}
export const Theme = Object.entries(OPENCODE_THEME).reduce((acc, [key, value]) => {
acc[key as keyof Theme] = value.dark
return acc
}, {} as Theme)
}, {} as Theme)
const syntaxThemeDark = [
{
scope: ["prompt"],
style: {
foreground: "#56b6c2",
},
},
{
scope: ["comment"],
style: {
foreground: "#808080",
italic: true,
},
},
{
scope: ["comment.documentation"],
style: {
foreground: "#808080",
italic: true,
},
},
{
scope: ["string", "symbol"],
style: {
foreground: "#7fd88f",
},
},
{
scope: ["number", "boolean"],
style: {
foreground: "#f5a742",
},
},
{
scope: ["character.special"],
style: {
foreground: "#7fd88f",
},
},
{
scope: ["keyword.return", "keyword.conditional", "keyword.repeat", "keyword.coroutine"],
style: {
foreground: "#9d7cd8",
italic: true,
},
},
{
scope: ["keyword.type"],
style: {
foreground: "#e5c07b",
bold: true,
italic: true,
},
},
{
scope: ["keyword.function", "function.method"],
style: {
foreground: "#fab283",
},
},
{
scope: ["keyword"],
style: {
foreground: "#9d7cd8",
italic: true,
},
},
{
scope: ["keyword.import"],
style: {
foreground: "#9d7cd8",
},
},
{
scope: ["operator", "keyword.operator", "punctuation.delimiter"],
style: {
foreground: "#56b6c2",
},
},
{
scope: ["keyword.conditional.ternary"],
style: {
foreground: "#56b6c2",
},
},
{
scope: ["variable", "variable.parameter", "function.method.call", "function.call"],
style: {
foreground: "#e06c75",
},
},
{
scope: ["variable.member", "function", "constructor"],
style: {
foreground: "#fab283",
},
},
{
scope: ["type", "module"],
style: {
foreground: "#e5c07b",
},
},
{
scope: ["constant"],
style: {
foreground: "#e06c75",
},
},
{
scope: ["property"],
style: {
foreground: "#e06c75",
},
},
{
scope: ["class"],
style: {
foreground: "#e5c07b",
},
},
{
scope: ["parameter"],
style: {
foreground: "#eeeeee",
},
},
{
scope: ["punctuation", "punctuation.bracket"],
style: {
foreground: "#eeeeee",
},
},
{
scope: ["variable.builtin", "type.builtin", "function.builtin", "module.builtin", "constant.builtin"],
style: {
foreground: "#7fd88f",
},
},
{
scope: ["variable.super"],
style: {
foreground: "#e06c75",
},
},
{
scope: ["string.escape", "string.regexp"],
style: {
foreground: "#7fd88f",
},
},
{
scope: ["keyword.directive"],
style: {
foreground: "#9d7cd8",
italic: true,
},
},
{
scope: ["punctuation.special"],
style: {
foreground: "#56b6c2",
},
},
{
scope: ["keyword.modifier"],
style: {
foreground: "#9d7cd8",
italic: true,
},
},
{
scope: ["keyword.exception"],
style: {
foreground: "#9d7cd8",
italic: true,
},
},
]
export const syntaxTheme = SyntaxStyle.fromTheme(syntaxThemeDark)

View file

@ -4,8 +4,8 @@ import path from "path"
import { useRouteData } from "@tui/context/route"
import { useSync } from "@tui/context/sync"
import { SplitBorder } from "@tui/component/border"
import { Theme } from "@tui/context/theme"
import { BoxRenderable, ScrollBoxRenderable } from "@opentui/core"
import { syntaxTheme, Theme } from "@tui/context/theme"
import { BoxRenderable, pathToFiletype, ScrollBoxRenderable } from "@opentui/core"
import { Prompt, type PromptRef } from "@tui/component/prompt"
import type { AssistantMessage, Part, ToolPart, UserMessage, TextPart, ReasoningPart } from "@opencode-ai/sdk"
import { useLocal } from "@tui/context/local"
@ -648,22 +648,6 @@ ToolRegistry.register<typeof BashTool>({
},
})
/*
const syntax = new SyntaxStyle({
keyword: { fg: RGBA.fromHex(Theme.syntaxKeyword), bold: true },
string: { fg: RGBA.fromHex(Theme.syntaxString) },
comment: { fg: RGBA.fromHex(Theme.syntaxComment), italic: true },
number: { fg: RGBA.fromHex(Theme.syntaxNumber) },
function: { fg: RGBA.fromHex(Theme.syntaxFunction) },
type: { fg: RGBA.fromHex(Theme.syntaxType) },
operator: { fg: RGBA.fromHex(Theme.syntaxOperator) },
variable: { fg: RGBA.fromHex(Theme.syntaxVariable) },
bracket: { fg: RGBA.fromHex(Theme.syntaxPunctuation) },
punctuation: { fg: RGBA.fromHex(Theme.syntaxPunctuation) },
default: { fg: RGBA.fromHex(Theme.syntaxVariable) },
})
*/
ToolRegistry.register<typeof ReadTool>({
name: "read",
container: "inline",
@ -708,7 +692,7 @@ ToolRegistry.register<typeof WriteTool>({
<For each={numbers()}>{(value) => <text style={{ fg: Theme.textMuted }}>{value}</text>}</For>
</box>
<box paddingLeft={1} flexGrow={1}>
<text>{code()}</text>
<code filetype={pathToFiletype(props.input.filePath!)} syntaxStyle={syntaxTheme} content={code()} />
</box>
</box>
</>
@ -819,13 +803,21 @@ ToolRegistry.register<typeof EditTool>({
</Match>
<Match when={code()}>
<box paddingLeft={1}>
<text>{code()}</text>
<code filetype={pathToFiletype(props.input.filePath!)} syntaxStyle={syntaxTheme} content={code()} />
</box>
</Match>
<Match when={props.input.newString && props.input.oldString}>
<box paddingLeft={1}>
<text>{props.input.oldString}</text>
<text>{props.input.newString}</text>
<code
filetype={pathToFiletype(props.input.filePath!)}
syntaxStyle={syntaxTheme}
content={props.input.oldString}
/>
<code
filetype={pathToFiletype(props.input.filePath!)}
syntaxStyle={syntaxTheme}
content={props.input.newString}
/>
</box>
</Match>
</Switch>

View file

@ -26,8 +26,10 @@ const parser = lazy(async () => {
p.setLanguage(Bash.language as any)
return p
} catch (e) {
const { default: Parser } = await import("web-tree-sitter")
const { default: treeWasm } = await import("web-tree-sitter/tree-sitter.wasm" as string, { with: { type: "wasm" } })
const { Parser, Language } = await import("web-tree-sitter")
const { default: treeWasm } = await import("web-tree-sitter/web-tree-sitter.wasm" as string, {
with: { type: "wasm" },
})
await Parser.init({
locateFile() {
return treeWasm
@ -36,7 +38,7 @@ const parser = lazy(async () => {
const { default: bashWasm } = await import("tree-sitter-bash/tree-sitter-bash.wasm" as string, {
with: { type: "wasm" },
})
const bashLanguage = await Parser.Language.load(bashWasm)
const bashLanguage = await Language.load(bashWasm)
const p = new Parser()
p.setLanguage(bashLanguage)
return p
@ -57,6 +59,9 @@ export const BashTool = Tool.define("bash", {
async execute(params, ctx) {
const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT)
const tree = await parser().then((p) => p.parse(params.command))
if (!tree) {
throw new Error("Failed to parse command")
}
const permissions = await Agent.get(ctx.agent).then((x) => x.permission.bash)
const askPatterns = new Set<string>()

View file

@ -6,8 +6,10 @@ const parser = async () => {
p.setLanguage(Bash.language as any)
return p
} catch (e) {
const { default: Parser } = await import("web-tree-sitter")
const { default: treeWasm } = await import("web-tree-sitter/tree-sitter.wasm" as string, { with: { type: "wasm" } })
const { Parser, Language } = await import("web-tree-sitter")
const { default: treeWasm } = await import("web-tree-sitter/web-tree-sitter.wasm" as string, {
with: { type: "wasm" },
})
await Parser.init({
locateFile() {
return treeWasm
@ -16,7 +18,7 @@ const parser = async () => {
const { default: bashWasm } = await import("tree-sitter-bash/tree-sitter-bash.wasm" as string, {
with: { type: "wasm" },
})
const bashLanguage = await Parser.Language.load(bashWasm)
const bashLanguage = await Language.load(bashWasm)
const p = new Parser()
p.setLanguage(bashLanguage)
return p
@ -62,6 +64,9 @@ function extractCommands(node: any): Array<{ command: string; args: string[] }>
// Extract and display commands
console.log("Source code: " + sourceCode)
if (!tree) {
throw new Error("Failed to parse command")
}
const commands = extractCommands(tree.rootNode)
console.log("Extracted commands:")
commands.forEach((cmd, index) => {

View file

@ -1,74 +0,0 @@
import { lazy } from "../util/lazy"
export namespace TreeSitter {
const Parser = lazy(async () => {
try {
return NativeParser()
} catch (e) {
return WasmParser()
}
})
const NativeParser = lazy(async () => {
const { default: Parser } = await import("tree-sitter")
return Parser
})
const WasmParser = lazy(async () => {
const { default: Parser } = await import("web-tree-sitter")
const { default: treeWasm } = await import("web-tree-sitter/tree-sitter.wasm" as string, {
with: { type: "wasm" },
})
await Parser.init({
locateFile() {
return treeWasm
},
})
return Parser
})
export async function parser() {
const p = await Parser()
const result = new p()
return result
}
const Languages: Record<string, { native: () => any; wasm: () => any }> = {
bash: {
native: () => import("tree-sitter-bash"),
wasm: () =>
import("tree-sitter-bash/tree-sitter-bash.wasm" as string, {
with: { type: "wasm" },
}),
},
typescript: {
native: () => import("tree-sitter-typescript"),
wasm: () =>
import("tree-sitter-typescript/tree-sitter-typescript.wasm" as string, {
with: { type: "wasm" },
}),
},
}
const Extensions = {
".ts": "typescript",
".tsx": "typescript",
".js": "typescript",
".jsx": "typescript",
".sh": "bash",
}
export async function language(extension: keyof typeof Extensions) {
const language = Extensions[extension]
if (!language) return undefined
const { native, wasm } = Languages[language]
try {
const { language } = await native()
return language
} catch (e) {
const { default: mod } = await wasm()
const language = await WasmParser().then((p) => p.Language.load(mod))
return language
}
}
}