Skip to content

feat: add opt-in session-title status indicator - #65

Merged
willytop8 merged 1 commit into
mainfrom
wr/session-title-indicator
Aug 29, 2026
Merged

feat: add opt-in session-title status indicator#65
willytop8 merged 1 commit into
mainfrom
wr/session-title-indicator

Conversation

@willytop8

Copy link
Copy Markdown
Owner

Problem

An unattended goal run gives no continuous signal that it is still alive. command.execute.before output 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:

▶ ship the release · 3/10 · 2m · 45k/200k

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:

  1. Streaming is excluded. The sync wraps the event hook, and message.updated fires 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.

  2. 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 clear would 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

  • Original title captured before the first overwrite, restored by /goal clear.
  • A render identical to the last one skips the API call, so /goal status costs nothing.
  • Title updates are cosmetic: failures log at debug level and never interrupt the goal loop.
  • Documented limitation: a hard kill loses the captured original. Clear then leaves the host's title alone, but cannot recover the pre-goal title.

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 finally so the display matches the state actually reached even if a hook throws.

No TUI plugin entrypoint, no @opentui peer 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 0

  • 411 tests / 411 pass (401 on main before this change; +10 new)
  • mutation contract 68/68, behavior benchmark 100
  • type contract (NodeNext + Bundler), command hook smoke, packed host contract, packed tool contract (11 tools)
  • 0 vulnerabilities
  • all internal documentation links and anchors verified to resolve

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

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>
@willytop8
willytop8 merged commit f15026b into main Aug 29, 2026
10 checks passed
@willytop8
willytop8 deleted the wr/session-title-indicator branch August 29, 2026 17:04
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