@vibelord/lanes 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/lanes/README.md +92 -7
- package/payload/lanes/VERSION +1 -1
- package/payload/lanes/bin/lanes +928 -150
- package/payload/lanes/client/ghostty/common.conf +3 -0
- package/payload/lanes/client/ghostty/lanes-palette-light.conf +25 -0
- package/payload/lanes/client/ghostty/lanes-palette.conf +25 -0
- package/payload/lanes/client/ghostty/omarchy.conf +3 -2
- package/payload/lanes/client/lanes-launch +62 -5
- package/payload/lanes/client/lanes-window +17 -1
- package/payload/lanes/container/env.sh +8 -0
- package/payload/lanes/container/hooks/claude-settings.json +10 -0
- package/payload/lanes/container/hooks/codex-hooks.json +22 -0
- package/payload/lanes/container/lane +326 -25
- package/payload/lanes/container/lane-signal +173 -0
- package/payload/lanes/docs/ARCHITECTURE.md +158 -6
- package/payload/lanes/docs/DESIGN.md +250 -0
- package/payload/lanes/docs/INSTALL.md +39 -0
- package/payload/lanes/docs/RUNBOOK.md +507 -24
- package/payload/lanes/docs/SHARING.md +1 -1
- package/payload/lanes/holders/exec.sh +2 -5
- package/payload/lanes/holders/ssh.sh +18 -0
- package/payload/lanes/holders.lock +3 -0
- package/payload/lanes/install/enable-phone.sh +15 -6
- package/payload/lanes/install/enable-web.sh +19 -6
- package/payload/lanes/install/host-install.sh +10 -1
- package/payload/lanes/install/services.py +30 -3
- package/payload/lanes/install.sh +108 -3
- package/payload/lanes/libexec/agent_state.py +246 -0
- package/payload/lanes/libexec/connection.py +178 -0
- package/payload/lanes/libexec/cursor_sync.py +147 -6
- package/payload/lanes/libexec/device_sync.py +134 -0
- package/payload/lanes/libexec/economy_poll.py +685 -0
- package/payload/lanes/libexec/guard.py +27 -1
- package/payload/lanes/libexec/lanes_lib.py +436 -36
- package/payload/lanes/libexec/notify.py +317 -0
- package/payload/lanes/libexec/registrations.py +148 -0
- package/payload/lanes/libexec/session_actions.py +53 -22
- package/payload/lanes/libexec/sidebar.py +1190 -186
- package/payload/lanes/libexec/statusd.py +352 -90
- package/payload/lanes/libexec/tokens.py +310 -0
- package/payload/lanes/libexec/tokens.sh +11 -0
- package/payload/lanes/libexec/view.sh +22 -11
- package/payload/lanes/systemd/lanes-economy.service +20 -0
- package/payload/lanes/systemd/lanes-notify.service +19 -0
- package/payload/lanes/systemd/lanes-sync.service +3 -0
- package/payload/lanes/web/README.md +96 -2
- package/payload/lanes/web/browser.css +22 -0
- package/payload/lanes/web/browser.js +73 -0
- package/payload/lanes/web/index.html +2 -1
- package/payload/lanes/web/mobile.css +108 -221
- package/payload/lanes/web/mobile.js +105 -55
- package/payload/lanes/workspaces.example/README.md +9 -3
- package/payload/lanes/zellij/config.kdl +128 -19
- package/payload/manifest.json +7 -3
- package/payload/lanes/libexec/legacy_sessions.py +0 -130
|
@@ -8,21 +8,117 @@ terminal. Installing or closing a view does not end a lane.
|
|
|
8
8
|
|
|
9
9
|
1. The deployment's `catalog.json` orders workspaces and selects their manifests.
|
|
10
10
|
2. Each manifest identifies container names and repository `.vscode/terminals.json` paths.
|
|
11
|
-
3. `lanes sync` reads those files through the container's mounted home
|
|
12
|
-
|
|
11
|
+
3. `lanes sync` reads those files through the container's mounted home, includes the site's optional
|
|
12
|
+
`registered-sessions.json`, and emits the host registry,
|
|
13
|
+
exact socket/session resolve files, and the container's `.lz/registry.json` projection. It asks docker about
|
|
14
|
+
every board container in ONE `docker inspect` and rewrites a projection only when its content changed
|
|
15
|
+
(`atomic_write_if_changed`; the two registry documents compare without `generated_at`), so a timer sync with
|
|
16
|
+
nothing new wakes no reader that watches an mtime. The sync time itself is kept in `registry.synced`.
|
|
13
17
|
4. The status daemon observes tmux sessions and publishes status/health snapshots. Failed or timed-out
|
|
14
18
|
probes mean unknown; they do not prove a missing session. A bad terminal registry retains the last good board.
|
|
15
19
|
|
|
20
|
+
Beside the six-word `state` (never changed by it), each live tmux lane, declared or unregistered, may carry
|
|
21
|
+
an additive `agent{}` key: `{harness, status, name, session_id?, pid?, since, source}` with `status` one of
|
|
22
|
+
`working | needs_input | needs_approval | done | errored | shell | idle`. An absent key means no signal names
|
|
23
|
+
an agent there. `statusd` builds it with `agent_state.merge` from signals the harnesses already emit, read once
|
|
24
|
+
per container per cycle through `/proc/<container init pid>/root` without spawning anything. Precedence: a
|
|
25
|
+
hook sidecar > a Claude `~/.claude/sessions/<pid>.json` naming the pane
|
|
26
|
+
`<session>:@<win>.%<pane>` (live pids only, checked in the container's pid namespace, recycled pids rejected
|
|
27
|
+
by `procStart`) > the Codex pane title (`[ ! ] Action Required` → `needs_approval`, braille spinner →
|
|
28
|
+
`working`, a task title → `idle`) > the window bell (an idle title plus a bell is `done`; an approval title
|
|
29
|
+
wins over the bell). A Codex title over a bare-shell foreground whose shell has no child process
|
|
30
|
+
(`pane_has_child`, read from `/proc/<pid>/task/*/children`) is `shell`: the harness ended and tmux kept its
|
|
31
|
+
title. Unknown liveness leaves that key unset and never demotes. The tmux columns this needs
|
|
32
|
+
(`window_bell_flag`, `window_id`, `pane_id`, `pane_title`) are appended to the listing format, the free-text
|
|
33
|
+
title last so a tab inside it cannot shift another column.
|
|
34
|
+
|
|
35
|
+
The hook sidecar is `~/.lz/state/hooks/<lane id>.json` in the container, written by `~/.lz/bin/lane-signal`
|
|
36
|
+
(Python 3 stdlib, installed by `lanes install-container`) when an opt-in Claude Code or Codex hook fires:
|
|
37
|
+
`{v: 1, lane, harness, status, since, at, event, pid, start, session}` — `since` when the status began (kept
|
|
38
|
+
while the same status is re-signalled), `at` the last event, `pid`/`start` the agent process and its
|
|
39
|
+
starttime. The lane id is `$LANE_ID` (in a tmux lane only through a canonical recipe's `-e` flags; in a zmx lane
|
|
40
|
+
`lane-run` exports it as the zmx session name) or the pane's tmux session looked up in `~/.lz/registry.json`; an unresolvable lane, a word outside the vocabulary or an
|
|
41
|
+
unwritable directory is a silent exit 0, and nothing is ever printed (a Claude prompt/session hook's stdout
|
|
42
|
+
becomes model context). `statusd` lists each container's `state/hooks/` once per cycle (`agent_sidecars`) and
|
|
43
|
+
`agent_state.read_sidecar_dict` drops a sidecar whose last event is older than `SIDECAR_MAX_AGE` (3600 s) or
|
|
44
|
+
whose agent pid is dead or recycled in the container's pid namespace. The sidecar has its own directory because
|
|
45
|
+
`lane-run` writes each zmx lane's `{phase, exit, at, pid}` record to `~/.lz/state/<zmx name>.json` and exports
|
|
46
|
+
that same name as `LANE_ID`: one shared file name would let a hook overwrite the record the zmx observer and the
|
|
47
|
+
container `lane ls` read (DEF-1436). A zmx lane's sidecar is therefore named by its zmx session, not by the
|
|
48
|
+
`<ws>--z-<name>` id statusd publishes, and does not reach that lane's `agent{}` yet — the zmx lane's `agent` key
|
|
49
|
+
stays lane-run's record (DEF-1437). A published sidecar adds `event` to `agent{}` and has `source: hook`. Hooks are
|
|
50
|
+
never installed implicitly: `lanes hooks install --agent claude|codex --container <name>` prints the merge and
|
|
51
|
+
writes only with `--write` (RUNBOOK "Installing agent hooks").
|
|
52
|
+
5. A lane's `source` is the container's spelling of its `terminals.json`. Every host-side reader of that file
|
|
53
|
+
maps it to the container's home volume first (`statusd.registry_source_paths`, deduplicated by that host path:
|
|
54
|
+
one repository cloned in two containers is two files): the status daemon's hot-sync
|
|
55
|
+
starts one `lanes sync` per edit (at most one per 10 s), `lanes doctor` reports "registry up to date" from the
|
|
56
|
+
same host-side mtimes (against the later of `generated_at` and `registry.synced`), and promoting an unregistered session writes the binding through the volume.
|
|
57
|
+
|
|
16
58
|
`lanes cursor-sync` is a separate, explicit writer. Its default is a diff; `--write` takes backups and changes
|
|
17
59
|
editor commands to `lane open <id>`. The original create recipe and exact tmux binding remain in the registry's
|
|
18
60
|
`lanes` block. Automatic sync never commits or pushes those edits. `cursor-commit --push` is an opt-in workflow.
|
|
19
61
|
|
|
62
|
+
Lane identity is an environment contract: `LANE_ID` (`<ws>--<slug>`), `LANE_WS` (`<ws>`), and the Throttle trace
|
|
63
|
+
`lanes/<ws>/<id>` as `THROTTLE_TRACE` and as `ANTHROPIC_CUSTOM_HEADERS='x-throttle-trace: lanes/<ws>/<id>'` (Claude
|
|
64
|
+
Code sends it on every request; the proxy's trace charset is `[A-Za-z0-9._-/]`, at most 256 characters, and a
|
|
65
|
+
trace outside it is never emitted). A tmux lane gets it in exactly one way: the `-e` flags of
|
|
66
|
+
`lanes_lib.canonical_recipe()` — `tmux -L <socket> new -ADs <session> -c <folder> -e … <cmd>` — which set the
|
|
67
|
+
SESSION environment of that one lane on a real create (`-A` ignores them on an attach). Never through `tmux
|
|
68
|
+
set-environment` and never through a client's environment, because of a tmux rule MEASURED on tmux 3.4 (the
|
|
69
|
+
container's version) with a throwaway socket: the client that STARTS a socket's server hands its whole environment
|
|
70
|
+
to the server's GLOBAL environment (`show-environment -g`), and every later session on that socket inherits it
|
|
71
|
+
unless its own `-e` overrides each key — a session created by a client carrying `LANE_ID=b` but no `-e` still got
|
|
72
|
+
the server starter's `LANE_ID=a`, and one created with `-e LANE_ID=c` alone kept `a`'s `THROTTLE_TRACE`. Board
|
|
73
|
+
sockets are shared (9 of the 10 bw-tools fixture lanes are on one socket), so `lane open` execs nothing — attach or
|
|
74
|
+
recipe — that carries a lane identity, this lane's or one inherited from another lane's editor terminal (DEF-1478);
|
|
75
|
+
a recipe run by it may start the server with an identity-free global environment only. An attach cannot change a
|
|
76
|
+
running session's env, so a running lane gains its identity only when it is next CREATED by a recipe carrying `-e`.
|
|
77
|
+
A legacy recipe without `-e` gives its lane no identity. `lanes cursor-sync --canonicalize` (dry-run unless
|
|
78
|
+
`--write`) gives every tmux lane whose recorded `create` cannot create it — null, attach-only (`attach-session`
|
|
79
|
+
without `new`), naming the retired in-container `~/.config/coder-launch` path (these three are
|
|
80
|
+
`lanes_lib.classify_recipe`'s classes, the ones `lanes doctor` warns about), or naming an absolute `.sh`/`.py`/…
|
|
81
|
+
script missing in the container — the canonical recipe with
|
|
82
|
+
`bash -l`; `--identity` gives a WORKING recipe of the plain shape `[exec] tmux -L <socket> new -ADs <session> [-c
|
|
83
|
+
<dir>] <command…>` the same `-e` flags, re-quoting the command's words one for one so tmux receives the same argv
|
|
84
|
+
(any expansion, glob, operator or other shape is left alone and stays identity-less). Both keep the old value as
|
|
85
|
+
`createPrevious`; a recipe whose paths cannot be checked (outside the home volume) is never counted as broken.
|
|
86
|
+
zmx lanes get `LANE_ID` (only) from `lane-run`, unchanged.
|
|
87
|
+
|
|
88
|
+
The trace is also a FAIRNESS key, not only an attribution key: the proxy uses `x-throttle-trace` when present and
|
|
89
|
+
synthesizes `cc-session/<session>[/<agent>]` only when it is absent (`src/proxy.rs`, `synthesize_flow_key`), and the
|
|
90
|
+
pool's parking queue is round-robin across trace keys at slot handoff (`src/pressure.rs`, `acquire_fair`). A lane
|
|
91
|
+
stamped `lanes/<ws>/<id>` therefore puts its main session, every subagent and any workflow fan-out started in it
|
|
92
|
+
into ONE fairness bucket where today each Claude session/agent is its own. `max_in_flight_per_trace` is configured
|
|
93
|
+
and reported but no enforcement site exists in `src/` (MEASURED by search; `pressure.rs` documents fairness as "not a
|
|
94
|
+
hard cap"). Whether a lane should be one bucket is an open decision for the plan owner before any live enablement
|
|
95
|
+
(DEF-1479); this tree does not change the header shape. Unit-proven only (`test_lib_probe` `LaneOpenIdentity`,
|
|
96
|
+
`CursorSyncCanonicalize`, `CursorSyncIdentity`, stopping at the exec boundary); the disposable-lane
|
|
97
|
+
`tests/cursor_lane.py` exercises the installed `/home/coder/.lz/bin/lane`, so it proves this only after `lanes
|
|
98
|
+
install-container`.
|
|
99
|
+
|
|
20
100
|
## Views and panes
|
|
21
101
|
|
|
22
102
|
`lanes attach DEVICE` creates or reattaches the device's Zellij view. Layouts have one tab per workspace,
|
|
23
103
|
a lane sidebar, and one to three viewports. The mobile layout uses separate picker and full-width view tabs.
|
|
24
104
|
Its hidden viewports do not attach at placeholder dimensions; they wait until a real client displays them.
|
|
25
105
|
|
|
106
|
+
**Visual system:** one token table (`libexec/tokens.py`) is rendered into the sidebar's escapes, the Zellij themes,
|
|
107
|
+
the phone CSS (and its xterm.js palette in `mobile.js`), the viewer's shell (`libexec/tokens.sh`), the optional Ghostty
|
|
108
|
+
palette files and the docs table; `tokens.py --check` fails on drift in any of them. `docs/DESIGN.md` is the legend:
|
|
109
|
+
glyphs with their ASCII twins, modes (`LANES_COLOR`, `LANES_THEME`, `LANES_GLYPHS`, `LANES_DIM`, `LANES_NOTIFY`), row
|
|
110
|
+
anatomy, mockups and budgets; `tests/unit/test_docs.py` keeps the README legend complete against the glyph tables.
|
|
111
|
+
|
|
112
|
+
Frames and bars take their colours from the `lanes-dark`/`lanes-light` themes generated by `libexec/tokens.py`; the
|
|
113
|
+
compact bar is the only plugin; the mobile picker's sidebar pane has no layout name so its own title (attention
|
|
114
|
+
counts) reaches the phone. `zellij/config.kdl` adds, after the generated token block, one `ui` block whose
|
|
115
|
+
`pane_frames` set `rounded_corners false` and `hide_session_name true` (straight frames to match the sidebar's `─`
|
|
116
|
+
rules, no session name in the bar) and `simplified_ui true` (no Private-Use-Area chevrons, so no Nerd Font is needed);
|
|
117
|
+
`load_plugins` stays empty. The desktop sidebar keeps `pane size=36 borderless=true name="Lanes"` (Throttle Studio's view rule reads
|
|
118
|
+
that name); the view pane is borderless at one viewport and framed at two or three. `tests/unit/test_layout.py` pins
|
|
119
|
+
the generated layout and `test_keybinds.Chrome` the config; `zellij --config zellij/config.kdl setup --check` is the
|
|
120
|
+
parse check (0.45.1).
|
|
121
|
+
|
|
26
122
|
The tmux holder reaches the container's socket through `/proc/<init-pid>/root`, scrubs the viewer environment,
|
|
27
123
|
and uses `tmux -N` so an attach cannot create a server accidentally. Guarded geometry and client identification
|
|
28
124
|
keep the visible view and editor attachments coordinated. Zmx is a separately pinned alternative holder;
|
|
@@ -39,20 +135,59 @@ manifest `discoveryRoots` extends those roots. A discovered assignment remains s
|
|
|
39
135
|
a session no single board can claim is listed by EVERY owner of its socket rather than dropped, and ambiguity is
|
|
40
136
|
logged rather than resolved by guess.
|
|
41
137
|
|
|
138
|
+
**+** beside an unregistered tmux session opens **Add to workspace**. It offers workspaces using the session's
|
|
139
|
+
container, with the current workspace first. Registration verifies the observed session ID/creation time and
|
|
140
|
+
saves its existing lane ID, name and exact binding in the version-1 `registered-sessions.json` site file.
|
|
141
|
+
It publishes the registered lane immediately without signaling viewers or processes. Sync, registration and
|
|
142
|
+
explicit close share a bounded registry lock; close also consults saved registrations if projection publication
|
|
143
|
+
failed. Stale observations cannot put a registered session back in Unregistered.
|
|
144
|
+
|
|
145
|
+
These registrations have no create recipe and do not change repository terminal lists. Repository declarations
|
|
146
|
+
win if they later declare the same binding. Unknown versions or malformed registration files refuse mutation;
|
|
147
|
+
registrations for removed workspaces are retained on disk and ignored until the workspace returns. Removing a
|
|
148
|
+
record and syncing unregisters it, including when a repository registry is temporarily invalid. Keep this site
|
|
149
|
+
file in operator backups; deleting derived state does not delete registrations.
|
|
150
|
+
|
|
42
151
|
Explicit **New +** starts a detached tmux shell inside the workspace container on its dedicated
|
|
43
152
|
`lanes-shells-<workspace>` socket. Reopening only attaches. Explicit **×** closes an unregistered session after
|
|
44
153
|
rechecking its session ID and creation time and refusing registered lanes; affected viewers become empty.
|
|
45
154
|
The tmux close condition is checked inside the server, protecting a replacement created between observation
|
|
46
155
|
and the action. `user-sessions/` stores display metadata; `status.json` and `resolve.dynamic/` are projections.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
156
|
+
The pre-persistence Docker-shell tracker (`legacy-sessions/`, `legacy_sessions.py`, `lanes verify-legacy`) was
|
|
157
|
+
retired by plan Task 19: statusd no longer reads that directory (a record still there is inert — kept on disk,
|
|
158
|
+
never observed, never closed), and a `legacy` row from an older status.json is refused by close. See
|
|
159
|
+
[state contracts](../../docs/master-datamodel.md).
|
|
51
160
|
|
|
52
161
|
The `lane` command lives in `/home/coder/.lz/bin`. `open` attaches or creates using its saved recipe;
|
|
53
162
|
`attach` never creates; `send`, `capture`, `ls` and `status` are explicit operations. Only `kill` ends a lane.
|
|
54
163
|
These commands operate inside the container, independently of the Zellij viewer's lifetime.
|
|
55
164
|
|
|
165
|
+
Every verb dispatches on the lane's holder. An id that `~/.lz/registry.json` holds as `holder: tmux` is driven
|
|
166
|
+
through its exact socket and session, by a tmux client that carries no lane identity (`identity_free`, as `open`):
|
|
167
|
+
`send` types the text literally and then Enter in one client (`send-keys -t =<session>: -l -- TEXT ; send-keys
|
|
168
|
+
Enter`; `--no-enter` omits it), `capture` is `capture-pane -p -J -S -N|- -t =<session>:` (`--vt` adds `-e`) trimmed
|
|
169
|
+
to the last N non-blank lines, `status` prints `{id, holder, ws, title, tmux, alive, agent}` from `has-session` and
|
|
170
|
+
the lane's `lane-signal` hook sidecar (exit 1 when not alive), `attach` refuses to nest and never creates, and
|
|
171
|
+
`kill` is `kill-session`. Two tmux 3.4 facts shape these argv (MEASURED on a disposable `lanes_test` server): a bare
|
|
172
|
+
`-t =<session>` is refused by `send-keys` and `capture-pane` ("can't find pane") while `=<session>:` resolves, and
|
|
173
|
+
tmux's argv parser ends a command at a trailing `;` of ANY argument, so `echo a;` would type `echo a` — `send`
|
|
174
|
+
writes that last `;` as `\;`. Any other id is a zmx lane (a registry `holder: zmx` entry maps to its `zmx.name`) and
|
|
175
|
+
keeps the zmx path; zmx `send` still writes the bytes as given, never an Enter of its own. `kill` now needs `--yes`
|
|
176
|
+
on both holders and refuses while the lane's hook sidecar says `working` unless `--force`; the container sees only
|
|
177
|
+
that sidecar (Claude session files and pane titles are judged on the host by `lanes-statusd`), so on a lane without
|
|
178
|
+
installed hooks the refusal cannot fire (DEF-1439). `ls --json` stays the zmx-only listing Throttle Studio reads —
|
|
179
|
+
its zmx holder takes EVERY row as a zmx lane, so tmux rows there would publish each tmux lane twice; `ls --all` adds
|
|
180
|
+
the registry's tmux lanes with `alive` from one `list-sessions` per socket (and still lists them in a root without the zmx binary, where the default listing keeps failing as before). zmx rows gain `alive`, and `status` of a
|
|
181
|
+
zmx lane gains `hook` (its sidecar); `agent` stays lane-run's phase record (DEF-1436).
|
|
182
|
+
|
|
183
|
+
The bw-tools agent launchers (`.bw-tools/bin/claude`, `codex`) exec the agent binary (shell `exec` → `agent.py`
|
|
184
|
+
`os.execvpe`), so a lane started through them shows the agent as the pane's foreground command. The operator's
|
|
185
|
+
`~/.local/bin/eval-claude` runs Claude as a child instead (it appends `session_end` to its trace file after the
|
|
186
|
+
exit), so a pane started through it keeps `bash` as its foreground command (DEF-1440); on the launcher path the end
|
|
187
|
+
of a session is the `SessionEnd` hook's `lane-signal shell`, when hooks are installed. `.bw-tools/bin/claude` runs
|
|
188
|
+
`$EVAL_CLAUDE_REAL_BIN` when it is set (the binary eval-claude would run) and skips it when it names a `.bw-tools`
|
|
189
|
+
shim, so `EVAL_CLAUDE_REAL_BIN=<checkout>/.bw-tools/bin/claude eval-claude` composes the two without a loop.
|
|
190
|
+
|
|
56
191
|
## Desktop and uploads
|
|
57
192
|
|
|
58
193
|
The client installer creates an SSH alias and dedicated terminal configuration, using the selected profile's
|
|
@@ -77,6 +212,23 @@ The phone shell uses the pinned client's window hooks for workspaces, panes, inp
|
|
|
77
212
|
swipe, refresh or server message cannot summon the keyboard. Native taps on input fields still work.
|
|
78
213
|
Safe-area insets and the visual viewport handle the Home indicator, rotation and keyboard.
|
|
79
214
|
|
|
215
|
+
`web/mobile.js` chooses one of three modes once at load, by capability (`laneShellMode`), and records it on
|
|
216
|
+
`<html data-lanes-shell>`. A **phone** (mobile user agent, or a touch screen at most 600 px wide or 500 px tall)
|
|
217
|
+
gets the shell above unchanged. A **tablet** (any other touch screen) gets the same shell with its dock in the top
|
|
218
|
+
bar and the workspace sheet as a left panel. It also asks the pinned client for its mobile UI, because iPad Safari
|
|
219
|
+
reports a desktop user agent. That UI opens in single pane on every load; `web/browser.js` then sends the client's
|
|
220
|
+
own full-view preference once, so a desktop layout's sidebar shows beside the lanes. A **desktop** browser (fine
|
|
221
|
+
pointer, any width) keeps the plain client. Only a **Lanes** chip is added; it toggles an overlay bar with
|
|
222
|
+
workspace and lane pickers. The bar reads the `MobileState` that Zellij sends to every web client and sends the
|
|
223
|
+
phone picker's `FocusPane`. It overlays the terminal, so the terminal's columns and rows do not change, and a
|
|
224
|
+
click on the chip leaves the keyboard focus in the terminal. The tablet and desktop code and styles live in
|
|
225
|
+
`web/browser.js` and `web/browser.css`, which `mobile.js` appends only in those two modes, so a phone never
|
|
226
|
+
downloads them: a phone fetches only the two files spec §8 budgets, MEASURED 2026-10-04 after the fan-in with
|
|
227
|
+
plan Tasks 13/28 at 21 964 B of 22 174 B (`mobile.js`, whose explanations live in `web/README.md`, Code notes)
|
|
228
|
+
and 18 945 B of 18 982 B (`mobile.css`). `lanes web <device> [--desktop|--mobile]` prepares a browser view,
|
|
229
|
+
enables the services through `install/enable-web.sh` and prints the private URL; without a flag a known device
|
|
230
|
+
keeps its layout.
|
|
231
|
+
|
|
80
232
|
## Installed files and services
|
|
81
233
|
|
|
82
234
|
`install/services.py` renders the service templates against the actual checkout and installed binary paths.
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# Lanes design system
|
|
2
|
+
|
|
3
|
+
The operator-facing legend for the Lanes cockpit: what every glyph, colour and switch means, how a sidebar row is
|
|
4
|
+
laid out, and the budgets each surface is held to. The token and glyph tables below are generated from
|
|
5
|
+
`libexec/tokens.py` (`python3 libexec/tokens.py --write`; `--check` fails on drift), the one source the sidebar's
|
|
6
|
+
escapes, the Zellij themes, the phone CSS, the viewer's shell (`libexec/tokens.sh`), the optional Ghostty palette and
|
|
7
|
+
this page are projected from. The design of record is
|
|
8
|
+
[`docs/superpowers/specs/2026-10-03-lanes-cockpit-design.md`](../../docs/superpowers/specs/2026-10-03-lanes-cockpit-design.md)
|
|
9
|
+
at the repository root; this page condenses it for daily use and never overrides it.
|
|
10
|
+
|
|
11
|
+
## Principles
|
|
12
|
+
|
|
13
|
+
1. **Attention before identity before telemetry:** column 2 says who needs you, columns 4–27 which lane, columns
|
|
14
|
+
29–34 how long and doing what.
|
|
15
|
+
2. **Never colour alone:** every state has a glyph, a tone and an ASCII twin, and a fixed column; `LANES_COLOR=none`
|
|
16
|
+
leaves a fully readable sidebar.
|
|
17
|
+
3. **One token set, many renderers:** `libexec/tokens.py` is the only place a colour or glyph is defined; every
|
|
18
|
+
other surface is a generated projection pinned by a drift test.
|
|
19
|
+
4. **Theme-agnostic by construction:** the sidebar paints foreground only, learns dark/light once (OSC 11), falls
|
|
20
|
+
back to dark, and keeps lane identity colours in your own 16-colour palette.
|
|
21
|
+
5. **Dim is a colour, not an attribute:** no SGR 2 in any frame; ink-dim in truecolor, bright black (`90`) in
|
|
22
|
+
16-colour mode.
|
|
23
|
+
6. **Restraint:** one accent, for selection, viewing marks and the brand only; no emoji in rows, one upload line, a
|
|
24
|
+
one-line legend plus the `?` key sheet.
|
|
25
|
+
7. **Every row says how long:** a 3-cell age on every row — the agent's `since`, else the last output.
|
|
26
|
+
8. **Marks never lose to names:** viewer dots and viewport marks are reserved first; the name is clipped and ends
|
|
27
|
+
in `…`.
|
|
28
|
+
9. **The cursor follows the lane, not the index:** re-sorts never move your focus in a list that scrolls.
|
|
29
|
+
10. **Performance is a design property:** cached row bytes, one width walk per name, frames written only when dirty,
|
|
30
|
+
OSC 2/9/11 in the same write; budgets below.
|
|
31
|
+
11. **Pinned bytes change deliberately:** a moved byte assertion is rewritten with a role name, never loosened.
|
|
32
|
+
12. **The phone is a list, not a terminal:** the system-font shell carries identity, counts and actions.
|
|
33
|
+
13. **Docs are the fourth surface:** this page, the README legend and the RUNBOOK column table are kept with the
|
|
34
|
+
code; the screenshots are user-supplied and never regenerated.
|
|
35
|
+
|
|
36
|
+
## Modes
|
|
37
|
+
|
|
38
|
+
Four switches pick the look; `devices/<device>.json` (written by `lanes attach DEVICE --ascii|--theme|--glyphs|--color`
|
|
39
|
+
and by `lanes-window --ascii|--light|--dark`) wins over the environment and reaches a running sidebar within one tick.
|
|
40
|
+
|
|
41
|
+
| Switch | Values | Default | Where read |
|
|
42
|
+
|---|---|---|---|
|
|
43
|
+
| `LANES_COLOR` | `truecolor` · `16` · `none` | `truecolor` when `COLORTERM` ∈ {`truecolor`, `24bit`} (config.kdl's `env` block forces it for every pane), else `16`; `NO_COLOR` set ⇒ `none` | `sidebar.py` at start **and on every tick**: `devices/<device>.json` is part of `reload()`'s mtime stamp (one `stat()`), its `color` key wins over env, and a changed file re-runs `resolve_modes()` → `sgr_table()`/`glyph_set()` and clears the row cache (`apply_look()`). Zellij panes inherit the *server's* environment at session creation, so an environment variable can never reach a running sidebar; the device file is the one channel that does |
|
|
44
|
+
| `LANES_THEME` | `dark` · `light` · `auto` | `auto`: one OSC 11 query `\033]11;?\033\\` written at start; the reply (`\033]11;rgb:RRRR/GGGG/BBBB\033\\` or BEL-terminated, 8- or 16-bit components) is parsed in `handle()` and **swallowed** — it never cancels a filter or prompt; relative luminance < 0.5 ⇒ dark; no reply within 300 ms ⇒ dark; no re-ask | same; `devices/<device>.json` key `theme` (per tick, as above); `STATE/clients/<device>.theme` is written (`dark`/`light`, only on change) for `glow` |
|
|
45
|
+
| `LANES_GLYPHS` | `unicode` · `ascii` · `emoji` | `unicode`; `ascii` automatically when none of `LC_ALL`/`LC_CTYPE`/`LANG` contains `UTF-8` | same; `devices/<device>.json` key `glyphs` (per tick, as above); `lanes attach DEVICE --ascii` / `--theme light` write them and every running sidebar of that device picks them up within one tick; `lanes-window --ascii`/`--light`/`--dark` pass them through. The sidebar publishes the resolved mode as `STATE/clients/<device>.glyphs` (`unicode`/`ascii`/`emoji` + newline, written on change or when absent) — the file `view.sh` reads with a builtin `read` for its status screens (§7) |
|
|
46
|
+
| `LANES_DIM` | `90` · `2` | `90` | 16-colour mode only: `2` is the documented escape hatch for palettes whose colour 8 is near the background (classic Solarized dark) |
|
|
47
|
+
| `LANES_NOTIFY` | `1` · `0` | `1` on desktop devices, `0` on mobile | OSC 9 emission (§4.9) |
|
|
48
|
+
|
|
49
|
+
## Tokens and glyphs
|
|
50
|
+
|
|
51
|
+
Every glyph table is `key -> (unicode, ascii, tone)`: agent glyphs are drawn only in column 2, stream glyphs only in
|
|
52
|
+
column 33, marks only where the row anatomy puts them. `LANES_GLYPHS=ascii` swaps in the ASCII column, so an ASCII
|
|
53
|
+
frame is seven-bit. The `ANSI-16` column is what `LANES_COLOR=16` emits; truecolor uses the Dark or Light value.
|
|
54
|
+
|
|
55
|
+
<!-- BEGIN lanes tokens (generated by libexec/tokens.py --md; do not edit by hand) -->
|
|
56
|
+
| Role | Dark | Light | ANSI-16 | CSS variable |
|
|
57
|
+
|---|---|---|---|---|
|
|
58
|
+
| surface | `#101318` | `#f3f5f2` | `—` | `--lanes-surface` |
|
|
59
|
+
| surface-raised | `#191e25` | `#ffffff` | `—` | `--lanes-surface-raised` |
|
|
60
|
+
| line | `#2c333d` | `#d5ded6` | `—` | `--lanes-line` |
|
|
61
|
+
| accent-wash | `#1a2721` | `#e5f0e6` | `—` | `--lanes-accent-wash` |
|
|
62
|
+
| accent-line | `#4b6955` | `#acc9b4` | `—` | `--lanes-accent-line` |
|
|
63
|
+
| press | `#26332c` | `#d8e9db` | `—` | `--lanes-press` |
|
|
64
|
+
| chip | `#ffffff0f` | `#edf3ec` | `—` | `--lanes-chip` |
|
|
65
|
+
| ink | `#edf1f5` | `#17241b` | `39` | `--lanes-ink` |
|
|
66
|
+
| ink-dim | `#9ca7b5` | `#56616d` | `90` | `--lanes-ink-dim` |
|
|
67
|
+
| ink-faint | `#6b7683` | `#7f8a96` | `90` | `--lanes-ink-faint` |
|
|
68
|
+
| accent | `#b4e3bf` | `#2f6b45` | `1;39` | `--lanes-accent` |
|
|
69
|
+
| on-accent | `#101318` | `#ffffff` | `—` | `--lanes-on-accent` |
|
|
70
|
+
| on-accent-urgent | `#7a1a12` | `#ffd3cc` | `—` | `--lanes-on-accent-urgent` |
|
|
71
|
+
| tone-urgent | `#f28b7d` | `#b7322a` | `31` | `--lanes-tone-urgent` |
|
|
72
|
+
| tone-attention | `#e5b84a` | `#8a5a00` | `33` | `--lanes-tone-attention` |
|
|
73
|
+
| tone-working | `#7fb3f2` | `#2456a8` | `34` | `--lanes-tone-working` |
|
|
74
|
+
| tone-done | `#6fd39a` | `#1b7142` | `32` | `--lanes-tone-done` |
|
|
75
|
+
| scrim | `#000000a6` | `#1a222b80` | `—` | `--lanes-scrim` |
|
|
76
|
+
| shadow | `0 -16px 80px #0007` | `0 -16px 80px #0003` | `—` | `--lanes-shadow` |
|
|
77
|
+
|
|
78
|
+
| Glyph | Codepoint | ASCII | Tone | Meaning |
|
|
79
|
+
|---|---|---|---|---|
|
|
80
|
+
| `◆` | U+25C6 | `!` | tone-urgent | agent `needs_approval` |
|
|
81
|
+
| `◇` | U+25C7 | `?` | tone-attention | agent `needs_input` |
|
|
82
|
+
| `▸` | U+25B8 | `>` | tone-working | agent `working` |
|
|
83
|
+
| `✓` | U+2713 | `v` | tone-done | agent `done` |
|
|
84
|
+
| `✗` | U+2717 | `X` | tone-urgent | agent `errored` |
|
|
85
|
+
| `_` | U+005F | `_` | ink-dim | agent `shell` |
|
|
86
|
+
| `·` | U+00B7 | `.` | ink-dim | agent `idle` |
|
|
87
|
+
| `●` | U+25CF | `*` | tone-done | stream `active` |
|
|
88
|
+
| `○` | U+25CB | `o` | ink-dim | stream `quiet` |
|
|
89
|
+
| `✗` | U+2717 | `x` | tone-urgent | stream `missing` |
|
|
90
|
+
| `–` | U+2013 | `-` | ink-dim | stream `absent` |
|
|
91
|
+
| `!` | U+0021 | `!` | tone-urgent | stream `dead` |
|
|
92
|
+
| `·` | U+00B7 | `.` | ink-dim | stream `helper` |
|
|
93
|
+
| `?` | U+003F | `?` | ink-dim | stream `unknown` |
|
|
94
|
+
| `▌` | U+258C | `\|` | accent | mark `cursor` |
|
|
95
|
+
| `›` | U+203A | `=` | accent | mark `viewing` |
|
|
96
|
+
| `²` | U+00B2 | `2` | accent | mark `viewport-b` |
|
|
97
|
+
| `³` | U+00B3 | `3` | accent | mark `viewport-c` |
|
|
98
|
+
| `·` | U+00B7 | `.` | ink-dim | mark `viewer` |
|
|
99
|
+
| `…` | U+2026 | `~` | ink | mark `truncation` |
|
|
100
|
+
| `×` | U+00D7 | `x` | tone-urgent | mark `close` |
|
|
101
|
+
| `+` | U+002B | `+` | tone-done | mark `new` |
|
|
102
|
+
| `─` | U+2500 | `-` | ink-faint | mark `rule` |
|
|
103
|
+
| `⋮` | U+22EE | `:` | ink-dim | mark `more` |
|
|
104
|
+
| `⚠` | U+26A0 | `!` | tone-attention | mark `warning` |
|
|
105
|
+
| `↑` | U+2191 | `^` | ink-dim | mark `upload` |
|
|
106
|
+
| `↓` | U+2193 | `v` | tone-done | mark `armed` |
|
|
107
|
+
| `⏎` | U+23CE | `Enter` | ink-dim | mark `enter` |
|
|
108
|
+
| `█` | U+2588 | `_` | tone-working | mark `caret` |
|
|
109
|
+
| `?` | U+003F | `?` | ink-dim | mark `stale` |
|
|
110
|
+
| `›` | U+203A | `>` | ink | mark `lead` |
|
|
111
|
+
| `·` | U+00B7 | `-` | ink-dim | mark `sep` |
|
|
112
|
+
| `‘` | U+2018 | `'` | ink | mark `quote-open` |
|
|
113
|
+
| `’` | U+2019 | `'` | ink | mark `quote-close` |
|
|
114
|
+
| `✓` | U+2713 | `OK` | tone-done | mark `ok` |
|
|
115
|
+
| `✗` | U+2717 | `X` | tone-urgent | mark `fail` |
|
|
116
|
+
| `⚠` | U+26A0 | `!` | tone-attention | economy `starving` |
|
|
117
|
+
| `↓` | U+2193 | `v` | tone-attention | economy `downgraded` |
|
|
118
|
+
| `$` | U+0024 | `$` | ink-dim | economy `paid` |
|
|
119
|
+
<!-- END lanes tokens -->
|
|
120
|
+
|
|
121
|
+
## Row anatomy
|
|
122
|
+
|
|
123
|
+
1-based columns; every lane row is exactly 36 cells by `cell_width`, and column 36 is never written.
|
|
124
|
+
|
|
125
|
+
| col | content | tone |
|
|
126
|
+
|---|---|---|
|
|
127
|
+
| 1 | ▌ cursor or space | accent |
|
|
128
|
+
| 2 | `AGENT_GLYPHS[agent.status]`; blank without `agent{}`; ink-dim when stale | its tone |
|
|
129
|
+
| 3 | › viewing (not on the cursor row) · ² / ³ another viewport shows it · else space | accent |
|
|
130
|
+
| 4–27 | name (24 cells) in `38;5;N` or ink, **bold when viewing**; viewer dots reserved first, name clipped to `24 − len(dots)` and ending in `…` when clipped; truncation detected by `len(clipped) < len(name)` (no second width walk) | identity colour |
|
|
131
|
+
| 28 | `+` register control on an unregistered row that can be added to a workspace (closable and `R.eligible`; CHA-positioned at `cols − 8`; hit box `cols − 8 ≤ x < cols − 4`), else space — amended by Task 23 (§10, DEF-1428) | tone-done |
|
|
132
|
+
| 29–31 | age, right-aligned: `NNs` < 60 s, `NNm` < 60 min, `NNh` < 24 h, `NNd` ≤ 99 d; blank when nothing is known; source `agent.since` else `activity` | ink-dim; attention inside Needs you |
|
|
133
|
+
| 32 | space | |
|
|
134
|
+
| 33 | `GLYPHS[state]` — full tone without an agent, ink-dim with one (▸ + ○ is the long-tool-call case and stays readable) | its tone |
|
|
135
|
+
| 34 | tail: stale `?` > starving ⚠ > downgraded ↓ > paid `$` > space | ink-dim / attention |
|
|
136
|
+
| 35 | × on closable rows, else space | urgent |
|
|
137
|
+
| 36 | never written | |
|
|
138
|
+
|
|
139
|
+
Budget: 3 + 24 + 1 + 3 + 1 + 1 + 1 + 1 + 1 = 36. The close hit box stays `x ≥ cols − 4` (a click at `cols − 2` closes,
|
|
140
|
+
at `x = 5` opens — the pinned test keeps both). The `×` moved from `cols − 2` to `cols − 1`.
|
|
141
|
+
|
|
142
|
+
## Mockups
|
|
143
|
+
|
|
144
|
+
Desktop, a 35-lane workspace at 36 × 40 in `unicode` and truecolor (spec §4.8). Every line is exactly 36 cells; the
|
|
145
|
+
real list scrolls where `⋮` is:
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
Sakio ◆1 ◇2 ▸6 ✓1
|
|
149
|
+
── ◆ Needs you (3) ─────────────────
|
|
150
|
+
▌◆ Compiler - Orchestrator 25m ○
|
|
151
|
+
◇›Runtime: supervisor 12m ○
|
|
152
|
+
◇ granite_games 4m ○ ×
|
|
153
|
+
── ▸ Working (6) ───────────────────
|
|
154
|
+
▸²Runtime - Orchestrator·· 3m ●$
|
|
155
|
+
▸ Runtime: rendering 41s ●
|
|
156
|
+
▸ Runtime: contracts 2m ●
|
|
157
|
+
▸ Runtime: rules 18m ○$
|
|
158
|
+
▸ Runtime: events 9m ●
|
|
159
|
+
▸ Runtime: value-hunter-r… 1h ○↓
|
|
160
|
+
── ✓ Done (1) ──────────────────────
|
|
161
|
+
✓ Sakio CLI 7m ○
|
|
162
|
+
── Sakio (25) ──────────────────────
|
|
163
|
+
Granite 2h ○
|
|
164
|
+
Sakio rename 3d –
|
|
165
|
+
_ Runtime: waits-effects 12h ○
|
|
166
|
+
· Runtime: trace-replay 1d ○
|
|
167
|
+
Runtime: source-assemb…· 5h !
|
|
168
|
+
Runtime: conformance 2d ○
|
|
169
|
+
Runtime: integration 6h ✗
|
|
170
|
+
Runtime: wave-b-critic-… 9h ○
|
|
171
|
+
Design review 3h ○?
|
|
172
|
+
Docs 11h ○
|
|
173
|
+
Release 4d –
|
|
174
|
+
Bench 2d ○
|
|
175
|
+
Fixtures 8h ○
|
|
176
|
+
Harness 1d ○
|
|
177
|
+
Site 5d –
|
|
178
|
+
Infra 9h ○
|
|
179
|
+
⋮ 9 more
|
|
180
|
+
── Unregistered (5) ────────────────
|
|
181
|
+
+ New shell
|
|
182
|
+
granite_brand_migration 2h ○ ×
|
|
183
|
+
refinery-workblocks 6h ? ×
|
|
184
|
+
↑ uploads · click, then drop (U)
|
|
185
|
+
$8.5k/16k 53% · spill 76% · free 3
|
|
186
|
+
────────────────────────────────────
|
|
187
|
+
⏎ open · / find · x close · ? keys
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Row 3 is the cursor row (▌, no ›, bold name); row 4 is the viewing lane in this slot (›, bold); row 7 is shown in
|
|
191
|
+
viewport b (²) with two viewers (··). The same frame in ASCII mode, first rows:
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
Sakio !1 ?2 >6 v1
|
|
195
|
+
-- ! Needs you (3) -----------------
|
|
196
|
+
|! Compiler - Orchestrator 25m o
|
|
197
|
+
?=Runtime: supervisor 12m o
|
|
198
|
+
>2Runtime - Orchestrator.. 3m *$
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Phone, the `render_mobile` picker at 48+ columns with three rows per lane (spec §4.10): row A is the viewing mark,
|
|
202
|
+
the agent glyph and the name; row B the status word, age and harness; row C the agent's task.
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
◆1 need approval · ◇2 need you · ▸6 working
|
|
206
|
+
|
|
207
|
+
── ◆ Needs you (3) ─────────────────────────────
|
|
208
|
+
›◆ Compiler - Orchestrator
|
|
209
|
+
Needs approval · 25m · claude
|
|
210
|
+
Fix buildctl bugs
|
|
211
|
+
◇ Runtime: supervisor··
|
|
212
|
+
Needs you · 12m · codex
|
|
213
|
+
Verify fleet plan mapping
|
|
214
|
+
◇ granite_games ×
|
|
215
|
+
Needs you · 4m · claude
|
|
216
|
+
Port the brand migration
|
|
217
|
+
── ▸ Working (6) ───────────────────────────────
|
|
218
|
+
▸ Runtime: rendering
|
|
219
|
+
Working · 41s · claude
|
|
220
|
+
Renderer contracts wave B
|
|
221
|
+
── Sakio (25) ──────────────────────────────────
|
|
222
|
+
Granite
|
|
223
|
+
Quiet · 2h
|
|
224
|
+
|
|
225
|
+
Swipe to scroll · Tap to open
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Budgets
|
|
229
|
+
|
|
230
|
+
Each budget is pinned by a test; a budget that is not met is an open defect row, never a raised number.
|
|
231
|
+
|
|
232
|
+
| Budget | Pin |
|
|
233
|
+
|---|---|
|
|
234
|
+
| `render(35 lanes + 5 unregistered, 36×60)` median of 50 after 5 warm-ups ≤ 2.0 ms hard (CI, loose); design target ≤ 0.9× the same-run baseline cold (today 0.459 ms here, binary-safe read) and ≤ 0.10 ms warm (row cache) — MEASURED 2026-10-04 after Task 23: cold 0.64× met; warm 0.27–0.31 ms at load ~23–30, NOT met, DEF-1429 open; the target is not raised | `test_sidebar_budget.test_desktop_render_time_and_frame` (time) + bench numbers in the commit message of every task touching `render()` |
|
|
235
|
+
| Frame ≤ 9216 B truecolor, ≤ 5632 B in `LANES_COLOR=16` at 36×60 (today 4330 B in both on the DEF-1417 fixture, 4297 B before it — `LANES_COLOR` is not read before Task 22; truecolor prefixes are the whole delta) | same test, `len(frame)` |
|
|
236
|
+
| Exactly `viewports` `open()` calls of target files per render (12 over 6 renders at 2 viewports today); `subprocess.Popen`/`run` never called | `test_render_opens_only_target_files_and_never_forks` (open counter on the view dir, Popen/run patched to raise) |
|
|
237
|
+
| Every emitted row ≤ 36 cells and column 36 blank on every row that is not made of the `rule` glyph alone. At Task 21 (before the one-row strip) the four upload-box rows are exempt as well — rows starting with `─ ┌ │ └`; Task 24 narrows the exemption to the rule when it removes the box | `test_no_row_exceeds_36_cells_and_column_36_is_blank` (ANSI- and OSC-stripped, binary-safe sink read, `cell_width`; and, because the strip hides positioned writes, every CHA/CUP column in the raw frame < 36 — today's maximum is 34; amendment 2026-10-04; and, because text positioned left of 36 can still run into it, every raw row walked cell by cell — `highest_cell` — writes no cell past 35, 36 on the exempt rows, over a fixture whose registrable ghosts carry both the `+` and the `×`; DEF-1417) with its detectors proven by `test_sidebar_budget_sabotage` |
|
|
238
|
+
| Per tick ≤ 7 `stat()` at one viewport — registry, status, target, health, `flags/sockets-lost` (today's five), economy (Task 12), `devices/<device>.json` (Task 22) — plus one per extra viewport; zero timers added; the OSC 11 query is one write at start | `test_tick_stats_at_most_seven_files` (one viewport); `test_theme_query_is_written_once` |
|
|
239
|
+
| Upload: select timeout stays 1.0 while uploading; `dirty` only on `.progress` mtime change | `test_upload_does_not_shorten_the_tick` (Task 24) |
|
|
240
|
+
| `render_mobile(35, 48×60)` ≤ 2.0 ms hard, frame ≤ 6144 B; target ≤ 0.45 ms cold / ≤ 0.10 ms warm (today 0.352 ms / 1663 B) | `test_mobile_render_time_and_frame` |
|
|
241
|
+
| `reload(force)` unchanged (0.14 ms at 35 / 0.39 ms at 183); fleet attention computed inside it from the loaded dict — NOT shown met: Task 24 measured 0.26–0.44 ms at 35 / 0.69–0.80 ms at 183 and Task 12 0.296 → 0.386 ms at 35 (load 30–62; the 0.14/0.39 figures are quiet-host figures), DEF-1483 open; the budget is not raised | bench in Task 24's and Task 12's commit messages; DEF-1483 |
|
|
242
|
+
| Phone: exactly one `setInterval(`, zero `setTimeout(`, no `backdrop-filter`, a painted `box-shadow` only in `.lanes-sheet-panel` and `.zj-mobile-menu` (a `box-shadow: none` override, as on the security modal, is not paint work and is not counted); `fetch(` absent; mobile.css grows ≤ 2048 B over today's 16 934 B, mobile.js ≤ 3072 B over today's 19 102 B (measured 2026-10-03; the caps are baseline + allowance, quoted in the test); `index.html` keeps its 9 SRI-pinned upstream assets | `test_web.test_js_timers_unchanged`, `test_web.test_no_scroll_time_effects`, `test_web.test_bundle_growth`, `test_web.test_integrity_pins_untouched` |
|
|
243
|
+
| Zellij: config.kdl grows by the theme + ui block only (~4 KB), `load_plugins {}` empty, no `rename-tab` from the sidebar | `test_keybinds.test_theme_block_and_plugin_free`, `test_tokens.test_generated_blocks_match_committed_files` |
|
|
244
|
+
| Unit suite stays under 75 s wall (MEASURED 2026-10-04 after Task 21: 833 tests, 124–176 s at load ~30–60 — exceeded, DEF-1415 open; the budget is not raised); new pins are exact-string asserts, no sleeps; `tokens.py --check` ≤ 50 ms, contrast test ≤ 10 ms | observed in each task's run step |
|
|
245
|
+
|
|
246
|
+
## Spec
|
|
247
|
+
|
|
248
|
+
The full design system — token values and contrast, the glyph rules, header, sections, footer ladder, title and
|
|
249
|
+
notification bytes, Zellij chrome, phone shell and client alignment, and the decisions log — is
|
|
250
|
+
[`docs/superpowers/specs/2026-10-03-lanes-cockpit-design.md`](../../docs/superpowers/specs/2026-10-03-lanes-cockpit-design.md).
|
|
@@ -155,6 +155,45 @@ Connect Tailscale on the phone, open the printed URL in Safari, paste only the t
|
|
|
155
155
|
`sh install/enable-web.sh` starts only the loopback services and prints no token. The frontend and services
|
|
156
156
|
run on the workspace host; a Mac does not need to stay awake for the phone to work.
|
|
157
157
|
|
|
158
|
+
### Laptop and tablet browser
|
|
159
|
+
|
|
160
|
+
The same private frontend serves every browser and chooses its controls once, when the page loads:
|
|
161
|
+
|
|
162
|
+
- **Laptop or desktop** (mouse or trackpad): the plain Zellij web client, unchanged, plus a small **Lanes**
|
|
163
|
+
chip in the top-right corner. The chip opens a thin overlay bar with a workspace picker and a lane picker;
|
|
164
|
+
the terminal keeps its size and all of its keys.
|
|
165
|
+
- **Tablet** (touch screen wider than 600 px): the phone controls arranged for the larger screen — Workspaces,
|
|
166
|
+
Lanes, Panes and **Type here** in the top bar, and the workspace list as a left panel. The terminal shows the
|
|
167
|
+
whole tab, so a desktop view's lane sidebar sits beside its lanes (**More → Single pane** shows one pane).
|
|
168
|
+
- **Phone**: the phone shell described above.
|
|
169
|
+
|
|
170
|
+
Give each browser its own view. On the server, one command prepares it, enables the loopback web services and
|
|
171
|
+
prints its private URL:
|
|
172
|
+
|
|
173
|
+
```sh
|
|
174
|
+
lanes web ipad --desktop # desktop sidebar layout (a new device's default); --mobile: phone lane pickers
|
|
175
|
+
lanes web ipad --desktop --token # the same, then a fresh login token in this terminal
|
|
176
|
+
lanes web ipad --print-only # shows the exact prepare call and URL; changes nothing
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
From an installed client, `sh install/enable-phone.sh --host lanes --desktop ipad` does the same over the
|
|
180
|
+
selected SSH key; without a name, `--desktop` uses the device `browser`. The server needs this version of Lanes
|
|
181
|
+
for `--desktop`. A device keeps the layout it was created with: `lanes web <device>` without a flag reuses it
|
|
182
|
+
(so `lanes web phone` reuses the phone's view), and an explicit request for the other layout is refused; choose
|
|
183
|
+
another device name. `lanes web` reads the node's Tailscale name. Tailscale Serve itself is set up once
|
|
184
|
+
by `enable-phone.sh`, as described above.
|
|
185
|
+
|
|
186
|
+
On a network with a captive portal (hotel, train or aircraft Wi-Fi), connect in this order:
|
|
187
|
+
|
|
188
|
+
1. Complete the portal's sign-in page in the browser. Tailscale cannot connect until the portal lets traffic out.
|
|
189
|
+
2. Connect Tailscale on the laptop or tablet and wait until it reports connected.
|
|
190
|
+
3. Open the printed `https://<server>.<tailnet>.ts.net:8443/<device>` URL.
|
|
191
|
+
4. Paste only the token value into the sign-in screen. Existing tokens remain valid; revoke one as shown above.
|
|
192
|
+
|
|
193
|
+
There is no public fallback by design: views are reachable only through the tailnet. On a host without a
|
|
194
|
+
Tailscale name, `lanes web` prints the loopback URL `http://127.0.0.1:8083/<device>` and says that it opens only
|
|
195
|
+
on the host itself.
|
|
196
|
+
|
|
158
197
|
## 5. Updates, optional operations and removal
|
|
159
198
|
|
|
160
199
|
A server installed with `npx @vibelord/lanes --server` runs from the staged tree `~/.local/share/lanes/pkg/<version>/`; updating is `npx @vibelord/lanes@latest --server`, and the previous versions a service may still run from are kept.
|