Skip to content

feat: agent status (OSC 9001) + terminal in-band protocol features#156

Open
matej21 wants to merge 12 commits into
mainfrom
feat/agent-status
Open

feat: agent status (OSC 9001) + terminal in-band protocol features#156
matej21 wants to merge 12 commits into
mainfrom
feat/agent-status

Conversation

@matej21

@matej21 matej21 commented Jun 23, 2026

Copy link
Copy Markdown
Member

Summary

A batch of in-band terminal-protocol features on this branch, headlined by
Agent Status (OSC 9001) — the focus of this session (final commit fc9d724e).

Agent Status (OSC 9001) — new this session

Report what an AI coding agent (Claude Code, Codex, …) is doing in each pane via
a push-based, in-band escape sequence, and remember/resume its session across
restarts.

Display & protocol

  • Per-tab lifecycle indicator, a cross-project sidebar AGENTS section (click
    to jump), a per-project terminal_agent_status field in the remote API, and a
    desktop notification on blocked/done. Runtime-only.
  • Bundled Claude Code plugin (integrations/claude-code/) maps lifecycle
    hooks → states; OKENA_TTY lets hooks without a controlling terminal emit the OSC.

Session capture, persistence & resume (harness-extensible)

  • Reserved lbl keys agent/session_id/transcript_path → a sticky per-pane
    AgentSession (UUID-validated, survives st=clear), persisted per terminal in
    workspace.json, resumed on restore behind the opt-in
    auto_resume_agent_sessions setting (claude --resume <id>).
  • Per-harness resume/transcript logic behind a gpui-free
    okena_core::agent_harness registry keyed by agent id (desktop + headless);
    Claude Code + Codex impls in the okena-ext-* crates — adding a harness is additive.
  • Auto-resume requires a session backend (the terminal_id the session is keyed
    by only survives a restart with tmux/dtach/screen).

Hardening (internal review pass)

  • Bound custom/labels (mirror the OSC 99 caps) against unbounded in-band input
    amplified to every remote client; base64 decode off-lock; empty msg → default body.
  • Headless PTY loop now drains the remote_dirty edge so agent status pushes to
    remote/mobile clients (was desktop-only).
  • reveal_terminal no longer clears individual-zoom on a same-project click;
    corrected the OKENA_TTY tmux-reattach docs.

Docs: docs/agent-status.md, crates/okena-terminal/CLAUDE.md.

Other terminal-protocol commits already on the branch

OSC 1337 cwd · OSC 9;4 progress + tab progress (+ percent) · OSC 133 jump-to-failed ·
OSC 52 clipboard read (opt-in) · CSI 14t pixel size · kitty keyboard protocol
(level 1) + Esc/Tab routing.

Testing

  • cargo build (workspace) ✅; tests green for the touched crates (okena-core,
    okena-terminal, okena-workspace, okena-state, okena-ext-claude, okena-views-terminal).
  • New tests: OSC 9001 parsing incl. hostile-input bounds & session capture,
    remote_dirty edges, reveal_terminal regressions, UUID validation, the Claude
    harness command, parse_labels_json.
  • ⚠️ Not runtime-verified: resume-on-restart (desktop + tmux + claude) is
    build-/unit-tested only, not exercised live.

Follow-ups (not in this PR)

  • Codex: confirm the exact codex resume invocation (harness stub returns None)
    • Codex session-capture glue.
  • Transcript stats UI (UC1) — okena-ext-codex already parses its JSONL.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KbKbY5iGGgXSnpHz2V5aSr

matej21 and others added 12 commits June 22, 2026 17:52
iTerm2 shell integration emits OSC 1337 ; CurrentDir=<path> instead of
OSC 7. Feed it into the same reported_cwd storage so the sidebar,
"new tab here", and cwd tracking work regardless of which sequence the
shell speaks. Other OSC 1337 subcommands are deliberately ignored.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbKbY5iGGgXSnpHz2V5aSr
Add per-terminal progress state for the ConEmu/Windows Terminal protocol
OSC 9 ; 4 ; st ; pr (also used by WezTerm, Ghostty, Kitty). Programs like
npm/cargo emit it to report progress. Previously these sequences were
swallowed as plain-text notifications; now they're parsed into a
TerminalProgress state exposed via Terminal::progress(). UI consumption
comes in a follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbKbY5iGGgXSnpHz2V5aSr
Surface OSC 9;4 progress as a thin 2px bar pinned to the bottom of each
terminal tab. Determinate states fill value%; indeterminate renders a
dim full-width busy bar. Colored by state: normal=border_active,
error=error, paused=warning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbKbY5iGGgXSnpHz2V5aSr
Add JumpToPrevious/NextFailedCommand actions that walk only the prompts
whose command finished with a non-zero exit code, mirroring the existing
prompt-jump navigation. Bound to cmd-shift-up / cmd-shift-down in the
TerminalPane context. The viewport scroll step is factored into a shared
helper reused by both jump-to-prompt and jump-to-failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbKbY5iGGgXSnpHz2V5aSr
Handle OSC 52 read requests (OSC 52 ; c ; ?) where a terminal app asks
for the clipboard contents. Gated behind a new allow_clipboard_read
setting that defaults to OFF, since clipboard read lets a program
exfiltrate copied data. Requests are queued by the event listener and
drained in the PTY event loop, where the setting and the system
clipboard are reachable; when disabled they're silently dropped. OSC 52
write is unaffected and always allowed. Adds a settings-panel toggle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbKbY5iGGgXSnpHz2V5aSr
Answer the XTWINOPS 'report text area size in pixels' query (CSI 14 t).
alacritty emits a TextAreaSizeRequest with a formatter; the listener now
supplies the current geometry (cols/rows x cell pixel dims) and writes
the reply back to the PTY. CSI 18 t (size in cells) already worked via
PtyWrite. The listener's shared query state (palette + size) is grouped
into a CurrentState struct to keep the constructor compact.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbKbY5iGGgXSnpHz2V5aSr
Honor the kitty keyboard protocol 'disambiguate escape codes' flag
(TermMode::DISAMBIGUATE_ESC_CODES), which alacritty already tracks but
okena ignored — it always sent legacy bytes. When an app enables
disambiguate mode, ambiguous keys now emit CSI u: Esc (always),
ctrl/alt+key (Ctrl+i vs Tab), and modified Enter/Tab/Backspace
(Shift+Tab). Plain text, arrows, and Home/End fall through to the
existing legacy logic, so there is zero change when the flag is off.

