Hudson Canvas
HudsonCanvas is the native Hudson surface for live, durable runtimes. It is
not a standalone product by itself. A product can embed a Canvas when it
needs a spatial operating view over terminals, agents, tmux sessions, remote
hosts, or cloud runtimes.
Example hosts include agent workspaces, terminal-heavy development tools, workflow runners, local sandboxes, and cloud runtime consoles.
The first implementation is terminal-oriented and backed by Termini.
Roadmap Anchor
When deciding "what's next" for Canvas, use this sequence as the working queue:
- External Control API v0 — Version the JSONL contract and make agent operations stable: create, restore, tile, select, inspect, focus, close, focus-mode, pop-out, status, metrics. The implementation is additive: each new command should extend the v0 envelope without breaking older callers.
- Perf Baseline + Instrumentation — Measure node count, visible/live renderers, control latency, drag/zoom cost, PTY/tmux attach time, memory, and beach-ball points before deeper interaction work.
- Canvas Interaction Hardening — Multi-select, hand/select refinements, zoom/minimap reset behavior, viewport replay, persistent layout, drag/resize throttling, and live-renderer hysteresis.
- Embeddable Mode — Make a product host Canvas with stable config, callbacks/events, product-owned paths, and workspace restore.
- Remote tmux Spike — Treat local tmux and SSH tmux as sibling runtime authorities with stronger validation, preflight, health reporting, and durable identity.
- iOS Viewer/Controller — Explore a remote-first Canvas surface for inspecting and orchestrating workspaces rather than local PTY ownership.
The bias is to keep each branch PR-sized: finish the current slice, then move to the next item instead of mixing all tracks at once.
Subteam Lanes
Keep the work split into these mandates when Canvas is moving quickly:
| Lane | Mandate | Next concrete slice |
|---|---|---|
| Control API | Lock the JSONL contract used by host apps and shell agents | Finish v0 selectors, raw commands, metrics, restore, and script tests |
| Perf | Make responsiveness measurable before optimizing | Keep counters/timings lightweight, then add drag/zoom/attach samples |
| Canvas | Harden the native spatial model | Extract viewport math into a Canvas canvas primitive and add viewport replay |
| Remote tmux | Prove durable sessions can live outside the local app | Keep remoteHost as the v0 contract, then harden SSH validation and health |
| Host embedding | Make Canvas product-owned rather than demo-owned | Evolve host callbacks/events and wire a first generic host surface |
The next recommended PR after the control/perf slice is Canvas State +
Viewport Replay v0: extract pure viewport math, add a viewport control
action, fix group-drag selection collapse, and debounce durable layout saves.
SwiftPM
Enable terminal-backed Hudson modules:
HUDSONKIT_WITH_TERMINAL=1 swift build
Then embed the surface:
import SwiftUI
import HudsonCanvas
struct RuntimeWorkspaceView: View {
var body: some View {
HudCanvasSurface(
configuration: HudCanvasConfiguration(
surfaceTitle: "Runtime Workspace",
surfaceSubtitle: "terminals, sessions, and remote runtimes",
commandURL: URL(fileURLWithPath: "/tmp/runtime-canvas-control.jsonl"),
responseURL: URL(fileURLWithPath: "/tmp/runtime-canvas-control.responses.jsonl"),
stateURL: URL(fileURLWithPath: "/tmp/runtime-canvas-state.json"),
workingDirectoryURL: URL(fileURLWithPath: "/Users/me/dev/project")
)
)
}
}
The Canvas host app uses this exact pattern via
HudCanvasConfiguration.hostApplication(...).
Control Plane
HudCanvasSurface watches a JSONL command file and writes JSONL responses.
The command path is supplied by HudCanvasConfiguration, so each product can
own its own control lane. Commands already present when the app starts are
processed, which lets an outside agent queue a workspace before opening the
native surface.
Generic command helper:
packages/native/apple/HudsonKit/Scripts/canvasctl.sh --wait status
packages/native/apple/HudsonKit/Scripts/canvasctl.sh --wait tile 8 8
packages/native/apple/HudsonKit/Scripts/canvasctl.sh --wait reattach --session hudson-lab --create
packages/native/apple/HudsonKit/Scripts/canvasctl.sh --wait reattach --remote user@host --session hudson-lab
packages/native/apple/HudsonKit/Scripts/canvasctl.sh --wait select NODE_ID
packages/native/apple/HudsonKit/Scripts/canvasctl.sh --wait inspect
packages/native/apple/HudsonKit/Scripts/canvasctl.sh --wait focus
packages/native/apple/HudsonKit/Scripts/canvasctl.sh --wait focus-mode
packages/native/apple/HudsonKit/Scripts/canvasctl.sh --wait popout
packages/native/apple/HudsonKit/Scripts/canvasctl.sh --wait exit-focus
packages/native/apple/HudsonKit/Scripts/canvasctl.sh --wait metrics
packages/native/apple/HudsonKit/Scripts/canvasctl.sh --wait perf-harness --sessions 64 --active 32 --mode tail --rate-ms 250
packages/native/apple/HudsonKit/Scripts/canvasctl.sh --wait perf-cleanup --prefix hudson-perf-1234
packages/native/apple/HudsonKit/Scripts/canvasctl.sh --wait viewport --fit
packages/native/apple/HudsonKit/Scripts/canvasctl.sh --wait viewport --pan-x -120 --pan-y 44 --scale 0.25
packages/native/apple/HudsonKit/Scripts/canvasctl.sh --wait ensure-tmux --confirm
packages/native/apple/HudsonKit/Scripts/canvasctl.sh --wait save
packages/native/apple/HudsonKit/Scripts/canvasctl.sh --wait save-workspace --state-file /tmp/project.canvas.json
packages/native/apple/HudsonKit/Scripts/canvasctl.sh --wait restore --create
packages/native/apple/HudsonKit/Scripts/canvasctl.sh --wait restore-workspace --state-file /tmp/project.canvas.json --create
packages/native/apple/HudsonKit/Scripts/canvasctl.sh --wait raw '{"action":"metrics","includeNodes":false}'
Set custom control paths for product-specific surfaces:
HUDSON_CANVAS_CONTROL_FILE=/tmp/runtime-canvas-control.jsonl \
HUDSON_CANVAS_RESPONSE_FILE=/tmp/runtime-canvas-control.responses.jsonl \
HUDSON_CANVAS_STATE_FILE=/tmp/runtime-canvas-state.json \
packages/native/apple/HudsonKit/Scripts/canvasctl.sh --wait status
Commands
The JSONL command contract is intentionally small and durable:
| Action | Purpose |
|---|---|
status | Report app PID, state path, node count, and structured node summaries |
tile / grid | Create a local PTY grid for performance and layout trials |
create / spawn / new | Add local PTY nodes |
reattach / attach / tmux | Attach tmux sessions, raw targets, or Graphite-style ids |
select | Select nodes by UUID prefix, title, tmux target, or Graphite ID |
inspect / node | Return selected or targeted node summaries |
focus / center / reveal | Select nodes and center the viewport on their bounds |
focus-mode / solo | Put exactly one selected or targeted terminal into an immersive focus view |
exit-focus / unfocus | Return from terminal focus mode to the full canvas |
popout / pop-out | Open selected or targeted terminals in a separate native window |
close / remove | Stop and remove selected or targeted nodes |
metrics / perf | Return lightweight node, runtime, viewport, and control latency counters |
perf-reset | Reset in-memory control latency counters |
perf-harness | Create a repeatable local tmux stress scene with idle and active tail sessions |
perf-cleanup | Kill harness tmux sessions and remove matching Canvas nodes |
viewport / view | Report, reset, fit, or replay exact pan/scale viewport state |
ensure-tmux / install-tmux | Detect local tmux and install it with Homebrew after explicit confirmation |
save / snapshot | Persist durable tmux-backed nodes, bounds, z-order, selection, and viewport |
save-workspace / export-workspace | Persist a portable Canvas workspace document |
restore / load | Recreate saved tmux-backed nodes from a state file |
restore-workspace / open-workspace | Recreate a portable Canvas workspace document |
clear | Remove all nodes |
reset | Return to the two-terminal local PTY starter layout |
The v0 JSONL envelope is additive and backward-compatible. New callers should
send apiVersion: "v0" and kind: "hudson.canvas.command". Older flat
commands without those fields still decode. Responses include
apiVersion: "v0" and kind: "hudson.canvas.response".
The wrapper raw command is an escape hatch for agents. If the JSON object
does not include id, apiVersion, or kind, the wrapper injects the
waitable request id and v0 envelope before queuing it.
Node-targeting actions accept nodeID, nodeIDs, or the legacy ids array.
Selectors can be full UUIDs, UUID prefixes, node titles, tmux targets,
Graphite paths, or remoteHost:target for remote tmux nodes. select supports
selectionMode values replace, add, remove, toggle, and clear.
The native canvas mirrors those modes in direct selection: Shift adds,
Option subtracts, and Command toggles clicked or marquee-selected nodes.
Hold Space to temporarily enter the hand tool and drag the canvas without
leaving Select mode.
viewport accepts reset: true, fit: true, and exact replay fields
panX, panY, and scale. The wrappers expose these as viewport --reset,
viewport --fit, and viewport --pan-x X --pan-y Y --scale N.
save/save-workspace and restore/restore-workspace use the configured
stateURL unless a command includes statePath. New saves are portable
workspace documents with kind: "hudson.canvas.workspace". They include
durable tmux-backed nodes, selection, an optional focused node, viewport,
active tool, navigator filters, optional node tags, tag-derived groups, panel
widths, and collapsed panel state. Older state files without kind, layout,
focusedNodeID, or groups still decode. Local PTYs remain useful as cheap
scratch terminals, but tmux is the durable runtime boundary.
ensure-tmux is permission-gated. If tmux is already available, it simply
returns the detected path. If tmux is missing, the UI shows a confirmation
dialog before running Homebrew, and control-plane callers must pass
confirmInstall: true. Without that field, Canvas returns
requiresPermission: true and does not start the installer.
Startup Reattach
Hosts can seed a Canvas at launch with environment variables:
HUDSON_CANVAS_REATTACH_IDS="hudson.lab.termini.canvas.0042.shell" \
HUDSON_CANVAS_REATTACH_SESSIONS="hudson-lab" \
HUDSON_CANVAS_REATTACH_CREATE=1 \
HUDSONKIT_WITH_TERMINAL=1 swift run --package-path apps/canvas CanvasApp
Legacy TERMINI_CANVAS_REATTACH_* variables still work for the case-study app.
Startup Restore
Hosts can also restore from a saved state file at launch:
HUDSON_CANVAS_STATE_FILE=/tmp/scout-canvas-state.json \
HUDSON_CANVAS_RESTORE_ON_LAUNCH=1 \
HUDSON_CANVAS_RESTORE_CREATE=1 \
HUDSONKIT_WITH_TERMINAL=1 swift run --package-path apps/canvas CanvasApp
HudCanvasConfiguration.hostApplication(...) opts into launch restore and
uses Application Support state paths by default. If the state file is missing or contains
no durable tmux nodes, the sample falls back to its starter local PTY layout.
Response Shape
Responses echo the request id when provided and include ok, message,
workspaceID, nodeCount, selectedNodeIDs, commandPath, responsePath,
statePath, durationMS, and timestamp. Responses also expose tmuxPath,
tmuxInstallInProgress, requiresPermission, and installerCommand when
relevant. status and normal command responses include a nodes array with
node IDs, title/subtitle, selection state, bounds, z-order, optional tag,
runtime kind, tmux target, Graphite path, and remote host when present.
When terminal focus mode is active, responses include focusedNodeID.
viewport reports the current pan/scale and visible world rect. metrics
reports node counts, runtime counts, live surface count, command count, focus
mode/pop-out gauges, and latest command latency.
The perf snapshot is intentionally lightweight. It keeps bounded recent timing
samples and named counters/gauges for control commands, canvas input pressure,
node move/resize deltas, persistence coalescing, and renderer virtualization
counts. perf-reset clears the in-memory snapshot without touching the
workspace.
perf-harness is the repeatable stress scene. By default it creates 64 local
tmux sessions, makes 32 of them active with a tail -n 50 -f workload, and
reattaches the canvas to those sessions. Use --prefix to make the run easy to
find or clean up later, --sessions and --active to change the shape, and
--rate-ms to control the log append cadence. perf-cleanup --prefix PREFIX
kills matching tmux sessions and removes matching nodes from the surface.
Canvas also emits OSLog signposts for control.command, tmux.reattach,
perf.harness, perf.cleanup, and state.persist, so the JSONL counters can
be correlated with Apple Instruments timelines.
Current Boundary
Hudson Canvas owns:
- spatial canvas, pan/zoom, minimap, selection, side panels, inspector
- runtime nodes and layout state
- JSONL control plane
- state snapshots for durable tmux-backed nodes
- local tmux, remote tmux over SSH, and Graphite-style IDs
- lightweight node tags and tag filters
- terminal focus mode and selected-group pop-out windows
- Termini terminal rendering and virtualization policy
Still intentionally thin / next to extract:
- adapter protocol for Scout, Fabric, and non-terminal runtimes
- full canvas documents and named saved views
- product-level command palette actions
- richer health checks and lifecycle events