@bakapiano/ccsm 0.22.6 → 0.22.7

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 (58) hide show
  1. package/CLAUDE.md +538 -538
  2. package/README.md +189 -189
  3. package/bin/ccsm.js +235 -235
  4. package/lib/cliActivity.js +139 -139
  5. package/lib/codexSeed.js +183 -183
  6. package/lib/config.js +279 -274
  7. package/lib/devices.js +229 -229
  8. package/lib/folders.js +124 -124
  9. package/lib/localCliSessions.js +519 -519
  10. package/lib/persistedSessions.js +129 -129
  11. package/lib/tunnel.js +621 -621
  12. package/lib/webTerminal.js +225 -225
  13. package/lib/workspace.js +233 -233
  14. package/package.json +57 -57
  15. package/public/css/base.css +99 -99
  16. package/public/css/cards.css +183 -183
  17. package/public/css/feedback.css +504 -504
  18. package/public/css/forms.css +453 -453
  19. package/public/css/layout.css +154 -154
  20. package/public/css/modal.css +190 -190
  21. package/public/css/responsive.css +176 -176
  22. package/public/css/sidebar.css +707 -707
  23. package/public/css/terminals.css +546 -546
  24. package/public/css/tokens.css +81 -81
  25. package/public/css/wco.css +196 -196
  26. package/public/css/widgets.css +2725 -2725
  27. package/public/index.html +152 -152
  28. package/public/js/api.js +371 -371
  29. package/public/js/backend.js +149 -149
  30. package/public/js/components/App.js +73 -73
  31. package/public/js/components/DirectoryPicker.js +203 -203
  32. package/public/js/components/EntityFormModal.js +153 -153
  33. package/public/js/components/Modal.js +57 -57
  34. package/public/js/components/OfflineBanner.js +67 -67
  35. package/public/js/components/PageTitleBar.js +13 -13
  36. package/public/js/components/PendingApprovalOverlay.js +128 -128
  37. package/public/js/components/Picker.js +179 -179
  38. package/public/js/components/Popover.js +55 -55
  39. package/public/js/components/RestartOverlay.js +36 -36
  40. package/public/js/components/Sidebar.js +380 -380
  41. package/public/js/components/TerminalInstance.js +28 -0
  42. package/public/js/components/useDragSort.js +67 -67
  43. package/public/js/dialog.js +67 -67
  44. package/public/js/icons.js +212 -212
  45. package/public/js/main.js +296 -296
  46. package/public/js/pages/AboutPage.js +90 -90
  47. package/public/js/pages/ConfigurePage.js +728 -713
  48. package/public/js/pages/LaunchPage.js +421 -421
  49. package/public/js/pages/RemotePage.js +743 -743
  50. package/public/js/pages/SessionsPage.js +53 -53
  51. package/public/js/state.js +335 -335
  52. package/scripts/dev.js +149 -149
  53. package/scripts/install.js +153 -153
  54. package/scripts/restart-helper.js +96 -96
  55. package/scripts/upgrade-helper.js +687 -687
  56. package/server.js +1820 -1807
  57. package/public/manifest.webmanifest +0 -25
  58. package/public/setup/index.html +0 -567
@@ -1,283 +1,283 @@
1
- /* Terminals tab · left rail (active sessions) + right pane (xterm host) */
2
-
3
- /* Terminal-chrome palette. The xterm canvas itself is painted from a JS
4
- theme object (TerminalView.js); these vars colour everything AROUND it —
5
- the pane backdrop, the tab strip, the empty/displaced states, and the
6
- mobile key bar — so the chrome tracks the canvas. Light defaults here;
7
- dark.css overrides under [data-theme="dark"] with the original dark set.
8
- Keep the two in lockstep with the JS THEME_LIGHT / THEME_DARK objects. */
9
- :root {
10
- /* VSCode Light+ neutral panel grays, to sit seamlessly around the
11
- white VSCode terminal canvas (THEME_LIGHT). */
12
- --term-surface: #ffffff; /* matches THEME_LIGHT.background */
13
- --term-on: #333333;
14
- --term-on-dim: rgba(51, 51, 51, 0.70);
15
- --term-on-faint: rgba(51, 51, 51, 0.45);
16
- --term-heading: #1a1a1a;
17
- --term-prompt: #cd3131; /* VSCode ansiRed */
18
- --term-cta-bg: #2c2c2c;
19
- --term-cta-fg: #ffffff;
20
- --term-cta-bg-hover: #000000;
21
- --term-tabstrip: #f0f0f0;
22
- --term-tab: #e4e4e4;
23
- --term-tab-hover: #d8d8d8;
24
- --term-tab-text: rgba(51, 51, 51, 0.70);
25
- --term-keybar-bg: #f0f0f0;
26
- --term-key-fg: #333333;
27
- --term-key-bg: rgba(0, 0, 0, 0.05);
28
- --term-key-border: rgba(0, 0, 0, 0.14);
29
- --term-key-active-bg: rgba(0, 0, 0, 0.12);
30
- --term-key-active-border: rgba(0, 0, 0, 0.28);
31
- --term-key-hint: rgba(0, 0, 0, 0.5);
32
- --term-pop-bg: #f6f6f6;
33
- --term-pop-border: rgba(0, 0, 0, 0.16);
34
- }
35
-
36
- .terminals-layout {
37
- display: grid;
38
- grid-template-columns: 240px 1fr;
39
- gap: var(--s-4);
40
- /* viewport minus page header + footer + padding; lets xterm fill height */
41
- height: calc(100vh - 220px);
42
- min-height: 480px;
43
- }
44
-
45
- .terminals-rail {
46
- background: var(--bg-elev);
47
- border: 1px solid var(--border);
48
- border-radius: var(--r-md);
49
- padding: var(--s-2);
50
- display: flex;
51
- flex-direction: column;
52
- gap: 2px;
53
- overflow-y: auto;
54
- }
55
- .terminals-rail-head {
56
- display: flex;
57
- justify-content: space-between;
58
- align-items: center;
59
- padding: var(--s-2) var(--s-3);
60
- font-size: 11px;
61
- text-transform: uppercase;
62
- letter-spacing: 0.06em;
63
- color: var(--ink-muted);
64
- border-bottom: 1px solid var(--border-soft);
65
- margin-bottom: var(--s-2);
66
- }
67
-
68
- /* button row showing one terminal in the rail */
69
- .terminal-row {
70
- appearance: none;
71
- background: transparent;
72
- border: 0;
73
- width: 100%;
74
- text-align: left;
75
- padding: 8px 10px;
76
- border-radius: var(--r-sm);
77
- cursor: pointer;
78
- display: grid;
79
- grid-template-columns: 10px 1fr auto auto;
80
- align-items: center;
81
- gap: 8px;
82
- color: var(--ink-mid);
83
- font-family: var(--body);
84
- font-size: 13px;
85
- transition: background .12s ease, color .12s ease;
86
- }
87
- .terminal-row:hover { background: var(--bg); color: var(--ink); }
88
- .terminal-row.is-active {
89
- background: var(--sidebar-active);
90
- color: var(--ink);
91
- }
92
- .terminal-row-title {
93
- white-space: nowrap;
94
- overflow: hidden;
95
- text-overflow: ellipsis;
96
- min-width: 0;
97
- font-weight: 500;
98
- }
99
- .terminal-row-meta {
100
- font-family: var(--mono);
101
- font-size: 10.5px;
102
- color: var(--ink-muted);
103
- font-variant-numeric: tabular-nums;
104
- }
105
- .terminal-row-actions { display: inline-flex; }
106
- .terminal-row .action.tiny.danger {
107
- padding: 1px 7px;
108
- font-size: 11px;
109
- min-width: 0;
110
- }
111
-
112
- /* right pane — full-height xterm host */
113
- .terminals-main {
114
- background: var(--bg);
115
- border: 1px solid var(--border);
116
- border-radius: var(--r-md);
117
- padding: var(--s-3);
118
- overflow: hidden;
119
- min-width: 0;
120
- display: flex;
121
- flex-direction: column;
122
- }
123
- .terminal-host {
124
- flex: 1;
125
- min-height: 0;
126
- width: 100%;
127
- /* IME composition (Chinese/Japanese pinyin) lives in absolutely-positioned
128
- .xterm-helper-textarea + .composition-view at the cursor. xterm 5.5 caps
129
- the view's width to the remaining columns, but as a belt-and-braces guard
130
- we still clip here so any residual right-edge overflow is absorbed rather
131
- than expanding the page (it used to trigger a horizontal scrollbar that
132
- "pushed" the layout). Do NOT touch the textarea/composition-view's own
133
- text properties — xterm relies on their single-line behaviour to keep IME
134
- events firing (forcing pre-wrap / break-all eats compositionupdate events
135
- in Chromium and Chinese input stops working entirely). */
136
- overflow: hidden;
137
- contain: layout;
138
- }
139
- /* IME composition box. While you're typing pinyin — before committing the
140
- Chinese characters — xterm writes the in-progress string into
141
- .composition-view at the cursor. VSCode's terminal shows this box in the
142
- terminal's own colours; we do the same. An earlier version hid it outright
143
- (opacity:0) to dodge a layout push, which is why the typed letters were
144
- invisible — but the host's overflow:hidden + contain:layout above already
145
- absorb any overflow, and xterm caps the box width, so hiding it was both
146
- unnecessary and the bug. The helper textarea is left exactly where xterm
147
- puts it (at the cursor, opacity:0) so the OS IME candidate popup anchors
148
- correctly — we no longer move or restyle it. */
149
- .terminal-host .composition-view {
150
- /* !important to beat xterm.css's own `.xterm .composition-view`
151
- (#000/#FFF, same specificity, loaded after us). var() still re-resolves
152
- per theme, so this tracks light/dark automatically. */
153
- background: var(--term-surface) !important;
154
- color: var(--term-on) !important;
155
- border: 1px solid var(--accent);
156
- border-radius: 3px;
157
- padding: 0 3px;
158
- z-index: 5;
159
- }
160
- /* Don't override xterm's background — its renderer (canvas/WebGL) assumes
161
- an opaque surface and ghosts on scroll if we force transparent. The
162
- theme object in TerminalView.js already paints #faf9f5 to match the
163
- surrounding card. */
164
- .terminal-host .xterm-viewport {
165
- scrollbar-width: thin;
166
- }
167
-
168
- .terminal-empty {
169
- height: 100%;
170
- display: flex;
171
- align-items: center;
172
- justify-content: center;
173
- font-size: 13px;
174
- color: var(--ink-muted);
175
- }
176
-
177
- .terminal-empty-page { width: 100%; }
178
-
179
- /* === v1.0 session pane === */
180
-
181
- .sessions-empty {
182
- display: flex;
183
- align-items: center;
184
- justify-content: center;
185
- min-height: 60vh;
186
- /* Decorative faint hairline grid, only in this empty state — adds
187
- editorial atmosphere without affecting any real content view. */
188
- background-image:
189
- linear-gradient(to right, rgba(216, 212, 198, 0.5) 1px, transparent 1px),
190
- linear-gradient(to bottom, rgba(216, 212, 198, 0.5) 1px, transparent 1px);
191
- background-size: 56px 56px;
192
- background-position: center;
193
- position: relative;
194
- }
195
- .sessions-empty::before,
196
- .sessions-empty::after {
197
- content: "";
198
- position: absolute;
199
- inset: 0;
200
- pointer-events: none;
201
- }
202
- .sessions-empty::before {
203
- background: radial-gradient(ellipse at center, transparent 0%, var(--bg) 75%);
204
- }
205
- .sessions-empty-card {
206
- text-align: center;
207
- padding: var(--s-12) var(--s-10);
208
- background: var(--bg-elev);
209
- border: 1px solid var(--border-soft);
210
- border-radius: 6px;
211
- max-width: 440px;
212
- position: relative;
213
- z-index: 1;
214
- box-shadow: var(--shadow-lg);
215
- }
216
- .sessions-empty-card::before {
217
- content: "· EMPTY ·";
218
- display: block;
219
- font-family: var(--mono);
220
- font-size: 10px;
221
- letter-spacing: 0.28em;
222
- color: var(--ink-faint);
223
- margin-bottom: var(--s-4);
224
- }
225
- .sessions-empty-card h2 {
226
- margin: 0 0 var(--s-3);
227
- font-size: 20px;
228
- font-weight: 600;
229
- letter-spacing: -0.015em;
230
- line-height: 1.2;
231
- color: var(--ink);
232
- }
233
- .sessions-empty-card p {
234
- margin: 0 0 var(--s-5);
235
- color: var(--ink-mid);
236
- font-size: 13.5px;
237
- line-height: 1.55;
238
- }
239
-
240
- .session-pane {
241
- display: flex;
242
- flex-direction: column;
243
- /* Fill the entire content area edge-to-edge — negative horizontal margin
244
- cancels .main's horizontal padding so the terminal touches the window
245
- edges (and the sidebar border) with no surrounding chrome. */
246
- flex: 1;
247
- min-height: 0;
248
- height: 100%;
249
- margin: 0 calc(-1 * var(--s-4));
250
- background: var(--bg-elev);
251
- overflow: hidden;
252
- }
253
-
254
- /* Session tabs · sits between the page-title-bar and the session-pane.
255
- One row of small tab buttons; the active one has a darker bg + accent
256
- underline. Last child is a "+" button that bounces to /launch. The
257
- strip is full-bleed (matches .session-pane horizontal extents). */
258
- .session-tabs {
259
- display: flex;
260
- align-items: stretch;
261
- gap: 0;
262
- height: 30px;
263
- flex-shrink: 0;
264
- padding: 0 2px 0 0;
265
- /* Negative bottom margin cancels the tab-panel's gap to the
266
- session-pane underneath, so the strip sits flush against the
267
- terminal. Negative horizontal margin cancels .main's padding so
268
- the strip is full-bleed like the terminal underneath. */
269
- margin: 0 calc(-1 * var(--s-4)) calc(-1 * var(--s-4));
270
- background: var(--term-tabstrip);
271
- border-bottom: 0;
272
- }
273
- .session-tabs-list {
274
- display: flex;
275
- align-items: stretch;
276
- gap: 0;
277
- flex: 1;
278
- min-width: 0;
279
- overflow: hidden;
280
- }
1
+ /* Terminals tab · left rail (active sessions) + right pane (xterm host) */
2
+
3
+ /* Terminal-chrome palette. The xterm canvas itself is painted from a JS
4
+ theme object (TerminalView.js); these vars colour everything AROUND it —
5
+ the pane backdrop, the tab strip, the empty/displaced states, and the
6
+ mobile key bar — so the chrome tracks the canvas. Light defaults here;
7
+ dark.css overrides under [data-theme="dark"] with the original dark set.
8
+ Keep the two in lockstep with the JS THEME_LIGHT / THEME_DARK objects. */
9
+ :root {
10
+ /* VSCode Light+ neutral panel grays, to sit seamlessly around the
11
+ white VSCode terminal canvas (THEME_LIGHT). */
12
+ --term-surface: #ffffff; /* matches THEME_LIGHT.background */
13
+ --term-on: #333333;
14
+ --term-on-dim: rgba(51, 51, 51, 0.70);
15
+ --term-on-faint: rgba(51, 51, 51, 0.45);
16
+ --term-heading: #1a1a1a;
17
+ --term-prompt: #cd3131; /* VSCode ansiRed */
18
+ --term-cta-bg: #2c2c2c;
19
+ --term-cta-fg: #ffffff;
20
+ --term-cta-bg-hover: #000000;
21
+ --term-tabstrip: #f0f0f0;
22
+ --term-tab: #e4e4e4;
23
+ --term-tab-hover: #d8d8d8;
24
+ --term-tab-text: rgba(51, 51, 51, 0.70);
25
+ --term-keybar-bg: #f0f0f0;
26
+ --term-key-fg: #333333;
27
+ --term-key-bg: rgba(0, 0, 0, 0.05);
28
+ --term-key-border: rgba(0, 0, 0, 0.14);
29
+ --term-key-active-bg: rgba(0, 0, 0, 0.12);
30
+ --term-key-active-border: rgba(0, 0, 0, 0.28);
31
+ --term-key-hint: rgba(0, 0, 0, 0.5);
32
+ --term-pop-bg: #f6f6f6;
33
+ --term-pop-border: rgba(0, 0, 0, 0.16);
34
+ }
35
+
36
+ .terminals-layout {
37
+ display: grid;
38
+ grid-template-columns: 240px 1fr;
39
+ gap: var(--s-4);
40
+ /* viewport minus page header + footer + padding; lets xterm fill height */
41
+ height: calc(100vh - 220px);
42
+ min-height: 480px;
43
+ }
44
+
45
+ .terminals-rail {
46
+ background: var(--bg-elev);
47
+ border: 1px solid var(--border);
48
+ border-radius: var(--r-md);
49
+ padding: var(--s-2);
50
+ display: flex;
51
+ flex-direction: column;
52
+ gap: 2px;
53
+ overflow-y: auto;
54
+ }
55
+ .terminals-rail-head {
56
+ display: flex;
57
+ justify-content: space-between;
58
+ align-items: center;
59
+ padding: var(--s-2) var(--s-3);
60
+ font-size: 11px;
61
+ text-transform: uppercase;
62
+ letter-spacing: 0.06em;
63
+ color: var(--ink-muted);
64
+ border-bottom: 1px solid var(--border-soft);
65
+ margin-bottom: var(--s-2);
66
+ }
67
+
68
+ /* button row showing one terminal in the rail */
69
+ .terminal-row {
70
+ appearance: none;
71
+ background: transparent;
72
+ border: 0;
73
+ width: 100%;
74
+ text-align: left;
75
+ padding: 8px 10px;
76
+ border-radius: var(--r-sm);
77
+ cursor: pointer;
78
+ display: grid;
79
+ grid-template-columns: 10px 1fr auto auto;
80
+ align-items: center;
81
+ gap: 8px;
82
+ color: var(--ink-mid);
83
+ font-family: var(--body);
84
+ font-size: 13px;
85
+ transition: background .12s ease, color .12s ease;
86
+ }
87
+ .terminal-row:hover { background: var(--bg); color: var(--ink); }
88
+ .terminal-row.is-active {
89
+ background: var(--sidebar-active);
90
+ color: var(--ink);
91
+ }
92
+ .terminal-row-title {
93
+ white-space: nowrap;
94
+ overflow: hidden;
95
+ text-overflow: ellipsis;
96
+ min-width: 0;
97
+ font-weight: 500;
98
+ }
99
+ .terminal-row-meta {
100
+ font-family: var(--mono);
101
+ font-size: 10.5px;
102
+ color: var(--ink-muted);
103
+ font-variant-numeric: tabular-nums;
104
+ }
105
+ .terminal-row-actions { display: inline-flex; }
106
+ .terminal-row .action.tiny.danger {
107
+ padding: 1px 7px;
108
+ font-size: 11px;
109
+ min-width: 0;
110
+ }
111
+
112
+ /* right pane — full-height xterm host */
113
+ .terminals-main {
114
+ background: var(--bg);
115
+ border: 1px solid var(--border);
116
+ border-radius: var(--r-md);
117
+ padding: var(--s-3);
118
+ overflow: hidden;
119
+ min-width: 0;
120
+ display: flex;
121
+ flex-direction: column;
122
+ }
123
+ .terminal-host {
124
+ flex: 1;
125
+ min-height: 0;
126
+ width: 100%;
127
+ /* IME composition (Chinese/Japanese pinyin) lives in absolutely-positioned
128
+ .xterm-helper-textarea + .composition-view at the cursor. xterm 5.5 caps
129
+ the view's width to the remaining columns, but as a belt-and-braces guard
130
+ we still clip here so any residual right-edge overflow is absorbed rather
131
+ than expanding the page (it used to trigger a horizontal scrollbar that
132
+ "pushed" the layout). Do NOT touch the textarea/composition-view's own
133
+ text properties — xterm relies on their single-line behaviour to keep IME
134
+ events firing (forcing pre-wrap / break-all eats compositionupdate events
135
+ in Chromium and Chinese input stops working entirely). */
136
+ overflow: hidden;
137
+ contain: layout;
138
+ }
139
+ /* IME composition box. While you're typing pinyin — before committing the
140
+ Chinese characters — xterm writes the in-progress string into
141
+ .composition-view at the cursor. VSCode's terminal shows this box in the
142
+ terminal's own colours; we do the same. An earlier version hid it outright
143
+ (opacity:0) to dodge a layout push, which is why the typed letters were
144
+ invisible — but the host's overflow:hidden + contain:layout above already
145
+ absorb any overflow, and xterm caps the box width, so hiding it was both
146
+ unnecessary and the bug. The helper textarea is left exactly where xterm
147
+ puts it (at the cursor, opacity:0) so the OS IME candidate popup anchors
148
+ correctly — we no longer move or restyle it. */
149
+ .terminal-host .composition-view {
150
+ /* !important to beat xterm.css's own `.xterm .composition-view`
151
+ (#000/#FFF, same specificity, loaded after us). var() still re-resolves
152
+ per theme, so this tracks light/dark automatically. */
153
+ background: var(--term-surface) !important;
154
+ color: var(--term-on) !important;
155
+ border: 1px solid var(--accent);
156
+ border-radius: 3px;
157
+ padding: 0 3px;
158
+ z-index: 5;
159
+ }
160
+ /* Don't override xterm's background — its renderer (canvas/WebGL) assumes
161
+ an opaque surface and ghosts on scroll if we force transparent. The
162
+ theme object in TerminalView.js already paints #faf9f5 to match the
163
+ surrounding card. */
164
+ .terminal-host .xterm-viewport {
165
+ scrollbar-width: thin;
166
+ }
167
+
168
+ .terminal-empty {
169
+ height: 100%;
170
+ display: flex;
171
+ align-items: center;
172
+ justify-content: center;
173
+ font-size: 13px;
174
+ color: var(--ink-muted);
175
+ }
176
+
177
+ .terminal-empty-page { width: 100%; }
178
+
179
+ /* === v1.0 session pane === */
180
+
181
+ .sessions-empty {
182
+ display: flex;
183
+ align-items: center;
184
+ justify-content: center;
185
+ min-height: 60vh;
186
+ /* Decorative faint hairline grid, only in this empty state — adds
187
+ editorial atmosphere without affecting any real content view. */
188
+ background-image:
189
+ linear-gradient(to right, rgba(216, 212, 198, 0.5) 1px, transparent 1px),
190
+ linear-gradient(to bottom, rgba(216, 212, 198, 0.5) 1px, transparent 1px);
191
+ background-size: 56px 56px;
192
+ background-position: center;
193
+ position: relative;
194
+ }
195
+ .sessions-empty::before,
196
+ .sessions-empty::after {
197
+ content: "";
198
+ position: absolute;
199
+ inset: 0;
200
+ pointer-events: none;
201
+ }
202
+ .sessions-empty::before {
203
+ background: radial-gradient(ellipse at center, transparent 0%, var(--bg) 75%);
204
+ }
205
+ .sessions-empty-card {
206
+ text-align: center;
207
+ padding: var(--s-12) var(--s-10);
208
+ background: var(--bg-elev);
209
+ border: 1px solid var(--border-soft);
210
+ border-radius: 6px;
211
+ max-width: 440px;
212
+ position: relative;
213
+ z-index: 1;
214
+ box-shadow: var(--shadow-lg);
215
+ }
216
+ .sessions-empty-card::before {
217
+ content: "· EMPTY ·";
218
+ display: block;
219
+ font-family: var(--mono);
220
+ font-size: 10px;
221
+ letter-spacing: 0.28em;
222
+ color: var(--ink-faint);
223
+ margin-bottom: var(--s-4);
224
+ }
225
+ .sessions-empty-card h2 {
226
+ margin: 0 0 var(--s-3);
227
+ font-size: 20px;
228
+ font-weight: 600;
229
+ letter-spacing: -0.015em;
230
+ line-height: 1.2;
231
+ color: var(--ink);
232
+ }
233
+ .sessions-empty-card p {
234
+ margin: 0 0 var(--s-5);
235
+ color: var(--ink-mid);
236
+ font-size: 13.5px;
237
+ line-height: 1.55;
238
+ }
239
+
240
+ .session-pane {
241
+ display: flex;
242
+ flex-direction: column;
243
+ /* Fill the entire content area edge-to-edge — negative horizontal margin
244
+ cancels .main's horizontal padding so the terminal touches the window
245
+ edges (and the sidebar border) with no surrounding chrome. */
246
+ flex: 1;
247
+ min-height: 0;
248
+ height: 100%;
249
+ margin: 0 calc(-1 * var(--s-4));
250
+ background: var(--bg-elev);
251
+ overflow: hidden;
252
+ }
253
+
254
+ /* Session tabs · sits between the page-title-bar and the session-pane.
255
+ One row of small tab buttons; the active one has a darker bg + accent
256
+ underline. Last child is a "+" button that bounces to /launch. The
257
+ strip is full-bleed (matches .session-pane horizontal extents). */
258
+ .session-tabs {
259
+ display: flex;
260
+ align-items: stretch;
261
+ gap: 0;
262
+ height: 30px;
263
+ flex-shrink: 0;
264
+ padding: 0 2px 0 0;
265
+ /* Negative bottom margin cancels the tab-panel's gap to the
266
+ session-pane underneath, so the strip sits flush against the
267
+ terminal. Negative horizontal margin cancels .main's padding so
268
+ the strip is full-bleed like the terminal underneath. */
269
+ margin: 0 calc(-1 * var(--s-4)) calc(-1 * var(--s-4));
270
+ background: var(--term-tabstrip);
271
+ border-bottom: 0;
272
+ }
273
+ .session-tabs-list {
274
+ display: flex;
275
+ align-items: stretch;
276
+ gap: 0;
277
+ flex: 1;
278
+ min-width: 0;
279
+ overflow: hidden;
280
+ }
281
281
  .session-tabs-right {
282
282
  display: flex;
283
283
  align-items: center;
@@ -285,13 +285,13 @@
285
285
  gap: 4px;
286
286
  padding-right: 2px;
287
287
  }
288
- /* Close the gap to the page-title-bar above — only when there IS one.
289
- In standalone PWA the title-bar is display:none and .session-tabs is
290
- the first tab-panel child; a negative margin-top there would push
291
- the strip up over the OS title-bar border. */
292
- .page-title-bar + .session-tabs {
293
- margin-top: calc(-1 * var(--s-4));
294
- }
288
+ /* Close the gap to the page-title-bar above — only when there IS one.
289
+ In standalone PWA the title-bar is display:none and .session-tabs is
290
+ the first tab-panel child; a negative margin-top there would push
291
+ the strip up over the OS title-bar border. */
292
+ .page-title-bar + .session-tabs {
293
+ margin-top: calc(-1 * var(--s-4));
294
+ }
295
295
  .session-tab {
296
296
  appearance: none;
297
297
  background: var(--term-tab);
@@ -303,7 +303,7 @@
303
303
  gap: 6px;
304
304
  font: inherit;
305
305
  font-size: 12px;
306
- color: var(--term-tab-text);
306
+ color: var(--term-tab-text);
307
307
  cursor: pointer;
308
308
  max-width: 200px;
309
309
  min-width: 0;
@@ -398,8 +398,8 @@
398
398
  background: transparent;
399
399
  max-width: none;
400
400
  padding: 0 8px;
401
- color: #fff;
402
- }
401
+ color: #fff;
402
+ }
403
403
  .session-tab-add:hover { background: rgba(255, 255, 255, 0.1); color: #fff; }
404
404
  .session-tab-add svg { width: 14px; height: 14px; }
405
405
 
@@ -427,56 +427,56 @@
427
427
  /* Kebab in the page-title-bar (top-right). Compact 24px square so it
428
428
  doesn't dominate the masthead. In WCO mode the title-bar already
429
429
  reserves padding-right for OS controls, so this slides cleanly to
430
- the left of them. */
431
- .session-menu-btn {
432
- appearance: none;
433
- background: transparent;
434
- border: 0;
435
- width: 26px;
436
- height: 26px;
437
- border-radius: 5px;
438
- display: inline-flex;
439
- align-items: center;
440
- justify-content: center;
441
- /* Follow the terminal foreground so the dots read on both the light
442
- (#f0f0f0) and dark (#252526) tab strip. The old hardcoded #fff was
443
- invisible on the light strip. */
444
- color: var(--term-on);
445
- cursor: pointer;
446
- flex-shrink: 0;
447
- transition: background-color .12s, color .12s;
448
- }
449
- /* Neutral-grey hover tint works on either strip colour (darkens the light
450
- one, lightens the dark one) without needing a per-theme override. */
430
+ the left of them. */
431
+ .session-menu-btn {
432
+ appearance: none;
433
+ background: transparent;
434
+ border: 0;
435
+ width: 26px;
436
+ height: 26px;
437
+ border-radius: 5px;
438
+ display: inline-flex;
439
+ align-items: center;
440
+ justify-content: center;
441
+ /* Follow the terminal foreground so the dots read on both the light
442
+ (#f0f0f0) and dark (#252526) tab strip. The old hardcoded #fff was
443
+ invisible on the light strip. */
444
+ color: var(--term-on);
445
+ cursor: pointer;
446
+ flex-shrink: 0;
447
+ transition: background-color .12s, color .12s;
448
+ }
449
+ /* Neutral-grey hover tint works on either strip colour (darkens the light
450
+ one, lightens the dark one) without needing a per-theme override. */
451
451
  .session-menu-btn:hover:not(:disabled) { background: rgba(128, 128, 128, 0.2); color: var(--term-on); }
452
452
  .session-menu-btn:disabled { opacity: .55; cursor: wait; }
453
453
  .session-menu-btn svg { width: 16px; height: 16px; }
454
-
455
- .session-menu {
456
- background: var(--bg-elev);
457
- border: 1px solid var(--border);
458
- border-radius: 6px;
459
- padding: 4px;
460
- box-shadow: var(--shadow-md, 0 4px 16px rgba(0,0,0,0.08));
461
- display: flex;
462
- flex-direction: column;
463
- gap: 2px;
464
- }
465
- .session-menu-item {
466
- appearance: none;
467
- background: transparent;
468
- border: 0;
469
- padding: 7px 10px;
470
- border-radius: 4px;
471
- display: flex;
472
- align-items: center;
473
- gap: 8px;
474
- font: inherit;
475
- font-size: 13px;
476
- color: var(--ink);
477
- cursor: pointer;
478
- text-align: left;
479
- }
454
+
455
+ .session-menu {
456
+ background: var(--bg-elev);
457
+ border: 1px solid var(--border);
458
+ border-radius: 6px;
459
+ padding: 4px;
460
+ box-shadow: var(--shadow-md, 0 4px 16px rgba(0,0,0,0.08));
461
+ display: flex;
462
+ flex-direction: column;
463
+ gap: 2px;
464
+ }
465
+ .session-menu-item {
466
+ appearance: none;
467
+ background: transparent;
468
+ border: 0;
469
+ padding: 7px 10px;
470
+ border-radius: 4px;
471
+ display: flex;
472
+ align-items: center;
473
+ gap: 8px;
474
+ font: inherit;
475
+ font-size: 13px;
476
+ color: var(--ink);
477
+ cursor: pointer;
478
+ text-align: left;
479
+ }
480
480
  .session-menu-item:hover { background: var(--bg); }
481
481
  .session-menu-item.danger { color: var(--danger, #b73f3f); }
482
482
  .session-menu-item.danger:hover { background: rgba(183, 63, 63, 0.08); }
@@ -497,48 +497,48 @@
497
497
  }
498
498
 
499
499
  .session-pane-head {
500
- display: flex;
501
- align-items: center;
502
- gap: var(--s-3);
503
- padding: var(--s-3) var(--s-4);
504
- border-bottom: 1px solid var(--border);
505
- background: var(--bg);
506
- flex-wrap: wrap;
507
- }
508
- .session-pane-title {
509
- display: flex;
510
- align-items: center;
511
- gap: var(--s-2);
512
- }
513
- .session-pane-title h2 {
514
- margin: 0;
515
- font-size: 14.5px;
516
- font-weight: 600;
517
- }
518
- .session-pane-meta {
519
- display: flex;
520
- gap: var(--s-3);
521
- align-items: center;
522
- font-size: 11.5px;
523
- flex: 1;
524
- overflow: hidden;
525
- }
526
- .session-pane-meta .mono {
527
- font-family: var(--mono);
528
- color: var(--ink-mid);
529
- white-space: nowrap;
530
- overflow: hidden;
531
- text-overflow: ellipsis;
532
- max-width: 50%;
533
- }
534
- .session-pane-meta .muted {
535
- color: var(--ink-muted);
536
- }
537
- .session-pane-actions {
538
- display: flex;
539
- gap: var(--s-2);
540
- flex-shrink: 0;
541
- }
500
+ display: flex;
501
+ align-items: center;
502
+ gap: var(--s-3);
503
+ padding: var(--s-3) var(--s-4);
504
+ border-bottom: 1px solid var(--border);
505
+ background: var(--bg);
506
+ flex-wrap: wrap;
507
+ }
508
+ .session-pane-title {
509
+ display: flex;
510
+ align-items: center;
511
+ gap: var(--s-2);
512
+ }
513
+ .session-pane-title h2 {
514
+ margin: 0;
515
+ font-size: 14.5px;
516
+ font-weight: 600;
517
+ }
518
+ .session-pane-meta {
519
+ display: flex;
520
+ gap: var(--s-3);
521
+ align-items: center;
522
+ font-size: 11.5px;
523
+ flex: 1;
524
+ overflow: hidden;
525
+ }
526
+ .session-pane-meta .mono {
527
+ font-family: var(--mono);
528
+ color: var(--ink-mid);
529
+ white-space: nowrap;
530
+ overflow: hidden;
531
+ text-overflow: ellipsis;
532
+ max-width: 50%;
533
+ }
534
+ .session-pane-meta .muted {
535
+ color: var(--ink-muted);
536
+ }
537
+ .session-pane-actions {
538
+ display: flex;
539
+ gap: var(--s-2);
540
+ flex-shrink: 0;
541
+ }
542
542
  .session-pane-body {
543
543
  flex: 1;
544
544
  min-height: 0;
@@ -576,170 +576,170 @@
576
576
  min-height: 0;
577
577
  height: auto;
578
578
  }
579
- .session-pane-body .terminal-empty {
580
- background: var(--term-surface);
581
- color: var(--term-on);
582
- display: flex;
583
- flex-direction: column;
584
- align-items: center;
585
- justify-content: center;
586
- gap: var(--s-3);
587
- height: 100%;
588
- font-size: 13px;
589
- }
590
- .session-pane-body .terminal-empty .mono {
591
- color: var(--term-prompt);
592
- }
593
- .session-pane-body .terminal-empty .action.primary {
594
- background: var(--term-cta-bg);
595
- color: var(--term-cta-fg);
596
- border-color: var(--term-cta-bg);
597
- }
598
- .session-pane-body .terminal-empty .action.primary:hover {
599
- background: var(--term-cta-bg-hover);
600
- border-color: var(--term-cta-bg-hover);
601
- }
602
-
603
- /* Displaced state — shown when the server kicks us off because another
604
- client attached to the same session (latest-wins). Same dark surface
605
- as terminal-empty so the transition from running terminal → displaced
606
- doesn't flash a colour change. */
607
- .terminal-displaced {
608
- background: var(--term-surface);
609
- color: var(--term-on);
610
- display: flex;
611
- align-items: center;
612
- justify-content: center;
613
- height: 100%;
614
- padding: var(--s-5);
615
- }
616
- .terminal-displaced-card {
617
- max-width: 460px;
618
- text-align: center;
619
- display: flex;
620
- flex-direction: column;
621
- gap: var(--s-3);
622
- }
623
- .terminal-displaced-card h2 {
624
- margin: 0;
625
- font-size: 16px;
626
- font-weight: 600;
627
- color: var(--term-heading);
628
- letter-spacing: -0.005em;
629
- }
630
- .terminal-displaced-card p {
631
- margin: 0;
632
- font-size: 13px;
633
- line-height: 1.55;
634
- color: var(--term-on-dim);
635
- }
636
- .terminal-displaced-actions {
637
- margin-top: var(--s-2);
638
- display: flex;
639
- justify-content: center;
640
- }
641
- .terminal-displaced-card .action.primary {
642
- background: var(--term-cta-bg);
643
- color: var(--term-cta-fg);
644
- border-color: var(--term-cta-bg);
645
- padding: 9px 20px;
646
- font-size: 13px;
647
- }
648
- .terminal-displaced-card .action.primary:hover {
649
- background: var(--term-cta-bg-hover);
650
- border-color: var(--term-cta-bg-hover);
651
- }
652
- .terminal-displaced-hint {
653
- font-size: 11.5px !important;
654
- color: var(--term-on-faint) !important;
655
- }
656
-
657
- /* ─── Mobile terminal accessory bar (TerminalKeyBar.js) ───────────────
658
- Floats just above the soft keyboard via a JS-set `bottom` offset
659
- (visualViewport keyboard height). Styled against the dark terminal
660
- palette — it visually belongs to the terminal, not the cream chrome —
661
- so it reads as one surface with the xterm canvas above it. */
662
- .term-keybar {
663
- position: fixed;
664
- left: 0;
665
- right: 0;
666
- z-index: 215; /* above the mobile FAB (210) */
667
- background: var(--term-keybar-bg);
668
- border-top: 1px solid var(--term-key-border);
669
- padding: 6px 8px;
670
- touch-action: manipulation; /* kill the 300ms double-tap-zoom delay */
671
- user-select: none;
672
- -webkit-user-select: none;
673
- /* NOT overflow:auto here — that would clip the Ctrl popover (which sits
674
- at bottom:100%, above the bar). The horizontal scroll lives on the
675
- inner .term-keybar-row instead. */
676
- }
677
- /* Inner scroll row — holds the keys; scrolls horizontally if they don't
678
- fit, without clipping the popover that escapes the bar upward. */
679
- .term-keybar-row {
680
- display: flex;
681
- gap: 6px;
682
- align-items: center;
683
- overflow-x: auto;
684
- -webkit-overflow-scrolling: touch;
685
- white-space: nowrap;
686
- }
687
- .term-keybar-row::-webkit-scrollbar { display: none; }
688
-
689
- .tkb-key {
690
- flex: 0 0 auto;
691
- min-width: 42px;
692
- height: 38px;
693
- display: inline-flex;
694
- align-items: center;
695
- justify-content: center;
696
- padding: 0 12px;
697
- font-family: var(--mono);
698
- font-size: 13px;
699
- line-height: 1;
700
- color: var(--term-key-fg);
701
- background: var(--term-key-bg);
702
- border: 1px solid var(--term-key-border);
703
- border-radius: 8px;
704
- touch-action: manipulation;
705
- -webkit-tap-highlight-color: transparent;
706
- }
707
- .tkb-key:active,
708
- .tkb-key.is-active {
709
- background: var(--term-key-active-bg);
710
- border-color: var(--term-key-active-border);
711
- }
712
- .tkb-arrow { padding: 0 10px; }
713
- .tkb-arrow svg { width: 18px; height: 18px; }
714
- /* S-Tab carries a multi-char label — let it size to content. */
715
- .tkb-wide { padding: 0 12px; }
716
- /* The ↵ glyph renders a touch small in the mono stack; bump it so it
717
- matches the arrow icons' optical weight. */
718
- .tkb-glyph { font-size: 17px; line-height: 1; }
719
-
720
- /* Ctrl combos — a wrap grid that pops ABOVE the bar (bottom:100%). */
721
- .term-keybar-pop {
722
- position: absolute;
723
- bottom: 100%;
724
- left: 8px;
725
- right: 8px;
726
- z-index: 1; /* above the key row inside the bar's context */
727
- margin-bottom: 6px;
728
- display: grid;
729
- grid-template-columns: repeat(5, 1fr);
730
- gap: 6px;
731
- padding: 8px;
732
- background: var(--term-pop-bg);
733
- border: 1px solid var(--term-pop-border);
734
- border-radius: 10px;
735
- box-shadow: 0 -8px 24px -8px rgba(0, 0, 0, 0.5);
736
- }
737
- .tkb-combo {
738
- flex-direction: column;
739
- height: auto;
740
- min-width: 0;
741
- padding: 7px 4px;
742
- gap: 2px;
743
- }
744
- .tkb-combo-label { font-family: var(--mono); font-size: 13px; color: var(--term-key-fg); }
745
- .tkb-combo-hint { font-size: 9.5px; color: var(--term-key-hint); letter-spacing: 0.01em; }
579
+ .session-pane-body .terminal-empty {
580
+ background: var(--term-surface);
581
+ color: var(--term-on);
582
+ display: flex;
583
+ flex-direction: column;
584
+ align-items: center;
585
+ justify-content: center;
586
+ gap: var(--s-3);
587
+ height: 100%;
588
+ font-size: 13px;
589
+ }
590
+ .session-pane-body .terminal-empty .mono {
591
+ color: var(--term-prompt);
592
+ }
593
+ .session-pane-body .terminal-empty .action.primary {
594
+ background: var(--term-cta-bg);
595
+ color: var(--term-cta-fg);
596
+ border-color: var(--term-cta-bg);
597
+ }
598
+ .session-pane-body .terminal-empty .action.primary:hover {
599
+ background: var(--term-cta-bg-hover);
600
+ border-color: var(--term-cta-bg-hover);
601
+ }
602
+
603
+ /* Displaced state — shown when the server kicks us off because another
604
+ client attached to the same session (latest-wins). Same dark surface
605
+ as terminal-empty so the transition from running terminal → displaced
606
+ doesn't flash a colour change. */
607
+ .terminal-displaced {
608
+ background: var(--term-surface);
609
+ color: var(--term-on);
610
+ display: flex;
611
+ align-items: center;
612
+ justify-content: center;
613
+ height: 100%;
614
+ padding: var(--s-5);
615
+ }
616
+ .terminal-displaced-card {
617
+ max-width: 460px;
618
+ text-align: center;
619
+ display: flex;
620
+ flex-direction: column;
621
+ gap: var(--s-3);
622
+ }
623
+ .terminal-displaced-card h2 {
624
+ margin: 0;
625
+ font-size: 16px;
626
+ font-weight: 600;
627
+ color: var(--term-heading);
628
+ letter-spacing: -0.005em;
629
+ }
630
+ .terminal-displaced-card p {
631
+ margin: 0;
632
+ font-size: 13px;
633
+ line-height: 1.55;
634
+ color: var(--term-on-dim);
635
+ }
636
+ .terminal-displaced-actions {
637
+ margin-top: var(--s-2);
638
+ display: flex;
639
+ justify-content: center;
640
+ }
641
+ .terminal-displaced-card .action.primary {
642
+ background: var(--term-cta-bg);
643
+ color: var(--term-cta-fg);
644
+ border-color: var(--term-cta-bg);
645
+ padding: 9px 20px;
646
+ font-size: 13px;
647
+ }
648
+ .terminal-displaced-card .action.primary:hover {
649
+ background: var(--term-cta-bg-hover);
650
+ border-color: var(--term-cta-bg-hover);
651
+ }
652
+ .terminal-displaced-hint {
653
+ font-size: 11.5px !important;
654
+ color: var(--term-on-faint) !important;
655
+ }
656
+
657
+ /* ─── Mobile terminal accessory bar (TerminalKeyBar.js) ───────────────
658
+ Floats just above the soft keyboard via a JS-set `bottom` offset
659
+ (visualViewport keyboard height). Styled against the dark terminal
660
+ palette — it visually belongs to the terminal, not the cream chrome —
661
+ so it reads as one surface with the xterm canvas above it. */
662
+ .term-keybar {
663
+ position: fixed;
664
+ left: 0;
665
+ right: 0;
666
+ z-index: 215; /* above the mobile FAB (210) */
667
+ background: var(--term-keybar-bg);
668
+ border-top: 1px solid var(--term-key-border);
669
+ padding: 6px 8px;
670
+ touch-action: manipulation; /* kill the 300ms double-tap-zoom delay */
671
+ user-select: none;
672
+ -webkit-user-select: none;
673
+ /* NOT overflow:auto here — that would clip the Ctrl popover (which sits
674
+ at bottom:100%, above the bar). The horizontal scroll lives on the
675
+ inner .term-keybar-row instead. */
676
+ }
677
+ /* Inner scroll row — holds the keys; scrolls horizontally if they don't
678
+ fit, without clipping the popover that escapes the bar upward. */
679
+ .term-keybar-row {
680
+ display: flex;
681
+ gap: 6px;
682
+ align-items: center;
683
+ overflow-x: auto;
684
+ -webkit-overflow-scrolling: touch;
685
+ white-space: nowrap;
686
+ }
687
+ .term-keybar-row::-webkit-scrollbar { display: none; }
688
+
689
+ .tkb-key {
690
+ flex: 0 0 auto;
691
+ min-width: 42px;
692
+ height: 38px;
693
+ display: inline-flex;
694
+ align-items: center;
695
+ justify-content: center;
696
+ padding: 0 12px;
697
+ font-family: var(--mono);
698
+ font-size: 13px;
699
+ line-height: 1;
700
+ color: var(--term-key-fg);
701
+ background: var(--term-key-bg);
702
+ border: 1px solid var(--term-key-border);
703
+ border-radius: 8px;
704
+ touch-action: manipulation;
705
+ -webkit-tap-highlight-color: transparent;
706
+ }
707
+ .tkb-key:active,
708
+ .tkb-key.is-active {
709
+ background: var(--term-key-active-bg);
710
+ border-color: var(--term-key-active-border);
711
+ }
712
+ .tkb-arrow { padding: 0 10px; }
713
+ .tkb-arrow svg { width: 18px; height: 18px; }
714
+ /* S-Tab carries a multi-char label — let it size to content. */
715
+ .tkb-wide { padding: 0 12px; }
716
+ /* The ↵ glyph renders a touch small in the mono stack; bump it so it
717
+ matches the arrow icons' optical weight. */
718
+ .tkb-glyph { font-size: 17px; line-height: 1; }
719
+
720
+ /* Ctrl combos — a wrap grid that pops ABOVE the bar (bottom:100%). */
721
+ .term-keybar-pop {
722
+ position: absolute;
723
+ bottom: 100%;
724
+ left: 8px;
725
+ right: 8px;
726
+ z-index: 1; /* above the key row inside the bar's context */
727
+ margin-bottom: 6px;
728
+ display: grid;
729
+ grid-template-columns: repeat(5, 1fr);
730
+ gap: 6px;
731
+ padding: 8px;
732
+ background: var(--term-pop-bg);
733
+ border: 1px solid var(--term-pop-border);
734
+ border-radius: 10px;
735
+ box-shadow: 0 -8px 24px -8px rgba(0, 0, 0, 0.5);
736
+ }
737
+ .tkb-combo {
738
+ flex-direction: column;
739
+ height: auto;
740
+ min-width: 0;
741
+ padding: 7px 4px;
742
+ gap: 2px;
743
+ }
744
+ .tkb-combo-label { font-family: var(--mono); font-size: 13px; color: var(--term-key-fg); }
745
+ .tkb-combo-hint { font-size: 9.5px; color: var(--term-key-hint); letter-spacing: 0.01em; }