hudson-canvas

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:

  1. 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.
  2. 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.
  3. Canvas Interaction Hardening — Multi-select, hand/select refinements, zoom/minimap reset behavior, viewport replay, persistent layout, drag/resize throttling, and live-renderer hysteresis.
  4. Embeddable Mode — Make a product host Canvas with stable config, callbacks/events, product-owned paths, and workspace restore.
  5. Remote tmux Spike — Treat local tmux and SSH tmux as sibling runtime authorities with stronger validation, preflight, health reporting, and durable identity.
  6. 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:

LaneMandateNext concrete slice
Control APILock the JSONL contract used by host apps and shell agentsFinish v0 selectors, raw commands, metrics, restore, and script tests
PerfMake responsiveness measurable before optimizingKeep counters/timings lightweight, then add drag/zoom/attach samples
CanvasHarden the native spatial modelExtract viewport math into a Canvas canvas primitive and add viewport replay
Remote tmuxProve durable sessions can live outside the local appKeep remoteHost as the v0 contract, then harden SSH validation and health
Host embeddingMake Canvas product-owned rather than demo-ownedEvolve 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:

ActionPurpose
statusReport app PID, state path, node count, and structured node summaries
tile / gridCreate a local PTY grid for performance and layout trials
create / spawn / newAdd local PTY nodes
reattach / attach / tmuxAttach tmux sessions, raw targets, or Graphite-style ids
selectSelect nodes by UUID prefix, title, tmux target, or Graphite ID
inspect / nodeReturn selected or targeted node summaries
focus / center / revealSelect nodes and center the viewport on their bounds
focus-mode / soloPut exactly one selected or targeted terminal into an immersive focus view
exit-focus / unfocusReturn from terminal focus mode to the full canvas
popout / pop-outOpen selected or targeted terminals in a separate native window
close / removeStop and remove selected or targeted nodes
metrics / perfReturn lightweight node, runtime, viewport, and control latency counters
perf-resetReset in-memory control latency counters
perf-harnessCreate a repeatable local tmux stress scene with idle and active tail sessions
perf-cleanupKill harness tmux sessions and remove matching Canvas nodes
viewport / viewReport, reset, fit, or replay exact pan/scale viewport state
ensure-tmux / install-tmuxDetect local tmux and install it with Homebrew after explicit confirmation
save / snapshotPersist durable tmux-backed nodes, bounds, z-order, selection, and viewport
save-workspace / export-workspacePersist a portable Canvas workspace document
restore / loadRecreate saved tmux-backed nodes from a state file
restore-workspace / open-workspaceRecreate a portable Canvas workspace document
clearRemove all nodes
resetReturn 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
For AI agents