-
Notifications
You must be signed in to change notification settings - Fork 0
Keyboard Canvas Navigation & Cross Panel Requests
This page covers three tightly related mechanisms in the renderer:
- Keyboard-driven node traversal — a deterministic reading order over canvas nodes so keyboard users can move selection predictably.
- Edge reveal / keep-open behavior — the small state machine and geometry helpers that decide when the file-tree chrome reveals at a canvas edge and when it stays open.
- Cross-panel request buses — one-shot Zustand stores that let UI rendered outside the Canvas or sidebar provider ask the canvas or sidebar to perform an action on the next render.
These pieces are intentionally separate from the live node store: Canvas.tsx remains the single live source of truth for node state, while the helpers here are pure or request-only.
| File | Responsibility |
|---|---|
src/renderer/src/state/canvas-knav.ts |
Pure keyboard navigation order: NAV_ORDER, nextSelection. |
src/renderer/src/state/edge-reveal.ts |
Pure file-tree chrome state, reveal/close actions, edge hot-zone and keep-open geometry. |
src/renderer/src/state/canvas-requests.ts |
One-shot canvas spawn/switch/organize/relay requests from outside the Canvas provider. |
src/renderer/src/state/sidebar-requests.ts |
One-shot sidebar section requests from outside the sidebar host. |
src/renderer/src/canvas/Canvas.tsx |
React Flow canvas; live node state; consumes canvas requests and owns canvas-side behavior. |
src/renderer/src/canvas/AGENTS.md |
Confirms Canvas is the single live source of truth for node state and hosts canvas menus. |
Keyboard traversal is implemented as pure functions in canvas-knav.ts. The module has no DOM, React Flow, or Electron dependency, so it remains unit-testable.
NAV_ORDER(nodes) produces a stable reading order:
- Filter out nodes where
type === 'group'. - Filter out nodes with a
parentId. - Sort by:
-
position.yascending - then
position.xascending - then
id.localeCompare
-
The exclusions mirror isOrganizable from workspace.ts: group frames and parented children do not participate in the traversal cycle. The function copies before sorting, so the input array is not mutated. It is generic over NavNode, allowing React Flow Node objects to pass through without re-wrapping.
flowchart TD
N[nodes array] --> F["filter: type !== 'group' && !parentId"]
F --> C[copy]
C --> S[sort by y asc, x asc, id asc]
S --> O[NAV_ORDER result]
O --> NS[nextSelection]
Key nodes:
- Filter removes group frames and parented children so keyboard focus follows the top-level reading order.
- Stable sort gives deterministic behavior even when nodes share coordinates.
-
nextSelectionconsumes the order and applies direction/wrap logic.
nextSelection(nodes, currentId, dir) resolves the next focus target:
-
dir = 1moves forward;dir = -1moves backward. - Wraps at both ends.
-
nullcurrent id, or an id no longer on the canvas, starts at the first node fordir = 1or the last node fordir = -1. - A single navigable node always resolves to itself.
- No navigable nodes resolves to
null.
This makes keyboard traversal resilient to selection loss: if the current node was deleted or is not in the navigable set, the next Tab/Shift-Tab lands at a predictable end of the reading order.
edge-reveal.ts models the VS Code-style edge sidebar chrome. It defines:
type TreeSide = 'left' | 'right'
type SidebarSection = 'files' | 'source' | 'plugins'
interface FileTreeChrome {
side: TreeSide
open: boolean
pinned: boolean
section: SidebarSection
}The initial state is:
- side:
left - open:
false - pinned:
false - section:
files
applyFileTreeChrome handles these actions:
| Action | Effect |
|---|---|
reveal |
Sets side and opens. |
flipSide |
Toggles left/right and opens. |
togglePin |
Toggles pinned and opens. |
requestClose |
Closes only if not pinned. |
switchSection |
Sets the active section and opens. |
requestClose explicitly refuses to close while pinned, so a pinned sidebar is stable even if surrounding focus or pointer state requests close. The active section survives close/reopen.
stateDiagram-v2
[*] --> Closed: initialFileTreeChrome()
Closed --> Open: reveal / flipSide / togglePin / switchSection
Open --> Open: reveal / flipSide / togglePin / switchSection
Open --> Open: requestClose when pinned
Open --> Closed: requestClose when not pinned
Key transitions:
- Any reveal-like action forces
open: true. -
requestCloseis the only action that can close, and only when unpinned. - The section is independent of open/closed state.
Two pure geometry functions decide reveal and keep-open behavior:
-
edgeHotZone(x, width, zone = 12)- Returns
nullifwidth <= 0. - Returns
'left'ifx <= zone. - Returns
'right'ifx >= width - zone. - Otherwise returns
null.
- Returns
-
shouldKeepTreeOpen({ x, width, side, panelWidth })- For
side === 'left', keeps open whilex <= panelWidth. - For
side === 'right', keeps open whilex >= width - panelWidth.
- For
These helpers are pointer-position based: the hot zone determines when the pointer is hugging a canvas edge, and the keep-open check determines whether the pointer is still over the open panel or its hot strip.
The Canvas provider and sidebar host do not wrap every piece of UI. The settings panel renders outside the Canvas provider, and the cog menu in the app toolbar renders outside the sidebar host. Those components cannot call useCanvas() or the sidebar directly.
To preserve Canvas as the single source of truth, the app uses two one-shot Zustand request stores.
canvas-requests.ts carries a one-shot CanvasSpawnRequest:
type CanvasSpawnRequest =
| { kind: 'agentLogin'; command: string }
| { kind: 'browser'; url: string }
| { kind: 'switchProject'; projectId: string }
| { kind: 'organize' }
| { kind: 'relayTerm'; term: string; title?: string }The store shape is:
interface CanvasRequestsState {
request: CanvasSpawnRequest | null
spawn(request: CanvasSpawnRequest): void
consume(): void
}Canvas consumes the request on its next render and then clears it. Requests never mutate Canvas node state directly.
sidebar-requests.ts carries a one-shot sidebar section request:
interface SidebarRequestsState {
request: SidebarSection | null
openSection(section: SidebarSection): void
consume(): void
}The cog menu publishes a section request; the file tree consumes it on its next render.
sequenceDiagram
participant External as Settings panel / cog menu
participant Bus as useCanvasRequests / useSidebarRequests
participant Canvas as Canvas.tsx
participant Tree as FileTree / sidebar host
External->>Bus: spawn(request) or openSection(section)
Bus-->>Canvas: request visible on next render
Canvas->>Bus: consume()
Bus-->>Tree: section request visible on next render
Tree->>Bus: consume()
Key nodes:
- External producer — any component outside the Canvas/sidebar provider.
- Request bus — a transient field, not a persistent command queue.
-
Consumer — Canvas or sidebar host reads and then calls
consume(). - Single source of truth — Canvas applies the requested mutation itself; the bus only carries intent.
-
canvas-knav.tsis pure: no DOM, no React Flow, no Electron. -
edge-reveal.tsis pure state/geometry logic; it does not render or directly observe pointers. -
canvas-requests.tsandsidebar-requests.tsare one-shot buses. If a consumer does not callconsume(), the request remains visible on subsequent renders. - Canvas remains the single live source of truth for nodes; request buses must not become a second node store.
- Group frames and parented children are excluded from keyboard traversal by design.
-
requestClosecannot close a pinned sidebar. -
edgeHotZonereturnsnullfor non-positive widths, avoiding edge reveals on invalid measurements. -
nextSelectionhandles empty, single-node, missing-current-id, and wrap-around cases explicitly.
- Add a new canvas-side command by extending the
CanvasSpawnRequestunion and handling it in Canvas’s consume path. - Add a new sidebar section by extending
SidebarSection; existing close/reopen behavior preserves the active section. - Adjust keyboard traversal exclusions or ordering inside
NAV_ORDERif a future node kind should be navigable or skipped. - Tune the edge reveal hot-zone width via the
zoneparameter ofedgeHotZone. - Reuse the one-shot request pattern for any other UI that renders outside the provider owning the target state.
Sources: src/renderer/src/state/canvas-knav.ts, src/renderer/src/state/edge-reveal.ts, src/renderer/src/state/canvas-requests.ts, src/renderer/src/state/sidebar-requests.ts, src/renderer/src/canvas/AGENTS.md, src/renderer/src/state/AGENTS.md
Generated from termsprawl at 0d4393be54c6200beedd91bb636e5296c30472c5.
App Shell & Platform Foundations
- Electron Main Process & Window Lifecycle
- Preload Bridge & IPC Contract
- Shared Domain Types and File/URL Helpers
- Renderer Bootstrap & App Composition
- Build Targets & TypeScript Configuration
Canvas, Nodes & Renderer State
- Infinite Canvas Surface & Viewport Interaction
- Workspace, Project & Tab State
- Node Links, Edges & Link Inspector
- Sticky, Group, Editor & Diff Nodes
- Keyboard Canvas Navigation & Cross-Panel Requests
- Theme, Accent & Visual Language
- Boot Overlay, Onboarding & Shared UI Kit
Terminals & Session Continuity
- PTY Lifecycle & Terminal Sessions
- tmux Session Naming & Reattach
- Scrollback Snapshots & Cold Replay
- Terminal Node Rendering (xterm.js)
- SSH Remote Projects, Terminals & Files
Persistence, Projects & Files
- Workspace Store & Project File Layout
- Project Scope, Deletion & Worktree Registry
- Workspace Bundle Export/Import
- File Service & File Tree UI
Agent Runtime & Tooling
- Agent Status Model & Hook Normalization
- Hook Server & CLI Hook Installers
- Agent Launch, CLI Probing & Managed Accounts
- Agent Tool Protocol & In-Process Server
- Agent Tool Client, CLI & MCP Entry
- Transcripts, Context Discovery & Context CLI
- Agent Canvas State & Status Badges
Chat Nodes & Model Providers
- Chat Runtime, Conversation & Cost
- Model Provider Adapters & Streaming
- Chat Tool Calling & Project Tools
- Chat Node UI
Git & Source Control
Embedded Browser Nodes
- Browser Manager & Guest Runtime
- CDP Facade & Browser Agent Server
- Browser Navigation Policy & Node UI
Server Edition
- Server Bootstrap & HTTP/WebSocket Entry
- RPC Dispatch, Handlers & Service Bridges
- Renderer Shim & Server Boundary
- Server Auth & Security Boundary
Relay & Remote Access
- Relay Hub & WebSocket Frame Routing
- Relay End-to-End Cryptography
- Relay Auth, Invites, Store & Admin API
- Relay Client, Pairing & Terminal Tunneling
- Relay Trust UI
Integrations & Secondary Surfaces
- Telegram Bot, Commands & Pairing
- A2A Peers: Protocol, Client & Server
- Node Link Engine, Registry & Scheduler
- Cloud Spaces, Snapshots & Sync
Settings, Updates & Maintenance