@vibelord/lanes 1.1.1 → 1.2.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 +45 -22
- package/payload/lanes/VERSION +1 -1
- package/payload/lanes/bin/lanes +2459 -164
- 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 +106 -92
- package/payload/lanes/container/lane-edit +100 -0
- package/payload/lanes/container/lane-mcp +1091 -0
- package/payload/lanes/container/lane-signal +109 -29
- package/payload/lanes/container/lane_core.py +1140 -0
- package/payload/lanes/container/vimrc +31 -0
- package/payload/lanes/docs/ARCHITECTURE.md +618 -13
- package/payload/lanes/docs/DESIGN.md +122 -44
- package/payload/lanes/docs/INSTALL.md +157 -17
- package/payload/lanes/docs/RUNBOOK.md +603 -72
- package/payload/lanes/docs/SHARING.md +3 -2
- package/payload/lanes/docs/USERGUIDE.md +777 -136
- package/payload/lanes/install/enable-web.sh +2 -0
- package/payload/lanes/install/host-install.sh +163 -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/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/members.py +16 -2
- package/payload/lanes/libexec/notify.py +104 -24
- 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 +2135 -361
- package/payload/lanes/libexec/statusd.py +137 -10
- package/payload/lanes/libexec/studio_host.py +8 -1
- package/payload/lanes/libexec/tokens.py +121 -30
- package/payload/lanes/libexec/tokens.sh +86 -2
- package/payload/lanes/libexec/update.py +972 -0
- package/payload/lanes/libexec/users.py +614 -0
- package/payload/lanes/libexec/view.sh +34 -6
- 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/README.md +83 -24
- package/payload/lanes/web/browser.js +3 -5
- package/payload/lanes/web/mobile.css +17 -17
- package/payload/lanes/web/mobile.js +44 -32
- package/payload/lanes/workspaces.example/README.md +3 -1
- package/payload/lanes/zellij/config.kdl +106 -56
- package/payload/manifest.json +5 -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
|
}
|
|
@@ -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
|
|
@@ -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
|
|
|
@@ -50,7 +52,9 @@ Run `lanes web <device> --token` on the server (it also adds that Tailscale Serv
|
|
|
50
52
|
`lanes-launch web phone|laptop` from an installed client, or `sh install/enable-phone.sh --host lanes` (`--local` on
|
|
51
53
|
the server).
|
|
52
54
|
The [installation guide](../docs/INSTALL.md) covers Caddy, Tailscale operator permission and the selected SSH key.
|
|
53
|
-
Service registration supplies `LANES_REPO`
|
|
55
|
+
Service registration supplies `LANES_REPO` — the `~/.local/share/lanes/current` link, which names the installed
|
|
56
|
+
tree (W9 U1) — so Caddy serves that tree from any installation path, and the files an update brings as soon as
|
|
57
|
+
the link moves; the routes of a new Caddyfile take a restart of `lanes-mobile` (the server installer does it).
|
|
54
58
|
|
|
55
59
|
`index.html` keeps the upstream asset integrity hashes. When upgrading Zellij, compare the HTML and
|
|
56
60
|
window hooks against the new version before changing the pinned binary. The mobile JS and CSS have
|
|
@@ -90,9 +94,16 @@ character of the old query after its common prefix with the new one, then the ne
|
|
|
90
94
|
prefix; an emptied field or the Esc key sends a lone `Esc`. The text is the `TextEncoder` bytes with control bytes and
|
|
91
95
|
DEL dropped, decoded back to text. There is no `setTimeout`: `send()` keeps a pause marker in its queue after a lone
|
|
92
96
|
`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
|
-
|
|
97
|
+
last query it sent (`laneFilterPrev`) and the picker it was typed on (`filterTab`, cockpit v3 S5); once a refresh
|
|
98
|
+
finds another tab current — a lane, or another workspace's picker by the Workspaces sheet — it forgets the query,
|
|
99
|
+
empties the field and drops a filter tail still waiting in the queue, without sending anything: the lane now has the
|
|
100
|
+
focus, and an `Esc` there can interrupt an agent (DEF-1752 here, DEF-1817 on S5's line; the sidebar clears its own
|
|
101
|
+
filter when it opens a lane), and another workspace's picker starts with an empty bar instead of the first one's
|
|
102
|
+
query, whose next letter the deltas would have sent there as a bare key (DEF-1820). The lone `Esc` of an emptied
|
|
103
|
+
field or of the Esc key, and the one a picker left filtered is owed, go through one debt (`owed`, picker tab names):
|
|
104
|
+
`updateLaneFilterBar` sends it only while the picker it is owed to is the current tab, so a picker left filtered shows
|
|
105
|
+
its whole list again on its next visit, and an Escape that arrives once the client has already moved on is held,
|
|
106
|
+
never sent into the lane or another picker. Limits: the deltas assume the
|
|
96
107
|
sidebar holds what the phone sent, which each new query's leading `Esc` restores — another client typing into the same
|
|
97
108
|
picker pane, or a character the sidebar does not print (a no-break space, a zero-width joiner), puts them out of step
|
|
98
109
|
until the field is cleared; and an `Esc` while the sidebar's upload strip is armed (`U`) returns the focus to the
|
|
@@ -155,6 +166,14 @@ on those facts (`node --test`, no jsdom).
|
|
|
155
166
|
Attention counts come from the picker sidebar's pane title (`WS · ◆1 ◇2 ▸6`, written by the sidebar as OSC 2):
|
|
156
167
|
`parseCounts` (also `window.__lanesParseCounts`) feeds the picker subtitle, `document.title`, the Lanes button's
|
|
157
168
|
`data-count` and the workspace cards, inside the existing `refresh()` / `renderCards()` — no fetch, file or timer.
|
|
169
|
+
The shell makes one request (W9 V3, LNS-85): once per page load, when it builds, `installFontControls()` asks
|
|
170
|
+
`/enrol/site` on the same origin (the `/enrol/*` route above; the enrolment service's `GET /site` answers
|
|
171
|
+
`{alias, host_name, host_key, version, request_mode, caller, source}`) and shows its `version` as the first row of
|
|
172
|
+
the client's **More** menu, **Lanes v1.2.0**, above **Text size** — the same `.lanes-font-controls` row, so no CSS.
|
|
173
|
+
Only a release is shown (`^\d[\w.+-]{0,31}$`, as text): `dev`, anything else, an error answer, a non-JSON 502 while
|
|
174
|
+
the service is down, or no network shows nothing and throws nothing. The version is the enrolment service's tree
|
|
175
|
+
(the server installer restarts it on every install). `tests/web/version.test.mjs` boots `mobile.js` against a fake
|
|
176
|
+
logged-in page to pin this; `test_web_view` pins one fetch, inside `installFontControls()`, which `build()` runs once.
|
|
158
177
|
Until plan Task 26 (2026-10-04) this reader was inert (DEF-1531): `bin/lanes` `mobile_workspace_tab` gave the
|
|
159
178
|
picker pane a fixed `name="Lanes"`, and Zellij reports a pane's layout name instead of its OSC 2 title, so
|
|
160
179
|
`pickerTitle()` read `Lanes`, `parseCounts` returned null and the shell showed no counts ("Choose a lane to continue").
|
|
@@ -163,7 +182,11 @@ phone device keeps the old picker pane until `lanes rebuild <device>` (`upgrade-
|
|
|
163
182
|
`lanes doctor` names such a device). Whether the OSC 2 title now reaches
|
|
164
183
|
`panes[].title` is still unchecked in a browser (no WebKit pass has run; fallback: the counts stay in the
|
|
165
184
|
sidebar header). The quick-key row (`⏎ y n Esc ^C ↑ ↓`, `.lanes-quick`) shows on view
|
|
166
|
-
tabs
|
|
185
|
+
tabs; on the picker tab it shows only while the sidebar asks for a key (a close, the Register confirmation or Add to
|
|
186
|
+
workspace, which the sidebar announces by ending its pane title with ` · › <verb>`, ` · > <verb>` under ASCII glyphs;
|
|
187
|
+
DM-LANES-SIDEBAR-TITLE), and then only `⏎ y Esc` (cockpit v3 S5) — `^C`, `n` and the arrows have no use in a prompt.
|
|
188
|
+
A name field (Rename, Register as) is never announced: there `y` is a letter of the name (DEF-1819). `refresh()` sets
|
|
189
|
+
`--lanes-quick` (48 px or 0), which the terminal height subtracts. `n` and `^C` send
|
|
167
190
|
only when `pointerup.timeStamp − pointerdown.timeStamp ≥ 400` (no `setTimeout`); `pointercancel` and `pointerleave`
|
|
168
191
|
disarm a key, and a `pointerup` without the key's own `pointerdown` sends nothing (before, a cancelled hold stayed
|
|
169
192
|
armed, so a pointer released over `^C` later sent it, DEF-1739). The buttons cancel `mousedown` so a tap never moves
|
|
@@ -183,26 +206,45 @@ assigning the server theme wholesale (`SetConfig` is sent on each config-file re
|
|
|
183
206
|
`!important` `--lanes-surface` rule for those two elements outranks it in either variant. An SRI bump of `app.js`
|
|
184
207
|
re-opens this contract: re-read `startWsControl`, `installSoftKeyboardCapture`, `initWebSockets` (`sendAnsiKey`) and
|
|
185
208
|
`installMenuDismissHook` before accepting it.
|
|
186
|
-
`index.html` carries one `theme-color` per colour scheme
|
|
187
|
-
`
|
|
209
|
+
`index.html` carries one `theme-color` per colour scheme, each that variant's `surface` token, and the manifest's
|
|
210
|
+
`background_color` and `theme_color` are the dark `surface`; they are hand-written, so `test_tokens.GeneratedSurfaces`
|
|
211
|
+
holds them to the table (cockpit v3 P5). The icon (`icon.svg`, `icon-192.png`, a raster with anti-aliased edges) is
|
|
212
|
+
the brand mark, drawn before the token table and not generated: palette v2 did not recolour it, and its bars keep
|
|
213
|
+
v1's dark accent. Bundle budget (spec §8, `test_web.test_bundle_growth`):
|
|
214
|
+
`mobile.css` ≤ 18 982 B and `mobile.js` ≤ 24 612 B; the rules sit one per line and the explanations live here rather
|
|
188
215
|
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 `
|
|
216
|
+
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
|
|
217
|
+
fetches `browser.js` and `browser.css` (1 947 B); `test_web_view` pins that download at `LAPTOP_TABLET_JS_RATCHET` and
|
|
218
|
+
`LAPTOP_TABLET_CSS_CAP` (DEF-1569).
|
|
219
|
+
|
|
220
|
+
The two lines that met at the cockpit v3 main sync (D2, LNS-99, 2026-10-07). On `wave/lanes-cockpit-v3`: after
|
|
221
|
+
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
|
|
222
|
+
its comments moved to Code notes below, to keep that download inside the ratchet, then pinned at 25 498 B); after S5
|
|
223
|
+
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
|
|
224
|
+
release row (+257 B of code; `mobile.js`'s line-1 pointer comment moved here first) measured 22 164 B on its own
|
|
225
|
+
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
|
|
226
|
+
equivalent code — the four viewport properties through one setter, one `fill` style shared by the typing field's
|
|
227
|
+
host and capture, the Workspaces icon's four squares drawn from their corners: 22 153 B, the download 25 487 B. On
|
|
228
|
+
`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
|
|
229
|
+
path, the text keyboard, paste, Copy and Paste, the dock, More and hint fixes grew `mobile.js` to 23 456 B MEASURED
|
|
230
|
+
after the dead `Write` path, the redundant focus/blur `is-typing` listeners and the file's header comment were
|
|
231
|
+
removed and the new code was written compactly (`kbd()`, `aria()`), so the cap rose by exactly 1 282 B to that size
|
|
232
|
+
(`mobile.css` unchanged); the independent review's fixes the same day (the filter's Esc-free frames and deltas,
|
|
233
|
+
replacement ranges, no paste on a picker, only the paste end marker neutralised; DEF-1751..DEF-1755) grew it to
|
|
234
|
+
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
|
|
235
|
+
802 B again. The composed file — input v2's frames, deltas, Copy/Paste and dock, plus S5's per-picker Esc debt and
|
|
236
|
+
prompt quick row and W9 V3's release row, the branch's equivalent rewrites applied to the code both kept —
|
|
237
|
+
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
|
|
238
|
+
those 354 B and the ratchet by 47 B (354 less the 307 B `browser.js` had already shed), each to the measured size
|
|
239
|
+
(`test_web_view.JS_COCKPIT_V3`, `LAPTOP_TABLET_JS_RATCHET`), and all four cap assertions red on one byte more.
|
|
199
240
|
|
|
200
241
|
## Code notes
|
|
201
242
|
|
|
202
243
|
`mobile.js` carries no explanations (the spec §8 budget above); each note is keyed to the code it explains, in file
|
|
203
244
|
order.
|
|
204
245
|
|
|
205
|
-
- The file: the Lanes browser shell (phone, tablet, desktop) over the pinned Zellij 0.45.1 client's window hooks.
|
|
246
|
+
- The file: the Lanes browser shell (phone, tablet, desktop) over the pinned Zellij 0.45.1 client's window hooks. Its
|
|
247
|
+
explanations are these notes (the file's line-1 pointer to them moved here too, W9 V3).
|
|
206
248
|
- `root.dataset.lanesShell = mode`: read by `browser.js` for the desktop bar and the tablet's full rendering.
|
|
207
249
|
- `if (mode !== 'phone')`: appends the laptop/tablet extras (`browser.css`, `browser.js`); a phone never fetches them.
|
|
208
250
|
- `if (mode === 'tablet')`: iPad Safari's user agent is a desktop one, so the shell asks for the client's mobile UI
|
|
@@ -236,12 +278,19 @@ order.
|
|
|
236
278
|
- `hint()`: holds the subtitle for eight ticks of the one interval (DEF-1739).
|
|
237
279
|
- `buildQuickRow()` (spec §6.5): `n` and `^C` send after a 400 ms hold, measured from event timestamps with no timer;
|
|
238
280
|
a cancelled or left key is disarmed and a release needs the key's own press (DEF-1739); nothing takes focus.
|
|
239
|
-
- `
|
|
240
|
-
|
|
281
|
+
- `clearLaneFilter()`: forgets the query and owes its picker (`filterTab`) the lone `Esc` that leaves the sidebar's
|
|
282
|
+
filter mode (`owed`, a set of picker tab names). `updateLaneFilterBar(tab, isPicker)`, on every refresh: once the
|
|
283
|
+
current tab is not the one the query was typed on, a filter tail still waiting in the queue is dropped and the query
|
|
284
|
+
cleared, sending nothing (the lane may have the focus now, DEF-1752); the lone `Esc` and its pause are sent only when
|
|
285
|
+
the current tab is an owed picker; then `filterTab` becomes the current tab if it is a picker. Escape in the bar and
|
|
286
|
+
an emptied field clear and call `refresh()`, so the `Esc` goes out at once while that picker is still current.
|
|
241
287
|
- `lanesTheme()`: the shell's own terminal palette for both variants (spec §6.3, DEF-1413).
|
|
242
288
|
- `refresh()`: leaves the subtitle to a live hint, hides **Panes** (and **Paste**, DEF-1754) on a picker tab so the
|
|
243
289
|
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
|
|
290
|
+
filter tail is dropped before it could reach a lane; `quick` is `true` on a view tab (every key), `2` on the picker
|
|
291
|
+
while its title ends ` · › <verb>` (only `⏎ y Esc`), else none.
|
|
292
|
+
- `menuRow()` / `installFontControls()`: a row of the client's **More** menu, prepended (Text size, then the release
|
|
293
|
+
above it once `/enrol/site` answers). The one `fetch` is there because `build()` runs once per page load.
|
|
245
294
|
- `connect()`: waits for server state, because an earlier resize throws `InvalidStateError` in 0.45.1. `applyTheme` is
|
|
246
295
|
re-applied after every `SetConfig` (the client calls the chained `__zjSyncInactiveCursorStyle` hook) and on the theme
|
|
247
296
|
toggle.
|
|
@@ -251,8 +300,18 @@ order.
|
|
|
251
300
|
window hooks just after appending its mobile root; `hint()` and a paused `send()` re-arm it; each tick flushes the
|
|
252
301
|
queue, and it clears itself once attached, with no hint up and no pause waiting.
|
|
253
302
|
|
|
303
|
+
`browser.js` (laptop/tablet extras) carries none either, since cockpit v3 S5:
|
|
304
|
+
|
|
305
|
+
- `laneFullRender()`: the client starts a tablet in single pane on every load; once, it sends the client's own
|
|
306
|
+
switch-to-desktop preference.
|
|
307
|
+
- `desktop()`: the chip toggles an overlay bar (no refit); a pick sends the phone picker's `FocusPane`. The chip
|
|
308
|
+
cancels `mousedown`, so the terminal keeps the keys.
|
|
309
|
+
- `ui()`: `window.__zjMobileUi` is set synchronously after the client appends its mobile root, which the
|
|
310
|
+
`MutationObserver` on `<body>` waits for.
|
|
311
|
+
|
|
254
312
|
Validate on Safari/WebKit at 440 × 763 (iPhone 17 Pro Max with browser chrome), 440 × 956 (Home Screen),
|
|
255
313
|
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,
|
|
314
|
+
lane selection, pane navigation, font controls and the release row in **More**, rotation and the visible viewport
|
|
315
|
+
while the keyboard opens, and the input checklist in the [runbook](../docs/RUNBOOK.md) (text keyboard, quick keys,
|
|
316
|
+
paste, Copy and Paste, the picker's filter and prompt row).
|
|
258
317
|
Use disposable view sessions and revoke any temporary test token afterward.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*
|
|
1
|
+
/* Lanes laptop/tablet extras; see web/README.md, Code notes */
|
|
2
2
|
function laneBarModel(d) {
|
|
3
3
|
const panes = (d?.panes || []).filter(p => !p.is_plugin), at = d?.active_pane || {}, ws = new Map();
|
|
4
4
|
const first = t => t && panes.find(p => p.tab_position === t.position);
|
|
@@ -14,7 +14,6 @@ function laneBarModel(d) {
|
|
|
14
14
|
.map(p => item(p.title || `Pane ${p.pane_id}`, p))].filter(Boolean),
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
|
-
// Tablet: upstream starts single pane on every load; once, send its own switch-to-desktop prefs.
|
|
18
17
|
function laneFullRender(api, send) {
|
|
19
18
|
let done;
|
|
20
19
|
return () => {
|
|
@@ -41,7 +40,6 @@ if (typeof module === 'object') module.exports = {laneBarModel, laneFullRender};
|
|
|
41
40
|
const full = laneFullRender(api, send);
|
|
42
41
|
return () => full() && dispatchEvent(new Event('zellij:rendering-resize'));
|
|
43
42
|
};
|
|
44
|
-
// Desktop: the chip toggles an overlay bar (no refit); picks send the phone picker's FocusPane.
|
|
45
43
|
const desktop = api => {
|
|
46
44
|
let last;
|
|
47
45
|
const picks = ['Workspace', 'Lane'].map(title => make('select', {title, onchange() {
|
|
@@ -61,13 +59,13 @@ if (typeof module === 'object') module.exports = {laneBarModel, laneFullRender};
|
|
|
61
59
|
render();
|
|
62
60
|
window.term?.focus();
|
|
63
61
|
}});
|
|
64
|
-
chip.addEventListener('mousedown', ev => ev.preventDefault());
|
|
62
|
+
chip.addEventListener('mousedown', ev => ev.preventDefault());
|
|
65
63
|
bar.addEventListener('keydown', ev => ev.key === 'Escape' && focus());
|
|
66
64
|
bar.append(...picks);
|
|
67
65
|
document.body.append(bar, chip);
|
|
68
66
|
return render;
|
|
69
67
|
};
|
|
70
|
-
const ui = () => window.__zjMobileUi;
|
|
68
|
+
const ui = () => window.__zjMobileUi;
|
|
71
69
|
if (ui()) return ready(ui());
|
|
72
70
|
new MutationObserver((_, o) => ui() && (o.disconnect(), ready(ui()))).observe(document.body, {childList: true});
|
|
73
71
|
})();
|
|
@@ -3,20 +3,20 @@ html.lanes-mobile {
|
|
|
3
3
|
--lanes-surface: #101318;
|
|
4
4
|
--lanes-surface-raised: #191e25;
|
|
5
5
|
--lanes-line: #2c333d;
|
|
6
|
-
--lanes-accent-wash: #
|
|
6
|
+
--lanes-accent-wash: #1c3326;
|
|
7
7
|
--lanes-accent-line: #4b6955;
|
|
8
8
|
--lanes-press: #26332c;
|
|
9
9
|
--lanes-chip: #ffffff0f;
|
|
10
|
-
--lanes-ink: #
|
|
11
|
-
--lanes-ink-dim: #
|
|
12
|
-
--lanes-ink-faint: #
|
|
13
|
-
--lanes-accent: #
|
|
10
|
+
--lanes-ink: #f2f5f8;
|
|
11
|
+
--lanes-ink-dim: #a6b1be;
|
|
12
|
+
--lanes-ink-faint: #6f7a87;
|
|
13
|
+
--lanes-accent: #8fe3a6;
|
|
14
14
|
--lanes-on-accent: #101318;
|
|
15
15
|
--lanes-on-accent-urgent: #7a1a12;
|
|
16
|
-
--lanes-tone-urgent: #
|
|
17
|
-
--lanes-tone-attention: #
|
|
18
|
-
--lanes-tone-working: #
|
|
19
|
-
--lanes-tone-done: #
|
|
16
|
+
--lanes-tone-urgent: #ff7b6e;
|
|
17
|
+
--lanes-tone-attention: #ffc233;
|
|
18
|
+
--lanes-tone-working: #6fb6ff;
|
|
19
|
+
--lanes-tone-done: #5fdc8a;
|
|
20
20
|
--lanes-scrim: #000000a6;
|
|
21
21
|
--lanes-shadow: 0 -16px 80px #0007;
|
|
22
22
|
color-scheme: dark;
|
|
@@ -29,16 +29,16 @@ html.lanes-mobile:root[data-zj-theme="light"] {
|
|
|
29
29
|
--lanes-accent-line: #acc9b4;
|
|
30
30
|
--lanes-press: #d8e9db;
|
|
31
31
|
--lanes-chip: #edf3ec;
|
|
32
|
-
--lanes-ink: #
|
|
33
|
-
--lanes-ink-dim: #
|
|
34
|
-
--lanes-ink-faint: #
|
|
35
|
-
--lanes-accent: #
|
|
32
|
+
--lanes-ink: #111a14;
|
|
33
|
+
--lanes-ink-dim: #4a5661;
|
|
34
|
+
--lanes-ink-faint: #6f7b87;
|
|
35
|
+
--lanes-accent: #1f6f3f;
|
|
36
36
|
--lanes-on-accent: #ffffff;
|
|
37
37
|
--lanes-on-accent-urgent: #ffd3cc;
|
|
38
|
-
--lanes-tone-urgent: #
|
|
39
|
-
--lanes-tone-attention: #
|
|
40
|
-
--lanes-tone-working: #
|
|
41
|
-
--lanes-tone-done: #
|
|
38
|
+
--lanes-tone-urgent: #c41c1c;
|
|
39
|
+
--lanes-tone-attention: #a04d00;
|
|
40
|
+
--lanes-tone-working: #1a56c4;
|
|
41
|
+
--lanes-tone-done: #0d7338;
|
|
42
42
|
--lanes-scrim: #1a222b80;
|
|
43
43
|
--lanes-shadow: 0 -16px 80px #0003;
|
|
44
44
|
color-scheme: light;
|
|
@@ -10,7 +10,7 @@ if (typeof module === 'object') module.exports = {laneShellMode};
|
|
|
10
10
|
width: innerWidth, height: innerHeight});
|
|
11
11
|
const root = document.documentElement;
|
|
12
12
|
// BEGIN lanes tokens (generated by libexec/tokens.py --js; do not edit by hand)
|
|
13
|
-
const LANES_ANSI = {dark: ['#
|
|
13
|
+
const LANES_ANSI = {dark: ['#8792a0', '#ff7b6e', '#5fdc8a', '#ffc233', '#6fb6ff', '#c58bff', '#3fd9cf', '#a6b1be', '#97a3b1', '#ff9a8e', '#86f0ad', '#ffd866', '#98cbff', '#dcb2ff', '#7deee6', '#f2f5f8'], light: ['#2f3b34', '#c41c1c', '#0d7338', '#a04d00', '#1a56c4', '#8228c4', '#006f73', '#4a5661', '#55625b', '#d0221a', '#0e7a3a', '#a85200', '#1f5fd8', '#9133d0', '#00797c', '#111a14']};
|
|
14
14
|
// END lanes tokens
|
|
15
15
|
root.dataset.lanesShell = mode;
|
|
16
16
|
if (mode !== 'phone') {
|
|
@@ -22,7 +22,7 @@ if (typeof module === 'object') module.exports = {laneShellMode};
|
|
|
22
22
|
root.classList.add('lanes-mobile');
|
|
23
23
|
if (mode === 'tablet') {
|
|
24
24
|
root.classList.add('lanes-tablet');
|
|
25
|
-
try {localStorage.getItem('zellij:mobile-ui') || localStorage.setItem('zellij:mobile-ui', 'on');} catch
|
|
25
|
+
try {localStorage.getItem('zellij:mobile-ui') || localStorage.setItem('zellij:mobile-ui', 'on');} catch {}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
const elementFocus = HTMLElement.prototype.focus;
|
|
@@ -37,9 +37,9 @@ if (typeof module === 'object') module.exports = {laneShellMode};
|
|
|
37
37
|
const theme = localStorage.getItem('zellij:mobile-theme') || 'dark';
|
|
38
38
|
localStorage.setItem('zellij:mobile-theme', theme);
|
|
39
39
|
root.dataset.zjTheme = theme;
|
|
40
|
-
} catch
|
|
40
|
+
} catch {}
|
|
41
41
|
const icons = {
|
|
42
|
-
workspaces:
|
|
42
|
+
workspaces: [[3, 3], [14, 3], [3, 14], [14, 14]].map(([x, y]) => `<rect x="${x}" y="${y}" width="7" height="7" rx="2"/>`).join(''),
|
|
43
43
|
lanes: '<path d="M8 5h13M8 12h13M8 19h13"/><path d="M3 5h.01M3 12h.01M3 19h.01"/>',
|
|
44
44
|
panes: '<rect x="3" y="4" width="18" height="16" rx="3"/><path d="M12 4v16"/>',
|
|
45
45
|
keyboard: '<rect x="2" y="5" width="20" height="14" rx="3"/><path d="M6 9h.01M10 9h.01M14 9h.01M18 9h.01M6 13h.01M10 13h.01M14 13h.01M18 13h.01M8 16h8"/>',
|
|
@@ -69,14 +69,14 @@ if (typeof module === 'object') module.exports = {laneShellMode};
|
|
|
69
69
|
return el;
|
|
70
70
|
};
|
|
71
71
|
let shell, heading, subtitle, dock, sheet, list, search, count, lanesButton, panesButton, pasteKey, typingField, quickRow, quickOn;
|
|
72
|
-
let laneFilterBar, laneFilterInput, laneFilterPrev = '', sentAt = 0;
|
|
72
|
+
let laneFilterBar, laneFilterInput, laneFilterPrev = '', sentAt = 0, filterTab, owed = new Set();
|
|
73
73
|
let api, term, previousCards = '', sheetOpen = false, returnFocus, menuWas, hintN = 0, timer;
|
|
74
74
|
const queue = [];
|
|
75
75
|
let fontSize = 16;
|
|
76
76
|
try {
|
|
77
77
|
const stored = Number(localStorage.getItem('lanes:font-size'));
|
|
78
78
|
if (stored >= 14 && stored <= 24) fontSize = stored;
|
|
79
|
-
} catch
|
|
79
|
+
} catch {}
|
|
80
80
|
const state = () => api?.getState();
|
|
81
81
|
const data = () => state()?.data;
|
|
82
82
|
const nativeClick = selector => document.querySelector(selector)?.click();
|
|
@@ -85,10 +85,9 @@ if (typeof module === 'object') module.exports = {laneShellMode};
|
|
|
85
85
|
|
|
86
86
|
function viewport() {
|
|
87
87
|
const vv = window.visualViewport;
|
|
88
|
-
root.style.setProperty(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
root.style.setProperty('--lanes-offset-left', `${vv?.offsetLeft ?? 0}px`);
|
|
88
|
+
const px = (k, v) => root.style.setProperty(`--lanes-${k}`, `${v}px`);
|
|
89
|
+
px('vh', vv?.height ?? innerHeight); px('vw', vv?.width ?? innerWidth);
|
|
90
|
+
px('offset-top', vv?.offsetTop ?? 0); px('offset-left', vv?.offsetLeft ?? 0);
|
|
92
91
|
const occluded = !!vv && innerHeight - vv.height - vv.offsetTop > 120;
|
|
93
92
|
const focused = kbd()?.isFocused || document.activeElement?.matches('input, textarea');
|
|
94
93
|
setKeyboard(occluded && focused);
|
|
@@ -110,14 +109,14 @@ if (typeof module === 'object') module.exports = {laneShellMode};
|
|
|
110
109
|
label.append(make('span', '', 'Type here'));
|
|
111
110
|
label.setAttribute('aria-hidden', 'true');
|
|
112
111
|
host.removeAttribute('aria-hidden');
|
|
113
|
-
|
|
112
|
+
const fill = 'position:absolute;inset:0;width:100%;height:100%;opacity:1;pointer-events:auto;';
|
|
113
|
+
host.style.cssText = fill;
|
|
114
114
|
capture.removeAttribute('aria-hidden');
|
|
115
115
|
aria(capture, 'Type in terminal');
|
|
116
116
|
for (const a of ['type text', 'autocomplete off', 'autocapitalize none']) capture.setAttribute(...a.split(' '));
|
|
117
117
|
capture.tabIndex = 0;
|
|
118
|
-
capture.style.cssText = '
|
|
119
|
-
'
|
|
120
|
-
'caret-color:transparent;outline:none;pointer-events:auto;user-select:text;-webkit-user-select:text;';
|
|
118
|
+
capture.style.cssText = fill + 'box-sizing:border-box;font-size:16px;border:0;border-radius:12px;padding:0;margin:0;' +
|
|
119
|
+
'background:transparent;color:transparent;caret-color:transparent;outline:none;user-select:text;-webkit-user-select:text;';
|
|
121
120
|
field.append(label, host);
|
|
122
121
|
capture.addEventListener('beforeinput', ev => {
|
|
123
122
|
const t = ev.data ?? ev.dataTransfer?.getData('text'), r = ev.getTargetRanges?.()[0];
|
|
@@ -289,7 +288,7 @@ if (typeof module === 'object') module.exports = {laneShellMode};
|
|
|
289
288
|
else if (Date.now() - sentAt < 60) return arm();
|
|
290
289
|
else queue.shift();
|
|
291
290
|
}
|
|
292
|
-
} catch
|
|
291
|
+
} catch { queue.length = 0; }
|
|
293
292
|
}
|
|
294
293
|
const paste = t => currentTab()?.name.endsWith(' ☰') ? hint('Open a lane to paste') :
|
|
295
294
|
(send('\x1b[200~' + t.replace(/\x1b+(?=\[201~)/g, '').replace(/\r?\n/g, '\r') + '\x1b[201~'), 1);
|
|
@@ -300,12 +299,12 @@ if (typeof module === 'object') module.exports = {laneShellMode};
|
|
|
300
299
|
let i = 0;
|
|
301
300
|
while (i < p.length && p[i] === q[i]) i++;
|
|
302
301
|
laneFilterPrev = s;
|
|
303
|
-
if (!s)
|
|
302
|
+
if (!s) { clearLaneFilter(); refresh(); }
|
|
304
303
|
else if (!p.length) send('\x1b', null, '/', s);
|
|
305
304
|
else send('\x7f'.repeat(p.length - i) + q.slice(i).join(''));
|
|
306
305
|
}
|
|
307
306
|
async function clip(f, ok) {
|
|
308
|
-
try { ok(await f(navigator.clipboard)); } catch
|
|
307
|
+
try { ok(await f(navigator.clipboard)); } catch { hint('Clipboard blocked'); }
|
|
309
308
|
}
|
|
310
309
|
function screen() {
|
|
311
310
|
if (term.hasSelection()) return term.getSelection();
|
|
@@ -333,6 +332,10 @@ if (typeof module === 'object') module.exports = {laneShellMode};
|
|
|
333
332
|
}
|
|
334
333
|
return row;
|
|
335
334
|
}
|
|
335
|
+
function clearLaneFilter() {
|
|
336
|
+
owed.add(filterTab);
|
|
337
|
+
laneFilterPrev = laneFilterInput.value = '';
|
|
338
|
+
}
|
|
336
339
|
function buildLaneFilterBar() {
|
|
337
340
|
const bar = make('div', 'lanes-filter-bar');
|
|
338
341
|
aria(bar, 'Filter lanes');
|
|
@@ -344,24 +347,26 @@ if (typeof module === 'object') module.exports = {laneShellMode};
|
|
|
344
347
|
aria(laneFilterInput, 'Filter lanes in this workspace');
|
|
345
348
|
laneFilterInput.addEventListener('input', () => applyLaneFilter(laneFilterInput.value));
|
|
346
349
|
laneFilterInput.addEventListener('keydown', ev => {
|
|
347
|
-
if (ev.key === 'Escape') {
|
|
348
|
-
if (ev.key
|
|
350
|
+
if (ev.key === 'Escape') { clearLaneFilter(); refresh(); }
|
|
351
|
+
if (/^E(scape|nter)$/.test(ev.key)) { ev.preventDefault(); laneFilterInput.blur(); }
|
|
349
352
|
});
|
|
350
353
|
bar.append(laneFilterInput);
|
|
351
354
|
return bar;
|
|
352
355
|
}
|
|
353
|
-
function updateLaneFilterBar(isPicker) {
|
|
354
|
-
if (!laneFilterBar) return;
|
|
356
|
+
function updateLaneFilterBar(tab, isPicker) {
|
|
355
357
|
laneFilterBar.hidden = !isPicker;
|
|
356
|
-
if (
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
358
|
+
if (tab !== filterTab) {
|
|
359
|
+
const wait = queue.indexOf(null);
|
|
360
|
+
if (wait >= 0) queue.length = wait;
|
|
361
|
+
if (laneFilterPrev) clearLaneFilter();
|
|
362
|
+
}
|
|
363
|
+
if (owed.delete(tab)) send('\x1b', null);
|
|
364
|
+
if (isPicker) filterTab = tab;
|
|
360
365
|
}
|
|
361
366
|
function applyFont() {
|
|
362
367
|
term.options.fontSize = fontSize;
|
|
363
368
|
term.options.lineHeight = 1.1;
|
|
364
|
-
try {localStorage.setItem('lanes:font-size', String(fontSize));} catch
|
|
369
|
+
try {localStorage.setItem('lanes:font-size', String(fontSize));} catch {}
|
|
365
370
|
text(document.getElementById('lanes-font-value'), `${fontSize} px`);
|
|
366
371
|
resize();
|
|
367
372
|
}
|
|
@@ -376,9 +381,14 @@ if (typeof module === 'object') module.exports = {laneShellMode};
|
|
|
376
381
|
return t;
|
|
377
382
|
}
|
|
378
383
|
const applyTheme = () => { if (term) term.options.theme = lanesTheme(); };
|
|
379
|
-
function
|
|
384
|
+
function menuRow(label) {
|
|
380
385
|
const row = make('div', 'lanes-font-controls');
|
|
381
|
-
row.append(make('span', '',
|
|
386
|
+
row.append(make('span', '', label));
|
|
387
|
+
document.querySelector('.zj-mobile-menu')?.prepend(row);
|
|
388
|
+
return row;
|
|
389
|
+
}
|
|
390
|
+
function installFontControls() {
|
|
391
|
+
const row = menuRow('Text size');
|
|
382
392
|
const smaller = make('button', '', 'A−'), larger = make('button', '', 'A+');
|
|
383
393
|
aria(smaller, 'Smaller terminal text');
|
|
384
394
|
aria(larger, 'Larger terminal text');
|
|
@@ -386,7 +396,8 @@ if (typeof module === 'object') module.exports = {laneShellMode};
|
|
|
386
396
|
smaller.addEventListener('click', () => {fontSize = Math.max(14, fontSize - 1); applyFont();});
|
|
387
397
|
larger.addEventListener('click', () => {fontSize = Math.min(24, fontSize + 1); applyFont();});
|
|
388
398
|
row.append(smaller, value, larger);
|
|
389
|
-
|
|
399
|
+
fetch('/enrol/site', {credentials: 'same-origin'}).then(r => r.json())
|
|
400
|
+
.then(({version: v}) => /^\d[\w.+-]{0,31}$/.test(v) && menuRow(`Lanes v${v}`)).catch(() => {});
|
|
390
401
|
}
|
|
391
402
|
function refresh() {
|
|
392
403
|
if (!api || !shell) return;
|
|
@@ -397,7 +408,7 @@ if (typeof module === 'object') module.exports = {laneShellMode};
|
|
|
397
408
|
const name = tab?.name.replace(/ ☰$/, '') || 'Lanes';
|
|
398
409
|
const picker = tab?.name.endsWith(' ☰');
|
|
399
410
|
const pane = data()?.panes.find(p => p.pane_id === data()?.active_pane?.pane_id && !p.is_plugin);
|
|
400
|
-
const
|
|
411
|
+
const title = pickerTitle(name), counts = parseCounts(title);
|
|
401
412
|
const chips = countParts(counts).map(([glyph, n]) => glyph + n).join(' ');
|
|
402
413
|
text(heading, name);
|
|
403
414
|
if (!hintN) text(subtitle, picker ? chips || 'Choose a lane to continue' : pane?.title || 'Your workspace');
|
|
@@ -405,12 +416,13 @@ if (typeof module === 'object') module.exports = {laneShellMode};
|
|
|
405
416
|
panesButton.hidden = pasteKey.hidden = !!picker;
|
|
406
417
|
const needs = counts ? counts.approval + counts.input : 0;
|
|
407
418
|
if (needs) lanesButton.dataset.count = String(needs); else delete lanesButton.dataset.count;
|
|
408
|
-
updateLaneFilterBar(picker);
|
|
419
|
+
updateLaneFilterBar(tab?.name, picker);
|
|
409
420
|
send();
|
|
410
|
-
const quick = active && !picker;
|
|
421
|
+
const quick = active && (!picker || / · [›>] \w+$/.test(title) && 2);
|
|
411
422
|
if (quick !== quickOn) {
|
|
412
423
|
quickOn = quick;
|
|
413
424
|
quickRow.hidden = !quick;
|
|
425
|
+
for (const key of quickRow.children) key.hidden = quick > 1 && !['⏎', 'y', 'Esc'].includes(key.textContent);
|
|
414
426
|
root.style.setProperty('--lanes-quick', quick ? '48px' : '0px');
|
|
415
427
|
resize();
|
|
416
428
|
}
|