Repository navigation
Conversation
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.
Generic tool output was drawn as plain text, and nothing knows its type: a plugin tool returns a string and an MCP text result carries no content type, so a JSON result from `browser` read as a grey wall. `syntax.tool_output` in tui.json maps a tool name or glob (`browser`, `github_*`, `*`) to a language id, `auto` or `none`; an exact name wins over globs and the most specific glob over the rest (packages/tui/src/util/tool-output.ts). `auto` colors output that is a JSON object or array: a bounded sniff (bracket pair, at most 100,000 characters, one parse) whose answer is cached by content. Only output that is shown is sniffed or colored. Unset by default, so output stays plain as upstream draws it. The output is one text renderable whose content becomes the same text as colored chunks when the parser worker answers, so it wraps exactly as the plain text does (test/cli/tui/tool-output.test.tsx compares the frames at 40, 80 and 160 columns), and it uses the non-red tool-code palette, since red means a failed tool call. The whole output is highlighted, once and cached, even while the block is collapsed: a preview cut through the middle of a JSON value does not parse, so the preview takes the whole output's highlights that fall inside the part it shares with the output, and its ellipsis stays plain.
syntax.user_messages (default false) runs the markdown grammar over a submitted message and colors headings at every level, numbered and bulleted lists, inline code, fenced code in its language, emphasis, links, quotes and tables. The colors are set on the typed characters through StyledOutput, so nothing is hidden and the message wraps exactly as the plain text does. forgivingMarkdown (util/user-markdown.ts) drops what people write by accident rather than as markdown: setext headings (a line followed by --- or ===), indented code blocks, a quote's lazy continuation lines, intra-word emphasis such as 2*3*4, and spell/nospell captures. The palette is toolCodeTheme, extended to the markdown slots, so a user message never draws in red, which in the transcript means failure. Messages over 100,000 characters stay plain.
A block in a submitted message, from an opening line to a closing
line, can be drawn by a formatter instead of as markdown. Rules come
from syntax.blocks in tui.json and from TUI plugins through the new
api.blocks.register({ open, close, format }), scoped to the plugin's
lifetime.
In open, * matches a run of non-space characters, and each * in close
stands for what it matched, so [tool_*] ... [/tool_*] covers every
such block and closes each one only at its own name (messageSegments
in util/message-blocks.ts). An opening line with no closing line stays
ordinary text.
format yaml colors key: value lines: keys, values by type, and the
opening and closing lines muted (yamlSegments, yamlColors). Any other
format is a language id highlighted by its grammar. Like
syntax.user_messages, only colors change: messageChunks covers the text
exactly, so it wraps as plain. Blocks are drawn whether or not
user_messages is on.
Contributor
|
The following comment was made by an LLM, it may be inaccurate: |
Nowaker
added a commit
to Nowaker/opencode
that referenced
this pull request
Oct 14, 2026
Feature record and session record for syntax.user_messages, syntax.blocks and api.blocks.register: the Vibeterm route plugin rule (opencode-tools 0497c41), upstream issues anomalyco#55439/anomalyco#55440 and PRs anomalyco#55441/anomalyco#55442 against v1, vt-292 on desktop and m4max with the gate passed, and the knob enforced through Vibeterm settings on both hosts. The tool syntax coloring timeline notes the palette slots and toolInputColors this shares. AI-Tool: opencode 1.19.0-vt-285-dfb56f76c7 AI-Model: anthropic/claude-opus-5-5 AI-Variant: high AI-Platform: linux AI-Harness: Vibeterm 1e793a4-dirty AI-Session-ID: ses_ec57be11affe3kxXJ4WyM3OwOY
1 task done
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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 #55440
Builds on #55441 (markdown in user messages), which builds on #54968 and #54704; their commits come first here. Review only the last commit.
Type of change
What does this PR do?
Tools and plugins that prompt on a user's behalf wrap metadata in delimited blocks (
[vibeterm_metadata]...[/vibeterm_metadata]). In the transcript that block is indistinguishable from what the person typed. This adds a way for a plugin, or the user's config, to say how such a block is drawn.api.blocks.register({ open, close, format })on the TUI plugin API, scoped to the plugin like its other registrations, andsyntax.blocksin tui.json, which is tried first.opento the next line that is exactlyclose, surrounding spaces aside. Inopen,*matches a run of non-space characters, and each*inclosestands for what it matched, so[tool_*]...[/tool_*]covers a family of blocks and closes each at its own name. An opening line with no closing line stays ordinary text (messageSegmentsinutil/message-blocks.ts).format: "yaml"colorskey: valuelines: keys, values by type (strings, numbers, booleans, null),#comments and the delimiter lines muted. Any otherformatis a language id colored by its grammar (json,sql, ...).syntax.user_messagesis on.<markdown>renderable, which has no per-range hook; extending it there would mean a markdown-level extension, so I left it out.If the tool input block (#54707) lands, its key/value colors and
yamlColorsare the same palette and can become one function; my fork has them merged that way.Bigger picture, and the part I'd most like feedback on: this is a narrow first step toward plugin-defined renderers for message content. A plugin can already add routes and slots; it cannot say how a span of message text is drawn. A small "content formatter" registry (match a span, return colors or a component) would let plugins format their own protocol blocks, tool-specific markers or structured attachments without each one patching the session view. Happy to reshape this toward whatever API you'd prefer.
How did you verify your code works?
bun typecheckin packages/tui, packages/plugin and packages/opencode; the full packages/tui suite passes.message-blocks.test.tscovers matching by name, a mismatched closing tag inside a block, unclosed blocks, rule order and exact coverage of the text;yamlSegmentskeeps every character and kinds values by type;user-text.test.tsxcompares plain and colored frames with a YAML block at 40, 80 and 160 columns.syntax.blocksand once registered by a TUI plugin throughapi.blocks.register: keys, strings, numbers and booleans are colored, the delimiter lines muted, and the screen text is byte-identical to the rule being absent.Screenshots / recordings
Checklist
This PR is AI generated, but under direct supervision and on request of @Nowaker.