feat(tui): add native status line template system#13885
feat(tui): add native status line template system#13885diegomarino wants to merge 5 commits intoanomalyco:devfrom
Conversation
Fixes anomalyco#8619 Users define per-target template strings in config (tui.status_line) resolved from (1) static built-in variables, (2) shell commands, and (3) plugin data. Display targets: terminal_title, session_footer, home_footer. Replaces the plugin breadcrumb approach without LLM calls. - StatusLine resolver with format specs (:basename, :bar, :k, time) - GET /tui/statusline endpoint with configurable polling interval - Plugin hook: tui.statusLine.variables - 24 unit tests
bd1113d to
6157cdd
Compare
There was a problem hiding this comment.
Pull request overview
This PR implements a native status line template system for the TUI that displays project info, session data, model/token stats, and custom shell command output in the terminal title and TUI footers. The feature addresses issue #8619 by providing a zero-LLM-cost alternative to the plugin breadcrumb approach, which was consuming ~30 tokens per message by injecting display-only content into the conversation context.
Changes:
- Added server-side template resolution system with built-in variables (project/session/model data), format specs (basename, k, bar, time), and shell command execution
- Introduced new API endpoint
/tui/statuslinefor polling resolved templates with configurable intervals - Extended plugin hook system with
tui.statusLine.variablesfor custom variable injection - Integrated status line display into TUI components (terminal title, session footer, home footer)
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/sdk/js/src/v2/gen/types.gen.ts | Generated TypeScript types for status line config and API response |
| packages/sdk/js/src/v2/gen/sdk.gen.ts | Generated SDK client method for /tui/statusline endpoint |
| packages/plugin/src/index.ts | Added tui.statusLine.variables hook for plugin-provided variables |
| packages/opencode/test/statusline/statusline.test.ts | Unit tests for format specs, template resolution, and shell command execution |
| packages/opencode/src/statusline/index.ts | Core implementation: variable builtins, shell commands, template resolution, format specs |
| packages/opencode/src/server/routes/tui.ts | API route handler for /tui/statusline endpoint |
| packages/opencode/src/config/config.ts | Zod schema validation for status_line config with template targets, interval, and commands |
| packages/opencode/src/cli/cmd/tui/routes/session/index.tsx | Added status line footer display in session view |
| packages/opencode/src/cli/cmd/tui/routes/home.tsx | Replaced default home footer with status line template when configured |
| packages/opencode/src/cli/cmd/tui/context/statusline.tsx | SolidJS context provider for polling and managing status line state |
| packages/opencode/src/cli/cmd/tui/app.tsx | Integrated StatusLineProvider and terminal title updates |
| docs/statusline.md | Comprehensive documentation of configuration, syntax, variables, and examples |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| body?: never | ||
| path?: never | ||
| query?: { | ||
| directory?: string |
There was a problem hiding this comment.
The API type definition includes a directory query parameter that is not used in the route implementation. The route only validates and uses sessionID. This unused parameter should be removed from the type definition to avoid confusion.
| directory?: string |
There was a problem hiding this comment.
directory is a global query parameter applied by middleware in server.ts (line 226) — it appears on every SDK method, not just this one. It scopes the request to the correct project instance. No change needed.
| */ | ||
| public statusline<ThrowOnError extends boolean = false>( | ||
| parameters?: { | ||
| directory?: string |
There was a problem hiding this comment.
The directory parameter is defined in the SDK method but is never used by the API endpoint. The server-side route at /tui/statusline only validates and uses sessionID. This parameter should be removed from the SDK method signature to match the actual API implementation.
There was a problem hiding this comment.
Same as above — directory is global middleware, present on all SDK methods. Not specific to this endpoint.
| const start = () => { | ||
| if (timer) clearInterval(timer) | ||
| poll() | ||
| timer = setInterval(poll, frequency() * 1000) | ||
| } |
There was a problem hiding this comment.
The start function uses the current value of frequency() when setting up the interval, but if the interval changes after the first poll (via setFrequency), the timer won't be updated to use the new interval. The timer continues using the old frequency value until start is called again. Consider restarting the timer when the frequency changes, or use a different approach to ensure the interval is dynamically updated.
There was a problem hiding this comment.
The frequency only changes once (first poll sets it from the default to the configured value). After that, start() is re-invoked on every session change via the createEffect, which restarts the timer with the current frequency. A dynamic timer update mechanism would add complexity without practical benefit.
| * Template strings per display target with {variable:format} placeholders | ||
| */ | ||
| templates?: { | ||
| [key: string]: string |
There was a problem hiding this comment.
The config schema in config.ts enforces specific template keys using z.enum(["terminal_title", "session_footer", "home_footer"]), but the generated type definition allows any string key. This creates a discrepancy where the TypeScript types are less strict than the runtime validation. The generated types should reflect the same constraint to provide better type safety and IDE support.
| [key: string]: string | |
| terminal_title?: string | |
| session_footer?: string | |
| home_footer?: string |
There was a problem hiding this comment.
The SDK types are auto-generated by ./packages/sdk/js/script/build.ts. z.record(z.enum([...]), z.string()) maps to { [key: string]: string } in OpenAPI 3.1. Hand-editing generated files would be overwritten on next regeneration.
📖 StatusLine Template System — Reference DocumentationConfigurationAdd {
"tui": {
"status_line": {
"templates": {
"terminal_title": "opencode | {project_name} ({shell:branch})",
"session_footer": "{model_id} | {total_tokens:k} tokens | ${total_cost}",
"home_footer": "{cwd:basename} on {shell:branch}",
},
"interval": 5,
"commands": {
"branch": "git branch --show-current",
"short_cwd": "echo $PWD | sed \"s|$HOME|~|\" | rev | cut -d/ -f1-3 | rev",
},
},
},
}Config Fields
Display Targets
Template SyntaxTemplates use Built-in VariablesAlways available: Session-only: Format Specs
Shell CommandsCommands run via "commands": {
"branch": "git branch --show-current",
"dirty": "git diff-index --quiet HEAD -- 2>/dev/null && [ -z \"$(git ls-files --others --exclude-standard | head -1)\" ] && echo '' || echo '*'",
"short_path": "echo $PWD | sed \"s|$HOME|~|\" | awk -F/ '{for(i=1;i<NF;i++) printf substr($i,1,1)\"/\"; print $NF}'",
"wt_path": "toplevel=$(git rev-parse --show-toplevel 2>/dev/null); main=$(git worktree list 2>/dev/null | head -1 | awk '{print $1}'); if [ \"$toplevel\" != \"$main\" ] && echo \"$toplevel\" | grep -q '.worktrees\\|.worktree'; then echo \"$(basename $main)/.w/$(basename $toplevel)\"; else basename \"${toplevel:-$PWD}\"; fi",
"changes": "git diff HEAD --numstat 2>/dev/null | awk '{a+=$1;r+=$2} END {if(a+r>0) printf \"+%d/-%d\",a,r; else print \"\"}'",
"stashes": "git stash list 2>/dev/null | wc -l | tr -d ' '",
"ahead_behind": "git rev-list --left-right --count HEAD...@{u} 2>/dev/null | awk '{if($1>0||$2>0) printf \"+%s -%s\",$1,$2; else print \"=\"}'"
}Plugin Variablesplugin.hook("tui.statusLine.variables", async (input, result) => {
result.variables.my_var = "custom value"
return result
})Full Example{
"tui": {
"status_line": {
"templates": {
"terminal_title": "{shell:wt_path} ({shell:branch}{shell:dirty}) [{model_id}]",
"session_footer": "ctx {context_used_pct:bar10} {context_used_pct}% | {total_tokens:k} tok | ${total_cost} | {session_duration:%H:%M:%S}",
"home_footer": "{shell:short_path} on {shell:branch} {shell:changes}",
},
"interval": 5,
"commands": {
"branch": "git branch --show-current",
"dirty": "git diff-index --quiet HEAD -- 2>/dev/null && [ -z \"$(git ls-files --others --exclude-standard | head -1)\" ] && echo '' || echo '*'",
"wt_path": "toplevel=$(git rev-parse --show-toplevel 2>/dev/null); main=$(git worktree list 2>/dev/null | head -1 | awk '{print $1}'); if [ \"$toplevel\" != \"$main\" ] && echo \"$toplevel\" | grep -q '.worktrees\\|.worktree'; then echo \"$(basename $main)/.w/$(basename $toplevel)\"; else basename \"${toplevel:-$PWD}\"; fi",
"short_path": "echo $PWD | sed \"s|$HOME|~|\" | awk -F/ '{for(i=1;i<NF;i++) printf substr($i,1,1)\"/\"; print $NF}'",
"changes": "git diff HEAD --numstat 2>/dev/null | awk '{a+=$1;r+=$2} END {if(a+r>0) printf \"+%d/-%d\",a,r; else print \"\"}'",
"stashes": "git stash list 2>/dev/null | wc -l | tr -d ' '",
},
},
},
} |
|
Based on the interest in visual formatting from #8619 (left/right segments, multi-line, colors): The current implementation resolves templates to plain strings. This works well for content, but makes it impossible for a rendering layer to style individual segments (color, alignment, truncation) — the resolved output is just If visual formatting is desired in the future, // Current: resolve() → string
"main | 15k tok | $0.02"
// Future: resolve() → Segment[]
[
{ value: "main", variable: "shell:branch" },
{ value: " | ", literal: true },
{ value: "15k", variable: "total_tokens", spec: "k" },
{ value: " tok | $", literal: true },
{ value: "0.02", variable: "total_cost" },
]This would let a renderer do things like:
The change would be localized to Not proposing this for this PR — just noting the path forward if the team wants richer visual control. |
What does this PR do?
Fixes #8619
Adds a native status line template system. Users define per-target template strings in config (
tui.status_line.templates) that are resolved server-side from built-in variables (project info, session data, model/token stats), shell commands, and plugin-provided data. Display targets:terminal_title,session_footer,home_footer.This replaces the plugin breadcrumb approach (which costs ~30 tokens/message and pollutes chat context) with a zero-LLM-cost alternative.
How did you verify your code works?
tsgo --noEmitclean across all 17 packagesGET /tui/statuslinereturns resolved templates