Higher levels (report-all-keys-as-esc, associated text, event types)
need input-dispatch changes and are tracked as a follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbKbY5iGGgXSnpHz2V5aSr
alacritty gates ALL kitty keyboard-mode handling (push/pop/set/report of
the CSI u mode sequences) behind config.kitty_keyboard, which defaults to
false. With it off, an app enabling the protocol was silently ignored and
term.mode() never reflected DISAMBIGUATE_ESC_CODES, so the level-1
encoder added in 71e69e81 never fired and CSI ? u queries went
unanswered. Enable the flag so the protocol actually works.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbKbY5iGGgXSnpHz2V5aSr
Determinate OSC 9;4 progress now shows the percentage in the tab label
(e.g. "build 60%") so it reads even when the bar is a thin sliver, and
bump the bar to 3px. Indeterminate progress stays bar-only (no value).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbKbY5iGGgXSnpHz2V5aSr
The input::tests unit tests drive key_to_bytes with hand-built flags,
bypassing the config->mode->flag chain that the missing kitty_keyboard
config flag broke. Add tests that push CSI > 1 u through process_output
and assert kitty_keyboard_flags() flips and the encoder emits CSI u, so
the wiring (and TermConfig.kitty_keyboard) can't silently regress.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbKbY5iGGgXSnpHz2V5aSr
Esc, Tab and Shift+Tab reach the terminal pane via dedicated GPUI
actions (SendEscape/SendTab/SendBacktab) so they participate in context
routing — but their handlers sent hard-coded legacy bytes, bypassing
key_to_bytes and the kitty encoder. So even with disambiguate mode on,
Esc stayed bare \x1b and Shift+Tab stayed \x1b[Z. Add kitty-aware
Terminal::send_escape/send_tab/send_backtab that route through
key_to_bytes, so Esc → CSI 27 u and Shift+Tab → CSI 9;2 u in
disambiguate mode (plain Tab stays \t).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbKbY5iGGgXSnpHz2V5aSr
… & resume

Report what an AI coding agent (Claude Code, Codex, …) is doing in each pane
via a push-based, in-band OSC 9001 protocol, and remember/resume its session
across restarts. The model is open: a small fixed lifecycle drives colour/sort/
notifications while a free-form message + labels carry whatever the agent wants.

Display & protocol:
- OSC 9001 (st/msg/lbl) parsed in the terminal sidecar into a canonical
  AgentStatus; surfaced as a per-tab indicator, a cross-project sidebar AGENTS
  section (click to jump), a per-project field in the remote API, and a desktop
  notification on blocked/done. Runtime-only, never persisted.
- reveal_terminal navigates to a hidden pane (retargets zoom/fullscreen) behind
  agent-row / notification / remote-focus clicks.
- Bundled Claude Code plugin (integrations/claude-code) maps lifecycle hooks to
  states; OKENA_TTY exports the pane's slave pts so hooks without a controlling
  terminal can emit the OSC.

Session capture, persistence & resume (harness-extensible):
- Reserved lbl keys agent/session_id/transcript_path are captured into a sticky
  per-pane AgentSession (UUID-validated, survives st=clear), persisted per
  terminal in workspace.json, and resumed on restore behind the opt-in
  auto_resume_agent_sessions setting (e.g. `claude --resume <id>`).
- Per-harness resume/transcript logic lives behind a gpui-free
  okena_core::agent_harness registry keyed by agent id (works desktop + headless);
  Claude Code + Codex impls in the okena-ext-* crates. Adding a harness is additive.
- Auto-resume requires a session backend (the terminal_id the session is keyed by
  only survives a restart with tmux/dtach/screen).

Hardening:
- Bound OSC 9001 custom/labels to mirror the OSC 99 caps (a hostile pane could
  otherwise pin unbounded in-band memory, amplified to every remote client);
  decode off-lock; empty msg falls back to the default notification body.
- Headless PTY loop now drains the remote_dirty edge so agent status pushes to
  remote/mobile clients (was desktop-only).
- reveal_terminal no longer clears individual-zoom on a same-project click
  (regressed sidebar/cursor clicks); corrected the OKENA_TTY tmux-reattach docs.

Docs: docs/agent-status.md, crates/okena-terminal/CLAUDE.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbKbY5iGGgXSnpHz2V5aSr
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