@vibelord/lanes 1.1.1 → 1.3.0
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/README.md +36 -5
- package/bin/lanes-install.mjs +166 -13
- package/lib/plan.mjs +113 -3
- package/package.json +1 -1
- package/payload/lanes/README.md +47 -23
- package/payload/lanes/VERSION +1 -1
- package/payload/lanes/bin/lanes +2753 -202
- package/payload/lanes/client/ghostty/lanes-palette-light.conf +19 -19
- package/payload/lanes/client/ghostty/lanes-palette.conf +19 -19
- package/payload/lanes/client/lanes-launch +289 -40
- package/payload/lanes/client/lanes-window +15 -3
- package/payload/lanes/container/hooks/claude-settings.json +1 -0
- package/payload/lanes/container/lane +147 -92
- package/payload/lanes/container/lane-edit +100 -0
- package/payload/lanes/container/lane-mcp +1150 -0
- package/payload/lanes/container/lane-signal +109 -29
- package/payload/lanes/container/lane_core.py +1318 -0
- package/payload/lanes/container/vimrc +31 -0
- package/payload/lanes/docs/ARCHITECTURE.md +793 -16
- package/payload/lanes/docs/DESIGN.md +127 -46
- package/payload/lanes/docs/INSTALL.md +175 -18
- package/payload/lanes/docs/RUNBOOK.md +968 -76
- package/payload/lanes/docs/SHARING.md +3 -2
- package/payload/lanes/docs/USERGUIDE.md +967 -150
- package/payload/lanes/holders.lock +5 -0
- package/payload/lanes/install/enable-docs.sh +78 -0
- package/payload/lanes/install/enable-web.sh +2 -0
- package/payload/lanes/install/host-install.sh +197 -7
- package/payload/lanes/install/services.py +148 -18
- package/payload/lanes/install.sh +68 -7
- package/payload/lanes/libexec/agent_state.py +74 -1
- package/payload/lanes/libexec/cursor_sync.py +6 -0
- package/payload/lanes/libexec/device_sync.py +47 -10
- package/payload/lanes/libexec/docs_store.py +995 -0
- package/payload/lanes/libexec/docsd.py +2010 -0
- package/payload/lanes/libexec/economy_poll.py +1 -0
- package/payload/lanes/libexec/enrold.py +41 -22
- package/payload/lanes/libexec/keys.py +54 -23
- package/payload/lanes/libexec/lanes_lib.py +620 -22
- package/payload/lanes/libexec/mdlite.py +575 -0
- package/payload/lanes/libexec/members.py +21 -2
- package/payload/lanes/libexec/notify.py +169 -29
- package/payload/lanes/libexec/registrations.py +216 -18
- package/payload/lanes/libexec/session_actions.py +182 -14
- package/payload/lanes/libexec/sidebar +11 -3
- package/payload/lanes/libexec/sidebar.py +2232 -365
- package/payload/lanes/libexec/statusd.py +231 -12
- package/payload/lanes/libexec/studio_host.py +8 -1
- package/payload/lanes/libexec/tokens.py +148 -35
- package/payload/lanes/libexec/tokens.sh +86 -2
- package/payload/lanes/libexec/update.py +1044 -0
- package/payload/lanes/libexec/uploads.py +538 -0
- package/payload/lanes/libexec/users.py +614 -0
- package/payload/lanes/libexec/view.sh +34 -6
- package/payload/lanes/systemd/lanes-docsd.service +29 -0
- package/payload/lanes/systemd/lanes-ensure.service +3 -0
- package/payload/lanes/systemd/lanes-gc.service +1 -1
- package/payload/lanes/systemd/lanes-update.service +17 -0
- package/payload/lanes/systemd/lanes-update.timer +17 -0
- package/payload/lanes/web/Caddyfile +24 -1
- package/payload/lanes/web/README.md +165 -28
- package/payload/lanes/web/browser.js +4 -8
- package/payload/lanes/web/docs.css +52 -0
- package/payload/lanes/web/docs.js +36 -0
- package/payload/lanes/web/mobile.css +17 -17
- package/payload/lanes/web/mobile.js +45 -32
- package/payload/lanes/web/upload.css +17 -0
- package/payload/lanes/web/upload.js +84 -0
- package/payload/lanes/workspaces.example/README.md +3 -1
- package/payload/lanes/zellij/config.kdl +106 -56
- package/payload/manifest.json +6 -3
|
@@ -4,12 +4,23 @@
|
|
|
4
4
|
# loops forever and shows a status screen instead of dying.
|
|
5
5
|
# usage: view.sh <device> <workspace> [slot]
|
|
6
6
|
# switch lane: write the lane id to $STATE/views/<device>/<workspace>.<slot>.target, then `kill -USR1` this pid.
|
|
7
|
+
# new tree (cockpit v3 U5): `kill -USR2` this pid after an update flipped ~/.local/share/lanes/current. The view
|
|
8
|
+
# re-executes itself (`exec "$0" …`: the same pid, the path it was started by — a `current` path runs the new tree) at
|
|
9
|
+
# the NEXT NATURAL EXIT of its child: an attached lane's viewer detaching or a USR1 switch, the end of a status
|
|
10
|
+
# screen's wait. USR2 itself never signals the child, so it never drops an attached tmux client.
|
|
7
11
|
set -u
|
|
12
|
+
# Trapped first, before this view's pid file names it: a USR2 must never find the default action (terminate).
|
|
13
|
+
reexec=0
|
|
14
|
+
on_reexec() { reexec=1; }
|
|
15
|
+
trap on_reexec USR2
|
|
8
16
|
device=${1:?device} ws=${2:?workspace} slot=${3:-main}
|
|
9
17
|
mobile=0; [ "${4:-}" != --mobile ] || mobile=1
|
|
10
18
|
case "$device$ws$slot" in *[!A-Za-z0-9_.-]*) echo 'invalid view name'; exit 64 ;; esac
|
|
11
19
|
state=${LANES_STATE:-$HOME/.local/state/lanes}
|
|
12
|
-
|
|
20
|
+
# -P: the PHYSICAL tree. A layout names this script through the `current` link (W9 U1); resolving it once pins every
|
|
21
|
+
# holder and connection.py this loop runs to the tree the view started from, so an update that flips the link never
|
|
22
|
+
# mixes an old view.sh with new holders. The view moves to the new tree when it is started again.
|
|
23
|
+
repo=$(cd "$(dirname "$0")/.." && pwd -P)
|
|
13
24
|
dir=$state/views/$device
|
|
14
25
|
mkdir -p "$dir"
|
|
15
26
|
target_file=$dir/$ws.$slot.target
|
|
@@ -50,6 +61,13 @@ run() {
|
|
|
50
61
|
# A trapped signal interrupts wait; make sure the child is really gone before moving on.
|
|
51
62
|
while kill -0 "$child" 2>/dev/null; do wait "$child" 2>/dev/null || :; done
|
|
52
63
|
child=
|
|
64
|
+
# A USR2 came while the child ran (or before it): it has ended by itself now, so run the tree `current` names. A
|
|
65
|
+
# failed exec ends a non-interactive shell, so a script that is gone (a pruned tree) is not exec'd: this one goes on.
|
|
66
|
+
if [ "$reexec" = 1 ]; then
|
|
67
|
+
reexec=0
|
|
68
|
+
if [ -x "$0" ] && [ "$mobile" = 1 ]; then exec "$0" "$device" "$ws" "$slot" --mobile; fi
|
|
69
|
+
if [ -x "$0" ]; then exec "$0" "$device" "$ws" "$slot"; fi
|
|
70
|
+
fi
|
|
53
71
|
return "$rc"
|
|
54
72
|
}
|
|
55
73
|
|
|
@@ -58,20 +76,30 @@ reset_terminal() {
|
|
|
58
76
|
printf '\033[?1049l\033[?1000l\033[?1002l\033[?1003l\033[?1006l\033[?2004l\033[<u\033[0m\033[?25h\033[2J\033[3J\033[H'
|
|
59
77
|
}
|
|
60
78
|
|
|
61
|
-
# The
|
|
79
|
+
# The SGR literals of the design tokens (libexec/tokens.py --sh): sourced once, printf only, no fork. A plain source
|
|
80
|
+
# assigns the 16-colour set; `lz_tones` re-points them to another set (assignments and a `case`, no fork either).
|
|
62
81
|
# They hold `\033[…m` as text, so they go into printf's FORMAT (which turns `\033` into ESC), never into a %s argument.
|
|
63
82
|
# shellcheck source=libexec/tokens.sh
|
|
64
83
|
. "$repo/libexec/tokens.sh"
|
|
65
84
|
|
|
66
85
|
# status_screen <title> <text> <dim line> [<lead>]: a lead, when given, is the bold headline above the title.
|
|
67
|
-
# The
|
|
68
|
-
#
|
|
69
|
-
# too slow for one, and `lanes attach DEVICE --ascii` against a
|
|
86
|
+
# The look follows the stamps the sidebar publishes for this device from its resolved modes (devices/<device>.json
|
|
87
|
+
# first): $STATE/clients/<device>.glyphs, .color and .theme, read on EVERY call with the builtin read, no fork: this
|
|
88
|
+
# runs exactly when the host may be too slow for one, and `lanes attach DEVICE --ascii|--color C|--theme T` against a
|
|
89
|
+
# live session must reach the next screen without a restart. The lead glyph is `!` in ascii. `.color` and `.theme`
|
|
90
|
+
# pick the colour set (cockpit v3 P3): truecolor or 256 of dark or light, none (NO_COLOR: bold only), and anything
|
|
91
|
+
# else — no stamp, a vivid mode with no known variant yet — the 16-colour set, the user's own palette, never a guess.
|
|
92
|
+
# In the vivid sets (LZ_VIVID: the colours Lanes defines) the lead takes the warning mark's tone, attention, as the
|
|
93
|
+
# sidebar's banner does; 16 and none keep v1's bytes, as the sidebar's rows do.
|
|
70
94
|
status_screen() {
|
|
71
95
|
reset_terminal
|
|
72
96
|
glyphs=unicode; { IFS= read -r glyphs < "$state/clients/$device.glyphs"; } 2>/dev/null || :
|
|
97
|
+
color=16; { IFS= read -r color < "$state/clients/$device.color"; } 2>/dev/null || :
|
|
98
|
+
theme=; { IFS= read -r theme < "$state/clients/$device.theme"; } 2>/dev/null || :
|
|
99
|
+
lz_tones "$color" "$theme"
|
|
73
100
|
lead_glyph='⚠'; [ "$glyphs" != ascii ] || lead_glyph='!'
|
|
74
|
-
|
|
101
|
+
lead_tone=; [ -z "${LZ_VIVID:-}" ] || lead_tone=$LZ_ATTENTION
|
|
102
|
+
if [ -n "${4:-}" ]; then printf "\n ${LZ_BOLD}${lead_tone}%s %s${LZ_RESET}\n\n %s\n" "$lead_glyph" "$4" "$1"
|
|
75
103
|
else printf "\n ${LZ_BOLD}%s${LZ_RESET}\n" "$1"; fi
|
|
76
104
|
printf "\n %s\n\n ${LZ_DIM}%s${LZ_RESET}\n" "$2" "$3"
|
|
77
105
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=lanes docs service (login-bound docs, artifacts and uploads) on loopback 127.0.0.1:8185
|
|
3
|
+
|
|
4
|
+
# Enabled and restarted by every server install (install/host-install.sh). lanes-mobile's Caddy forwards
|
|
5
|
+
# https://<host>:8443/lanes-docs/* here (web/Caddyfile `handle_path /lanes-docs/*`). It reads and writes the docs store
|
|
6
|
+
# ~/.local/state/lanes/docs/ and docs.log and reads members/, enrol-mode and registry.json; its publish pick-up (W4)
|
|
7
|
+
# reads each workspace container's ~/.lz/state/publish/ through /proc/<pid>/root, as lanes-statusd does, and writes the
|
|
8
|
+
# receipts back there; and (W5) it lands uploads in a workspace's uploads/ through the container's home volume. Hence
|
|
9
|
+
# it has no ProtectHome.
|
|
10
|
+
# It answers nothing until the operator pins the Serve header check (`lanes enrol mode request`, read on every
|
|
11
|
+
# request). Optional settings (LANES_DOCS_DOMAINS, LANES_DOCS_PORT, and the default-off encryption at rest
|
|
12
|
+
# LANES_DOCS_AGE_RECIPIENT / LANES_DOCS_AGE_IDENTITY, which `lanes upload` reads from the same drop-in) go in a drop-in,
|
|
13
|
+
# `systemctl --user edit lanes-docsd.service`, which a re-install keeps (RUNBOOK "Docs service").
|
|
14
|
+
# Memory: docsd holds at most two 32 MiB buffers at once (UPLOAD_SLOTS, shared by uploads and decryptions, one per
|
|
15
|
+
# login), two Markdown renders and an 8 MiB render cache, and its publish pick-up reads at most 4,096 directory entries
|
|
16
|
+
# of a container per cycle, so what a container plants or a member sends stays under MemoryMax (RUNBOOK "Docs service",
|
|
17
|
+
# Limits; the composed peak is not measured: DEF-1917).
|
|
18
|
+
[Service]
|
|
19
|
+
ExecStart=/usr/bin/python3 -S -E "@REPO@/libexec/docsd.py"
|
|
20
|
+
Restart=on-failure
|
|
21
|
+
RestartSec=10
|
|
22
|
+
NoNewPrivileges=true
|
|
23
|
+
UMask=0077
|
|
24
|
+
MemoryHigh=128M
|
|
25
|
+
MemoryMax=192M
|
|
26
|
+
Slice=lanes.slice
|
|
27
|
+
|
|
28
|
+
[Install]
|
|
29
|
+
WantedBy=default.target
|
|
@@ -3,5 +3,8 @@ Description=lanes: recreate any missing device view session (agents are never to
|
|
|
3
3
|
|
|
4
4
|
[Service]
|
|
5
5
|
Type=oneshot
|
|
6
|
+
# No After=/Conflicts= with lanes-update.service: `lanes ensure` holds the view-session lock (locks/sessions.lock,
|
|
7
|
+
# DEF-1792) itself, as the update's `ensure --if-layout-changed`, `lanes rebuild` and every view create do. A tick that
|
|
8
|
+
# finds it held waits 10 s, then skips (exit 0). Conflicts= would stop a running update; After= orders queued jobs only.
|
|
6
9
|
ExecStart="@REPO@/bin/lanes" ensure
|
|
7
10
|
Slice=lanes.slice
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=lanes: install the newest npm release when there is one (lanes update --timer)
|
|
3
|
+
Documentation=https://github.com/bwllaming/bw-tools/blob/main/lanes/docs/USERGUIDE.md#update
|
|
4
|
+
|
|
5
|
+
[Service]
|
|
6
|
+
Type=oneshot
|
|
7
|
+
# Asks the npm registry for the release @vibelord/lanes's dist-tag names (`latest`, or `lanes-stable` with channel=stable
|
|
8
|
+
# in ~/.config/lanes/update.conf) and, when it is newer than the installed one, runs the npx the npm wrapper recorded at
|
|
9
|
+
# install (~/.local/state/lanes/update/npx): `npx --yes @vibelord/lanes@<release> --server --update-only`. A last run
|
|
10
|
+
# that ended `partial` gets its post-commit again first (once per release from here). A checkout install, an update
|
|
11
|
+
# already running and an older or rolled-back release are a line in the journal and exit 0 (plan W9 U3, LNS-91).
|
|
12
|
+
ExecStart="@REPO@/bin/lanes" update --timer
|
|
13
|
+
# A oneshot has no start timeout by default. The update's own bounds are npx (50 min) and each post-commit step (300 s);
|
|
14
|
+
# this one stops a run wedged anywhere else. A stop is SIGTERM to every process of the run: the installer records it.
|
|
15
|
+
TimeoutStartSec=1h
|
|
16
|
+
Nice=5
|
|
17
|
+
Slice=lanes.slice
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=lanes: look for a newer npm release 15 min after boot, then every 30 min (enabled by the server install)
|
|
3
|
+
|
|
4
|
+
[Timer]
|
|
5
|
+
# Enabled by every full server install (install/host-install.sh) with the other timers. OnBootSec= is already in the
|
|
6
|
+
# past on a running host, so enabling it starts one check at once — refused while that install still holds the run lock
|
|
7
|
+
# (~/.local/state/lanes/update/.lock), harmless after it (the release just installed is the newest, or the next one is).
|
|
8
|
+
OnBootSec=15min
|
|
9
|
+
OnUnitActiveSec=30min
|
|
10
|
+
RandomizedDelaySec=5min
|
|
11
|
+
# Persistent= acts only on OnCalendar= timers (systemd.timer(5)): a host that was off misses no check here because
|
|
12
|
+
# OnBootSec= checks 15 min after every boot. Kept as the plan sets it; `lanes update` itself skips a check made less than
|
|
13
|
+
# 20 min before (update.INTERVAL).
|
|
14
|
+
Persistent=true
|
|
15
|
+
|
|
16
|
+
[Install]
|
|
17
|
+
WantedBy=timers.target
|
|
@@ -8,12 +8,35 @@
|
|
|
8
8
|
http://:8083 {
|
|
9
9
|
bind 127.0.0.1
|
|
10
10
|
root * "{$LANES_REPO}/web"
|
|
11
|
-
|
|
11
|
+
# lanes-docsd answers /lanes-docs/* with its own security headers per route (an agent's HTML or SVG artifact is a
|
|
12
|
+
# sandboxed top-level document), so the site-wide set below skips that prefix: docsd's are the only ones it sends.
|
|
13
|
+
@notdocs not path /lanes-docs/*
|
|
14
|
+
header @notdocs {
|
|
12
15
|
X-Content-Type-Options nosniff
|
|
13
16
|
X-Frame-Options DENY
|
|
14
17
|
Referrer-Policy strict-origin-when-cross-origin
|
|
15
18
|
Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; connect-src 'self' ws: wss:; img-src 'self' data:"
|
|
16
19
|
}
|
|
20
|
+
# lanes-docsd.service (libexec/docsd.py): the docs cockpit, login-bound pages, downloads, artifacts and uploads.
|
|
21
|
+
# handle_path strips /lanes-docs. It stays the FIRST handle of the site: Caddy never reaches a block declared after
|
|
22
|
+
# a handle with a named matcher (the Zellij routes below).
|
|
23
|
+
handle_path /lanes-docs/* {
|
|
24
|
+
reverse_proxy 127.0.0.1:8185 {
|
|
25
|
+
# As on /enrol/*: keep the X-Forwarded-For Tailscale Serve sets (the client's tailnet address): docsd
|
|
26
|
+
# answers only a request that carries one, and only with Serve's identity headers. Caddy >= 2.5.
|
|
27
|
+
trusted_proxies 127.0.0.0/8
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
# The docs page's pinned Mermaid (plan W10 W6, LNS-107): install/enable-docs.sh puts holders.lock's pin into
|
|
31
|
+
# $LANES_SHARE/vendor (services.py sets LANES_SHARE in lanes-mobile.service). Immutable: a page names a VERSIONED
|
|
32
|
+
# file, so a new pin is a new URL (DEF-1870). Caddy sorts a single-path route ahead of /lanes-assets/* only past
|
|
33
|
+
# routes that also have a single path (Caddy issue #5037; DEF-1872): keep this block with the single-path routes
|
|
34
|
+
# above the named-matcher ones (/lanes-docs/* does not overlap it, so their order between themselves is free).
|
|
35
|
+
handle_path /lanes-assets/vendor/* {
|
|
36
|
+
root * "{$LANES_SHARE}/vendor"
|
|
37
|
+
header Cache-Control "public, max-age=31536000, immutable"
|
|
38
|
+
file_server
|
|
39
|
+
}
|
|
17
40
|
handle_path /lanes-assets/* {
|
|
18
41
|
header Cache-Control "no-cache"
|
|
19
42
|
file_server
|
|
@@ -17,7 +17,9 @@ undoes the client's per-load single-pane start once
|
|
|
17
17
|
(`laneFullRender`, the client's own `SetMobileRenderPreferences {single_pane: false, fit: true}`), so a desktop
|
|
18
18
|
view's sidebar shows beside its lanes. `tests/web/activation.test.mjs` (`node --test`) pins the activation matrix,
|
|
19
19
|
the picker model and that render preference; `tests/unit/test_web_view.py` pins the size budgets and runs that
|
|
20
|
-
suite when node is installed.
|
|
20
|
+
suite when node is installed; `tests/web/phone.test.mjs` boots `mobile.js` as a phone against a fake page and
|
|
21
|
+
client and records every frame it sends (input v2's bytes, keyboard, clipboard, dock and menu; cockpit v3 S5's filter
|
|
22
|
+
Esc and prompt quick row; `test_web_view.PhoneKeys` names S5's tests and counts the file's). After an upgrade,
|
|
21
23
|
check that `__zjMobileUi`, `__zjSendControl`, `__zjSoftKbdCapture` (with `sendFn` = `websockets.sendAnsiKey`),
|
|
22
24
|
`__zjSyncInactiveCursorStyle` and `window.term` still exist in the served `app.js`.
|
|
23
25
|
|
|
@@ -41,16 +43,22 @@ before the control socket opens makes 0.45.1 throw `InvalidStateError` and leave
|
|
|
41
43
|
|
|
42
44
|
`lanes-mobile.service` runs a separate, loopback-only Caddy on port 8083 with its admin API disabled.
|
|
43
45
|
It does not change the system Caddy. Tailscale Serve forwards private HTTPS on 8443 to this frontend;
|
|
44
|
-
the original Zellij service stays on 8082. Caddy (2.5 or later) must already be installed on the host. `/enrol/*`
|
|
45
|
-
the
|
|
46
|
-
service on 127.0.0.1:8184 (`lanes-enrold.service`, [runbook](../docs/RUNBOOK.md#enrolment-service-lanes-enrold-w8-a3));
|
|
46
|
+
the original Zellij service stays on 8082. Caddy (2.5 or later) must already be installed on the host. `/enrol/*` and
|
|
47
|
+
`/lanes-docs/*` are the routes that are neither these assets nor Zellij: `handle_path` strips `/enrol` and forwards to
|
|
48
|
+
the enrolment service on 127.0.0.1:8184 (`lanes-enrold.service`, [runbook](../docs/RUNBOOK.md#enrolment-service-lanes-enrold-w8-a3));
|
|
47
49
|
it must stay before the Zellij routes, and its `trusted_proxies 127.0.0.0/8` keeps the `X-Forwarded-For` Tailscale
|
|
48
|
-
Serve sets (the client's tailnet address), so the redeem rate limit counts per device.
|
|
50
|
+
Serve sets (the client's tailnet address), so the redeem rate limit counts per device. `/lanes-docs/*` (prefix
|
|
51
|
+
stripped) goes to the docs service on 127.0.0.1:8185 (`lanes-docsd.service`,
|
|
52
|
+
[runbook](../docs/RUNBOOK.md#docs-service-lanes-docsd-w10-w3)): it is the FIRST handle of the site, keeps Serve's
|
|
53
|
+
`X-Forwarded-For` the same way, and the site-wide `header` block skips it (`header @notdocs`, `@notdocs not path
|
|
54
|
+
/lanes-docs/*`) so docsd's own per-route security headers are the only ones a document gets.
|
|
49
55
|
Run `lanes web <device> --token` on the server (it also adds that Tailscale Serve mapping when it is missing),
|
|
50
56
|
`lanes-launch web phone|laptop` from an installed client, or `sh install/enable-phone.sh --host lanes` (`--local` on
|
|
51
57
|
the server).
|
|
52
58
|
The [installation guide](../docs/INSTALL.md) covers Caddy, Tailscale operator permission and the selected SSH key.
|
|
53
|
-
Service registration supplies `LANES_REPO`
|
|
59
|
+
Service registration supplies `LANES_REPO` — the `~/.local/share/lanes/current` link, which names the installed
|
|
60
|
+
tree (W9 U1) — so Caddy serves that tree from any installation path, and the files an update brings as soon as
|
|
61
|
+
the link moves; the routes of a new Caddyfile take a restart of `lanes-mobile` (the server installer does it).
|
|
54
62
|
|
|
55
63
|
`index.html` keeps the upstream asset integrity hashes. When upgrading Zellij, compare the HTML and
|
|
56
64
|
window hooks against the new version before changing the pinned binary. The mobile JS and CSS have
|
|
@@ -90,9 +98,16 @@ character of the old query after its common prefix with the new one, then the ne
|
|
|
90
98
|
prefix; an emptied field or the Esc key sends a lone `Esc`. The text is the `TextEncoder` bytes with control bytes and
|
|
91
99
|
DEL dropped, decoded back to text. There is no `setTimeout`: `send()` keeps a pause marker in its queue after a lone
|
|
92
100
|
`Esc` and re-arms the one interval, whose next tick sends what follows once 60 ms have passed. The phone keeps the
|
|
93
|
-
last query it sent (`laneFilterPrev`)
|
|
94
|
-
|
|
95
|
-
|
|
101
|
+
last query it sent (`laneFilterPrev`) and the picker it was typed on (`filterTab`, cockpit v3 S5); once a refresh
|
|
102
|
+
finds another tab current — a lane, or another workspace's picker by the Workspaces sheet — it forgets the query,
|
|
103
|
+
empties the field and drops a filter tail still waiting in the queue, without sending anything: the lane now has the
|
|
104
|
+
focus, and an `Esc` there can interrupt an agent (DEF-1752 here, DEF-1817 on S5's line; the sidebar clears its own
|
|
105
|
+
filter when it opens a lane), and another workspace's picker starts with an empty bar instead of the first one's
|
|
106
|
+
query, whose next letter the deltas would have sent there as a bare key (DEF-1820). The lone `Esc` of an emptied
|
|
107
|
+
field or of the Esc key, and the one a picker left filtered is owed, go through one debt (`owed`, picker tab names):
|
|
108
|
+
`updateLaneFilterBar` sends it only while the picker it is owed to is the current tab, so a picker left filtered shows
|
|
109
|
+
its whole list again on its next visit, and an Escape that arrives once the client has already moved on is held,
|
|
110
|
+
never sent into the lane or another picker. Limits: the deltas assume the
|
|
96
111
|
sidebar holds what the phone sent, which each new query's leading `Esc` restores — another client typing into the same
|
|
97
112
|
picker pane, or a character the sidebar does not print (a no-break space, a zero-width joiner), puts them out of step
|
|
98
113
|
until the field is cleared; and an `Esc` while the sidebar's upload strip is armed (`U`) returns the focus to the
|
|
@@ -155,6 +170,14 @@ on those facts (`node --test`, no jsdom).
|
|
|
155
170
|
Attention counts come from the picker sidebar's pane title (`WS · ◆1 ◇2 ▸6`, written by the sidebar as OSC 2):
|
|
156
171
|
`parseCounts` (also `window.__lanesParseCounts`) feeds the picker subtitle, `document.title`, the Lanes button's
|
|
157
172
|
`data-count` and the workspace cards, inside the existing `refresh()` / `renderCards()` — no fetch, file or timer.
|
|
173
|
+
The shell makes one request (W9 V3, LNS-85): once per page load, when it builds, `installFontControls()` asks
|
|
174
|
+
`/enrol/site` on the same origin (the `/enrol/*` route above; the enrolment service's `GET /site` answers
|
|
175
|
+
`{alias, host_name, host_key, version, request_mode, caller, source}`) and shows its `version` as the first row of
|
|
176
|
+
the client's **More** menu, **Lanes v1.2.0**, above **Text size** — the same `.lanes-font-controls` row, so no CSS.
|
|
177
|
+
Only a release is shown (`^\d[\w.+-]{0,31}$`, as text): `dev`, anything else, an error answer, a non-JSON 502 while
|
|
178
|
+
the service is down, or no network shows nothing and throws nothing. The version is the enrolment service's tree
|
|
179
|
+
(the server installer restarts it on every install). `tests/web/version.test.mjs` boots `mobile.js` against a fake
|
|
180
|
+
logged-in page to pin this; `test_web_view` pins one fetch, inside `installFontControls()`, which `build()` runs once.
|
|
158
181
|
Until plan Task 26 (2026-10-04) this reader was inert (DEF-1531): `bin/lanes` `mobile_workspace_tab` gave the
|
|
159
182
|
picker pane a fixed `name="Lanes"`, and Zellij reports a pane's layout name instead of its OSC 2 title, so
|
|
160
183
|
`pickerTitle()` read `Lanes`, `parseCounts` returned null and the shell showed no counts ("Choose a lane to continue").
|
|
@@ -163,7 +186,11 @@ phone device keeps the old picker pane until `lanes rebuild <device>` (`upgrade-
|
|
|
163
186
|
`lanes doctor` names such a device). Whether the OSC 2 title now reaches
|
|
164
187
|
`panes[].title` is still unchecked in a browser (no WebKit pass has run; fallback: the counts stay in the
|
|
165
188
|
sidebar header). The quick-key row (`⏎ y n Esc ^C ↑ ↓`, `.lanes-quick`) shows on view
|
|
166
|
-
tabs
|
|
189
|
+
tabs; on the picker tab it shows only while the sidebar asks for a key (a close, the Register confirmation or Add to
|
|
190
|
+
workspace, which the sidebar announces by ending its pane title with ` · › <verb>`, ` · > <verb>` under ASCII glyphs;
|
|
191
|
+
DM-LANES-SIDEBAR-TITLE), and then only `⏎ y Esc` (cockpit v3 S5) — `^C`, `n` and the arrows have no use in a prompt.
|
|
192
|
+
A name field (Rename, Register as) is never announced: there `y` is a letter of the name (DEF-1819). `refresh()` sets
|
|
193
|
+
`--lanes-quick` (48 px or 0), which the terminal height subtracts. `n` and `^C` send
|
|
167
194
|
only when `pointerup.timeStamp − pointerdown.timeStamp ≥ 400` (no `setTimeout`); `pointercancel` and `pointerleave`
|
|
168
195
|
disarm a key, and a `pointerup` without the key's own `pointerdown` sends nothing (before, a cancelled hold stayed
|
|
169
196
|
armed, so a pointer released over `^C` later sent it, DEF-1739). The buttons cancel `mousedown` so a tap never moves
|
|
@@ -183,26 +210,111 @@ assigning the server theme wholesale (`SetConfig` is sent on each config-file re
|
|
|
183
210
|
`!important` `--lanes-surface` rule for those two elements outranks it in either variant. An SRI bump of `app.js`
|
|
184
211
|
re-opens this contract: re-read `startWsControl`, `installSoftKeyboardCapture`, `initWebSockets` (`sendAnsiKey`) and
|
|
185
212
|
`installMenuDismissHook` before accepting it.
|
|
186
|
-
`index.html` carries one `theme-color` per colour scheme
|
|
187
|
-
`
|
|
213
|
+
`index.html` carries one `theme-color` per colour scheme, each that variant's `surface` token, and the manifest's
|
|
214
|
+
`background_color` and `theme_color` are the dark `surface`; they are hand-written, so `test_tokens.GeneratedSurfaces`
|
|
215
|
+
holds them to the table (cockpit v3 P5). The icon (`icon.svg`, `icon-192.png`, a raster with anti-aliased edges) is
|
|
216
|
+
the brand mark, drawn before the token table and not generated: palette v2 did not recolour it, and its bars keep
|
|
217
|
+
v1's dark accent. Bundle budget (spec §8, `test_web.test_bundle_growth`):
|
|
218
|
+
`mobile.css` ≤ 18 982 B and `mobile.js` ≤ 24 747 B; the rules sit one per line and the explanations live here rather
|
|
188
219
|
than in the shipped files. MEASURED 2026-10-04 with `wc -c` after LNS-31 (plan Task 31) met Tasks 13/28: `mobile.js`
|
|
189
|
-
21 964 B (23 268 B before its comments moved to Code notes below) and `mobile.css` 18 945 B.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
and `
|
|
220
|
+
21 964 B (23 268 B before its comments moved to Code notes below) and `mobile.css` 18 945 B. A laptop or tablet also
|
|
221
|
+
fetches `browser.js` and `browser.css` (1 947 B); `test_web_view` pins that download at `LAPTOP_TABLET_JS_RATCHET` and
|
|
222
|
+
`LAPTOP_TABLET_CSS_CAP` (DEF-1569).
|
|
223
|
+
|
|
224
|
+
The two lines that met at the cockpit v3 main sync (D2, LNS-99, 2026-10-07). On `wave/lanes-cockpit-v3`: after
|
|
225
|
+
cockpit v3 S5 (the filter's Esc, the prompt quick row) `mobile.js` 22 164 B, and `browser.js` 3 334 B (3 641 B before
|
|
226
|
+
its comments moved to Code notes below, to keep that download inside the ratchet, then pinned at 25 498 B); after S5
|
|
227
|
+
fix cycle 1 (the Esc owed to the picker it was typed on) `mobile.js` 22 162 B and the ratchet 25 496 B. W9 V3's
|
|
228
|
+
release row (+257 B of code; `mobile.js`'s line-1 pointer comment moved here first) measured 22 164 B on its own
|
|
229
|
+
track; the fan-in of the two tracks measured 22 362 B, over §8's 22 174 B, and paid for it inside `mobile.js` with
|
|
230
|
+
equivalent code — the four viewport properties through one setter, one `fill` style shared by the typing field's
|
|
231
|
+
host and capture, the Workspaces icon's four squares drawn from their corners: 22 153 B, the download 25 487 B. On
|
|
232
|
+
`bwtools/main`: the JS cap was spec §8's 22 174 B until phone shell input v2 (2026-10-07, DEF-1733): the working send
|
|
233
|
+
path, the text keyboard, paste, Copy and Paste, the dock, More and hint fixes grew `mobile.js` to 23 456 B MEASURED
|
|
234
|
+
after the dead `Write` path, the redundant focus/blur `is-typing` listeners and the file's header comment were
|
|
235
|
+
removed and the new code was written compactly (`kbd()`, `aria()`), so the cap rose by exactly 1 282 B to that size
|
|
236
|
+
(`mobile.css` unchanged); the independent review's fixes the same day (the filter's Esc-free frames and deltas,
|
|
237
|
+
replacement ranges, no paste on a picker, only the paste end marker neutralised; DEF-1751..DEF-1755) grew it to
|
|
238
|
+
24 258 B MEASURED, and the cap and the ratchet (27 899 B: 25 605 B plus the same 1 492 + 802 B) rose by exactly those
|
|
239
|
+
802 B again. The composed file — input v2's frames, deltas, Copy/Paste and dock, plus S5's per-picker Esc debt and
|
|
240
|
+
prompt quick row and W9 V3's release row, the branch's equivalent rewrites applied to the code both kept —
|
|
241
|
+
MEASURED `mobile.js` 24 612 B (+354 B over input v2) and the download 24 612 + 3 334 = 27 946 B: the phone cap rose by
|
|
242
|
+
those 354 B and the ratchet by 47 B (354 less the 307 B `browser.js` had already shed), each to the measured size
|
|
243
|
+
(`test_web_view.JS_COCKPIT_V3`, `LAPTOP_TABLET_JS_RATCHET`), and all four cap assertions red on one byte more.
|
|
244
|
+
|
|
245
|
+
The docs cockpit (plan W10 W7, LNS-108, 2026-10-08): **More** gains a **Docs** row, a link to `/lanes-docs/` (the docs
|
|
246
|
+
page, `lanes-docsd` behind Caddy's `/lanes-docs/*`) in a new tab, and the desktop bar a **Docs** link beside its two
|
|
247
|
+
pickers. The operator raised the phone cap by the measured amount, pinned exact: `mobile.js` MEASURED 24 747 B with
|
|
248
|
+
`wc -c`, +135 B (`test_web_view.JS_DOCS`; `menuRow()` takes a tag and an extra class, and the row borrows
|
|
249
|
+
`.lanes-session-link` for its 48 px touch target, so `mobile.css` stays 18 945 B). `browser.js` paid for its link
|
|
250
|
+
inside its own bytes — its line-1 pointer comment moved to Code notes below and the bar's Escape listener became an
|
|
251
|
+
`onkeydown` property — 3 334 -> 3 318 B MEASURED, so the laptop/tablet download is 24 747 + 3 318 = 28 065 B and the
|
|
252
|
+
DEF-1569 ratchet rose by the 135 `mobile.js` bytes net of those 16, to that measured size.
|
|
253
|
+
|
|
254
|
+
| File | Cap | MEASURED 2026-10-08 (`wc -c`) | Bound by |
|
|
255
|
+
| --- | --- | --- | --- |
|
|
256
|
+
| `mobile.js` | 24 747 B (exact) | 24 747 B | spec §8 phone row, `test_web.test_bundle_growth`, `test_web_view` `JS_DOCS` |
|
|
257
|
+
| `mobile.css` | 18 982 B | 18 945 B | spec §8 phone row |
|
|
258
|
+
| `browser.js` | 4 096 B | 3 318 B | `test_web_view.BROWSER_JS_CAP` |
|
|
259
|
+
| `browser.css` | 2 048 B | 1 947 B | `test_web_view.BROWSER_CSS_CAP` |
|
|
260
|
+
| `mobile.js` + `browser.js` | 28 065 B (exact) | 28 065 B | DEF-1569, `LAPTOP_TABLET_JS_RATCHET` |
|
|
261
|
+
| `mobile.css` + `browser.css` | 20 993 B | 20 892 B | DEF-1569's CSS twin, `LAPTOP_TABLET_CSS_CAP` |
|
|
262
|
+
|
|
263
|
+
## Docs page (plan W10 W6, LNS-107)
|
|
264
|
+
|
|
265
|
+
The docs cockpit's pages (Markdown, Mermaid, files; served by `lanes-docsd` behind `/lanes-docs/*`, plan W3) are their
|
|
266
|
+
own HTML documents with their own assets, outside the phone budget above: `docs.css` (cap 2 560 B, MEASURED 2 351 B)
|
|
267
|
+
and `docs.js` (cap 1 536 B, MEASURED 1 530 B), both from `/lanes-assets/` with `no-cache` like the shell's. A page links
|
|
268
|
+
`/lanes-assets/docs.css` and loads `<script src="/lanes-assets/docs.js" defer></script>`, and renders a ```` ```mermaid ````
|
|
269
|
+
fence as `<pre class="mermaid">` holding the escaped source (`libexec/mdlite.py`, plan W2).
|
|
270
|
+
|
|
271
|
+
- `docs.css` is phone first: a 16 px gutter (safe-area aware), a 46 rem measure, tables and code blocks that scroll
|
|
272
|
+
sideways instead of the page, `pre.mermaid` centred, system fonts only (the docs CSP allows no font source). Its
|
|
273
|
+
colours are the block between the `BEGIN`/`END lanes tokens` markers, generated by `libexec/tokens.py --docs-css`:
|
|
274
|
+
the six roles it paints (`DOCS_ROLES`) on `:root`, dark by default and light under `prefers-color-scheme` — not the
|
|
275
|
+
phone's `html.lanes-mobile` / `data-zj-theme` block. `tokens.py --check` covers it; no colour lies outside it.
|
|
276
|
+
- `docs.js` is a classic script that does nothing on a page without `pre.mermaid`. With one, it appends one
|
|
277
|
+
`<script>` for `/lanes-assets/vendor/mermaid-<version>.min.js` with `integrity="sha256-…"` (base64 of the raw
|
|
278
|
+
sha256 of the pinned member, `holders.lock`) and `crossorigin="anonymous"`; on load it calls
|
|
279
|
+
`mermaid.initialize({startOnLoad: false, securityLevel: 'strict', suppressErrorRendering: true, theme})` (`dark`
|
|
280
|
+
when the system scheme is dark, else `default`) and `mermaid.run({querySelector: 'pre.mermaid'})`. Mermaid empties a
|
|
281
|
+
fence before it draws, so each fence left without an `<svg>` gets its source text back (`textContent`, never markup;
|
|
282
|
+
DEF-1873). A missing file (`install/enable-docs.sh` not run yet: 404) or an integrity mismatch fires no load, and every
|
|
283
|
+
fence stays a `<pre>` with its source. It never evaluates strings, writes markup or names the unversioned link.
|
|
284
|
+
- The vendor route: `handle_path /lanes-assets/vendor/*` serves `$LANES_SHARE/vendor` (`install/services.py` puts
|
|
285
|
+
`LANES_SHARE` in `lanes-mobile.service`, as it does `LANES_REPO`) with `Cache-Control: public, max-age=31536000,
|
|
286
|
+
immutable` and no listing. Immutable is safe only because a page names the VERSIONED file: under the stable
|
|
287
|
+
`mermaid.min.js` link a browser would keep the old bytes after a pin bump and refuse them under the new integrity
|
|
288
|
+
(DEF-1870). The block sits first among the `handle` routes: Caddy sorts it ahead of `/lanes-assets/*` (longest path
|
|
289
|
+
first) only past routes that also have exactly one path (Caddy issue #5037), so after `@backend` it would never be
|
|
290
|
+
reached (DEF-1872). `test_docs_assets.VendorRoute` proves the order on a model of Caddy's sort; no Caddy runs in the
|
|
291
|
+
tests (DEF-1871).
|
|
292
|
+
- CSP needs, MEASURED 2026-10-08 in headless Chromium 153 with Mermaid 11.17.2: `script-src 'self'` suffices — no
|
|
293
|
+
`'unsafe-eval'` (a flowchart and a sequence diagram rendered with no violation; a control page's `eval` was refused
|
|
294
|
+
and reported) — but the SVG Mermaid draws carries inline `<style>` and `style` attributes, so `style-src` needs
|
|
295
|
+
`'unsafe-inline'` (without it: 88 violations, unstyled diagrams). The docs page's CSP (plan W3) has both.
|
|
296
|
+
- Tests: `tests/unit/test_docs_assets.py` (the pin, the enable script against a fake curl, the route model, the unit's
|
|
297
|
+
environment, both files' caps, tokens and SRI) and `tests/web/docs.test.mjs` (docs.js in a vm against a fake page and
|
|
298
|
+
a fake Mermaid that empties fences as the real one does). How to bump the pin: [runbook](../docs/RUNBOOK.md#mermaid-pin).
|
|
299
|
+
- The drop area (plan W10 W5, LNS-106): the index's upload form is a plain form that works without a script — a
|
|
300
|
+
`label.docs-drop` wrapping a multi-file input, so a tap opens the picker and a drop onto the input takes the files.
|
|
301
|
+
`upload.js` (cap 2 892 B, MEASURED 2 892 B, exact) and `upload.css` (cap 1 033 B, MEASURED 1 033 B, exact) make it
|
|
302
|
+
better: a drop anywhere on the area (`input.files` = the dropped files), a size check against the form's `data-max`
|
|
303
|
+
before anything is sent, an `XMLHttpRequest` `POST up` asking for JSON with a `<progress>`, then one line per receipt
|
|
304
|
+
— a link to `d/<id>` named with `textContent`, its size, sha256 and the in-container path it `landed` at — and a
|
|
305
|
+
refusal or a network error in words. They are their own files because `docs.js` had 6 B of its cap left and
|
|
306
|
+
`docs.css` 209 B; docsd serves them like `docs.css`/`docs.js`, and only the index (the page with the form) loads them.
|
|
307
|
+
The page CSP has `connect-src 'self'` for that request. The styles use `docs.css`'s token roles only (no colour of
|
|
308
|
+
their own), 44 px touch targets, phone first. Tests: `test_docs_assets.UploadAssets` (caps, classic script, no
|
|
309
|
+
markup, roles) and `tests/web/upload.test.mjs` (upload.js in a vm against a fake page and XHR).
|
|
199
310
|
|
|
200
311
|
## Code notes
|
|
201
312
|
|
|
202
313
|
`mobile.js` carries no explanations (the spec §8 budget above); each note is keyed to the code it explains, in file
|
|
203
314
|
order.
|
|
204
315
|
|
|
205
|
-
- The file: the Lanes browser shell (phone, tablet, desktop) over the pinned Zellij 0.45.1 client's window hooks.
|
|
316
|
+
- The file: the Lanes browser shell (phone, tablet, desktop) over the pinned Zellij 0.45.1 client's window hooks. Its
|
|
317
|
+
explanations are these notes (the file's line-1 pointer to them moved here too, W9 V3).
|
|
206
318
|
- `root.dataset.lanesShell = mode`: read by `browser.js` for the desktop bar and the tablet's full rendering.
|
|
207
319
|
- `if (mode !== 'phone')`: appends the laptop/tablet extras (`browser.css`, `browser.js`); a phone never fetches them.
|
|
208
320
|
- `if (mode === 'tablet')`: iPad Safari's user agent is a desktop one, so the shell asks for the client's mobile UI
|
|
@@ -236,12 +348,23 @@ order.
|
|
|
236
348
|
- `hint()`: holds the subtitle for eight ticks of the one interval (DEF-1739).
|
|
237
349
|
- `buildQuickRow()` (spec §6.5): `n` and `^C` send after a 400 ms hold, measured from event timestamps with no timer;
|
|
238
350
|
a cancelled or left key is disarmed and a release needs the key's own press (DEF-1739); nothing takes focus.
|
|
239
|
-
- `
|
|
240
|
-
|
|
351
|
+
- `clearLaneFilter()`: forgets the query and owes its picker (`filterTab`) the lone `Esc` that leaves the sidebar's
|
|
352
|
+
filter mode (`owed`, a set of picker tab names). `updateLaneFilterBar(tab, isPicker)`, on every refresh: once the
|
|
353
|
+
current tab is not the one the query was typed on, a filter tail still waiting in the queue is dropped and the query
|
|
354
|
+
cleared, sending nothing (the lane may have the focus now, DEF-1752); the lone `Esc` and its pause are sent only when
|
|
355
|
+
the current tab is an owed picker; then `filterTab` becomes the current tab if it is a picker. Escape in the bar and
|
|
356
|
+
an emptied field clear and call `refresh()`, so the `Esc` goes out at once while that picker is still current.
|
|
241
357
|
- `lanesTheme()`: the shell's own terminal palette for both variants (spec §6.3, DEF-1413).
|
|
242
358
|
- `refresh()`: leaves the subtitle to a live hint, hides **Panes** (and **Paste**, DEF-1754) on a picker tab so the
|
|
243
359
|
dock's four columns hold its four items (DEF-1737), and flushes the `send()` queue after `updateLaneFilterBar()`, so a
|
|
244
|
-
filter tail is dropped before it could reach a lane
|
|
360
|
+
filter tail is dropped before it could reach a lane; `quick` is `true` on a view tab (every key), `2` on the picker
|
|
361
|
+
while its title ends ` · › <verb>` (only `⏎ y Esc`), else none.
|
|
362
|
+
- `menuRow()` / `installFontControls()`: a row of the client's **More** menu, prepended (Text size, then **Docs** above
|
|
363
|
+
it, then the release at the top once `/enrol/site` answers). The one `fetch` is there because `build()` runs once per
|
|
364
|
+
page load. `menuRow(label, tag, more)`: the **Docs** row is an `<a>` (plan W10 W7) to `/lanes-docs/` with
|
|
365
|
+
`target="_blank"` — a new tab, so the terminal and its socket stay where they are, and a Home Screen app, which has
|
|
366
|
+
no back button, is never navigated away — carrying `lanes-session-link` as well, the existing rule that gives it a
|
|
367
|
+
48 px touch target with no CSS of its own.
|
|
245
368
|
- `connect()`: waits for server state, because an earlier resize throws `InvalidStateError` in 0.45.1. `applyTheme` is
|
|
246
369
|
re-applied after every `SetConfig` (the client calls the chained `__zjSyncInactiveCursorStyle` hook) and on the theme
|
|
247
370
|
toggle.
|
|
@@ -251,8 +374,22 @@ order.
|
|
|
251
374
|
window hooks just after appending its mobile root; `hint()` and a paused `send()` re-arm it; each tick flushes the
|
|
252
375
|
queue, and it clears itself once attached, with no hint up and no pause waiting.
|
|
253
376
|
|
|
377
|
+
`browser.js` (laptop/tablet extras) carries none either, since cockpit v3 S5:
|
|
378
|
+
|
|
379
|
+
- The file: the Lanes laptop/tablet extras (its line-1 pointer to these notes moved here in plan W10 W7, to pay for the
|
|
380
|
+
bar's Docs link).
|
|
381
|
+
|
|
382
|
+
- `laneFullRender()`: the client starts a tablet in single pane on every load; once, it sends the client's own
|
|
383
|
+
switch-to-desktop preference.
|
|
384
|
+
- `desktop()`: the chip toggles an overlay bar (no refit); a pick sends the phone picker's `FocusPane`. The chip
|
|
385
|
+
cancels `mousedown`, so the terminal keeps the keys. The bar ends in a **Docs** link to `/lanes-docs/` in a new tab
|
|
386
|
+
(plan W10 W7); Escape in the bar gives the terminal its keys back (`onkeydown`, one handler, as the listener was).
|
|
387
|
+
- `ui()`: `window.__zjMobileUi` is set synchronously after the client appends its mobile root, which the
|
|
388
|
+
`MutationObserver` on `<body>` waits for.
|
|
389
|
+
|
|
254
390
|
Validate on Safari/WebKit at 440 × 763 (iPhone 17 Pro Max with browser chrome), 440 × 956 (Home Screen),
|
|
255
391
|
838 × 390 (landscape Safari), and smaller phone widths. Check login, workspace search and switching,
|
|
256
|
-
lane selection, pane navigation, font controls
|
|
257
|
-
input checklist in the [runbook](../docs/RUNBOOK.md) (text keyboard, quick keys,
|
|
392
|
+
lane selection, pane navigation, font controls and the release row in **More**, rotation and the visible viewport
|
|
393
|
+
while the keyboard opens, and the input checklist in the [runbook](../docs/RUNBOOK.md) (text keyboard, quick keys,
|
|
394
|
+
paste, Copy and Paste, the picker's filter and prompt row).
|
|
258
395
|
Use disposable view sessions and revoke any temporary test token afterward.
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* LNS-31 laptop/tablet extras; see docs/ARCHITECTURE.md (Web and phone). */
|
|
2
1
|
function laneBarModel(d) {
|
|
3
2
|
const panes = (d?.panes || []).filter(p => !p.is_plugin), at = d?.active_pane || {}, ws = new Map();
|
|
4
3
|
const first = t => t && panes.find(p => p.tab_position === t.position);
|
|
@@ -14,7 +13,6 @@ function laneBarModel(d) {
|
|
|
14
13
|
.map(p => item(p.title || `Pane ${p.pane_id}`, p))].filter(Boolean),
|
|
15
14
|
};
|
|
16
15
|
}
|
|
17
|
-
// Tablet: upstream starts single pane on every load; once, send its own switch-to-desktop prefs.
|
|
18
16
|
function laneFullRender(api, send) {
|
|
19
17
|
let done;
|
|
20
18
|
return () => {
|
|
@@ -41,14 +39,13 @@ if (typeof module === 'object') module.exports = {laneBarModel, laneFullRender};
|
|
|
41
39
|
const full = laneFullRender(api, send);
|
|
42
40
|
return () => full() && dispatchEvent(new Event('zellij:rendering-resize'));
|
|
43
41
|
};
|
|
44
|
-
// Desktop: the chip toggles an overlay bar (no refit); picks send the phone picker's FocusPane.
|
|
45
42
|
const desktop = api => {
|
|
46
43
|
let last;
|
|
47
44
|
const picks = ['Workspace', 'Lane'].map(title => make('select', {title, onchange() {
|
|
48
45
|
send({type: 'FocusPane', pane_id: +this.value, is_plugin: false});
|
|
49
46
|
focus();
|
|
50
47
|
}}));
|
|
51
|
-
const bar = make('nav', {className: 'lanes-bar', hidden: true});
|
|
48
|
+
const bar = make('nav', {className: 'lanes-bar', hidden: true, onkeydown: ev => ev.key === 'Escape' && focus()});
|
|
52
49
|
const render = () => {
|
|
53
50
|
const m = !bar.hidden && laneBarModel(api.getState()?.data), sig = JSON.stringify(m);
|
|
54
51
|
if (!m || sig === last) return;
|
|
@@ -61,13 +58,12 @@ if (typeof module === 'object') module.exports = {laneBarModel, laneFullRender};
|
|
|
61
58
|
render();
|
|
62
59
|
window.term?.focus();
|
|
63
60
|
}});
|
|
64
|
-
chip.addEventListener('mousedown', ev => ev.preventDefault());
|
|
65
|
-
bar.
|
|
66
|
-
bar.append(...picks);
|
|
61
|
+
chip.addEventListener('mousedown', ev => ev.preventDefault());
|
|
62
|
+
bar.append(...picks, make('a', {href: '/lanes-docs/', target: '_blank', textContent: 'Docs'}));
|
|
67
63
|
document.body.append(bar, chip);
|
|
68
64
|
return render;
|
|
69
65
|
};
|
|
70
|
-
const ui = () => window.__zjMobileUi;
|
|
66
|
+
const ui = () => window.__zjMobileUi;
|
|
71
67
|
if (ui()) return ready(ui());
|
|
72
68
|
new MutationObserver((_, o) => ui() && (o.disconnect(), ready(ui()))).observe(document.body, {childList: true});
|
|
73
69
|
})();
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/* Lanes docs page (plan W10 W6, LNS-107), phone first. Colours: the generated block below; system fonts only.
|
|
2
|
+
Notes: web/README.md "Docs page". */
|
|
3
|
+
/* BEGIN lanes tokens (generated by libexec/tokens.py --docs-css; do not edit by hand) */
|
|
4
|
+
:root {
|
|
5
|
+
--lanes-surface: #101318;
|
|
6
|
+
--lanes-surface-raised: #191e25;
|
|
7
|
+
--lanes-line: #2c333d;
|
|
8
|
+
--lanes-ink: #f2f5f8;
|
|
9
|
+
--lanes-ink-dim: #a6b1be;
|
|
10
|
+
--lanes-accent: #8fe3a6;
|
|
11
|
+
color-scheme: dark;
|
|
12
|
+
}
|
|
13
|
+
@media (prefers-color-scheme: light) {
|
|
14
|
+
:root {
|
|
15
|
+
--lanes-surface: #f3f5f2;
|
|
16
|
+
--lanes-surface-raised: #ffffff;
|
|
17
|
+
--lanes-line: #d5ded6;
|
|
18
|
+
--lanes-ink: #111a14;
|
|
19
|
+
--lanes-ink-dim: #4a5661;
|
|
20
|
+
--lanes-accent: #1f6f3f;
|
|
21
|
+
color-scheme: light;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/* END lanes tokens */
|
|
25
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
26
|
+
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
|
|
27
|
+
body {
|
|
28
|
+
max-width: 46rem; margin: 0 auto;
|
|
29
|
+
padding: 16px max(16px, env(safe-area-inset-right)) 32px max(16px, env(safe-area-inset-left));
|
|
30
|
+
background: var(--lanes-surface); color: var(--lanes-ink);
|
|
31
|
+
font: 16px/1.55 system-ui, -apple-system, "Segoe UI", sans-serif; overflow-wrap: break-word;
|
|
32
|
+
}
|
|
33
|
+
h1, h2, h3 { line-height: 1.25; margin: 1.4em 0 .5em; }
|
|
34
|
+
h1 { font-size: 1.6em; margin-top: .4em; }
|
|
35
|
+
h2 { font-size: 1.3em; padding-bottom: .2em; border-bottom: 1px solid var(--lanes-line); }
|
|
36
|
+
a { color: var(--lanes-accent); }
|
|
37
|
+
small { color: var(--lanes-ink-dim); }
|
|
38
|
+
img { max-width: 100%; height: auto; }
|
|
39
|
+
hr { border: 0; border-top: 1px solid var(--lanes-line); }
|
|
40
|
+
blockquote { margin: 1em 0; padding-left: 12px; border-left: 3px solid var(--lanes-line); color: var(--lanes-ink-dim); }
|
|
41
|
+
code, pre { font: .875em/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
42
|
+
code { padding: .1em .3em; border-radius: 4px; background: var(--lanes-surface-raised); }
|
|
43
|
+
pre {
|
|
44
|
+
margin: 1em 0; padding: 12px; overflow-x: auto; tab-size: 4;
|
|
45
|
+
background: var(--lanes-surface-raised); border: 1px solid var(--lanes-line); border-radius: 8px;
|
|
46
|
+
}
|
|
47
|
+
pre code { padding: 0; font-size: 1em; background: none; }
|
|
48
|
+
pre.mermaid { text-align: center; }
|
|
49
|
+
pre.mermaid svg { max-width: 100%; height: auto; }
|
|
50
|
+
table { display: block; max-width: 100%; overflow-x: auto; border-collapse: collapse; margin: 1em 0; }
|
|
51
|
+
th, td { padding: 6px 10px; border: 1px solid var(--lanes-line); text-align: left; }
|
|
52
|
+
th { background: var(--lanes-surface-raised); }
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Lanes docs page (plan W10 W6, LNS-107): render ```mermaid fences with the pinned Mermaid, only on a page that has
|
|
2
|
+
// one: its versioned file (DEF-1870), its sha256 as SRI. Without it every fence keeps its source. web/README.md.
|
|
3
|
+
(function () {
|
|
4
|
+
'use strict';
|
|
5
|
+
var SRC = '/lanes-assets/vendor/mermaid-11.17.2.min.js';
|
|
6
|
+
var SRI = 'sha256-WB7X10vZBI0OOpE2OSfXLvIpQtdyJUayf3zCnjU5Drg=';
|
|
7
|
+
|
|
8
|
+
function render() {
|
|
9
|
+
var pres = document.querySelectorAll('pre.mermaid'), text = [], i;
|
|
10
|
+
for (i = 0; i < pres.length; i++) text[i] = pres[i].textContent;
|
|
11
|
+
// Mermaid empties each fence first: one left without a diagram gets its source back (DEF-1873).
|
|
12
|
+
function restore() {
|
|
13
|
+
for (i = 0; i < pres.length; i++) if (!pres[i].querySelector('svg')) pres[i].textContent = text[i];
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
mermaid.initialize({startOnLoad: false, securityLevel: 'strict', suppressErrorRendering: true,
|
|
17
|
+
theme: matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'default'});
|
|
18
|
+
mermaid.run({querySelector: 'pre.mermaid'}).then(restore, restore);
|
|
19
|
+
} catch (e) {
|
|
20
|
+
restore();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function start() {
|
|
25
|
+
if (!document.querySelector('pre.mermaid')) return;
|
|
26
|
+
var s = document.createElement('script');
|
|
27
|
+
s.setAttribute('integrity', SRI);
|
|
28
|
+
s.setAttribute('crossorigin', 'anonymous');
|
|
29
|
+
s.setAttribute('src', SRC);
|
|
30
|
+
s.onload = render;
|
|
31
|
+
document.head.appendChild(s);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start);
|
|
35
|
+
else start();
|
|
36
|
+
}());
|