Skip to content

visual: vim-key navigation for the agent tree + peek panel#136

Merged
davidj4tech merged 7 commits into
mainfrom
agent-tree-vim-nav
Jul 11, 2026
Merged

visual: vim-key navigation for the agent tree + peek panel#136
davidj4tech merged 7 commits into
mainfrom
agent-tree-vim-nav

Conversation

@davidj4tech

Copy link
Copy Markdown
Owner

Adds tmux-chooser-style keyboard navigation to the agent tree in the visual canvas, plus the peek panel. Keyboard-only, no change to the touch/mouse paths.

Agent tree (#agents)

  • a — focus the tree (expands it, drops a .cursor on the first row).
  • j/k or ↓/↑ — move the cursor through the visible .shead/.pane rows.
  • l / Enter / → — expand a session, or (on a pane) aim the reply box at it.
  • h / ← — collapse (a pane collapses its parent and lands on the head).
  • g / G — jump to top / bottom.
  • p — peek that pane's output.
  • Esc / q — exit.
  • pollAgents() returns early while the tree is focused, so the 4 s poll can't re-render the DOM out from under the cursor.

Peek panel (#peek)

  • j/k — move between .turn snapshots.
  • Enter — toggle expand of the current turn.
  • p — play (speak) that turn.
  • Esc — close.

Navigation is dispatched before the existing mode machinery in the global keydown handler, so Enter/p/Esc/q mean "navigate" while a cursor is active rather than reply/control/toggle. Cursor highlight is an amber inset ring (black outline in e-ink mode); the focused tree gets a ring on its pill. Help overlay documents the new keys.

Validation

  • Extracted the largest served <script> from the rendered PAGE and ran node --check — passes.
  • python3 -m py_compile on the module — passes.
  • Restarted agent-media-visual-canvas.service (user scope) — active; / serves the new code, /healthz ok.

🤖 Generated with Claude Code

'a' focuses the #agents tree; j/k or arrows walk a .cursor highlight
through the visible .shead/.pane rows; l/Enter opens a session or aims
the reply box at a pane; h collapses; g/G jump top/bottom; p peeks; Esc/q
exit. pollAgents freezes while the tree is focused so it can't re-render
under the cursor. In #peek: j/k walk .turn snapshots, Enter toggles
expand, p plays that turn, Esc closes.
…n render

- Layout: the #agents tree drops DOWN from the pill now (flex column, not
  column-reverse) — the 'agents' pill sits above the session list.
- Nav: the pill is the first navigable row; l/Enter expands the whole tree,
  h/Left collapses it back into the pill. j/k walk pill → heads → panes.
- Fix 'play a turn' (and replay): media say renders TTS in-process, so the
  edge-tts renderer is resolved on PATH — but this systemd user service ships
  a bare PATH (unlike sink-speech). Give the say/replay shell-outs an env
  with the venv bin + ~/.local/bin prepended so edge-tts is found.
media say / replay render TTS in-process, resolving edge-tts on PATH; the
canvas user service shipped a bare PATH so per-turn audio silently dropped.
Pin the venv bin + ~/.local/bin (mirrors agent-media-sink-speech.service).
canvas.py augments the subprocess env too, so both layers cover it.
- Spinner: say/replay block for seconds (edge-tts render + speech queue)
  before audio starts, with no feedback. A tapped play button (peek turn ▶
  and the pane 'play last clip') now spins until the SSE speaking:true event
  fires (audio started) — clearing it that way, not on the request, avoids
  spinning through the whole 12–20s blocking playback. 25s fallback so it
  never hangs; failed replay/say (ok:false) drops it immediately. No motion
  on e-ink (dims instead — DU4 ghosts animation).
- Peek: h/l (and ←/→) now expand/collapse the truncated transcript snippet
  under the cursor, alongside Enter (toggle).
…rict tmux sends to claude panes

Three contained security fixes to the visual canvas server:

- #138: /show, /ctl, /say, /play changed state with no auth and were
  CSRF-able. Route all four through the same _authorized() gate as
  /input, returning 401 when unauthorized. Read-only GET endpoints and
  /input's own gate are unchanged.

- #139: _read_json read Content-Length bytes uncapped — one POST with a
  5 GB Content-Length forced a multi-GB alloc (remote OOM on a RAM-tight
  host). Reject bodies over 64 KiB with 413 before reading; _read_json
  also caps its read as defence in depth.

- #140: send_input resolved tmux:<target> via raw `tmux display-message`
  against any session, so text+Enter could land in a bare shell = host
  command execution. Validate the target against _tmux_cc_panes() (which
  filters cmd=="claude") and reject anything that isn't a live claude pane.

Closes #138, #139, #140
canvas: auth-gate state-changing POSTs, cap request bodies, restrict tmux sends (security trio)
…ion)

PR #143 auth-gated the state-changing POSTs (incl. /say and /play) for CSRF
safety, but the play-turn / play-last-clip client calls still used a plain
fetch — so on the merged branch they 401'd and the spinner hung. Route both
through authed() (X-Auth-Token, prompt-on-401), same as /input, and drop the
spinner on a non-ok response. Verified: /say is 401 without a token, 200 with.
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