From effb48ea91c21301724e321f487de23f46b78c78 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 12 Aug 2026 22:47:37 -0400 Subject: [PATCH] feat(tui): highlight Bash commands --- packages/tui/src/component/prompt/index.tsx | 43 +++++++++++++++++++++ packages/tui/src/routes/session/index.tsx | 26 +++++++++++-- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/packages/tui/src/component/prompt/index.tsx b/packages/tui/src/component/prompt/index.tsx index af2f40bf793..d524a8bc739 100644 --- a/packages/tui/src/component/prompt/index.tsx +++ b/packages/tui/src/component/prompt/index.tsx @@ -5,6 +5,7 @@ import { MouseEvent, PasteEvent, decodePasteBytes, + getTreeSitterClient, type ColorInput, type KeyEvent, } from "@opentui/core" @@ -98,6 +99,7 @@ export type PromptRef = { } const DRAFT_RETENTION_MIN_CHARS = 20 +const SHELL_SYNTAX_HIGHLIGHT_REF = 65_535 function randomIndex(count: number) { if (count <= 0) return 0 @@ -340,6 +342,47 @@ export function Prompt(props: PromptProps) { }) let disposed = false let pasteQueue = Promise.resolve() + let syntaxHighlightVersion = 0 + + createEffect(() => { + const mode = store.mode + const content = store.prompt.text + const style = syntax() + const version = ++syntaxHighlightVersion + + if (input && !input.isDestroyed) input.editBuffer.removeHighlightsByRef(SHELL_SYNTAX_HIGHLIGHT_REF) + if (mode !== "shell" || !content || !input || input.isDestroyed) return + + const timeout = setTimeout(() => { + getTreeSitterClient() + .highlightOnce(content, "bash") + .then((result) => { + if (version !== syntaxHighlightVersion || !result.highlights || !input || input.isDestroyed) return + + const bytes = new TextEncoder().encode(content) + const decoder = new TextDecoder() + result.highlights.forEach(([start, end, group]) => { + const styleId = style.getStyleId(group) + if (styleId === null) return + const before = decoder.decode(bytes.subarray(0, start)) + const highlighted = decoder.decode(bytes.subarray(start, end)) + input.editBuffer.addHighlightByCharRange({ + start: promptOffsetWidth(before) - (before.match(/\n/g)?.length ?? 0), + end: + promptOffsetWidth(before) + + promptOffsetWidth(highlighted) - + ((before + highlighted).match(/\n/g)?.length ?? 0), + styleId, + priority: 0, + hlRef: SHELL_SYNTAX_HIGHLIGHT_REF, + }) + }) + renderer.requestRender() + }) + .catch(() => {}) + }, 50) + onCleanup(() => clearTimeout(timeout)) + }) function enqueuePaste(run: (changed: () => boolean) => Promise) { pasteQueue = pasteQueue diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index f9c1eeb1808..44c4f719d4c 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -1938,6 +1938,7 @@ function RevertMessage(props: { function ShellMessage(props: { message: Extract }) { const theme = useTheme("elevated") + const { currentSyntax: syntax } = useThemes() const output = createMemo(() => stripAnsi(props.message.output?.output.trim() ?? "")) return ( @@ -1952,7 +1953,16 @@ function ShellMessage(props: { message: Extract - $ {props.message.command} + + $ + + {output()} @@ -2784,6 +2794,7 @@ const SHELL_DISPLAY_LIMIT = 1024 * 1024 function Shell(props: ToolProps) { const theme = useTheme() + const { currentSyntax: syntax } = useThemes() const ctx = use() const client = useClient() const data = useData() @@ -2915,11 +2926,20 @@ function Shell(props: ToolProps) { fallback={ {prompt()} - {limitedInput()} + } > - {limitedInput()} + + + + {limitedOutput()}