feat: add opt-in session-title status indicator - #65
Merged
Conversation
An unattended goal run has no continuous signal that it is alive: command
hook output is not rendered in the TUI on current OpenCode builds, so the
only feedback is whatever the model happens to say.
Mirror live goal status into the OpenCode session title, which the TUI
renders persistently:
▶ ship the release · 3/10 · 2m · 45k/200k
Status icon, objective, auto-continues used/limit, elapsed, and context
tokens/budget. Blocked outranks paused in the icon because it needs the
user rather than a resume; a paused goal freezes its elapsed clock.
Off by default, since it overwrites a user-visible field. When enabled the
original title is captured before the first overwrite and restored by
`/goal clear`. A render identical to the last one skips the API call, so
read-only commands cost nothing, and update failures log at debug level
without interrupting the goal loop.
Two things this deliberately gets right:
- The sync is skipped for `message.updated`, which streams many times per
assistant turn. Syncing there would put an API round-trip in the
response path for a cosmetic update; idle, compaction, and interruption
events already cover every state the indicator renders.
- Titles the plugin itself wrote are recognized and never captured as the
user's "original". After a hard process kill the session still carries a
status line, and capturing it would make `/goal clear` promote stale
goal status to the permanent session title.
Also documents OpenCode 2 as unsupported and untested — the peer/engine
pin is `>=1.17.15 <2` — with a checklist of what a supported claim would
require, and records that this plugin is server-only so its configuration
lives entirely in opencode.json.
Needs no TUI plugin entrypoint, no @OpenTui peer dependencies, and no
build step, preserving the zero-dependency, no-build posture.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
Problem
An unattended goal run gives no continuous signal that it is still alive.
command.execute.beforeoutput is not rendered in the TUI on current OpenCode builds, so the only feedback is whatever the model happens to say between turns.Change
Mirror live goal status into the OpenCode session title, which the TUI renders persistently:
Status icon, objective, auto-continues used / limit, elapsed, and context tokens / budget.
▶running,⏸paused,⛔blocked — blocked outranks paused because it needs the user, not just a resume. A paused goal freezes its elapsed clock rather than running on.Opt in with
sessionTitleStatus: true. Off by default, because it overwrites a user-visible field.Two details worth reviewing
Both of these were bugs in the first draft, caught before this PR and now covered by tests proven to fail without the fix:
Streaming is excluded. The sync wraps the
eventhook, andmessage.updatedfires many times per assistant turn. Syncing there put an API round-trip in the response path for a cosmetic update. It now syncs on commands and on idle/compaction/interruption only — which is also the cadence a human reads the indicator at.The plugin recognizes its own titles. The captured "original" title is in-memory only. After a hard process kill the session still carries a status line, so the next capture would take that as the user's title and
/goal clearwould promote stale goal status to the permanent session title — worse than not restoring. Titles starting with a status icon are never captured as an original.Behavior
/goal clear./goal statuscosts nothing.Implemented by wrapping the two hooks that gate all state change rather than threading a sync call through every mutation site — a missed site would show a stale status. Runs in
finallyso the display matches the state actually reached even if a hook throws.No TUI plugin entrypoint, no
@opentuipeer dependencies, no build step.Also included
Documents OpenCode 2 as unsupported and untested (the peer/engine pin is
>=1.17.15 <2), notes the dual-shape session adapter already in place, and lists what would have to pass against a real v2 build before the pin is widened. Records that this plugin is server-only, so the two-file TUI plugin config split does not apply to it.Verification
npm run release:check→ exit 0mainbefore this change; +10 new)Three separate mutants were used to confirm the new tests are not vacuous: removing the streaming exclusion, removing the stale-title recognition, and flipping the opt-in default each fail their corresponding test.
Not verified
Not exercised against a live OpenCode host — unit/contract level only. Worth confirming the title actually renders as expected in the TUI before the next release.
🤖 Generated with Claude Code