tmux-graphite-workspaces

tmux + Graphite-style workspace identity

Draft design note for durable, externally addressable terminal canvases.

Core idea

Hudson should own spatial identity. tmux should own durable runtime identity. Termini should own rendering.

Hudson canvas node
  spatial identity: node id, bounds, zIndex, selection, workspace
  runtime identity: graphite path, tmux session/window/pane target
  render identity: pending, attached, live, preview, detached, failed

The canvas creates lightweight nodes first. tmux sessions start in parallel. Termini surfaces attach as each runtime target becomes ready. The UI does not block on terminal creation, and terminal identity survives Hudson relaunches.

Logical path convention

Use a Graphite-like path as the durable logical name:

hudson.<workspace>.<namespace>.<app>.<instance>.<role>

Examples:

hudson.lab.termini.canvas.0042.shell
hudson.lab.agents.codex.0007.worker
hudson.lab.build.test.0013.logs

Path segment meanings:

SegmentMeaning
hudsonReserved product root
<workspace>Human workspace slug, like lab, demo, client-acme
<namespace>Domain grouping, like termini, agents, build, data
<app>App or tool family, like canvas, codex, pytest
<instance>Stable instance id, usually zero-padded or short hash
<role>Role inside the instance, like shell, logs, worker, preview

Names should be slug-safe: lowercase ASCII, digits, dot separators, hyphens inside segments when useful. The path is an index, not the whole source of truth.

tmux mapping

Keep tmux target names boring and target-safe, while preserving the richer logical path in Hudson's registry:

tmux session: hudson-<workspace>
tmux window:  <namespace>-<app>-<instance>
tmux pane:    <role>

Example:

logical path: hudson.lab.termini.canvas.0042.shell
tmux target:  hudson-lab:termini-canvas-0042.0
pane title:   shell

Hudson persists a registry record:

{
  "path": "hudson.lab.termini.canvas.0042.shell",
  "nodeID": "terminal-0042",
  "workspaceID": "lab",
  "namespace": "termini",
  "app": "canvas",
  "instance": "0042",
  "role": "shell",
  "tmuxSession": "hudson-lab",
  "tmuxWindow": "termini-canvas-0042",
  "tmuxPaneID": "%87",
  "bounds": { "x": 56, "y": 66, "width": 300, "height": 200 },
  "zIndex": 42
}

On relaunch, Hudson reconciles:

persisted registry
+ tmux list-sessions/windows/panes
= restored canvas with attachable nodes

If a tmux target exists without a canvas node, Hudson can offer to import it. If a canvas node exists without a tmux target, Hudson can show it as detached and offer to restart it.

Creation flow

Creating a large terminal grid should be staged:

1. Allocate node ids and graphite paths.
2. Place lightweight cards on the canvas immediately.
3. Ask tmux to create windows/panes in parallel.
4. Mark cards as ready as tmux targets appear.
5. Attach Termini renderers only for visible, selected, or zoomed-in cards.

The control plane is async. The canvas should never wait for 64 terminal surfaces to mount before showing the spatial result.

Reattach flow

Agentic callers should be able to provide an arbitrary set of durable IDs and ask Hudson to rebuild a canvas around them.

Initial sample command shape:

{
  "action": "reattach",
  "ids": [
    "hudson.lab.termini.canvas.0042.shell",
    "hudson.lab.agents.codex.0007.worker"
  ]
}

The sample also accepts raw tmux sessions and targets. Graphite ids resolve to tmux targets through the registry mapping; raw targets are used directly. For day-to-day agent use, the sample exposes this through:

apps/canvas/scripts/canvasctl.sh --wait reattach \
  --id hudson.lab.termini.canvas.0042.shell \
  --id hudson.lab.agents.codex.0007.worker

Remote tmux uses the same identity model. The local PTY runs SSH, and the remote host owns the durable tmux server:

apps/canvas/scripts/canvasctl.sh --wait reattach \
  --remote user@host \
  --session hudson-lab

This is still a sample-level control API, but it exercises the intended contract: runtime identity is durable and the canvas can be reconstructed from outside the app.

Save and restore

