Skip to content

feat(tui): color heredocs in bash commands in their own language - #54705

Closed
Nowaker wants to merge 2 commits into
anomalyco:v1from
Nowaker:heredoc-syntax
Closed

Nowaker wants to merge 2 commits into
anomalyco:v1from
Nowaker:heredoc-syntax

Conversation

@Nowaker

@Nowaker Nowaker commented Oct 12, 2026 •

Copy link
Copy Markdown

Note: I'm aware v1 is now the maintenance branch and new features go to v2 only. This PR is here for others to use, and I'll submit the v2 version once I've migrated my whole plugin system to it.

Issue for this PR

Closes #54701

Builds on the bash command coloring PR (#54704); its commit is the first one here. Review only the second commit.

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Each heredoc body in a bash command is colored in its own language. tree-sitter-bash marks the body but not its language, and opentui's injection mapping maps only fixed node types, so util/heredoc.ts finds each body and infers the language from the command that reads it:

  • python/uv → python, node → javascript, bun/deno/tsx → typescript, ruby/bundle/rails, perl, php, lua
  • sqlite3/psql/mysql/mariadb/duckdb/clickhouse-client → sql; jq/curl → json; kubectl/yq → yaml; bash/sh/zsh/ssh → bash
  • git commit/gh/glab → markdown; git apply/patch → diff; cat > file / tee file → the file's extension
  • through sudo, env, timeout, ssh, docker exec, kubectl exec, and cat <<EOF | psql
  • otherwise the body's #! line; otherwise nothing, so an unknown body keeps the shell string color

Each known body is highlighted on its own through the same cache, and its highlights replace bash's string color on that range. Bodies use the same non-red colors as the command. syntax.heredoc: false turns it off; on by default. Documented in tui.mdx with the table above.

Together with #54706: once both land, fileLanguage in util/heredoc.ts should call detectFiletype (one line) instead of filetype, so cat > work.slice <<'EOF' or tee x.timer colors a systemd unit as ini the way the file tools do. Whichever PR lands second can carry it; my fork runs that combination.

How did you verify your code works?

  • bun typecheck; bun test test/util test/config.test.tsx test/cli/tui: 154 pass, including 38 consumer cases and the highlight merge.
  • Real worker: Python, SQL, TypeScript and YAML bodies colored in place; a real TUI shows them under the $ prompt.

Screenshots / recordings

import/SELECT/export keywords, # comment/-- count them/// sum in the comment color:

$ python3 - <<'PY'
  import json, os
  print(json.dumps(data))  # comment
  PY
$ sqlite3 app.db <<'SQL'
  SELECT id, name FROM users WHERE active = 1;
  -- count them
  SQL

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

This PR is AI generated, but under direct supervision and on request of @Nowaker.

@github-actions

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

The bash tool's command was the one block of code in a session drawn as
plain text, while write, edit and apply_patch already go through
tree-sitter. It is now the same `<text>$ command</text>` as before, with
each token a colored span, so it wraps, indents and fits the block
exactly as the plain command does; only the colors differ.

The text appears plain at once and takes its colors when the parser
worker answers. Highlighting goes through a cache keyed by language and
content (packages/tui/src/util/highlight-cache.ts), so a remounted
transcript or a reopened session does not reparse every command.
Identical concurrent requests share one round trip, a failed one is
retried, and the cache holds at most 1000 entries or 4M characters.

In the transcript red means a failed or rejected tool call, so the
command is colored with a syntax style that never uses red
(packages/tui/src/theme/tool-code.ts): every syntax slot that comes out
red, or in the theme's error color, takes a non-red alternative, such as
the function color for variables, commands and builtins. A test checks
every built-in theme in dark and light.

nvim-treesitter's bash query narrows its number, constant and shebang
captures with `#lua-match?`, which web-tree-sitter does not evaluate, so
every word came back as a number and every variable as a constant.
packages/tui/src/util/bash-highlight.ts applies those patterns after the
worker answers. It also drops the query's `@spell` captures: the theme
colors spell as plain text, which turned every comment back to the text
color.

`syntax.bash: false` in tui.json restores the plain-text command.
Nowaker added a commit to Nowaker/opencode that referenced this pull request Oct 12, 2026
The colored bash command now lays out byte-identically to the plain one,
and tool-call code never draws in red (b236851, 91e33c5); upstream
branches amended, PRs anomalyco#54704, anomalyco#54705 and anomalyco#54707 updated, vt-241 on
desktop and m4max.

AI-Model: anthropic/claude-opus-5-5
AI-Variant: high
AI-Tool: opencode 1.19.0-vt-227-dfb56f76c7
AI-Platform: linux
AI-Harness: Vibeterm e6434eb
AI-Session-ID: ses_ed1e2b932ffe6tTqhsOiGuYXRE
A heredoc body in a bash command is colored as one shell string, though
it is usually a whole program: Python fed to `python3 -`, SQL to
`psql`, a TypeScript file written with `cat > x.ts`. tree-sitter-bash
marks the body but not its language, and opentui's injection mapping
only maps fixed node types, so the language comes from the command
that reads the body (packages/tui/src/util/heredoc.ts):

- python/uv -> python, node -> javascript, bun/deno/tsx -> typescript,
  ruby/bundle/rails -> ruby, perl, php, lua
- sqlite3/psql/mysql/mariadb/duckdb/clickhouse-client -> sql
- jq/curl -> json, kubectl/yq -> yaml, bash/sh/zsh/ssh -> bash
- git commit/gh/glab -> markdown, git apply/patch -> diff
- cat/tee to a file -> the file's language by extension
- a command behind sudo, env, timeout, ssh, docker exec or kubectl
  exec counts as the command it runs; `cat <<EOF | psql` as psql
- otherwise a #! line on the body, otherwise nothing: an unknown body
  keeps the shell's string color rather than a guess

After the worker colors the command as bash, each body with a known
language is highlighted on its own, through the same cache, and its
highlights replace bash's string color on that range. A language
without a parser, or a failed request, leaves the bash coloring.
Bodies use the same non-red syntax style as the command.

`syntax.heredoc: false` in tui.json keeps every body a shell string.
Nowaker added a commit to Nowaker/opencode that referenced this pull request Oct 13, 2026
Heredoc bodies written to systemd, networkd and quadlet files are
colored as ini (246c6c1); PRs anomalyco#54705 and anomalyco#54706 amended; vt-259 on
desktop and m4max.

AI-Model: anthropic/claude-opus-5-5
AI-Variant: high
AI-Tool: opencode 1.19.0-vt-227-dfb56f76c7
AI-Platform: linux
AI-Harness: Vibeterm 41f09c2
AI-Session-ID: ses_ed1e2b932ffe6tTqhsOiGuYXRE
@Nowaker

Nowaker commented Nov 2, 2026

Copy link
Copy Markdown
Author

Closing: this targets the v1 TUI, which upstream now maintains for critical fixes only, so it is not going to be merged. The branch stays on Nowaker/opencode if anyone wants to pick it up.

@Nowaker Nowaker closed this Nov 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant