@vibelord/lanes 1.0.1 → 1.0.3

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.
Files changed (52) hide show
  1. package/package.json +1 -1
  2. package/payload/lanes/README.md +37 -7
  3. package/payload/lanes/VERSION +1 -1
  4. package/payload/lanes/bin/lanes +664 -50
  5. package/payload/lanes/client/ghostty/common.conf +3 -0
  6. package/payload/lanes/client/ghostty/lanes-palette-light.conf +25 -0
  7. package/payload/lanes/client/ghostty/lanes-palette.conf +25 -0
  8. package/payload/lanes/client/ghostty/omarchy.conf +3 -2
  9. package/payload/lanes/client/lanes-launch +18 -3
  10. package/payload/lanes/client/lanes-window +17 -1
  11. package/payload/lanes/container/env.sh +1 -1
  12. package/payload/lanes/container/hooks/claude-settings.json +10 -0
  13. package/payload/lanes/container/hooks/codex-hooks.json +22 -0
  14. package/payload/lanes/container/lane +310 -26
  15. package/payload/lanes/container/lane-signal +173 -0
  16. package/payload/lanes/docs/ARCHITECTURE.md +87 -13
  17. package/payload/lanes/docs/DESIGN.md +182 -4
  18. package/payload/lanes/docs/INSTALL.md +37 -2
  19. package/payload/lanes/docs/RUNBOOK.md +458 -39
  20. package/payload/lanes/docs/SHARING.md +240 -5
  21. package/payload/lanes/holders/exec.sh +2 -5
  22. package/payload/lanes/holders.lock +13 -0
  23. package/payload/lanes/install/enable-studio-host.sh +14 -0
  24. package/payload/lanes/install/host-install.sh +15 -3
  25. package/payload/lanes/install/services.py +128 -4
  26. package/payload/lanes/install.sh +108 -3
  27. package/payload/lanes/libexec/agent_state.py +32 -0
  28. package/payload/lanes/libexec/cursor_sync.py +10 -2
  29. package/payload/lanes/libexec/economy_poll.py +685 -0
  30. package/payload/lanes/libexec/guard.py +27 -1
  31. package/payload/lanes/libexec/lanes_lib.py +285 -38
  32. package/payload/lanes/libexec/notify.py +317 -0
  33. package/payload/lanes/libexec/registrations.py +1 -1
  34. package/payload/lanes/libexec/session_actions.py +7 -8
  35. package/payload/lanes/libexec/sidebar.py +911 -218
  36. package/payload/lanes/libexec/statusd.py +258 -74
  37. package/payload/lanes/libexec/studio_host.py +1608 -0
  38. package/payload/lanes/libexec/tokens.py +38 -7
  39. package/payload/lanes/libexec/view.sh +13 -3
  40. package/payload/lanes/systemd/lanes-economy.service +20 -0
  41. package/payload/lanes/systemd/lanes-notify.service +19 -0
  42. package/payload/lanes/systemd/lanes-studio-host-enrol.service +11 -0
  43. package/payload/lanes/systemd/lanes-studio-host-enrol.timer +10 -0
  44. package/payload/lanes/systemd/lanes-studio-host.service +37 -0
  45. package/payload/lanes/systemd/lanes-sync.service +3 -0
  46. package/payload/lanes/web/README.md +83 -4
  47. package/payload/lanes/web/index.html +2 -1
  48. package/payload/lanes/web/mobile.css +53 -198
  49. package/payload/lanes/web/mobile.js +87 -55
  50. package/payload/lanes/zellij/config.kdl +67 -16
  51. package/payload/manifest.json +5 -3
  52. package/payload/lanes/libexec/legacy_sessions.py +0 -130
@@ -62,231 +62,100 @@ html.lanes-mobile:root {
62
62
  --zj-yellow: var(--lanes-tone-attention);
63
63
  }
64
64
  /* END lanes tokens */
65
- /* Non-colour layout variables; colours come from the generated token block above.
66
- Safe areas apply in Home Screen mode and on both sides of landscape Safari. */
67
- html.lanes-mobile {
68
- --lanes-safe-top: env(safe-area-inset-top, 0px);
69
- --lanes-safe-bottom: env(safe-area-inset-bottom, 0px);
70
- --lanes-safe-left: env(safe-area-inset-left, 0px);
71
- --lanes-safe-right: env(safe-area-inset-right, 0px);
72
- --lanes-head: calc(58px + var(--lanes-safe-top));
73
- --lanes-dock: calc(66px + var(--lanes-safe-bottom));
74
- --lanes-keys: 0px;
75
- --lanes-vh: 100dvh;
76
- --lanes-vw: 100vw;
77
- --lanes-offset-top: 0px;
78
- --lanes-offset-left: 0px;
79
- -webkit-text-size-adjust: 100%;
80
- overscroll-behavior: none;
81
- }
82
- html.lanes-keyboard {
83
- --lanes-dock: 48px;
84
- --lanes-keys: 48px;
85
- --lanes-safe-bottom: 0px;
86
- }
65
+ /* Layout; colours only via the tokens above */
66
+ html.lanes-mobile { --lanes-safe-top: env(safe-area-inset-top, 0px); --lanes-safe-bottom: env(safe-area-inset-bottom, 0px); --lanes-safe-left: env(safe-area-inset-left, 0px); --lanes-safe-right: env(safe-area-inset-right, 0px); --lanes-head: calc(58px + var(--lanes-safe-top)); --lanes-dock: calc(66px + var(--lanes-safe-bottom)); --lanes-keys: 0px; --lanes-quick: 0px; --lanes-vh: 100dvh; --lanes-vw: 100vw; --lanes-offset-top: 0px; --lanes-offset-left: 0px; -webkit-text-size-adjust: 100%; overscroll-behavior: none; }
67
+ html.lanes-keyboard { --lanes-dock: 48px; --lanes-keys: 48px; --lanes-safe-bottom: 0px; }
87
68
  .lanes-mobile body { overscroll-behavior: none; }
88
- .lanes-shell, .lanes-shell *, .lanes-mobile .security-modal *, .lanes-mobile #zj-mobile-root * {
89
- box-sizing: border-box;
90
- }
91
- .lanes-shell {
92
- color: var(--lanes-text);
93
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
94
- -webkit-tap-highlight-color: transparent;
95
- }
69
+ .lanes-mobile body, .lanes-mobile #terminal { background: var(--lanes-surface) !important; }
70
+ .lanes-shell, .lanes-shell *, .lanes-mobile .security-modal *, .lanes-mobile #zj-mobile-root * { box-sizing: border-box; }
71
+ .lanes-shell { color: var(--lanes-text); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; -webkit-tap-highlight-color: transparent; }
96
72
  .lanes-shell [hidden], .lanes-shell[hidden] { display: none !important; }
97
- .lanes-shell button, .lanes-mobile #zj-mobile-root button {
98
- touch-action: manipulation;
99
- -webkit-tap-highlight-color: transparent;
100
- cursor: pointer;
101
- }
73
+ .lanes-shell button, .lanes-mobile #zj-mobile-root button { touch-action: manipulation; -webkit-tap-highlight-color: transparent; cursor: pointer; }
102
74
  .lanes-shell button { font: inherit; color: inherit; }
103
- .lanes-shell button:focus-visible, .lanes-shell input:focus-visible {
104
- outline: 2px solid var(--lanes-accent); outline-offset: -3px;
105
- }
75
+ .lanes-shell button:focus-visible, .lanes-shell input:focus-visible { outline: 2px solid var(--lanes-accent); outline-offset: -3px; }
106
76
  .lanes-ready .zj-mobile-topbar { display: none !important; }
107
- .lanes-ready body.zj-mobile-active #terminal {
108
- position: fixed;
109
- top: calc(var(--lanes-offset-top) + var(--lanes-head) + 8px);
110
- left: calc(var(--lanes-offset-left) + var(--lanes-safe-left) + 8px);
111
- width: calc(var(--lanes-vw) - var(--lanes-safe-left) - var(--lanes-safe-right) - 16px);
112
- height: calc(var(--lanes-vh) - var(--lanes-head) - var(--lanes-dock) - var(--lanes-keys) - 16px);
113
- margin: 0;
114
- padding: 0;
115
- }
116
- .lanes-header {
117
- position: fixed; z-index: 510;
118
- top: var(--lanes-offset-top); left: var(--lanes-offset-left);
119
- width: var(--lanes-vw); height: var(--lanes-head);
120
- padding: var(--lanes-safe-top) calc(8px + var(--lanes-safe-right)) 0 calc(12px + var(--lanes-safe-left));
121
- display: flex; align-items: center; gap: 10px;
122
- background: var(--lanes-bg); border-bottom: 1px solid var(--lanes-border);
123
- }
77
+ .lanes-ready body.zj-mobile-active #terminal { position: fixed; top: calc(var(--lanes-offset-top) + var(--lanes-head) + 8px); left: calc(var(--lanes-offset-left) + var(--lanes-safe-left) + 8px); width: calc(var(--lanes-vw) - var(--lanes-safe-left) - var(--lanes-safe-right) - 16px); height: calc(var(--lanes-vh) - var(--lanes-head) - var(--lanes-dock) - var(--lanes-keys) - var(--lanes-quick) - 16px); margin: 0; padding: 0; }
78
+ .lanes-header { position: fixed; z-index: 510; top: var(--lanes-offset-top); left: var(--lanes-offset-left); width: var(--lanes-vw); height: var(--lanes-head); padding: var(--lanes-safe-top) calc(8px + var(--lanes-safe-right)) 0 calc(16px + var(--lanes-safe-left)); display: flex; align-items: center; gap: 10px; background: var(--lanes-bg); border-bottom: 1px solid var(--lanes-border); }
124
79
  .lanes-brand { height: 34px; width: 34px; flex: 0 0 34px; }
125
- .lanes-context {
126
- display: flex; flex: 1; min-width: 0; height: 54px;
127
- flex-direction: column; justify-content: center; align-items: flex-start;
128
- border: 0; background: none; padding: 0; text-align: left;
129
- }
130
- .lanes-context strong { font-size: 16px; letter-spacing: -.25px; line-height: 22px; }
131
- .lanes-context span { font-size: 12px; color: var(--lanes-muted); line-height: 18px; }
80
+ .lanes-context { display: flex; flex: 1; min-width: 0; height: 54px; flex-direction: column; justify-content: center; align-items: flex-start; border: 0; background: none; padding: 0; text-align: left; }
81
+ .lanes-context strong { font-size: 17px; letter-spacing: -.3px; line-height: 22px; }
82
+ .lanes-context span { font-size: 13px; color: var(--lanes-muted); line-height: 18px; }
132
83
  .lanes-context strong, .lanes-context span { width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
133
- .lanes-button {
134
- display: flex; flex-direction: column; align-items: center; justify-content: center;
135
- gap: 5px; min-width: 48px; min-height: 48px; padding: 5px;
136
- border: 0; background: transparent; border-radius: 12px;
137
- }
84
+ .lanes-button { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 5px; min-width: 48px; min-height: 48px; padding: 5px; border: 0; background: transparent; border-radius: 12px; }
138
85
  .lanes-button svg { width: 23px; height: 23px; flex: 0 0 23px; }
139
- .lanes-button span { font-size: 10px; font-weight: 600; letter-spacing: .1px; }
86
+ .lanes-button span { font-size: 12px; font-weight: 600; letter-spacing: .1px; }
140
87
  .lanes-more, .lanes-close { flex: 0 0 48px; }
141
88
  .lanes-more span, .lanes-close span { display: none; }
142
- .lanes-button:active, .lanes-workspace:active { background: var(--lanes-press); }
143
- .lanes-dock {
144
- position: fixed; z-index: 510;
145
- top: calc(var(--lanes-offset-top) + var(--lanes-vh) - var(--lanes-dock));
146
- left: var(--lanes-offset-left); width: var(--lanes-vw); height: var(--lanes-dock);
147
- display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
148
- padding: 4px calc(8px + var(--lanes-safe-right)) var(--lanes-safe-bottom) calc(8px + var(--lanes-safe-left));
149
- border-top: 1px solid var(--lanes-border); background: var(--lanes-bg);
150
- }
151
- .lanes-dock .lanes-button { color: var(--lanes-muted); }
89
+ .lanes-button:active, .lanes-workspace:active, .lanes-quick button:active { background: var(--lanes-press); }
90
+ .lanes-dock { position: fixed; z-index: 510; top: calc(var(--lanes-offset-top) + var(--lanes-vh) - var(--lanes-dock)); left: var(--lanes-offset-left); width: var(--lanes-vw); height: var(--lanes-dock); display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); padding: 4px calc(8px + var(--lanes-safe-right)) var(--lanes-safe-bottom) calc(8px + var(--lanes-safe-left)); border-top: 1px solid var(--lanes-border); background: var(--lanes-bg); }
91
+ .lanes-dock .lanes-button { color: var(--lanes-muted); position: relative; }
152
92
  .lanes-dock .lanes-button[aria-pressed="true"] { color: var(--lanes-accent); background: var(--lanes-current); }
153
- .lanes-terminal-input {
154
- position: relative; align-self: center; min-width: 0; height: 52px;
155
- margin: 0 2px; border: 1px solid var(--lanes-border); border-radius: 12px;
156
- background: var(--lanes-panel); color: var(--lanes-muted);
157
- }
158
- .lanes-input-label {
159
- position: absolute; inset: 0; pointer-events: none;
160
- display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 5px;
161
- }
93
+ .lanes-button[data-count]::after { content: attr(data-count); position: absolute; top: 2px; right: 8px; display: grid; place-items: center; min-width: 20px; height: 20px; padding: 0 6px; border-radius: 10px; background: var(--lanes-tone-attention); color: var(--lanes-on-accent); font-size: 12px; font-weight: 600; }
94
+ .lanes-terminal-input { position: relative; align-self: center; min-width: 0; height: 52px; margin: 0 2px; border: 1px solid var(--lanes-border); border-radius: 12px; background: var(--lanes-panel); color: var(--lanes-muted); }
95
+ .lanes-input-label { position: absolute; inset: 0; pointer-events: none; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 5px; }
162
96
  .lanes-input-label svg { width: 23px; height: 23px; }
163
- .lanes-input-label span { font-size: 10px; font-weight: 600; }
164
- .lanes-terminal-input:focus-within, .lanes-terminal-input.is-typing {
165
- border-color: var(--lanes-accent); color: var(--lanes-accent); background: var(--lanes-current);
166
- }
97
+ .lanes-input-label span { font-size: 12px; font-weight: 600; letter-spacing: .1px; }
98
+ .lanes-terminal-input:focus-within, .lanes-terminal-input.is-typing { border-color: var(--lanes-accent); color: var(--lanes-accent); background: var(--lanes-current); }
167
99
  .lanes-keyboard .lanes-dock { padding-top: 0; }
168
100
  .lanes-keyboard .lanes-dock .lanes-button { min-height: 44px; padding: 0; }
169
101
  .lanes-keyboard .lanes-dock .lanes-button span { display: none; }
170
102
  .lanes-keyboard .lanes-terminal-input { height: 44px; }
171
103
  .lanes-keyboard .lanes-input-label { flex-direction: row; gap: 5px; }
172
- .lanes-ready .zj-mobile-modbar {
173
- top: calc(var(--lanes-offset-top) + var(--lanes-vh) - var(--lanes-dock) - var(--lanes-keys));
174
- bottom: auto; left: calc(var(--lanes-offset-left) + var(--lanes-safe-left));
175
- right: auto; width: calc(var(--lanes-vw) - var(--lanes-safe-left) - var(--lanes-safe-right));
176
- height: 48px; z-index: 510;
177
- overflow-x: auto; overscroll-behavior: contain;
178
- scrollbar-width: none; background: var(--lanes-panel); border-color: var(--lanes-border);
179
- }
104
+ .lanes-ready .zj-mobile-modbar, .lanes-quick { top: calc(var(--lanes-offset-top) + var(--lanes-vh) - var(--lanes-dock) - var(--lanes-keys)); bottom: auto; left: calc(var(--lanes-offset-left) + var(--lanes-safe-left)); right: auto; width: calc(var(--lanes-vw) - var(--lanes-safe-left) - var(--lanes-safe-right)); height: 48px; z-index: 510; overflow-x: auto; overscroll-behavior: contain; scrollbar-width: none; background: var(--lanes-panel); border-color: var(--lanes-border); }
180
105
  .lanes-ready:not(.lanes-keyboard) .zj-mobile-modbar { display: none !important; }
106
+ .lanes-quick { position: fixed; top: calc(var(--lanes-offset-top) + var(--lanes-vh) - var(--lanes-dock) - var(--lanes-keys) - var(--lanes-quick)); display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; padding: 2px 16px; border-top: 1px solid var(--lanes-border); }
107
+ .lanes-quick button { min-height: 44px; min-width: 44px; border: 1px solid var(--lanes-border); border-radius: 12px; background: var(--lanes-bg); font: 600 15px ui-monospace, Menlo, monospace; }
108
+ .lanes-quick .is-destructive { color: var(--lanes-tone-urgent); }
109
+ .lanes-quick .is-destructive:active { background: var(--lanes-tone-urgent); color: var(--lanes-on-accent); transition: background 0s 400ms, color 0s 400ms; }
181
110
  .lanes-ready.lanes-keyboard .zj-mobile-modbar { display: flex !important; }
182
- .lanes-ready .zj-mobile-modbar button {
183
- flex: 1 0 44px; min-width: 44px; min-height: 48px;
184
- color: var(--lanes-text); font: 500 13px -apple-system, sans-serif;
185
- border-color: var(--lanes-border);
186
- }
187
- .lanes-ready .zj-mobile-modbar button.zj-armed { background: var(--lanes-accent); color: #101318; }
188
- .lanes-sheet {
189
- position: fixed; z-index: 800;
190
- top: var(--lanes-offset-top); left: var(--lanes-offset-left);
191
- height: var(--lanes-vh); width: var(--lanes-vw);
192
- display: flex; align-items: flex-end; background: #0009;
193
- padding: var(--lanes-safe-top) var(--lanes-safe-right) 0 var(--lanes-safe-left);
194
- }
195
- .lanes-sheet-panel {
196
- display: flex; flex-direction: column; width: 100%; min-height: 0;
197
- max-height: calc(var(--lanes-vh) - var(--lanes-safe-top) - 20px);
198
- background: var(--lanes-bg); border: 1px solid var(--lanes-border);
199
- border-bottom: 0; border-radius: 24px 24px 0 0;
200
- padding: 20px 16px calc(8px + var(--lanes-safe-bottom));
201
- box-shadow: 0 -16px 80px #0007;
202
- }
111
+ .lanes-ready .zj-mobile-modbar button { flex: 1 0 44px; min-width: 44px; min-height: 48px; color: var(--lanes-text); font: 500 13px -apple-system, sans-serif; border-color: var(--lanes-border); }
112
+ .lanes-ready .zj-mobile-modbar button.zj-armed { background: var(--lanes-accent); color: var(--lanes-on-accent); }
113
+ .lanes-sheet { position: fixed; z-index: 800; top: var(--lanes-offset-top); left: var(--lanes-offset-left); height: var(--lanes-vh); width: var(--lanes-vw); display: flex; align-items: flex-end; background: var(--lanes-scrim); padding: var(--lanes-safe-top) var(--lanes-safe-right) 0 var(--lanes-safe-left); }
114
+ .lanes-sheet-panel { display: flex; flex-direction: column; width: 100%; min-height: 0; max-height: calc(var(--lanes-vh) - var(--lanes-safe-top) - 20px); background: var(--lanes-bg); border: 1px solid var(--lanes-border); border-bottom: 0; border-radius: 24px 24px 0 0; padding: 20px 16px calc(8px + var(--lanes-safe-bottom)); box-shadow: var(--lanes-shadow); }
203
115
  .lanes-sheet-head { display: flex; justify-content: space-between; align-items: center; flex: 0 0 auto; margin-bottom: 18px; }
204
116
  .lanes-sheet-head h2 { font-size: 25px; line-height: 30px; letter-spacing: -.8px; margin: 0 0 3px; }
205
117
  .lanes-sheet-head p { font-size: 13px; color: var(--lanes-muted); margin: 0; }
206
118
  .lanes-close { background: var(--lanes-panel); border-radius: 50%; }
207
- .lanes-search {
208
- display: flex; align-items: center; gap: 10px; flex: 0 0 48px;
209
- height: 48px; background: var(--lanes-panel); border: 1px solid var(--lanes-border);
210
- border-radius: 12px; padding: 0 14px; margin-bottom: 12px;
211
- }
119
+ .lanes-search { display: flex; align-items: center; gap: 10px; flex: 0 0 48px; height: 48px; background: var(--lanes-panel); border: 1px solid var(--lanes-border); border-radius: 12px; padding: 0 14px; margin-bottom: 12px; }
212
120
  .lanes-search svg { width: 20px; height: 20px; color: var(--lanes-muted); flex-shrink: 0; }
213
- .lanes-search input {
214
- border: 0; outline: none; background: transparent; color: var(--lanes-text);
215
- font: 16px -apple-system, sans-serif; min-width: 0; width: 100%; height: 44px;
216
- }
121
+ .lanes-search input { border: 0; outline: none; background: transparent; color: var(--lanes-text); font: 16px -apple-system, sans-serif; min-width: 0; width: 100%; height: 44px; }
217
122
  .lanes-search input::placeholder { color: var(--lanes-muted); }
218
- .lanes-workspaces {
219
- min-height: 0; overflow-y: auto; overscroll-behavior: contain;
220
- display: flex; flex-direction: column; gap: 6px;
221
- -webkit-overflow-scrolling: touch;
222
- }
223
- .lanes-workspace {
224
- display: flex; align-items: center; gap: 12px;
225
- flex: 0 0 auto; width: 100%; min-height: 74px; padding: 12px;
226
- background: var(--lanes-panel); border: 1px solid transparent; border-radius: 14px;
227
- text-align: left;
228
- }
123
+ .lanes-workspaces { min-height: 0; overflow-y: auto; overscroll-behavior: contain; display: flex; flex-direction: column; gap: 8px; -webkit-overflow-scrolling: touch; }
124
+ .lanes-workspace { display: flex; align-items: center; gap: 12px; flex: 0 0 auto; width: 100%; min-height: 74px; padding: 12px; background: var(--lanes-panel); border: 1px solid transparent; border-radius: 14px; text-align: left; }
229
125
  .lanes-workspace.is-current { border-color: var(--lanes-current-border); background: var(--lanes-current); }
230
- .lanes-monogram {
231
- display: grid; place-items: center; width: 40px; height: 40px; flex: 0 0 40px;
232
- font-size: 17px; font-weight: 650; color: var(--lanes-accent);
233
- background: var(--lanes-chip); border: 1px solid var(--lanes-border); border-radius: 11px;
234
- }
126
+ .lanes-monogram { display: grid; place-items: center; width: 40px; height: 40px; flex: 0 0 40px; font-size: 17px; font-weight: 650; color: var(--lanes-accent); background: var(--lanes-chip); border: 1px solid var(--lanes-border); border-radius: 11px; }
235
127
  .lanes-workspace-copy { display: flex; flex-direction: column; gap: 4px; min-width: 0; flex: 1; }
236
128
  .lanes-workspace-copy strong { font-size: 16px; font-weight: 600; line-height: 21px; overflow-wrap: anywhere; }
237
- .lanes-workspace-copy small { font-size: 12px; color: var(--lanes-muted); }
129
+ .lanes-workspace-copy small { font-size: 13px; color: var(--lanes-muted); }
130
+ .lanes-tone-urgent { color: var(--lanes-tone-urgent); }
131
+ .lanes-tone-attention { color: var(--lanes-tone-attention); }
132
+ .lanes-tone-working { color: var(--lanes-tone-working); }
133
+ .lanes-tone-done { color: var(--lanes-tone-done); }
238
134
  .lanes-workspace-arrow { width: 18px; flex: 0 0 18px; color: var(--lanes-muted); }
239
135
  .is-current .lanes-workspace-arrow { color: var(--lanes-accent); }
240
136
  .lanes-session-link { border: 0; background: transparent; min-height: 48px; color: var(--lanes-muted) !important; font-size: 13px !important; flex-shrink: 0; }
241
137
  .lanes-empty { text-align: center; color: var(--lanes-muted); padding: 30px 12px; }
242
- .lanes-mobile #zj-mobile-root { font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
243
- .lanes-mobile .zj-mobile-menu {
244
- top: calc(var(--lanes-offset-top) + var(--lanes-head) + 6px);
245
- right: calc(var(--lanes-safe-right) + 8px); left: auto;
246
- max-height: calc(var(--lanes-vh) - var(--lanes-head) - 16px);
247
- max-width: calc(var(--lanes-vw) - var(--lanes-safe-left) - var(--lanes-safe-right) - 16px);
248
- min-width: min(290px, 90vw); overflow-y: auto;
249
- background: var(--lanes-panel); border: 1px solid var(--lanes-border);
250
- border-radius: 16px; box-shadow: 0 12px 48px #0009;
251
- }
138
+ .lanes-mobile #zj-mobile-root, .lanes-mobile .security-modal { font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
139
+ .lanes-mobile .zj-mobile-menu { top: calc(var(--lanes-offset-top) + var(--lanes-head) + 6px); right: calc(var(--lanes-safe-right) + 8px); left: auto; max-height: calc(var(--lanes-vh) - var(--lanes-head) - 16px); max-width: calc(var(--lanes-vw) - var(--lanes-safe-left) - var(--lanes-safe-right) - 16px); min-width: min(290px, 90vw); overflow-y: auto; background: var(--lanes-panel); border: 1px solid var(--lanes-border); border-radius: 16px; box-shadow: var(--lanes-shadow); }
252
140
  .lanes-mobile .zj-mobile-menu button { min-height: 48px; color: var(--lanes-text); font-size: 15px; }
253
141
  .lanes-font-controls { display: flex; align-items: center; gap: 4px; padding: 8px 12px; border-bottom: 1px solid var(--lanes-border); }
254
142
  .lanes-font-controls > span { margin-right: auto; font-size: 13px; color: var(--lanes-muted); }
255
143
  .lanes-font-controls button { min-width: 44px; padding: 8px !important; text-align: center !important; }
256
144
  .lanes-font-controls output { font: 12px -apple-system, sans-serif; width: 36px; text-align: center; color: var(--lanes-muted); }
257
- .lanes-mobile .zj-mobile-overlay {
258
- top: var(--lanes-offset-top); left: var(--lanes-offset-left); right: auto;
259
- width: var(--lanes-vw); height: var(--lanes-vh); max-height: var(--lanes-vh);
260
- padding: var(--lanes-safe-top) var(--lanes-safe-right) var(--lanes-safe-bottom) var(--lanes-safe-left);
261
- background: var(--lanes-bg); color: var(--lanes-text);
262
- }
145
+ .lanes-mobile .zj-mobile-overlay { top: var(--lanes-offset-top); left: var(--lanes-offset-left); right: auto; width: var(--lanes-vw); height: var(--lanes-vh); max-height: var(--lanes-vh); padding: var(--lanes-safe-top) var(--lanes-safe-right) var(--lanes-safe-bottom) var(--lanes-safe-left); background: var(--lanes-bg); color: var(--lanes-text); }
263
146
  .lanes-mobile .zj-mobile-overlay-header { border-color: var(--lanes-border); padding: 8px 12px; min-height: 64px; }
264
147
  .lanes-mobile .zj-mobile-overlay-header .zj-back { min-width: 64px; min-height: 48px; border-radius: 12px; border-color: var(--lanes-border); }
265
148
  .lanes-mobile .zj-mobile-overlay-header .zj-title { font-size: 19px; font-weight: 600; }
266
149
  .lanes-mobile .zj-mobile-search-row { padding: 12px; }
267
- .lanes-mobile .zj-mobile-overlay input.zj-search, .lanes-mobile .zj-mobile-overlay input.zj-name {
268
- min-width: 0; font-size: 16px; min-height: 48px; border-radius: 10px; padding: 10px;
269
- color: var(--lanes-text); background: var(--lanes-panel); border-color: var(--lanes-border);
270
- }
150
+ .lanes-mobile .zj-mobile-overlay input.zj-search, .lanes-mobile .zj-mobile-overlay input.zj-name { min-width: 0; font-size: 16px; min-height: 48px; border-radius: 10px; padding: 10px; color: var(--lanes-text); background: var(--lanes-panel); border-color: var(--lanes-border); }
271
151
  .lanes-mobile .zj-mobile-cards { padding: 0 12px 12px; overscroll-behavior: contain; }
272
152
  .lanes-mobile .zj-mobile-card { min-height: 72px; padding: 16px; border: 1px solid var(--lanes-border); border-radius: 14px; background: var(--lanes-panel); }
273
153
  .lanes-mobile .zj-mobile-card .zj-card-title { color: var(--lanes-accent); font-size: 16px; overflow-wrap: anywhere; }
274
- .lanes-mobile .zj-mobile-card .zj-card-meta { color: var(--lanes-muted); font-size: 12px; overflow-wrap: anywhere; }
154
+ .lanes-mobile .zj-mobile-card .zj-card-meta { color: var(--lanes-muted); font-size: 13px; overflow-wrap: anywhere; }
275
155
  .lanes-mobile .zj-mobile-footer { padding-bottom: 8px; }
276
156
  .lanes-mobile .zj-mobile-footer button { min-height: 48px; border-radius: 12px; color: var(--lanes-accent); border-color: var(--lanes-border); }
277
- .lanes-mobile .security-modal {
278
- position: fixed; z-index: 9999; box-sizing: border-box;
279
- display: flex; align-items: center; justify-content: center;
280
- top: var(--lanes-offset-top); left: var(--lanes-offset-left);
281
- width: var(--lanes-vw); height: var(--lanes-vh);
282
- padding: calc(16px + var(--lanes-safe-top)) calc(20px + var(--lanes-safe-right)) calc(16px + var(--lanes-safe-bottom)) calc(20px + var(--lanes-safe-left));
283
- overflow: auto; background: var(--lanes-bg); font-family: -apple-system, BlinkMacSystemFont, sans-serif;
284
- }
285
- .lanes-mobile .security-modal .security-modal-content {
286
- width: 100%; max-width: 400px; max-height: 100%; overflow-y: auto;
287
- border: 1px solid var(--lanes-border); border-radius: 22px;
288
- padding: 24px; background: var(--lanes-panel); color: var(--lanes-text); box-shadow: none;
289
- }
157
+ .lanes-mobile .security-modal { box-sizing: border-box; top: var(--lanes-offset-top); left: var(--lanes-offset-left); width: var(--lanes-vw); height: var(--lanes-vh); padding: calc(16px + var(--lanes-safe-top)) calc(20px + var(--lanes-safe-right)) calc(16px + var(--lanes-safe-bottom)) calc(20px + var(--lanes-safe-left)); overflow: auto; background: var(--lanes-bg); }
158
+ .lanes-mobile .security-modal .security-modal-content { width: 100%; max-width: 400px; max-height: 100%; overflow-y: auto; border: 1px solid var(--lanes-border); border-radius: 24px; padding: 24px; background: var(--lanes-panel); color: var(--lanes-text); box-shadow: none; }
290
159
  .lanes-mobile .security-modal .security-modal-content::before { display: none; }
291
160
  .lanes-mobile .security-modal h3 { font-size: 25px; font-weight: 650; text-transform: none; letter-spacing: -.7px; color: var(--lanes-text); border: 0; margin-bottom: 18px; }
292
161
  .lanes-mobile .security-modal input[type="password"] { display: block; width: 100%; padding: 12px; margin-bottom: 12px; font-size: 16px; min-height: 52px; border-radius: 12px; background: var(--lanes-bg); color: var(--lanes-text); border: 1px solid var(--lanes-border); appearance: none; }
@@ -294,18 +163,15 @@ html.lanes-keyboard {
294
163
  .lanes-mobile .security-modal input[type="checkbox"] { width: 22px; height: 22px; border-radius: 6px; flex: 0 0 22px; accent-color: var(--lanes-accent); }
295
164
  .lanes-mobile .security-modal .button-row { display: flex; gap: 12px; margin-top: 20px; }
296
165
  .lanes-mobile .security-modal button { min-height: 48px; border: 1px solid; border-radius: 12px; font-size: 15px; padding: 10px 20px; appearance: none; }
297
- .lanes-mobile .security-modal .submit-btn { background: var(--lanes-accent); border-color: var(--lanes-accent); color: #101318; font-weight: 650; flex: 1; }
166
+ .lanes-mobile .security-modal .submit-btn { background: var(--lanes-accent); border-color: var(--lanes-accent); color: var(--lanes-on-accent); font-weight: 650; flex: 1; }
298
167
  .lanes-mobile .security-modal .cancel-btn { background: transparent; color: var(--lanes-muted); border-color: var(--lanes-border); }
299
168
  @media (orientation: landscape) and (max-height: 500px) {
300
169
  html.lanes-mobile { --lanes-head: calc(48px + var(--lanes-safe-top)); --lanes-dock: calc(48px + var(--lanes-safe-bottom)); }
301
- html.lanes-keyboard { --lanes-dock: 48px; }
302
170
  .lanes-context { height: 46px; }
303
171
  .lanes-dock { padding-top: 0; }
304
172
  .lanes-dock .lanes-button { flex-direction: row; gap: 8px; min-height: 44px; }
305
- .lanes-dock .lanes-button span { font-size: 12px; }
306
173
  .lanes-terminal-input { height: 44px; }
307
174
  .lanes-input-label { flex-direction: row; gap: 8px; }
308
- .lanes-input-label span { font-size: 12px; }
309
175
  .lanes-sheet-panel { padding-top: 12px; }
310
176
  .lanes-sheet-head { margin-bottom: 8px; }
311
177
  .lanes-workspaces { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
@@ -314,20 +180,9 @@ html.lanes-keyboard {
314
180
  .lanes-sheet-panel { animation: lanes-sheet-in 150ms ease-out; }
315
181
  @keyframes lanes-sheet-in { from {transform: translateY(20px); opacity: .4;} to {transform: translateY(0); opacity: 1;} }
316
182
  }
317
-
318
- /* Lane filter bar — visible in the dock when the ☰ picker tab is active */
319
- .lanes-filter-bar {
320
- display: flex; align-items: center; gap: 8px;
321
- flex: 1 1 auto; min-width: 0;
322
- padding: 0 12px; height: 44px;
323
- background: var(--lanes-chip); border: 1px solid var(--lanes-border); border-radius: 12px;
324
- }
325
- .lanes-filter-bar[hidden] { display: none; }
183
+ /* Lane filter bar (☰ picker tab) */
184
+ .lanes-filter-bar { display: flex; align-items: center; gap: 8px; flex: 1 1 auto; min-width: 0; padding: 0 12px; height: 44px; background: var(--lanes-chip); border: 1px solid var(--lanes-border); border-radius: 12px; }
326
185
  .lanes-filter-bar > svg { width: 18px; height: 18px; flex: 0 0 18px; color: var(--lanes-muted); }
327
- .lanes-filter-bar input {
328
- flex: 1 1 auto; min-width: 0;
329
- background: transparent; border: 0; outline: none;
330
- color: var(--lanes-text); font-size: 16px; caret-color: var(--lanes-accent);
331
- }
186
+ .lanes-filter-bar input { flex: 1 1 auto; min-width: 0; background: transparent; border: 0; outline: none; color: var(--lanes-text); font-size: 16px; caret-color: var(--lanes-accent); }
332
187
  .lanes-filter-bar input::placeholder { color: var(--lanes-muted); }
333
188
  .lanes-filter-bar input::-webkit-search-cancel-button { -webkit-appearance: none; }
@@ -1,5 +1,4 @@
1
- /* Lanes browser shell over the pinned Zellij 0.45.1 client's window hooks; auth, input, rendering and transport
2
- * stay upstream. LNS-31: phone (today's rule or a coarse landscape phone), tablet (other touch), desktop. */
1
+ /* Lanes browser shell; see web/README.md, Code notes */
3
2
  function laneShellMode({userAgent, coarse, width, height} = {}) {
4
3
  if (/iPhone|iPod|Android.*Mobile/i.test(userAgent) || (coarse && (width <= 600 || height <= 500))) return 'phone';
5
4
  return coarse ? 'tablet' : 'desktop';
@@ -11,8 +10,11 @@ if (typeof module === 'object') module.exports = {laneShellMode};
11
10
  const mode = laneShellMode({userAgent: navigator.userAgent, coarse: matchMedia('(pointer: coarse)').matches,
12
11
  width: innerWidth, height: innerHeight});
13
12
  const root = document.documentElement;
14
- root.dataset.lanesShell = mode; // web/browser.js: the desktop bar and the tablet's full rendering
15
- if (mode !== 'phone') { // laptop/tablet extras: a phone never fetches them
13
+ // BEGIN lanes tokens (generated by libexec/tokens.py --js; do not edit by hand)
14
+ const LANES_ANSI = {dark: ['#7d8896', '#f28b7d', '#6fd39a', '#e5b84a', '#7fb3f2', '#c9a6ff', '#6fd6d0', '#9ca7b5', '#8a95a3', '#ff9d96', '#8fe0a3', '#f7cf7f', '#a5cbff', '#dcc6ff', '#9ae6e0', '#edf1f5'], light: ['#3b4a42', '#b7322a', '#1b7142', '#8a5a00', '#2456a8', '#7a3fb8', '#0f7b7b', '#56616d', '#5f6d66', '#c2332a', '#1b7a38', '#8f5a00', '#2860c4', '#8a4dc8', '#0f7272', '#17241b']};
15
+ // END lanes tokens
16
+ root.dataset.lanesShell = mode;
17
+ if (mode !== 'phone') {
16
18
  const add = (tag, props) => document.head.append(Object.assign(document.createElement(tag), props));
17
19
  add('link', {rel: 'stylesheet', href: '/lanes-assets/browser.css'});
18
20
  add('script', {src: '/lanes-assets/browser.js'});
@@ -20,14 +22,10 @@ if (typeof module === 'object') module.exports = {laneShellMode};
20
22
  if (mode === 'desktop') return;
21
23
  root.classList.add('lanes-mobile');
22
24
  if (mode === 'tablet') {
23
- // iPad Safari's UA is desktop: ask for the mobile UI (a stored 'off' stays).
24
25
  root.classList.add('lanes-tablet');
25
26
  try {localStorage.getItem('zellij:mobile-ui') || localStorage.setItem('zellij:mobile-ui', 'on');} catch (_) {}
26
27
  }
27
28
 
28
- // Only a browser's native tap/focus on an input may summon the keyboard. Zellij
29
- // otherwise refocuses a hidden input on touchstart, touchend, click and server
30
- // SetSoftKeyboard messages. Install before its first terminal or login modal.
31
29
  const elementFocus = HTMLElement.prototype.focus;
32
30
  HTMLElement.prototype.focus = function(...args) {
33
31
  const textInput = this instanceof HTMLInputElement &&
@@ -35,11 +33,7 @@ if (typeof module === 'object') module.exports = {laneShellMode};
35
33
  if (textInput || this instanceof HTMLTextAreaElement || this.isContentEditable) return;
36
34
  return elementFocus.apply(this, args);
37
35
  };
38
- Object.defineProperty(window, '__zjSoftKbdEnabled', {
39
- configurable: false,
40
- get: () => false,
41
- set: () => {},
42
- });
36
+ Object.defineProperty(window, '__zjSoftKbdEnabled', {configurable: false, get: () => false, set: () => {}});
43
37
  try {
44
38
  const theme = localStorage.getItem('zellij:mobile-theme') || 'dark';
45
39
  localStorage.setItem('zellij:mobile-theme', theme);
@@ -73,7 +67,7 @@ if (typeof module === 'object') module.exports = {laneShellMode};
73
67
  el.addEventListener('click', action);
74
68
  return el;
75
69
  };
76
- let shell, heading, subtitle, dock, sheet, list, search, count, lanesButton, typingField;
70
+ let shell, heading, subtitle, dock, sheet, list, search, count, lanesButton, typingField, quickRow, quickOn;
77
71
  let laneFilterBar, laneFilterInput, laneFilterPrev = '';
78
72
  let api, term, previousCards = '', sheetOpen = false, returnFocus;
79
73
  let fontSize = 16;
@@ -92,9 +86,7 @@ if (typeof module === 'object') module.exports = {laneShellMode};
92
86
  root.style.setProperty('--lanes-vw', `${vv?.width ?? innerWidth}px`);
93
87
  root.style.setProperty('--lanes-offset-top', `${vv?.offsetTop ?? 0}px`);
94
88
  root.style.setProperty('--lanes-offset-left', `${vv?.offsetLeft ?? 0}px`);
95
- // Browser toolbars alone must not put the UI into keyboard mode.
96
89
  const occluded = !!vv && innerHeight - vv.height - vv.offsetTop > 120;
97
- // Zellij's keyboard input lives in a closed shadow root, so activeElement is its host.
98
90
  const focused = window.__zjSoftKbdCapture?.isFocused || document.activeElement?.matches('input, textarea');
99
91
  setKeyboard(occluded && focused);
100
92
  }
@@ -114,19 +106,14 @@ if (typeof module === 'object') module.exports = {laneShellMode};
114
106
  label.innerHTML = svg('keyboard');
115
107
  label.append(make('span', '', 'Type here'));
116
108
  label.setAttribute('aria-hidden', 'true');
117
- // Keep the original input and its event handlers inside its closed shadow
118
- // root (including its protection from password-manager extensions). Make it
119
- // a real, tappable field instead of a 1px invisible focus target. Keystrokes
120
- // appear in the terminal; its internal padding/history stays invisible.
121
109
  host.removeAttribute('aria-hidden');
122
110
  host.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;opacity:1;pointer-events:auto;';
123
111
  capture.removeAttribute('aria-hidden');
124
112
  capture.setAttribute('aria-label', 'Type in terminal');
125
113
  capture.tabIndex = 0;
126
- capture.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;box-sizing:border-box;' +
127
- 'font-size:16px;border:0;border-radius:12px;padding:0;margin:0;opacity:1;' +
128
- 'background:transparent;color:transparent;caret-color:transparent;outline:none;' +
129
- 'pointer-events:auto;user-select:text;-webkit-user-select:text;';
114
+ capture.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;box-sizing:border-box;font-size:16px;' +
115
+ 'border:0;border-radius:12px;padding:0;margin:0;opacity:1;background:transparent;color:transparent;' +
116
+ 'caret-color:transparent;outline:none;pointer-events:auto;user-select:text;-webkit-user-select:text;';
130
117
  field.append(label, host);
131
118
  capture.addEventListener('focus', () => field.classList.add('is-typing'));
132
119
  capture.addEventListener('blur', () => field.classList.remove('is-typing'));
@@ -141,6 +128,20 @@ if (typeof module === 'object') module.exports = {laneShellMode};
141
128
  function currentTab() {
142
129
  return data()?.tabs.find(t => t.position === data()?.active_pane?.tab_position);
143
130
  }
131
+ const GLYPHS = '◆◇▸✓', TONES = ['urgent', 'attention', 'working', 'done'];
132
+ function parseCounts(title) {
133
+ const c = {approval: 0, input: 0, working: 0, done: 0}, keys = Object.keys(c);
134
+ let found = 0;
135
+ for (const [, g, n] of String(title || '').matchAll(/([◆◇▸✓])(\d+)/g)) { c[keys[GLYPHS.indexOf(g)]] = +n; found = 1; }
136
+ return found ? c : null;
137
+ }
138
+ window.__lanesParseCounts = parseCounts;
139
+ const countParts = c => c ? Object.values(c).map((n, i) => [GLYPHS[i], n, TONES[i]]).filter(p => p[1]) : [];
140
+ const pickerTab = name => data()?.tabs.find(t => t.name === `${name} ☰`);
141
+ function pickerTitle(name) {
142
+ const picker = pickerTab(name);
143
+ return picker ? data()?.panes.find(p => p.tab_position === picker.position && !p.is_plugin)?.title || '' : '';
144
+ }
144
145
  function workspaces() {
145
146
  const tabs = data()?.tabs || [];
146
147
  const pickers = tabs.filter(t => t.name.endsWith(' ☰'));
@@ -176,12 +177,14 @@ if (typeof module === 'object') module.exports = {laneShellMode};
176
177
  const items = workspaces();
177
178
  const activeName = (currentTab()?.name || '').replace(/ ☰$/, '');
178
179
  const query = search.value.trim().toLocaleLowerCase();
179
- const signature = JSON.stringify([items, activeName, query]);
180
+ const titles = items.map(w => w.picker ? pickerTitle(w.name) : '');
181
+ const signature = JSON.stringify([items, titles, activeName, query]);
180
182
  if (signature === previousCards) return;
181
183
  previousCards = signature;
182
184
  list.replaceChildren();
183
185
  text(count, `${items.length} workspaces`);
184
- for (const item of items.filter(w => w.name.toLocaleLowerCase().includes(query))) {
186
+ for (const [index, item] of items.entries()) {
187
+ if (!item.name.toLocaleLowerCase().includes(query)) continue;
185
188
  const active = item.name === activeName;
186
189
  const card = make('button', `lanes-workspace${active ? ' is-current' : ''}`);
187
190
  card.type = 'button';
@@ -189,7 +192,10 @@ if (typeof module === 'object') module.exports = {laneShellMode};
189
192
  if (active) card.setAttribute('aria-current', 'true');
190
193
  const monogram = make('span', 'lanes-monogram', Array.from(item.name)[0]?.toUpperCase() || 'L');
191
194
  const copy = make('span', 'lanes-workspace-copy');
192
- copy.append(make('strong', '', item.name), make('small', '', item.picker ? 'Choose a lane' : 'Open workspace'));
195
+ const small = make('small'), parts = countParts(parseCounts(titles[index]));
196
+ parts.forEach(([g, n, tone], i) => small.append(i ? ' · ' : '', make('span', `lanes-tone-${tone}`, `${g} ${n}`)));
197
+ if (!parts.length) small.textContent = item.picker ? 'Choose a lane' : 'Open workspace';
198
+ copy.append(make('strong', '', item.name), small);
193
199
  const arrow = make('span', 'lanes-workspace-arrow');
194
200
  arrow.innerHTML = svg(active ? 'check' : 'arrow');
195
201
  card.append(monogram, copy, arrow);
@@ -214,8 +220,7 @@ if (typeof module === 'object') module.exports = {laneShellMode};
214
220
  dock = make('nav', 'lanes-dock'); dock.setAttribute('aria-label', 'Workspace controls');
215
221
  const spaces = button('Workspaces', 'workspaces', openSheet);
216
222
  lanesButton = button('Lanes', 'lanes', () => {
217
- const name = (currentTab()?.name || '').replace(/ ☰$/, '');
218
- const picker = data()?.tabs.find(t => t.name === `${name} ☰`);
223
+ const picker = pickerTab((currentTab()?.name || '').replace(/ ☰$/, ''));
219
224
  if (picker) focusTab(picker); else openSheet();
220
225
  });
221
226
  const panes = button('Panes', 'panes', () => nativeClick('.zj-pane'));
@@ -250,12 +255,10 @@ if (typeof module === 'object') module.exports = {laneShellMode};
250
255
  if (ev.shiftKey && document.activeElement === first) {ev.preventDefault(); last.focus();}
251
256
  else if (!ev.shiftKey && document.activeElement === last) {ev.preventDefault(); first.focus();}
252
257
  });
253
- shell.append(header, dock, sheet); document.body.append(shell);
258
+ quickRow = buildQuickRow();
259
+ shell.append(header, quickRow, dock, sheet); document.body.append(shell);
254
260
  if (mode === 'tablet') more.before(dock);
255
261
  root.classList.add('lanes-ready');
256
- // Upstream cancels pointerdown on extra keys, which also suppresses their
257
- // touch click in Safari. Cancel mousedown instead: keep typing focus while
258
- // allowing the native click to activate CTRL, arrows, and the other keys.
259
262
  for (const key of document.querySelectorAll('.zj-mobile-modbar button')) {
260
263
  key.addEventListener('pointerdown', ev => ev.stopImmediatePropagation(), true);
261
264
  key.addEventListener('mousedown', ev => ev.preventDefault());
@@ -264,31 +267,39 @@ if (typeof module === 'object') module.exports = {laneShellMode};
264
267
  resize();
265
268
  }
266
269
  function sendToTerminal(bytes) {
267
- // Inject bytes into the active Zellij terminal. Try the control API first,
268
- // then fall back to the keyboard capture element.
269
- if (window.__zjSendControl) {
270
- try { window.__zjSendControl({type: 'Write', bytes}); return; } catch (_) {}
271
- }
272
- const el = window.__zjSoftKbdCapture?.element;
273
- if (!el) return;
274
- const text = String.fromCharCode(...bytes);
275
- el.focus({preventScroll: true});
276
- try { document.execCommand('insertText', false, text); } catch (_) {}
270
+ try { window.__zjSendControl?.({type: 'Write', bytes}); } catch (_) {}
277
271
  }
278
272
  function applyLaneFilter(query) {
279
- const esc = [0x1b], slash = [0x2f];
280
- const bytes = [...esc, ...slash, ...Array.from(query).map(c => c.charCodeAt(0) & 0x7f).filter(b => b >= 0x20)];
281
- sendToTerminal(bytes);
273
+ const bytes = [...new TextEncoder().encode(query)].filter(b => b >= 0x20 && b !== 0x7f);
274
+ sendToTerminal([0x1b, 0x2f, ...bytes]);
275
+ }
276
+ function buildQuickRow() {
277
+ const row = make('div', 'lanes-quick');
278
+ row.hidden = true;
279
+ for (const [label, bytes, name, hold] of [['⏎', [13], 'Enter'], ['y', [121]], ['n', [110], 0, 1], ['Esc', [27]],
280
+ ['^C', [3], 'Control C', 1], ['↑', [27, 91, 65], 'Up'], ['↓', [27, 91, 66], 'Down']]) {
281
+ const key = make('button', hold ? 'is-destructive' : '', label);
282
+ let down = -1;
283
+ key.type = 'button'; if (name) key.setAttribute('aria-label', name);
284
+ key.addEventListener('mousedown', ev => ev.preventDefault());
285
+ key.addEventListener('pointerdown', ev => { down = ev.timeStamp; });
286
+ key.addEventListener('pointerup', ev => {
287
+ if (!hold || (down >= 0 && ev.timeStamp - down >= 400)) sendToTerminal(bytes); else text(subtitle, 'Hold to send');
288
+ down = -1;
289
+ });
290
+ row.append(key);
291
+ }
292
+ return row;
282
293
  }
283
294
  function clearLaneFilter() {
284
- sendToTerminal([0x1b]); // Esc clears the sidebar's filter mode
295
+ sendToTerminal([0x1b]);
285
296
  laneFilterPrev = '';
286
297
  if (laneFilterInput) laneFilterInput.value = '';
287
298
  }
288
299
  function buildLaneFilterBar() {
289
300
  const bar = make('div', 'lanes-filter-bar');
290
301
  bar.setAttribute('aria-label', 'Filter lanes');
291
- bar.setAttribute('hidden', '');
302
+ bar.hidden = true;
292
303
  bar.innerHTML = svg('search');
293
304
  laneFilterInput = make('input');
294
305
  laneFilterInput.type = 'search'; laneFilterInput.placeholder = 'Filter lanes';
@@ -317,6 +328,17 @@ if (typeof module === 'object') module.exports = {laneShellMode};
317
328
  text(document.getElementById('lanes-font-value'), `${fontSize} px`);
318
329
  resize();
319
330
  }
331
+ function lanesTheme() {
332
+ const css = getComputedStyle(root), v = p => css.getPropertyValue(p).trim();
333
+ const t = {background: v('--lanes-surface'), foreground: v('--lanes-ink'), cursor: v('--lanes-accent'),
334
+ selectionBackground: v('--lanes-accent-wash'), selectionForeground: v('--lanes-ink')};
335
+ LANES_ANSI[root.dataset.zjTheme === 'light' ? 'light' : 'dark'].forEach((c, i) => {
336
+ const k = 'Black Red Green Yellow Blue Magenta Cyan White'.split(' ')[i % 8];
337
+ t[i < 8 ? k.toLowerCase() : 'bright' + k] = c;
338
+ });
339
+ return t;
340
+ }
341
+ const applyTheme = () => { if (term) term.options.theme = lanesTheme(); };
320
342
  function installFontControls() {
321
343
  const row = make('div', 'lanes-font-controls');
322
344
  row.append(make('span', '', 'Text size'));
@@ -338,20 +360,32 @@ if (typeof module === 'object') module.exports = {laneShellMode};
338
360
  const name = tab?.name.replace(/ ☰$/, '') || 'Lanes';
339
361
  const picker = tab?.name.endsWith(' ☰');
340
362
  const pane = data()?.panes.find(p => p.pane_id === data()?.active_pane?.pane_id && !p.is_plugin);
363
+ const counts = parseCounts(pickerTitle(name));
364
+ const chips = countParts(counts).map(([glyph, n]) => glyph + n).join(' ');
341
365
  text(heading, name);
342
- text(subtitle, picker ? 'Choose a lane to continue' : pane?.title || 'Your workspace');
366
+ text(subtitle, picker ? chips || 'Choose a lane to continue' : pane?.title || 'Your workspace');
343
367
  lanesButton.setAttribute('aria-pressed', String(!!picker));
368
+ const needs = counts ? counts.approval + counts.input : 0;
369
+ if (needs) lanesButton.dataset.count = String(needs); else delete lanesButton.dataset.count;
344
370
  updateLaneFilterBar(picker);
345
- document.title = `${name} · Lanes`;
371
+ const quick = active && !picker;
372
+ if (quick !== quickOn) {
373
+ quickOn = quick;
374
+ quickRow.hidden = !quick;
375
+ root.style.setProperty('--lanes-quick', quick ? '48px' : '0px');
376
+ resize();
377
+ }
378
+ document.title = `${chips ? chips + ' · ' : ''}${name} · Lanes`;
346
379
  renderCards();
347
380
  }
348
381
  function connect() {
349
382
  if (api || !window.__zjMobileUi || !window.term || !window.__zjSoftKbdCapture?.element) return;
350
- // Wait for server state: resizing before the control socket opens makes 0.45.1
351
- // throw InvalidStateError and leaves the server with the old terminal size.
352
383
  if (!window.__zjMobileUi.getState()?.data?.tabs?.length) return;
353
384
  api = window.__zjMobileUi; term = window.term;
354
- build(); applyFont();
385
+ build(); applyFont(); applyTheme();
386
+ const sync = window.__zjSyncInactiveCursorStyle;
387
+ window.__zjSyncInactiveCursorStyle = () => { sync?.(); applyTheme(); };
388
+ new MutationObserver(applyTheme).observe(root, {attributes: true, attributeFilter: ['data-zj-theme']});
355
389
  const setData = api.setData;
356
390
  api.setData = function(value) {setData.call(api, value); refresh();};
357
391
  const nativeRoot = document.getElementById('zj-mobile-root');
@@ -372,14 +406,12 @@ if (typeof module === 'object') module.exports = {laneShellMode};
372
406
  window.visualViewport?.addEventListener('scroll', viewport);
373
407
  window.addEventListener('zellij:soft-keyboard-visibility', () => {
374
408
  viewport();
375
- // A rotation also shrinks the viewport. Correct upstream's height-only keyboard heuristic.
376
409
  requestAnimationFrame(() => window.__zjSendControl?.({
377
410
  type: 'SoftKeyboardVisibilityChanged', visible: root.classList.contains('lanes-keyboard'),
378
411
  }));
379
412
  });
380
413
  document.addEventListener('focusout', () => requestAnimationFrame(viewport));
381
414
  new MutationObserver(() => {styleLogin(); connect();}).observe(document.body, {childList: true});
382
- // The upstream window hooks are installed just after it appends its mobile root.
383
415
  const awaitingClient = setInterval(() => {connect(); if (api) clearInterval(awaitingClient);}, 200);
384
416
  styleLogin(); connect();
385
417
  })();