visual: vim-key navigation for the agent tree + peek panel#136
Merged
Conversation
'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.
This was referenced Jul 11, 2026
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.
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.cursoron the first row).j/kor ↓/↑ — move the cursor through the visible.shead/.panerows.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.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.turnsnapshots.p— play (speak) that turn.Navigation is dispatched before the existing mode machinery in the global keydown handler, so
Enter/p/Esc/qmean "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
<script>from the renderedPAGEand rannode --check— passes.python3 -m py_compileon the module — passes.agent-media-visual-canvas.service(user scope) — active;/serves the new code,/healthzok.🤖 Generated with Claude Code