Skip to content

Agent Canvas State & Status Badges

dazeb edited this page Sep 17, 2026 · 1 revision

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.

Module responsibilities

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.

Status vocabulary and notification rule

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, or done.
  • 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.

Renderer tracking pipeline

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]
Loading

Key nodes:

  • TerminalNode only subscribes when data.command is present, meaning the node was spawned with an agent/command preset such as claude or codex.
  • 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 === undefined are ignored before reaching the store.
  • set records the latest status in byId and marks unread when shouldNotify returns true. Non-notifying updates preserve an existing unread flag.
  • clearUnread clears only the dot. clear removes 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.onSessionName updates 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 via pty.write(id, "/rename ${title}\r").

Agent-tool canvas mutations

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]
Loading

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, using style.width, then width, then 720 as fallbacks.
  • Place the new node at { x: right + 40, y: 60 }.
  • Assign zIndex = topZ(nodes) and selected = true.
  • Clear selected on all existing nodes.
  • Return { nodeId }, plus { tabId } for browser nodes.

Boundary conditions and extension points

  • 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 id receive 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.kind includes session, subagent, and recurring, but the store itself is only keyed by sessionId; 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 clearUnread is called; later working updates 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 ToolRequest contract and the applyCanvasTool switch while preserving the pure planner boundary.
  • Adding a new status requires updating the shared AgentStatus union, the terminal-status list used by shouldNotify, and STATUS_LABEL for badge text.
  • New node kinds used by agent tools should be produced through the workspace.ts factories, consistent with the state-layer convention that workspace.ts stays 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

termsprawl

App Shell & Platform Foundations

Canvas, Nodes & Renderer State

Terminals & Session Continuity

Persistence, Projects & Files

Agent Runtime & Tooling

Chat Nodes & Model Providers

Git & Source Control

Embedded Browser Nodes

Server Edition

Relay & Remote Access

Integrations & Secondary Surfaces

Settings, Updates & Maintenance

Clone this wiki locally