@vibelord/lanes 1.0.0 → 1.0.1
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 +56 -1
- package/payload/lanes/VERSION +1 -1
- package/payload/lanes/bin/lanes +281 -104
- package/payload/lanes/client/lanes-launch +44 -2
- package/payload/lanes/container/env.sh +8 -0
- package/payload/lanes/container/lane +19 -2
- package/payload/lanes/docs/ARCHITECTURE.md +86 -1
- package/payload/lanes/docs/DESIGN.md +72 -0
- package/payload/lanes/docs/INSTALL.md +39 -0
- package/payload/lanes/docs/RUNBOOK.md +94 -1
- package/payload/lanes/holders/ssh.sh +18 -0
- package/payload/lanes/install/enable-phone.sh +15 -6
- package/payload/lanes/install/enable-web.sh +19 -6
- package/payload/lanes/libexec/agent_state.py +214 -0
- package/payload/lanes/libexec/connection.py +178 -0
- package/payload/lanes/libexec/cursor_sync.py +139 -6
- package/payload/lanes/libexec/device_sync.py +134 -0
- package/payload/lanes/libexec/lanes_lib.py +158 -5
- package/payload/lanes/libexec/registrations.py +148 -0
- package/payload/lanes/libexec/session_actions.py +47 -15
- package/payload/lanes/libexec/sidebar.py +372 -61
- package/payload/lanes/libexec/statusd.py +126 -33
- package/payload/lanes/libexec/tokens.py +279 -0
- package/payload/lanes/libexec/tokens.sh +11 -0
- package/payload/lanes/libexec/view.sh +9 -8
- package/payload/lanes/web/README.md +15 -0
- package/payload/lanes/web/browser.css +22 -0
- package/payload/lanes/web/browser.js +73 -0
- package/payload/lanes/web/mobile.css +65 -33
- package/payload/lanes/web/mobile.js +23 -5
- package/payload/lanes/workspaces.example/README.md +9 -3
- package/payload/lanes/zellij/config.kdl +61 -3
- package/payload/manifest.json +6 -3
|
@@ -67,6 +67,8 @@ log() {
|
|
|
67
67
|
# Run ssh as an async child so HUP/TERM (window closed, logout) end it immediately; a foreground child
|
|
68
68
|
# would defer the trap until ssh returned. Keep the terminal on fd 9: dash gives async jobs /dev/null.
|
|
69
69
|
child=
|
|
70
|
+
dog=
|
|
71
|
+
dog_ready=
|
|
70
72
|
reader=
|
|
71
73
|
holding=
|
|
72
74
|
now=
|
|
@@ -76,6 +78,7 @@ stop() {
|
|
|
76
78
|
trap - HUP TERM INT
|
|
77
79
|
exec 2>/dev/null # bash reports every child it sees die of a signal: noise in a terminal that stays open
|
|
78
80
|
if [ -n "$child" ]; then kill -TERM "$child" 2>/dev/null || :; fi
|
|
81
|
+
cancel_watchdog
|
|
79
82
|
if [ -n "$reader" ]; then kill -TERM "$reader" 2>/dev/null || :; fi
|
|
80
83
|
end_helper
|
|
81
84
|
log 'stopped: window closed, or a signal'
|
|
@@ -89,17 +92,56 @@ end_helper() {
|
|
|
89
92
|
# INT too: in a plain terminal Ctrl-C is the way out, now that nothing else ends the client.
|
|
90
93
|
trap stop HUP TERM INT
|
|
91
94
|
# With $limit set, a watchdog ends a child that the server leaves hanging (macOS has no timeout(1)).
|
|
95
|
+
cancel_watchdog() {
|
|
96
|
+
if [ -n "$dog" ]; then
|
|
97
|
+
# A very fast probe may finish before the forked shell has installed
|
|
98
|
+
# its trap. Wait for that handshake before sending cancellation.
|
|
99
|
+
while [ ! -s "$dog_ready" ] && kill -0 "$dog" 2>/dev/null; do sleep .01; done
|
|
100
|
+
kill -TERM "$dog" 2>/dev/null || :
|
|
101
|
+
wait "$dog" 2>/dev/null || :
|
|
102
|
+
dog=
|
|
103
|
+
fi
|
|
104
|
+
if [ -n "$dog_ready" ]; then rm -f "$dog_ready"; dog_ready=; fi
|
|
105
|
+
}
|
|
92
106
|
run() {
|
|
93
107
|
exec 9<&0
|
|
94
108
|
"$@" <&9 9<&- &
|
|
95
109
|
child=$!
|
|
96
110
|
exec 9<&-
|
|
97
111
|
dog=
|
|
98
|
-
|
|
112
|
+
dog_ready=
|
|
113
|
+
if [ -n "$limit" ]; then
|
|
114
|
+
if ! dog_ready=$(mktemp "${TMPDIR:-/tmp}/lanes-watchdog.XXXXXXXX"); then
|
|
115
|
+
kill -TERM "$child" 2>/dev/null || :
|
|
116
|
+
wait "$child" 2>/dev/null || :
|
|
117
|
+
child=; rc=1; return "$rc"
|
|
118
|
+
fi
|
|
119
|
+
(
|
|
120
|
+
# A foreground sleep can defer the shell's TERM handler, and
|
|
121
|
+
# killing only that shell leaves its sleep behind. An async
|
|
122
|
+
# timer lets wait handle cancellation and reap the owned child.
|
|
123
|
+
timer=; cancelled=
|
|
124
|
+
# The timer has no cleanup. KILL also covers its fork-to-exec
|
|
125
|
+
# window, before sleep has acquired its default signal handlers.
|
|
126
|
+
trap 'cancelled=1; [ -z "$timer" ] || kill -KILL "$timer" 2>/dev/null || :' HUP TERM INT
|
|
127
|
+
sleep "$limit" &
|
|
128
|
+
timer=$!
|
|
129
|
+
printf 'ready\n' > "$dog_ready"
|
|
130
|
+
# TERM can arrive between spawning the timer and assigning $!.
|
|
131
|
+
if [ -n "$cancelled" ]; then kill -KILL "$timer" 2>/dev/null || :; fi
|
|
132
|
+
wait "$timer" 2>/dev/null || :
|
|
133
|
+
if [ -n "$cancelled" ]; then
|
|
134
|
+
wait "$timer" 2>/dev/null || :
|
|
135
|
+
else
|
|
136
|
+
kill -TERM "$child" 2>/dev/null || :
|
|
137
|
+
fi
|
|
138
|
+
) >/dev/null 2>&1 &
|
|
139
|
+
dog=$!
|
|
140
|
+
fi
|
|
99
141
|
rc=0
|
|
100
142
|
wait "$child" || rc=$?
|
|
101
143
|
child=
|
|
102
|
-
|
|
144
|
+
cancel_watchdog
|
|
103
145
|
return "$rc"
|
|
104
146
|
}
|
|
105
147
|
# hold <seconds> [jitter]: wait, but Enter continues at once. No raw mode, so there is no terminal state to restore
|
|
@@ -8,6 +8,14 @@
|
|
|
8
8
|
# * tmux is NOT affected: it honours TMUX_TMPDIR only, so lane sockets stay in /tmp/tmux-<uid>.
|
|
9
9
|
# * Nothing is created here: no directory (or a read-only one) = no change. Opt out per shell or for good with
|
|
10
10
|
# LANES_TMP_OPTOUT=1 (any value, even empty, counts). LANES_TMP_DIR names another directory.
|
|
11
|
+
#
|
|
12
|
+
# Lane identity is NOT set here (this file is shared by every shell in the container). A tmux lane's shell gets it
|
|
13
|
+
# ONLY from the `-e` flags of its create recipe (`lanes_lib.canonical_recipe`, `tmux new -e …`): LANE_ID
|
|
14
|
+
# (<ws>--<slug>), LANE_WS (<ws>), THROTTLE_TRACE and ANTHROPIC_CUSTOM_HEADERS ('x-throttle-trace: lanes/<ws>/<id>';
|
|
15
|
+
# trace charset [A-Za-z0-9._-/], at most 256). Never export them from a profile or a client: the client that starts
|
|
16
|
+
# a shared board socket's tmux server copies its env into the server's GLOBAL env, which every later session there
|
|
17
|
+
# without its own -e inherits (DEF-1422) — so `lane open` strips them. A running lane gains its identity only when it
|
|
18
|
+
# is next created by a recipe carrying -e; a legacy recipe without -e gives none (`lanes cursor-sync --identity`).
|
|
11
19
|
if [ -z "${LANES_TMP_OPTOUT+set}" ] && [ -d "${LANES_TMP_DIR:-/home/coder/.tmp}" ] \
|
|
12
20
|
&& [ -w "${LANES_TMP_DIR:-/home/coder/.tmp}" ] && [ -x "${LANES_TMP_DIR:-/home/coder/.tmp}" ]; then
|
|
13
21
|
TMPDIR=${LANES_TMP_DIR:-/home/coder/.tmp}
|
|
@@ -148,8 +148,22 @@ def registry_entry(lane):
|
|
|
148
148
|
return lanes[lane]
|
|
149
149
|
|
|
150
150
|
|
|
151
|
+
IDENTITY = ('LANE_ID', 'LANE_WS', 'THROTTLE_TRACE') # + a lanes/ ANTHROPIC_CUSTOM_HEADERS (identity_free)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def identity_free(environ, drop=('TMUX',)):
|
|
155
|
+
"""`environ` without any lane identity (lanes/docs/ARCHITECTURE.md "Lane identity"). Whatever tmux client STARTS
|
|
156
|
+
a socket's server hands its whole environment to the server's GLOBAL environment, and every later session on
|
|
157
|
+
that socket created without its own `-e` inherits it (MEASURED, tmux 3.4: DEF-1422). Board sockets are shared by
|
|
158
|
+
many lanes, so a lane identity in the env of anything that may start one would be stamped on unrelated lanes.
|
|
159
|
+
Identity therefore reaches a lane's shell ONLY through `-e` in its create recipe (lanes_lib.canonical_recipe).
|
|
160
|
+
An operator's own ANTHROPIC_CUSTOM_HEADERS that is not a lane trace is theirs and stays."""
|
|
161
|
+
return {k: v for k, v in environ.items() if k not in drop and k not in IDENTITY
|
|
162
|
+
and not (k == 'ANTHROPIC_CUSTOM_HEADERS' and v.startswith('x-throttle-trace: lanes/'))}
|
|
163
|
+
|
|
164
|
+
|
|
151
165
|
def tmux_cmd(socket, *args):
|
|
152
|
-
env =
|
|
166
|
+
env = identity_free(os.environ)
|
|
153
167
|
return subprocess.run(['tmux', '-L', socket, *args], capture_output=True, text=True, timeout=15, env=env)
|
|
154
168
|
|
|
155
169
|
|
|
@@ -159,7 +173,10 @@ def cmd_open(args):
|
|
|
159
173
|
entry = registry_entry(args[0])
|
|
160
174
|
if os.environ.get('TMUX') and '--force' not in args:
|
|
161
175
|
sys.exit(f"lane open: already inside tmux — refusing to nest. The lane is: {json.dumps(entry.get('tmux') or entry.get('zmx'))}")
|
|
162
|
-
|
|
176
|
+
# Nothing `lane open` execs carries a lane identity — neither this lane's nor one inherited from ANOTHER lane (an
|
|
177
|
+
# editor terminal started inside a lane): the recipe may be the first client on a shared board socket and would
|
|
178
|
+
# hand it to every later session there (DEF-1422). The canonical recipe's own `-e` flags give the identity.
|
|
179
|
+
env = identity_free(os.environ, drop=('TMUX', 'TMUX_PANE'))
|
|
163
180
|
if entry.get('holder') == 'zmx':
|
|
164
181
|
name = entry['zmx']['name']
|
|
165
182
|
if alive(name):
|
|
@@ -8,15 +8,72 @@ 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
|
|
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,
|
|
12
13
|
exact socket/session resolve files, and the container's `.lz/registry.json` projection.
|
|
13
14
|
4. The status daemon observes tmux sessions and publishes status/health snapshots. Failed or timed-out
|
|
14
15
|
probes mean unknown; they do not prove a missing session. A bad terminal registry retains the last good board.
|
|
15
16
|
|
|
17
|
+
Beside the six-word `state` (never changed by it), each live tmux lane, declared or unregistered, may carry
|
|
18
|
+
an additive `agent{}` key: `{harness, status, name, session_id?, pid?, since, source}` with `status` one of
|
|
19
|
+
`working | needs_input | needs_approval | done | errored | shell | idle`. An absent key means no signal names
|
|
20
|
+
an agent there. `statusd` builds it with `agent_state.merge` from signals the harnesses already emit, read once
|
|
21
|
+
per container per cycle through `/proc/<container init pid>/root` without spawning anything. Precedence: a
|
|
22
|
+
hook sidecar (planned, not read yet) > a Claude `~/.claude/sessions/<pid>.json` naming the pane
|
|
23
|
+
`<session>:@<win>.%<pane>` (live pids only, checked in the container's pid namespace, recycled pids rejected
|
|
24
|
+
by `procStart`) > the Codex pane title (`[ ! ] Action Required` → `needs_approval`, braille spinner →
|
|
25
|
+
`working`, a task title → `idle`) > the window bell (an idle title plus a bell is `done`; an approval title
|
|
26
|
+
wins over the bell). A Codex title over a bare-shell foreground whose shell has no child process
|
|
27
|
+
(`pane_has_child`, read from `/proc/<pid>/task/*/children`) is `shell`: the harness ended and tmux kept its
|
|
28
|
+
title. Unknown liveness leaves that key unset and never demotes. The tmux columns this needs
|
|
29
|
+
(`window_bell_flag`, `window_id`, `pane_id`, `pane_title`) are appended to the listing format, the free-text
|
|
30
|
+
title last so a tab inside it cannot shift another column.
|
|
31
|
+
5. A lane's `source` is the container's spelling of its `terminals.json`. Every host-side reader of that file
|
|
32
|
+
maps it to the container's home volume first (`statusd.registry_source_paths`, deduplicated by that host path:
|
|
33
|
+
one repository cloned in two containers is two files): the status daemon's hot-sync
|
|
34
|
+
starts one `lanes sync` per edit (at most one per 10 s), `lanes doctor` reports "registry up to date" from the
|
|
35
|
+
same host-side mtimes, and promoting an unregistered session writes the binding through the volume.
|
|
36
|
+
|
|
16
37
|
`lanes cursor-sync` is a separate, explicit writer. Its default is a diff; `--write` takes backups and changes
|
|
17
38
|
editor commands to `lane open <id>`. The original create recipe and exact tmux binding remain in the registry's
|
|
18
39
|
`lanes` block. Automatic sync never commits or pushes those edits. `cursor-commit --push` is an opt-in workflow.
|
|
19
40
|
|
|
41
|
+
Lane identity is an environment contract: `LANE_ID` (`<ws>--<slug>`), `LANE_WS` (`<ws>`), and the Throttle trace
|
|
42
|
+
`lanes/<ws>/<id>` as `THROTTLE_TRACE` and as `ANTHROPIC_CUSTOM_HEADERS='x-throttle-trace: lanes/<ws>/<id>'` (Claude
|
|
43
|
+
Code sends it on every request; the proxy's trace charset is `[A-Za-z0-9._-/]`, at most 256 characters, and a
|
|
44
|
+
trace outside it is never emitted). A tmux lane gets it in exactly one way: the `-e` flags of
|
|
45
|
+
`lanes_lib.canonical_recipe()` — `tmux -L <socket> new -ADs <session> -c <folder> -e … <cmd>` — which set the
|
|
46
|
+
SESSION environment of that one lane on a real create (`-A` ignores them on an attach). Never through `tmux
|
|
47
|
+
set-environment` and never through a client's environment, because of a tmux rule MEASURED on tmux 3.4 (the
|
|
48
|
+
container's version) with a throwaway socket: the client that STARTS a socket's server hands its whole environment
|
|
49
|
+
to the server's GLOBAL environment (`show-environment -g`), and every later session on that socket inherits it
|
|
50
|
+
unless its own `-e` overrides each key — a session created by a client carrying `LANE_ID=b` but no `-e` still got
|
|
51
|
+
the server starter's `LANE_ID=a`, and one created with `-e LANE_ID=c` alone kept `a`'s `THROTTLE_TRACE`. Board
|
|
52
|
+
sockets are shared (9 of the 10 bw-tools fixture lanes are on one socket), so `lane open` execs nothing — attach or
|
|
53
|
+
recipe — that carries a lane identity, this lane's or one inherited from another lane's editor terminal (DEF-1422);
|
|
54
|
+
a recipe run by it may start the server with an identity-free global environment only. An attach cannot change a
|
|
55
|
+
running session's env, so a running lane gains its identity only when it is next CREATED by a recipe carrying `-e`.
|
|
56
|
+
A legacy recipe without `-e` gives its lane no identity. `lanes cursor-sync --canonicalize` (dry-run unless
|
|
57
|
+
`--write`) gives every tmux lane whose recorded `create` cannot create it — null, attach-only (`attach-session`
|
|
58
|
+
without `new`), or naming an absolute `.sh`/`.py`/… script missing in the container — the canonical recipe with
|
|
59
|
+
`bash -l`; `--identity` gives a WORKING recipe of the plain shape `[exec] tmux -L <socket> new -ADs <session> [-c
|
|
60
|
+
<dir>] <command…>` the same `-e` flags, re-quoting the command's words one for one so tmux receives the same argv
|
|
61
|
+
(any expansion, glob, operator or other shape is left alone and stays identity-less). Both keep the old value as
|
|
62
|
+
`createPrevious`; a recipe whose paths cannot be checked (outside the home volume) is never counted as broken.
|
|
63
|
+
zmx lanes get `LANE_ID` (only) from `lane-run`, unchanged.
|
|
64
|
+
|
|
65
|
+
The trace is also a FAIRNESS key, not only an attribution key: the proxy uses `x-throttle-trace` when present and
|
|
66
|
+
synthesizes `cc-session/<session>[/<agent>]` only when it is absent (`src/proxy.rs`, `synthesize_flow_key`), and the
|
|
67
|
+
pool's parking queue is round-robin across trace keys at slot handoff (`src/pressure.rs`, `acquire_fair`). A lane
|
|
68
|
+
stamped `lanes/<ws>/<id>` therefore puts its main session, every subagent and any workflow fan-out started in it
|
|
69
|
+
into ONE fairness bucket where today each Claude session/agent is its own. `max_in_flight_per_trace` is configured
|
|
70
|
+
and reported but no enforcement site exists in `src/` (MEASURED by search; `pressure.rs` documents fairness as "not a
|
|
71
|
+
hard cap"). Whether a lane should be one bucket is an open decision for the plan owner before any live enablement
|
|
72
|
+
(DEF-1423); this tree does not change the header shape. Unit-proven only (`test_lib_probe` `LaneOpenIdentity`,
|
|
73
|
+
`CursorSyncCanonicalize`, `CursorSyncIdentity`, stopping at the exec boundary); the disposable-lane
|
|
74
|
+
`tests/cursor_lane.py` exercises the installed `/home/coder/.lz/bin/lane`, so it proves this only after `lanes
|
|
75
|
+
install-container`.
|
|
76
|
+
|
|
20
77
|
## Views and panes
|
|
21
78
|
|
|
22
79
|
`lanes attach DEVICE` creates or reattaches the device's Zellij view. Layouts have one tab per workspace,
|
|
@@ -39,6 +96,19 @@ manifest `discoveryRoots` extends those roots. A discovered assignment remains s
|
|
|
39
96
|
a session no single board can claim is listed by EVERY owner of its socket rather than dropped, and ambiguity is
|
|
40
97
|
logged rather than resolved by guess.
|
|
41
98
|
|
|
99
|
+
**+** beside an unregistered tmux session opens **Add to workspace**. It offers workspaces using the session's
|
|
100
|
+
container, with the current workspace first. Registration verifies the observed session ID/creation time and
|
|
101
|
+
saves its existing lane ID, name and exact binding in the version-1 `registered-sessions.json` site file.
|
|
102
|
+
It publishes the registered lane immediately without signaling viewers or processes. Sync, registration and
|
|
103
|
+
explicit close share a bounded registry lock; close also consults saved registrations if projection publication
|
|
104
|
+
failed. Stale observations cannot put a registered session back in Unregistered.
|
|
105
|
+
|
|
106
|
+
These registrations have no create recipe and do not change repository terminal lists. Repository declarations
|
|
107
|
+
win if they later declare the same binding. Unknown versions or malformed registration files refuse mutation;
|
|
108
|
+
registrations for removed workspaces are retained on disk and ignored until the workspace returns. Removing a
|
|
109
|
+
record and syncing unregisters it, including when a repository registry is temporarily invalid. Keep this site
|
|
110
|
+
file in operator backups; deleting derived state does not delete registrations.
|
|
111
|
+
|
|
42
112
|
Explicit **New +** starts a detached tmux shell inside the workspace container on its dedicated
|
|
43
113
|
`lanes-shells-<workspace>` socket. Reopening only attaches. Explicit **×** closes an unregistered session after
|
|
44
114
|
rechecking its session ID and creation time and refusing registered lanes; affected viewers become empty.
|
|
@@ -77,6 +147,21 @@ The phone shell uses the pinned client's window hooks for workspaces, panes, inp
|
|
|
77
147
|
swipe, refresh or server message cannot summon the keyboard. Native taps on input fields still work.
|
|
78
148
|
Safe-area insets and the visual viewport handle the Home indicator, rotation and keyboard.
|
|
79
149
|
|
|
150
|
+
`web/mobile.js` chooses one of three modes once at load, by capability (`laneShellMode`), and records it on
|
|
151
|
+
`<html data-lanes-shell>`. A **phone** (mobile user agent, or a touch screen at most 600 px wide or 500 px tall)
|
|
152
|
+
gets the shell above unchanged. A **tablet** (any other touch screen) gets the same shell with its dock in the top
|
|
153
|
+
bar and the workspace sheet as a left panel. It also asks the pinned client for its mobile UI, because iPad Safari
|
|
154
|
+
reports a desktop user agent. That UI opens in single pane on every load; `web/browser.js` then sends the client's
|
|
155
|
+
own full-view preference once, so a desktop layout's sidebar shows beside the lanes. A **desktop** browser (fine
|
|
156
|
+
pointer, any width) keeps the plain client. Only a **Lanes** chip is added; it toggles an overlay bar with
|
|
157
|
+
workspace and lane pickers. The bar reads the `MobileState` that Zellij sends to every web client and sends the
|
|
158
|
+
phone picker's `FocusPane`. It overlays the terminal, so the terminal's columns and rows do not change, and a
|
|
159
|
+
click on the chip leaves the keyboard focus in the terminal. The tablet and desktop code and styles live in
|
|
160
|
+
`web/browser.js` and `web/browser.css`, which `mobile.js` appends only in those two modes, so a phone never
|
|
161
|
+
downloads them and the phone's `mobile.js`/`mobile.css` budget (spec §8) stays with the phone. `lanes web <device> [--desktop|--mobile]`
|
|
162
|
+
prepares a browser view, enables the services through `install/enable-web.sh` and prints the private URL; without
|
|
163
|
+
a flag a known device keeps its layout.
|
|
164
|
+
|
|
80
165
|
## Installed files and services
|
|
81
166
|
|
|
82
167
|
`install/services.py` renders the service templates against the actual checkout and installed binary paths.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Lanes design system
|
|
2
|
+
|
|
3
|
+
The operator-facing legend for the Lanes cockpit. The tables below are generated from `libexec/tokens.py`
|
|
4
|
+
(`python3 libexec/tokens.py --write`; `--check` fails on drift); the design of record is
|
|
5
|
+
`docs/superpowers/specs/2026-10-03-lanes-cockpit-design.md` at the repository root. Prose, modes, row anatomy and
|
|
6
|
+
mockups land with the docs task of the cockpit v2 plan.
|
|
7
|
+
|
|
8
|
+
<!-- BEGIN lanes tokens (generated by libexec/tokens.py --md; do not edit by hand) -->
|
|
9
|
+
| Role | Dark | Light | ANSI-16 | CSS variable |
|
|
10
|
+
|---|---|---|---|---|
|
|
11
|
+
| surface | `#101318` | `#f3f5f2` | `—` | `--lanes-surface` |
|
|
12
|
+
| surface-raised | `#191e25` | `#ffffff` | `—` | `--lanes-surface-raised` |
|
|
13
|
+
| line | `#2c333d` | `#d5ded6` | `—` | `--lanes-line` |
|
|
14
|
+
| accent-wash | `#1a2721` | `#e5f0e6` | `—` | `--lanes-accent-wash` |
|
|
15
|
+
| accent-line | `#4b6955` | `#acc9b4` | `—` | `--lanes-accent-line` |
|
|
16
|
+
| press | `#26332c` | `#d8e9db` | `—` | `--lanes-press` |
|
|
17
|
+
| chip | `#ffffff0f` | `#edf3ec` | `—` | `--lanes-chip` |
|
|
18
|
+
| ink | `#edf1f5` | `#17241b` | `39` | `--lanes-ink` |
|
|
19
|
+
| ink-dim | `#9ca7b5` | `#56616d` | `90` | `--lanes-ink-dim` |
|
|
20
|
+
| ink-faint | `#6b7683` | `#7f8a96` | `90` | `--lanes-ink-faint` |
|
|
21
|
+
| accent | `#b4e3bf` | `#2f6b45` | `1;39` | `--lanes-accent` |
|
|
22
|
+
| on-accent | `#101318` | `#ffffff` | `—` | `--lanes-on-accent` |
|
|
23
|
+
| on-accent-urgent | `#7a1a12` | `#ffd3cc` | `—` | `--lanes-on-accent-urgent` |
|
|
24
|
+
| tone-urgent | `#f28b7d` | `#b7322a` | `31` | `--lanes-tone-urgent` |
|
|
25
|
+
| tone-attention | `#e5b84a` | `#8a5a00` | `33` | `--lanes-tone-attention` |
|
|
26
|
+
| tone-working | `#7fb3f2` | `#2456a8` | `34` | `--lanes-tone-working` |
|
|
27
|
+
| tone-done | `#6fd39a` | `#1b7142` | `32` | `--lanes-tone-done` |
|
|
28
|
+
| scrim | `#000000a6` | `#1a222b80` | `—` | `--lanes-scrim` |
|
|
29
|
+
| shadow | `0 -16px 80px #0007` | `0 -16px 80px #0003` | `—` | `--lanes-shadow` |
|
|
30
|
+
|
|
31
|
+
| Glyph | Codepoint | ASCII | Tone | Meaning |
|
|
32
|
+
|---|---|---|---|---|
|
|
33
|
+
| `◆` | U+25C6 | `!` | tone-urgent | agent `needs_approval` |
|
|
34
|
+
| `◇` | U+25C7 | `?` | tone-attention | agent `needs_input` |
|
|
35
|
+
| `▸` | U+25B8 | `>` | tone-working | agent `working` |
|
|
36
|
+
| `✓` | U+2713 | `v` | tone-done | agent `done` |
|
|
37
|
+
| `✗` | U+2717 | `X` | tone-urgent | agent `errored` |
|
|
38
|
+
| `_` | U+005F | `_` | ink-dim | agent `shell` |
|
|
39
|
+
| `·` | U+00B7 | `.` | ink-dim | agent `idle` |
|
|
40
|
+
| `●` | U+25CF | `*` | tone-done | stream `active` |
|
|
41
|
+
| `○` | U+25CB | `o` | ink-dim | stream `quiet` |
|
|
42
|
+
| `✗` | U+2717 | `x` | tone-urgent | stream `missing` |
|
|
43
|
+
| `–` | U+2013 | `-` | ink-dim | stream `absent` |
|
|
44
|
+
| `!` | U+0021 | `!` | tone-urgent | stream `dead` |
|
|
45
|
+
| `·` | U+00B7 | `.` | ink-dim | stream `helper` |
|
|
46
|
+
| `?` | U+003F | `?` | ink-dim | stream `unknown` |
|
|
47
|
+
| `▌` | U+258C | `\|` | accent | mark `cursor` |
|
|
48
|
+
| `›` | U+203A | `=` | accent | mark `viewing` |
|
|
49
|
+
| `²` | U+00B2 | `2` | accent | mark `viewport-b` |
|
|
50
|
+
| `³` | U+00B3 | `3` | accent | mark `viewport-c` |
|
|
51
|
+
| `·` | U+00B7 | `.` | ink-dim | mark `viewer` |
|
|
52
|
+
| `…` | U+2026 | `~` | ink | mark `truncation` |
|
|
53
|
+
| `×` | U+00D7 | `x` | tone-urgent | mark `close` |
|
|
54
|
+
| `+` | U+002B | `+` | tone-done | mark `new` |
|
|
55
|
+
| `─` | U+2500 | `-` | ink-faint | mark `rule` |
|
|
56
|
+
| `⋮` | U+22EE | `:` | ink-dim | mark `more` |
|
|
57
|
+
| `⚠` | U+26A0 | `!` | tone-attention | mark `warning` |
|
|
58
|
+
| `↑` | U+2191 | `^` | ink-dim | mark `upload` |
|
|
59
|
+
| `↓` | U+2193 | `v` | tone-done | mark `armed` |
|
|
60
|
+
| `⏎` | U+23CE | `Enter` | ink-dim | mark `enter` |
|
|
61
|
+
| `█` | U+2588 | `_` | tone-working | mark `caret` |
|
|
62
|
+
| `?` | U+003F | `?` | ink-dim | mark `stale` |
|
|
63
|
+
| `›` | U+203A | `>` | ink | mark `lead` |
|
|
64
|
+
| `·` | U+00B7 | `-` | ink-dim | mark `sep` |
|
|
65
|
+
| `‘` | U+2018 | `'` | ink | mark `quote-open` |
|
|
66
|
+
| `’` | U+2019 | `'` | ink | mark `quote-close` |
|
|
67
|
+
| `✓` | U+2713 | `OK` | tone-done | mark `ok` |
|
|
68
|
+
| `✗` | U+2717 | `X` | tone-urgent | mark `fail` |
|
|
69
|
+
| `⚠` | U+26A0 | `!` | tone-attention | economy `starving` |
|
|
70
|
+
| `↓` | U+2193 | `v` | tone-attention | economy `downgraded` |
|
|
71
|
+
| `$` | U+0024 | `$` | ink-dim | economy `paid` |
|
|
72
|
+
<!-- END lanes tokens -->
|
|
@@ -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.
|
|
@@ -30,6 +30,16 @@ other ghost: the grammar governs what may be DECLARED, never what may be SEEN. (
|
|
|
30
30
|
on such a row saying why it cannot be declared; no surface draws it yet.) A session on a socket SEVERAL boards
|
|
31
31
|
declare, whose name claims no board and whose directory is under nobody's roots, is listed by EVERY owner of
|
|
32
32
|
that socket rather than dropped — one id, one row, several sidebars.
|
|
33
|
+
Agent glyphs (column 2, beside the cursor bar; from `status.json`'s additive `agent{}`, never from `state`): `◆`
|
|
34
|
+
approval prompt blocking · `✗` errored · `◇` turn finished, waiting for your prompt · `▸` working · `✓` done, until
|
|
35
|
+
you open the lane from this device (`clients/<device>.seen.json`) · `_` harness ended into a shell · `·` harness up,
|
|
36
|
+
no turn · blank = a plain shell. With an agent the stream glyph after the age is dimmed (`▸` + `○` is a long tool
|
|
37
|
+
call, not a stuck lane); a stale lane's agent glyph is dimmed too. Sections: **Needs you** (◆, ✗, ◇ — oldest first),
|
|
38
|
+
**Working** (oldest first), **Done** (newest first), then the workspace's other lanes under its own header — only
|
|
39
|
+
when an agent section exists; a filter flattens them. Header: `◆1 ◇2 ▸6 ✓1` (zeros omitted; `N lanes` without
|
|
40
|
+
agents; dimmed while the footer warns that status is old). Every row ends with an age: the agent's `since`, else
|
|
41
|
+
the last output (`45s`, ` 3m`, ` 2h`, ` 4d`). `x` / `×` asks ` › close ‘name’? y/n` first; `y` closes, any other
|
|
42
|
+
key cancels. Design of record: `docs/superpowers/specs/2026-10-03-lanes-cockpit-design.md` §3–§4.
|
|
33
43
|
A `·` after a lane's name (up to three) = other sessions VIEW that lane: any session GROUPED onto it, a Throttle
|
|
34
44
|
Studio browser attach (`view-<studio>-<ch>`) included — counted in the lane's `attached`/`viewers` in `status.json`,
|
|
35
45
|
never listed as unregistered (`statusd.viewer_of`; a ghost carries its viewers too, so a browser on an undeclared
|
|
@@ -52,6 +62,19 @@ process running under the survivor, which returns next cycle as a new `👻` (DE
|
|
|
52
62
|
have no close control. `+` creates a shell from the keyboard. New and Close are explicit user actions; discovery,
|
|
53
63
|
reattachment, and reconnects never create or close a session.
|
|
54
64
|
|
|
65
|
+
To keep an unregistered tmux session in a workspace's regular lane list, click/tap **+** beside it (or select it
|
|
66
|
+
and press `a`), then choose a workspace. Only workspaces using its container are offered. **Cancel** or Esc leaves
|
|
67
|
+
it unregistered. This preserves its name, running programs, scrollback and active views. It appears immediately
|
|
68
|
+
and survives sync and reconnects. Registered lanes have no close control. Legacy Docker shells cannot be registered.
|
|
69
|
+
|
|
70
|
+
From the server CLI, use `lanes ls --unregistered` to find the ID, then
|
|
71
|
+
`lanes register <lane-id> [workspace-id]`; omitting the workspace keeps the current assignment.
|
|
72
|
+
The site's version-1 `registered-sessions.json` is durable configuration: back it up with the catalog.
|
|
73
|
+
To undo registration, remove its record from `sessions` and run `lanes sync`; the terminal keeps running.
|
|
74
|
+
If publication fails after saving, `lanes sync` finishes it. Invalid/newer file versions are refused without
|
|
75
|
+
overwriting the file. These registrations have no restart recipe: an ended session stays absent until the operator
|
|
76
|
+
starts it. Add a repository terminal declaration when editor integration and a saved create recipe are wanted.
|
|
77
|
+
|
|
55
78
|
Old plain Docker shells recovered during the transition are labelled **Legacy**. They remain tracked,
|
|
56
79
|
including their existing running jobs. A still-connected viewer continues working; after leaving it, the old
|
|
57
80
|
Docker connection has no normal reattach operation. Selecting that entry explains its status, and × ends the
|
|
@@ -70,6 +93,15 @@ systemctl --user status lanes-statusd lanes-sync.timer lanes-ensure.timer lanes-
|
|
|
70
93
|
- **A view looks wrong / stuck:** `lanes rebuild mac`. Views are disposable; agents are untouched; the client reconnects.
|
|
71
94
|
- **A lane was added/renamed/removed in a repo's `.vscode/terminals.json`:** nothing to do. `lanes-sync.timer`
|
|
72
95
|
re-reads registries every 2 minutes and the sidebar follows. The zellij layout is never edited live.
|
|
96
|
+
- **Give a registered lane a different display name:** sidebar `R`, or `lanes rename <lane-id> <name>`. It writes
|
|
97
|
+
`repositories[i].displayNames` in the board's `workspaces.<board>.json` (a `.bak-<UTC>` copy is kept on the first
|
|
98
|
+
write) and syncs; the lane id, its resolve file and Cursor's `lane open <id>` stay the same. Renaming back to the
|
|
99
|
+
terminal's own title removes the override. A session added through Add to workspace / `lanes register` is renamed
|
|
100
|
+
in its `registered-sessions.json` record instead (same id, same binding); an unregistered session in its
|
|
101
|
+
`user-sessions/<id>.json`.
|
|
102
|
+
- **Promote (`P`) vs Add to workspace (`a`):** `P` declares an UNREGISTERED tmux session in the board's single
|
|
103
|
+
repository `terminals.json` (it then gets a repository lane id, `<ws>--<slug>`); `a` keeps the session's id. `P` is
|
|
104
|
+
refused on any lane that is already registered — a repository lane or an added session.
|
|
73
105
|
- **A newer layout added a tab (e.g. `Throttle`):** `lanes upgrade-session` adds it to the live sessions without a
|
|
74
106
|
rebuild and leaves you on the tab you were on.
|
|
75
107
|
- **A new workspace:** add it to `~/.config/lanes/workspaces/catalog.json` + a manifest, `lanes sync`, then
|
|
@@ -236,7 +268,8 @@ Cursor terminals need a reopen. Never `rm -rf /tmp/*` in a workspace container;
|
|
|
236
268
|
## Cursor / VS Code terminals = the same lanes
|
|
237
269
|
|
|
238
270
|
Cursor uses the Terminals Manager extension, which opens what `<folder>/.vscode/terminals.json` lists. Those files
|
|
239
|
-
stay the source of truth for
|
|
271
|
+
stay the source of truth for editor-defined lanes; sidebar registrations supplement the Lanes view without
|
|
272
|
+
changing the editor's terminal list. `lanes cursor-sync`
|
|
240
273
|
only normalizes HOW a terminal reaches its lane:
|
|
241
274
|
|
|
242
275
|
- every `command` becomes `exec /home/coder/.lz/bin/lane open <lane-id>` — attach; if the lane is missing, run its
|
|
@@ -247,6 +280,16 @@ only normalizes HOW a terminal reaches its lane:
|
|
|
247
280
|
`lanes cursor-sync <ws> --write` applies (backup under `~/.local/state/lanes/cursor-sync-backups/`). The repo then
|
|
248
281
|
shows an ordinary git diff to review and commit. Boards that keep their registry outside a repository
|
|
249
282
|
are additionally projected into the file Cursor reads (`"cursor"` in `workspaces/*.json`).
|
|
283
|
+
- `lanes cursor-sync <ws> --canonicalize` (dry run; add `--write` to apply) replaces every tmux lane's recorded
|
|
284
|
+
create recipe that cannot create the lane — none, attach-only, or a script missing in the container — with the
|
|
285
|
+
canonical `tmux new -ADs … -e LANE_ID=… -e LANE_WS=… bash -l` recipe (old value kept as `createPrevious`).
|
|
286
|
+
`--identity` adds the same `-e` flags to a working plain `tmux -L S new -ADs N [-c D] CMD` recipe, keeping CMD's
|
|
287
|
+
argv; any other working recipe is left alone and its lane carries no identity. These `-e` flags are the ONLY way a
|
|
288
|
+
tmux lane gets `LANE_ID`/the trace: `lane open` deliberately exports none (DEF-1422 — the first client on a shared
|
|
289
|
+
board socket would otherwise hand its identity to every later lane there). Both change the file only: a RUNNING
|
|
290
|
+
lane keeps its environment and gains the identity when it is next created. Before enabling it on a live board,
|
|
291
|
+
read DEF-1423: a `lanes/<ws>/<id>` trace makes the whole lane — main session, subagents, fan-out — one proxy
|
|
292
|
+
fairness bucket.
|
|
250
293
|
- Size policy "the lanes view wins": lanes-view tmux clients run with `TERM=xterm-lanesview` (a user-level terminfo
|
|
251
294
|
alias of xterm-256color). While such a client shows a lane, every other client of that lane (Cursor terminals,
|
|
252
295
|
ad-hoc attaches) is in tmux `ignore-size`; `lanes-statusd` reconciles every few seconds and gives editors the size
|
|
@@ -365,6 +408,23 @@ a real client before attaching; visible views accept 20 columns / 4 rows for the
|
|
|
365
408
|
Revoke a named token with `~/.local/share/lanes/bin/zellij-0.45.1 web --revoke-token NAME`.
|
|
366
409
|
An SSH-only phone client can run `lanes attach phone --mobile` instead.
|
|
367
410
|
|
|
411
|
+
Laptop and tablet browsers: see the [installation guide](INSTALL.md#laptop-and-tablet-browser). Troubleshooting:
|
|
412
|
+
|
|
413
|
+
- **A tablet landed on the lane-picker layout** (the device was made for a phone, for example by
|
|
414
|
+
`enable-phone.sh`'s default): give the tablet its own desktop-layout view with `lanes web <device> --desktop`
|
|
415
|
+
under a new device name. The old view stays as it is; a running view is never rebuilt into the other layout,
|
|
416
|
+
and an explicit `--desktop` against it is refused instead. Without a flag, `lanes web <device>` reuses
|
|
417
|
+
whatever layout that device already has. A tablet shows the whole tab (sidebar beside the lanes): the pinned
|
|
418
|
+
client opens its mobile UI in single pane on every load, and `web/browser.js` asks once per load for the full
|
|
419
|
+
view with the client's own preference. **More → Single pane** still switches to one pane for that load.
|
|
420
|
+
- **A laptop shows no Lanes chip**: the chip appears after sign-in, when the first workspace state arrives. A
|
|
421
|
+
reload after an upgrade picks up the new `mobile.js` and `browser.js` (both served with `no-cache`; `mobile.js`
|
|
422
|
+
appends `browser.js`/`browser.css` only on a tablet or desktop, so a phone's network panel never lists them).
|
|
423
|
+
The chip keeps the keyboard focus in the terminal; if typing stops after using a picker, Escape in the bar or a
|
|
424
|
+
click in the terminal returns it.
|
|
425
|
+
- **`lanes web` printed a `127.0.0.1` URL**: the host has no Tailscale name. Connect Tailscale on the host;
|
|
426
|
+
there is no public fallback.
|
|
427
|
+
|
|
368
428
|
## Safety rules baked into the code (do not remove)
|
|
369
429
|
|
|
370
430
|
- tmux is always called with `-N` (`attach-session`, `new-session` and `start-server` on a missing socket start
|
|
@@ -383,3 +443,36 @@ An SSH-only phone client can run `lanes attach phone --mobile` instead.
|
|
|
383
443
|
`tests/smoke_real.py` (read-only, real lanes), `tests/latency.py` (from a client). All use disposable lanes except
|
|
384
444
|
the explicitly read-only smoke test; every one refuses to start on a busy host (exit 75) and removes its lanes and
|
|
385
445
|
sessions when it is killed with SIGTERM / SIGHUP.
|
|
446
|
+
|
|
447
|
+
## Workspace catalog propagation
|
|
448
|
+
|
|
449
|
+
Register and rename workspaces in the host site catalog. Device-local copies are
|
|
450
|
+
not a second authority. `lanes sync` publishes the registry and reconciles all
|
|
451
|
+
known live devices; `lanes upgrade-session mac anarchy phone` performs only the
|
|
452
|
+
view reconciliation. Device metadata chooses desktop or mobile layout. Missing
|
|
453
|
+
sessions are recreated by `lanes ensure` with that same catalog.
|
|
454
|
+
For an existing session with no attached client, additions wait until a client
|
|
455
|
+
connects and the next ensure tick. Zellij 0.45.1 otherwise acknowledges a new
|
|
456
|
+
background tab without spawning its panes. Names and ordering can still sync.
|
|
457
|
+
|
|
458
|
+
Reconciliation identifies managed tabs by the workspace IDs in their launched
|
|
459
|
+
sidebar/view commands, then adds or renames tabs and orders them using Zellij's
|
|
460
|
+
stable tab IDs. It never closes tabs, restarts agents or changes client focus.
|
|
461
|
+
An unknown session/pane response or an ambiguous identity refuses changes for that
|
|
462
|
+
device and reports an error. Another device can still be updated. An unmanaged
|
|
463
|
+
name collision is preserved for explicit inspection; it is never adopted by title.
|
|
464
|
+
Per-device file locks serialize a sync with the ensure timer.
|
|
465
|
+
SSH split tabs match the host, absolute helper and declared pane ID together.
|
|
466
|
+
Different pane IDs may share one helper; overlapping endpoint identities refuse
|
|
467
|
+
before tab mutations and require an unambiguous catalog assignment.
|
|
468
|
+
|
|
469
|
+
For a connection wait, the Syncing screen owns and drains terminal input until
|
|
470
|
+
the attach client produces output. Terminal modes and sizes are restored when a
|
|
471
|
+
viewer ends; TERM/HUP stop only that attach process group. The persistent lane
|
|
472
|
+
continues independently. Connection diagnostics remain in the view's `.err` file.
|
|
473
|
+
Existing views pick up the connection helper on their next attach; restart only
|
|
474
|
+
the relevant view panes when activating a changed supervisor, never the lane.
|
|
475
|
+
|
|
476
|
+
Catalog fields are additive to site schema 1. Back up the catalog and running
|
|
477
|
+
checkout before updating the server. Restore those files to roll back; do not
|
|
478
|
+
rebuild device sessions as a rollback shortcut. Previously added tabs remain open.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Reconnecting viewer for an existing persistent session on another host.
|
|
3
|
+
# The remote command is an attach-only helper: disconnecting never stops its agent.
|
|
4
|
+
set -u
|
|
5
|
+
host=${1:-} command=${2:-} pane=${3:-}
|
|
6
|
+
# View sessions can be created by a clean server environment with no TERM. ssh
|
|
7
|
+
# then starts the remote tmux client without a terminal type and the helper
|
|
8
|
+
# exits immediately with “open terminal failed”. Keep the viewer attach-only,
|
|
9
|
+
# but give the remote terminal a conservative type when the caller omitted it.
|
|
10
|
+
export TERM=${TERM:-xterm-256color}
|
|
11
|
+
case "$host" in ''|-*|*@*@*|*[!A-Za-z0-9_.@-]*) echo 'invalid SSH host' >&2; exit 64 ;; esac
|
|
12
|
+
case "$command" in /*) ;; *) echo 'SSH command must be absolute' >&2; exit 64 ;; esac
|
|
13
|
+
case "$command" in *[!A-Za-z0-9_./-]*|*/../*|*/..) echo 'invalid SSH command' >&2; exit 64 ;; esac
|
|
14
|
+
case "$pane" in ''|-*|*[!A-Za-z0-9_.-]*) echo 'invalid pane' >&2; exit 64 ;; esac
|
|
15
|
+
repo=$(cd "$(dirname "$0")/.." && pwd)
|
|
16
|
+
exec python3 "$repo/libexec/connection.py" --title "$pane on $host" --retry -- \
|
|
17
|
+
ssh -tt -o BatchMode=yes -o ConnectTimeout=8 -o ServerAliveInterval=15 -o ServerAliveCountMax=3 \
|
|
18
|
+
"$host" "exec $command $pane"
|
|
@@ -2,20 +2,27 @@
|
|
|
2
2
|
# Key-authenticated client entry point and local server setup. Tokens stay in the caller's terminal.
|
|
3
3
|
set -eu
|
|
4
4
|
repo=$(cd "$(dirname "$0")/.." && pwd)
|
|
5
|
-
host= device=
|
|
5
|
+
host= device= local=0 named=0 layout=
|
|
6
6
|
while [ "$#" -gt 0 ]; do
|
|
7
7
|
case "$1" in
|
|
8
8
|
--host) host=${2:?--host SSH_ALIAS}; shift ;;
|
|
9
9
|
--local) local=1 ;;
|
|
10
|
+
--desktop|--mobile)
|
|
11
|
+
[ -z "$layout" ] || [ "$layout" = "$1" ] || { echo '--desktop and --mobile are exclusive' >&2; exit 2; }
|
|
12
|
+
layout=$1 ;;
|
|
10
13
|
-h|--help)
|
|
11
|
-
echo 'usage: enable-phone.sh [--host SSH_ALIAS | --local] [device]'
|
|
14
|
+
echo 'usage: enable-phone.sh [--host SSH_ALIAS | --local] [--desktop|--mobile] [device]'
|
|
12
15
|
echo 'Client: --host lanes uses the SSH key from ./install.sh. Server: --local.'
|
|
16
|
+
echo 'Mobile (phone lane pickers, device phone) is the default; --desktop serves the desktop sidebar layout'
|
|
17
|
+
echo '(device browser) for a laptop or tablet browser. A running view of the other layout is refused.'
|
|
13
18
|
exit 0 ;;
|
|
14
19
|
-*) echo "unknown option: $1" >&2; exit 2 ;;
|
|
15
20
|
*) [ "$named" = 0 ] || { echo 'only one device name is allowed' >&2; exit 2; }; device=$1; named=1 ;;
|
|
16
21
|
esac
|
|
17
22
|
shift
|
|
18
23
|
done
|
|
24
|
+
# Mobile stays the default (and sends no flag to an older server); --desktop names its own default device.
|
|
25
|
+
if [ "$layout" = --desktop ]; then device=${device:-browser} suffix=' --desktop'; else layout=--mobile device=${device:-phone} suffix=; fi
|
|
19
26
|
case "$device" in ''|-*|*[!A-Za-z0-9_.-]*) echo 'invalid device name' >&2; exit 2 ;; esac
|
|
20
27
|
case "$host" in -*|*[!A-Za-z0-9_.@:-]*) echo 'invalid SSH host' >&2; exit 2 ;; esac
|
|
21
28
|
[ "$local" = 0 ] || [ -z "$host" ] || { echo '--local and --host are mutually exclusive' >&2; exit 2; }
|
|
@@ -23,10 +30,10 @@ if [ "$local" = 0 ] && [ "$(uname -s)" = Darwin ] && [ -z "$host" ]; then host=l
|
|
|
23
30
|
[ -t 0 ] && [ -t 1 ] || { echo 'Run enable-phone.sh in your own terminal; it displays a private login token.' >&2; exit 64; }
|
|
24
31
|
if [ -n "$host" ]; then
|
|
25
32
|
exec ssh -t -o BatchMode=yes -o PreferredAuthentications=publickey -o ForwardAgent=no "$host" \
|
|
26
|
-
"exec sh \"\$HOME/.local/share/lanes/current/install/enable-phone.sh\" --local '$device'"
|
|
33
|
+
"exec sh \"\$HOME/.local/share/lanes/current/install/enable-phone.sh\" --local '$device'$suffix"
|
|
27
34
|
fi
|
|
28
35
|
command -v tailscale >/dev/null 2>&1 || { echo 'Install and connect Tailscale on the server first.' >&2; exit 1; }
|
|
29
|
-
sh "$repo/install/enable-web.sh" "$device"
|
|
36
|
+
sh "$repo/install/enable-web.sh" "$device" "$layout"
|
|
30
37
|
if ! tailscale serve --bg --https=8443 http://127.0.0.1:8083; then
|
|
31
38
|
echo 'The server admin must allow this account to manage Tailscale Serve:' >&2
|
|
32
39
|
printf ' sudo tailscale set --operator=%s\n' "$(id -un)" >&2
|
|
@@ -35,9 +42,11 @@ if ! tailscale serve --bg --https=8443 http://127.0.0.1:8083; then
|
|
|
35
42
|
fi
|
|
36
43
|
dns=$(tailscale status --json | python3 -c 'import json,sys; print(json.load(sys.stdin)["Self"]["DNSName"].rstrip("."))')
|
|
37
44
|
[ -n "$dns" ] || { echo 'This node needs Tailscale MagicDNS and HTTPS enabled.' >&2; exit 1; }
|
|
38
|
-
|
|
45
|
+
if [ -n "$suffix" ]; then where='In a browser on your laptop or tablet'; else where='On your phone'; fi
|
|
46
|
+
printf '\n%s, connect Tailscale and open:\n\n https://%s:8443/%s\n\n' "$where" "$dns" "$device"
|
|
39
47
|
printf 'The last line is NAME: TOKEN. Paste only TOKEN into the sign-in screen.\n'
|
|
40
|
-
printf 'In Safari, Share > Add to Home Screen installs the app.\n
|
|
48
|
+
[ -n "$suffix" ] || printf 'In Safari, Share > Add to Home Screen installs the app.\n'
|
|
49
|
+
printf '\n'
|
|
41
50
|
# Zellij 0.45.1 requires --create-token by itself; --token-name cannot accompany it.
|
|
42
51
|
exec "${LANES_SHARE:-$HOME/.local/share/lanes}/bin/zellij-0.45.1" \
|
|
43
52
|
--config-dir "$repo/zellij" --config "$repo/zellij/config.kdl" web --create-token
|
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
2
|
# Enable loopback web services as the installed workspace user. Does not print a token.
|
|
3
|
+
# The view is mobile (the phone's lane pickers, default device `phone`) unless --desktop asks for the desktop
|
|
4
|
+
# sidebar layout (default device `browser`); a running view of the other layout is refused, never rebuilt.
|
|
3
5
|
set -eu
|
|
4
6
|
repo=$(cd "$(dirname "$0")/.." && pwd)
|
|
5
|
-
device
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
usage='usage: enable-web.sh [device] [--desktop|--mobile]'
|
|
8
|
+
device= layout=
|
|
9
|
+
while [ "$#" -gt 0 ]; do
|
|
10
|
+
case "$1" in
|
|
11
|
+
--desktop|--mobile)
|
|
12
|
+
[ -z "$layout" ] || [ "$layout" = "$1" ] || { echo '--desktop and --mobile are exclusive' >&2; exit 2; }
|
|
13
|
+
layout=$1 ;;
|
|
14
|
+
-*) echo "$usage" >&2; exit 2 ;;
|
|
15
|
+
*) [ -z "$device" ] || { echo "$usage" >&2; exit 2; }; device=$1 ;;
|
|
16
|
+
esac
|
|
17
|
+
shift
|
|
18
|
+
done
|
|
19
|
+
if [ "$layout" = --desktop ]; then device=${device:-browser} suffix=' --desktop'; else layout=--mobile device=${device:-phone} suffix=; fi
|
|
20
|
+
case "$device" in -*|*[!A-Za-z0-9_.-]*) echo 'invalid device name' >&2; exit 2 ;; esac
|
|
8
21
|
command -v caddy >/dev/null 2>&1 || { echo 'Install Caddy first (see docs/INSTALL.md).' >&2; exit 1; }
|
|
9
22
|
export XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-/run/user/$(id -u)}
|
|
10
|
-
"$repo/bin/lanes" prepare "$device"
|
|
23
|
+
"$repo/bin/lanes" prepare "$device" "$layout"
|
|
11
24
|
python3 "$repo/install/services.py"
|
|
12
25
|
systemctl --user daemon-reload
|
|
13
26
|
systemctl --user enable --now lanes-web.service lanes-mobile.service
|
|
14
27
|
systemctl --user is-active --quiet lanes-web.service lanes-mobile.service
|
|
15
|
-
printf '\
|
|
16
|
-
printf 'For private HTTPS and a login token, run in your terminal:\n sh "%s/install/enable-phone.sh" --local %s\n' "$repo" "$device"
|
|
28
|
+
printf '\nWeb frontend: 127.0.0.1:8083; Zellij: 127.0.0.1:8082 (loopback only).\n'
|
|
29
|
+
printf 'For private HTTPS and a login token, run in your terminal:\n sh "%s/install/enable-phone.sh" --local %s%s\n' "$repo" "$device" "$suffix"
|