docs/02-data-model.md
02 — Data Model (Server / Workspace / Tab / Pane)
| 1 | Server |
| 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.
| Field | Meaning |
|---|---|
id | opaque, e.g. ws_… |
name | human label, editable, non-unique |
cwd | default working directory for new panes |
repo_root, repo_name, branch, dirty | cached Git context (see below) |
tabs | ordered list of tab ids |
active_tab_id | last focused tab (per last client; server keeps one canonical value) |
created_at, updated_at | timestamps |
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).
| Field | Meaning |
|---|---|
id | opaque, e.g. tab_… |
workspace_id | parent |
title | human label |
layout | binary split tree (see below) |
active_pane_id | last focused pane in this tab |
created_at | timestamp |
Layout is a binary tree:
| 1 | enum 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.
| Field | Meaning |
|---|---|
id | opaque, e.g. pane_… |
workspace_id, tab_id | parents (pane can be moved) |
title | from OSC 0/1/2 title sequences, else argv[0] |
cwd | launch cwd; refreshed from /proc/<pid>/cwd on Linux |
argv | launch command (persisted; see restore policy in 09) |
pty_size | {cols, rows} last arbitrated size |
live | `Live |
agent | AgentInfo (below), all optional for plain shells |
lifecycle / attention | see 03 |
lifecycle_since | when lifecycle last changed (optional; absent in old snapshots) |
last_run_secs | length of the latest working stretch — GUI renders "Working for 2m…" → "Worked for 2m" |
last_message | latest explicit notification / hook payload summary (never raw scrollback scrape as primary) |
scrollback_len | bytes/lines retained (bounded ring) |
created_at, last_activity_at, last_seen_at | timestamps |
AgentInfo (per pane, adapter-owned)
| Field | Meaning |
|---|---|
kind | `codex |
agent_session_id | native session id reported by adapter/hook (persisted) |
resume_argv | adapter-built official resume command (persisted, never auto-run without consent) |
model, extra | small 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
| Field | Meaning |
|---|---|
id | opaque |
pane_id, workspace_id | routing; pane may be unknown for server-level notes |
title, body | sanitized plain text (control chars stripped) |
severity | `info |
source | `osc9 |
created_at, read_at | timestamps |
IDs
UUIDv4 strings with a short type prefix for human greppability
(ws_, tab_, pane_, notif_). Prefix is cosmetic; treat as opaque.