docs/02-data-model.md

02 — Data Model (Server / Workspace / Tab / Pane)

text
1Server
2└── Workspace "signaltty" (repo / project / task)
3 ├── Tab "agents" (one layout)
4 │ ├── Pane p1 (codex, ~/code/foo)
5 │ └── Pane p2 (claude, ~/code/foo)
6 └── Tab "tests" (another layout)
7 └── Pane p3 (shell, ~/code/foo)

Server

  • id: opaque server id (stable per socket path).
  • socket_path: $XDG_RUNTIME_DIR/signaltty/signaltty.sock.
  • state_dir: $XDG_STATE_HOME/signaltty (~/.local/state/signaltty).
  • Owns: workspaces, PTYs, event bus, persistence.

Workspace

Usually one repo/project/task/investigation.

FieldMeaning
idopaque, e.g. ws_…
namehuman label, editable, non-unique
cwddefault working directory for new panes
repo_root, repo_name, branch, dirtycached Git context (see below)
tabsordered list of tab ids
active_tab_idlast focused tab (per last client; server keeps one canonical value)
created_at, updated_attimestamps

Git context is a lightweight cached enrichment, refreshed on pane.cwd change, explicit refresh, and cheap filesystem events — never a Git client, never aggressive polling.

Tab

One named layout inside a workspace (agents, shell, tests, logs).

FieldMeaning
idopaque, e.g. tab_…
workspace_idparent
titlehuman label
layoutbinary split tree (see below)
active_pane_idlast focused pane in this tab
created_attimestamp

Layout is a binary tree:

rust
1enum Layout { Pane(PaneId), Split { dir: H|V, ratio: f32, first: Box<Layout>, second: Box<Layout> } }

The GUI renders this tree; the server only stores it. Splitting a pane replaces a Pane leaf with a Split, then rebalances the run: every visual sibling in the same direction gets an equal share, so repeated splits divide a tab into thirds, quarters, and so on instead of halving the newest pane. Closing the last pane of a tab closes the tab (configurable: keep empty tabs or not; default close). Divider positions are plain ratios in the tree; clients move one divider with tab.set_ratio (path of first/second choices from the root, ratio clamped to 0.05–0.95), which persists like any other tab change.

Pane

One terminal/PTY + process. Survives client detach while the server lives.

FieldMeaning
idopaque, e.g. pane_…
workspace_id, tab_idparents (pane can be moved)
titlefrom OSC 0/1/2 title sequences, else argv[0]
cwdlaunch cwd; refreshed from /proc/<pid>/cwd on Linux
argvlaunch command (persisted; see restore policy in 09)
pty_size{cols, rows} last arbitrated size
live`Live
agentAgentInfo (below), all optional for plain shells
lifecycle / attentionsee 03
lifecycle_sincewhen lifecycle last changed (optional; absent in old snapshots)
last_run_secslength of the latest working stretch — GUI renders "Working for 2m…" → "Worked for 2m"
last_messagelatest explicit notification / hook payload summary (never raw scrollback scrape as primary)
scrollback_lenbytes/lines retained (bounded ring)
created_at, last_activity_at, last_seen_attimestamps

AgentInfo (per pane, adapter-owned)

FieldMeaning
kind`codex
agent_session_idnative session id reported by adapter/hook (persisted)
resume_argvadapter-built official resume command (persisted, never auto-run without consent)
model, extrasmall adapter metadata map

agent_session_id must come from hooks/APIs/env (CLAUDE_CODE_SESSION_ID, CODEX_THREAD_ID, hook JSON payloads, OpenCode plugin events, Cursor hooks.json), never from scraping terminal text when a better channel exists.

Notification

FieldMeaning
idopaque
pane_id, workspace_idrouting; pane may be unknown for server-level notes
title, bodysanitized plain text (control chars stripped)
severity`info
source`osc9
created_at, read_attimestamps

IDs

UUIDv4 strings with a short type prefix for human greppability (ws_, tab_, pane_, notif_). Prefix is cosmetic; treat as opaque.