-
Notifications
You must be signed in to change notification settings - Fork 0
Agent Canvas State & Status Badges
Renderer-side agent tracking is split between a pure status/notification contract, a small Zustand store, the terminal node that binds IPC streams to that store, and a pure canvas-operation planner used by agent tools. The same sessionId/node id convention ties a terminal session to its badge state.
| File | Responsibility | Key state / exports |
|---|---|---|
src/shared/agent-status.ts |
Shared status vocabulary, hook event shape, and pure notification decision. No Electron, no fs. |
AgentStatus, AgentSessionKind, AgentStatusEvent, shouldNotify
|
src/renderer/src/state/agents.ts |
Renderer store for latest hook-derived status and unread flags per session/node. |
byId, unread, set, clearUnread, clear
|
src/renderer/src/nodes/TerminalNode.tsx |
Subscribes agent status/session-name IPC, maps statuses to badge labels, exposes badge/unread selectors, and syncs node titles. |
STATUS_LABEL, useAgentStatuses, agent.onStatus, agent.onSessionName
|
src/renderer/src/state/agent-tool-canvas.ts |
Pure planner that applies agent-tool canvas operations to a nodes array and returns updated nodes plus a result value. | applyCanvasTool |
TerminalNode.tsx also tracks a separate IntegrationStatus for AgentTools via window.termsprawl.agentTools.status/onStatus; that is distinct from the hook-derived useAgentStatuses badge state.
The shared model defines four agent statuses:
| Status | Badge label in TerminalNode
|
Terminal / notifiable? |
|---|---|---|
working |
RUNNING |
No |
waiting |
NEEDS YOU |
Yes |
blocked |
BLOCKED |
Yes |
done |
DONE |
Yes |
AgentStatusEvent can omit status for lifecycle-only events such as Codex session pings, subagent starts, and compaction events. Consumers must not churn the badge for those events.
shouldNotify(prev, next, opts) centralizes the notification/unread trigger:
- Only for a known session.
- Only while the window is not focused.
- Only when transitioning into
waiting,blocked, ordone. - Only when the terminal status differs from the previous status.
- A first event that is already terminal also notifies when the window is unfocused.
The actual OS notification side effect is outside this read slice; the renderer-visible policy decision is shouldNotify, and its result is used to set the unread dot.
sequenceDiagram
participant Hook as Hook/status producer
participant Preload as window.termsprawl.agent
participant Node as TerminalNode
participant Store as useAgentStatuses
participant Notify as shouldNotify
participant UI as Badge/unread selectors
Node->>Preload: onStatus(sessionId, callback)
Hook->>Preload: AgentStatusEvent
Preload-->>Node: callback(event)
Node->>Node: ignore when event.status is undefined
Node->>Store: set(sessionId, status)
Store->>Notify: shouldNotify(prev, next, { knownSession: true, windowFocused: document.hasFocus() })
Notify-->>Store: notify boolean
Store-->>UI: byId[sessionId], unread[sessionId]
Key nodes:
-
TerminalNodeonly subscribes whendata.commandis present, meaning the node was spawned with an agent/command preset such asclaudeorcodex. - The PTY session id is intended to equal the React Flow node id for agent nodes, so status events key naturally by node id.
- For resumed sessions,
resumedSessionId(data.command)returns the original session id. The node subscribes to both the current node id and the resumed id because hook events for a resumed conversation carry the old session id. - Lifecycle-only events with
event.status === undefinedare ignored before reaching the store. -
setrecords the latest status inbyIdand marksunreadwhenshouldNotifyreturns true. Non-notifying updates preserve an existing unread flag. -
clearUnreadclears only the dot.clearremoves both the status and unread entries. - On unmount or command change, subscriptions are removed and both the node id and any resumed id are cleared.
Badge selectors in TerminalNode read useAgentStatuses((s) => s.byId[id]), unread[id] === true, and call clearUnread. STATUS_LABEL maps the stored status to RUNNING, NEEDS YOU, BLOCKED, or DONE.
Session-name handling is adjacent to status handling:
-
agent.onSessionNameupdates the node title with the agent transcript's session name. - Inline title edits call
updateNodeData(id, { title }, true). - For commands starting with
claude, the edit is also pushed to the live session viapty.write(id, "/rename ${title}\r").
applyCanvasTool(nodes, request, cwd?) is a pure operation planner. It takes the current node array and a ToolRequest, then returns { nodes, value }. The caller, Canvas, applies the returned nodes to its existing live state.
flowchart TD
A[ToolRequest: operation + args] --> B{operation}
B -->|canvas_list| C[serializeNodes]
B -->|move / resize / select / group| D[Validate target or selection]
D --> E[Return copied nodes + value]
B -->|open / launch| F[Call workspace node factory]
F --> G[Place at right + 40, y 60; topZ; selected]
G --> E
E --> H[Canvas applies returned nodes to live state]
Supported operations:
| Operation | Behavior |
|---|---|
canvas_list |
Returns serializeNodes(nodes) and does not mutate. |
canvas_move |
Requires the target node to exist in the visible project; updates position. |
canvas_resize |
Validates size; browser minimum is 320×240, other nodes 240×140; maximum is 8000×8000; writes width, height, and style. |
canvas_select |
Requires every requested id to exist; sets selected on matching nodes. |
canvas_group |
Requires top-level non-group members; creates a group above the members and reparents children through createGroup. |
terminal_open |
Creates a terminal node from cwd; title defaults to shell. |
agent_launch |
Creates an agent node for the requested AgentId. |
browser_open |
Creates a browser node for the URL. |
sticky_open |
Creates a sticky note with args.text. |
artifact_open |
Creates a diff node when args.view === 'diff', otherwise an editor node for the path. |
Creation operations share placement rules:
- Compute the right edge from top-level nodes as
x + width, usingstyle.width, thenwidth, then720as fallbacks. - Place the new node at
{ x: right + 40, y: 60 }. - Assign
zIndex = topZ(nodes)andselected = true. - Clear
selectedon all existing nodes. - Return
{ nodeId }, plus{ tabId }for browser nodes.
- Badges only work for terminal nodes with
data.command; plain shell terminals have no agent status subscription. - CLIs that do not pin
session-id = node idreceive no status events and fail open. - Resume handling subscribes to both ids, but the store writes under whichever subscribed id delivered the event while the visible selector in this slice reads
byId[id]. -
AgentStatusEvent.kindincludessession,subagent, andrecurring, but the store itself is only keyed bysessionId; additional per-kind UI would be an extension. - Notification policy is pure and centralized in
shouldNotify; changing focus semantics or terminal-state semantics should happen there first. - Unread state persists until
clearUnreadis called; laterworkingupdates do not clear it. - Canvas tool operations validate unknown nodes, unknown selections, group members, and resize bounds with thrown errors.
- Adding a canvas operation requires extending the shared
ToolRequestcontract and theapplyCanvasToolswitch while preserving the pure planner boundary. - Adding a new status requires updating the shared
AgentStatusunion, the terminal-status list used byshouldNotify, andSTATUS_LABELfor badge text. - New node kinds used by agent tools should be produced through the
workspace.tsfactories, consistent with the state-layer convention thatworkspace.tsstays pure and owns node factories.
Sources: src/shared/agent-status.ts, src/renderer/src/state/agents.ts, src/renderer/src/state/agent-tool-canvas.ts, src/renderer/src/nodes/TerminalNode.tsx, 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