The first Canvas restore slice persists a tiny state file rather than a full canvas document system. It contains:

  • schema version and workspace id
  • viewport pan and zoom
  • durable tmux-backed node ids, bounds, z-order, tint, title, selection
  • runtime references: tmux target, optional Graphite path, optional remote host

Example:

packages/native/apple/HudsonKit/Scripts/canvasctl.sh --wait save \
  --state-file /tmp/scout-canvas-state.json

packages/native/apple/HudsonKit/Scripts/canvasctl.sh --wait restore \
  --state-file /tmp/scout-canvas-state.json \
  --create

For now, local PTYs are intentionally not persisted as durable nodes. The runtime boundary is tmux: Hudson restores spatial identity and asks Termini to reattach renderers to the saved tmux targets.

Selection and group actions

Selection is a first-class model, not a visual side effect:

public struct HudCanvasSelection: Codable, Equatable {
    public var ids: Set<String>
    public var query: HudPathQuery?
}

Selection sources:

  • Click and shift-click.
  • Marquee rectangle.
  • Path query, such as hudson.lab.termini.*.
  • Namespace/app filters.
  • Search result selection.

Group actions become natural:

ActionMeaning
MoveChange bounds for all selected nodes
TileReflow selected nodes into grid/columns/rows
SendSend keys/text/command to selected tmux targets
CaptureCapture pane output for selected targets
DetachKeep tmux alive, remove renderer
KillStop runtime targets
Fork workspaceMove or link selected targets into a new workspace canvas

Offshoot workspaces

The Graphite path makes lightweight workspace branching possible.

Example: select 20 nodes and create a new workspace called lab-debug.

Possible policies:

PolicyRuntime behaviorUse case
Movetmux windows move to hudson.lab-debugThe group becomes its own workspace
Linktmux windows are linked into another sessionTwo workspaces view the same live targets
Clonenew windows start from the same launch specsFresh sandbox with similar topology

Hudson should treat this as a canvas operation first: selected spatial nodes become a new workspace layout. Runtime movement/linking/cloning follows asynchronously.

Core HudsonKit extraction

The Termini sample should be reduced over time as these primitives move into HudsonShell:

PrimitiveResponsibility
HudCanvasViewportStatepan, scale, selected ids, active tool
HudCanvasTransformscreen/world conversion, zoom around anchor
HudCanvasViewportmulti-touch pan/zoom, grid, tool routing
HudCanvasZoomToolbottom-right zoom control
HudCanvasSelectionModelclick, shift-click, marquee, path queries
HudCanvasNodeStorestable ids, bounds, z-order, persistence
HudCanvasCarddrag, resize, focus, z-order without array churn
HudCanvasVirtualizationpreview/live renderer policy
HudTerminalOrchestratorruntime creation, attach/detach, status stream
HudTmuxBackendtmux-backed session/window/pane identity

The sample should eventually become mostly composition:

HudCanvasViewport(state: $viewport, nodes: $nodes) { node, context in
    HudTerminalCanvasCard(node: node, runtime: tmuxRuntime[node.id])
}

Viewport behavior

The native viewport should have the same explanation everywhere:

  • World coordinates are stable canvas coordinates.
  • Screen coordinates are current window pixels/points.
  • pan is the world offset projected into the viewport.
  • scale is zoom.
  • Zoom always has an anchor, preferably cursor/gesture centroid.
  • Trackpad pinch changes scale around the pinch centroid.
  • Two-finger scroll can pan the canvas when the event is not claimed by a live terminal.
  • Hand mode always pans.
  • Select mode click/marquee selects nodes.
  • Space can temporarily activate hand mode later, matching web.

This needs to be a real documented primitive because every canvas app will otherwise invent slightly different math.

Acceptance bar

  • 64 tmux-backed terminal nodes can be created as placeholders, then attached as runtimes become ready.
  • The same nodes are discoverable outside Hudson via tmux.
  • Hudson can relaunch and reattach from persisted registry plus tmux inspection.
  • Multi-select and group actions operate on both spatial ids and Graphite path queries.
  • A selected group can become a new workspace without tearing down its running terminals.
  • The Termini renderer is mounted only when the viewport/render policy says it is useful.
For AI agents