@bakapiano/ccsm 0.22.6 → 0.22.8

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 (61) hide show
  1. package/CLAUDE.md +521 -540
  2. package/README.md +186 -189
  3. package/bin/ccsm.js +235 -235
  4. package/lib/cliActivity.js +36 -139
  5. package/lib/codexSeed.js +126 -183
  6. package/lib/config.js +277 -274
  7. package/lib/devices.js +229 -229
  8. package/lib/folders.js +124 -124
  9. package/lib/persistedSessions.js +179 -139
  10. package/lib/tunnel.js +621 -621
  11. package/lib/webTerminal.js +225 -225
  12. package/lib/winPath.js +1 -1
  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 +2347 -2725
  27. package/public/index.html +152 -152
  28. package/public/js/api.js +349 -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 +730 -713
  48. package/public/js/pages/LaunchPage.js +403 -421
  49. package/public/js/pages/RemotePage.js +743 -743
  50. package/public/js/pages/SessionsPage.js +54 -54
  51. package/public/js/state.js +335 -335
  52. package/public/js/util.js +1 -1
  53. package/scripts/dev.js +149 -149
  54. package/scripts/install.js +153 -153
  55. package/scripts/restart-helper.js +96 -96
  56. package/scripts/upgrade-helper.js +687 -687
  57. package/server.js +1748 -1817
  58. package/lib/localCliSessions.js +0 -519
  59. package/public/js/components/AdoptModal.js +0 -261
  60. package/public/manifest.webmanifest +0 -25
  61. package/public/setup/index.html +0 -567
@@ -1,707 +1,707 @@
1
- /* Left collapsible sidebar nav · brand mark · util items · collapse toggle */
2
-
3
- .sidebar {
4
- /* One value drives all three "section break" gaps in the sidebar
5
- column: brand-strip → first nav item, last nav item → "Sessions"
6
- header, and "Sessions" header → first folder. Bump or shrink to
7
- adjust how breathy the rail feels. */
8
- --sidebar-section-gap: var(--s-2);
9
- position: sticky;
10
- top: 0;
11
- height: 100vh;
12
- background: var(--ui-bg);
13
- border-right: 1px solid var(--ui-border);
14
- display: flex;
15
- flex-direction: column;
16
- padding: 0 var(--s-2);
17
- overflow: visible;
18
- transition: padding .25s cubic-bezier(.4, 0, .2, 1);
19
- }
20
- .sidebar[data-collapsed="true"] {
21
- padding: 0;
22
- }
23
- /* Collapsed: every clickable row becomes a 28x28 square, centered in
24
- the narrow sidebar column. Width auto + margin auto so the square
25
- sits centered regardless of what padding the parent has. */
26
- .sidebar[data-collapsed="true"] .nav-item,
27
- .sidebar[data-collapsed="true"] .util-item,
28
- .sidebar[data-collapsed="true"] .sidebar-brand-button,
29
- .sidebar[data-collapsed="true"] .tree-folder-head,
30
- .sidebar[data-collapsed="true"] .tree-session {
31
- width: 28px;
32
- height: 28px;
33
- min-height: 28px;
34
- padding: 0;
35
- margin: 0 auto;
36
- justify-content: center;
37
- gap: 0;
38
- }
39
- .sidebar[data-collapsed="true"] .sidebar-top {
40
- padding: 0;
41
- min-height: 40px;
42
- justify-content: center;
43
- }
44
- .sidebar[data-collapsed="true"] .sidebar-top .collapse-toggle {
45
- width: 40px;
46
- height: 40px;
47
- min-height: 40px;
48
- margin: 0;
49
- }
50
-
51
- .sidebar-brand {
52
- display: flex;
53
- align-items: center;
54
- gap: var(--s-2);
55
- padding: 0 var(--s-2);
56
- min-height: 28px;
57
- height: 28px;
58
- }
59
- .brand-mark {
60
- display: inline-flex;
61
- align-items: center;
62
- justify-content: center;
63
- width: 20px;
64
- height: 20px;
65
- flex: 0 0 20px;
66
- background: transparent;
67
- color: var(--accent);
68
- }
69
- .brand-mark svg { display: block; width: 20px; height: 20px; }
70
- .brand-name {
71
- font-size: 14px;
72
- font-weight: 600;
73
- letter-spacing: -0.02em;
74
- color: var(--ink);
75
- white-space: nowrap;
76
- opacity: 1;
77
- line-height: 1;
78
- transition: opacity .15s ease;
79
- }
80
- .brand-dot { color: var(--accent); }
81
- .sidebar[data-collapsed="true"] .brand-name { display: none; }
82
- .sidebar[data-collapsed="true"] .brand-version { display: none; }
83
- .sidebar[data-collapsed="true"] .sidebar-brand { justify-content: center; padding-left: 0; padding-right: 0; }
84
-
85
- .sidebar-nav {
86
- display: flex;
87
- flex-direction: column;
88
- gap: 2px;
89
- flex: 0 0 auto;
90
- }
91
-
92
- .nav-item, .util-item {
93
- appearance: none;
94
- background: transparent;
95
- border: 0;
96
- display: flex;
97
- align-items: center;
98
- gap: var(--s-3);
99
- width: 100%;
100
- padding: 0 12px;
101
- min-height: 42px;
102
- border-radius: var(--r-sm);
103
- cursor: pointer;
104
- color: var(--ink);
105
- font-family: var(--body);
106
- font-size: 13px;
107
- font-weight: 400;
108
- text-align: left;
109
- transition: background .12s ease, color .12s ease;
110
- position: relative;
111
- }
112
- .nav-item:hover, .util-item:hover {
113
- background: var(--sidebar-hover);
114
- }
115
- .nav-item[aria-selected="true"] {
116
- background: var(--sidebar-active);
117
- color: var(--ink);
118
- }
119
-
120
- /* Unsaved-changes dot next to nav label */
121
- .nav-item.has-changes::after {
122
- content: "";
123
- position: absolute;
124
- right: 10px;
125
- top: 50%;
126
- transform: translateY(-50%);
127
- width: 7px;
128
- height: 7px;
129
- border-radius: 50%;
130
- background: var(--ink);
131
- box-shadow: 0 0 0 0 rgba(26, 24, 21, 0.30);
132
- animation: dirty-pulse 2s ease-in-out infinite;
133
- }
134
- @keyframes dirty-pulse {
135
- 0%, 100% { box-shadow: 0 0 0 0 rgba(26, 24, 21, 0.22); }
136
- 50% { box-shadow: 0 0 0 4px rgba(26, 24, 21, 0); }
137
- }
138
- .sidebar[data-collapsed="true"] .nav-item.has-changes::after {
139
- right: auto;
140
- top: 6px;
141
- left: 28px;
142
- }
143
-
144
- .nav-icon {
145
- display: inline-flex;
146
- width: 20px;
147
- height: 20px;
148
- flex: 0 0 20px;
149
- color: currentColor;
150
- }
151
- .nav-icon svg { width: 100%; height: 100%; }
152
- .nav-label {
153
- white-space: nowrap;
154
- opacity: 1;
155
- transition: opacity .15s ease;
156
- flex: 1;
157
- }
158
- .sidebar[data-collapsed="true"] .nav-label { opacity: 0; pointer-events: none; }
159
- /* Collapsed sidebar (60px wide): hide the label entirely so the icon
160
- centers in the narrow column instead of being pushed off-screen by
161
- the still-laid-out (but invisible) label text. Same for the badge. */
162
- .sidebar[data-collapsed="true"] .nav-label,
163
- .sidebar[data-collapsed="true"] .nav-badge {
164
- display: none;
165
- }
166
-
167
- .nav-badge {
168
- font-family: var(--mono);
169
- font-size: 10.5px;
170
- background: var(--border-soft);
171
- color: var(--ink-muted);
172
- padding: 1px 6px;
173
- border-radius: 4px;
174
- font-variant-numeric: tabular-nums;
175
- opacity: 1;
176
- transition: opacity .15s ease;
177
- }
178
- .sidebar[data-collapsed="true"] .nav-badge { opacity: 0; }
179
- .nav-item[aria-selected="true"] .nav-badge {
180
- background: var(--bg-elev);
181
- color: var(--ink-mid);
182
- }
183
-
184
- .sidebar-divider {
185
- margin: var(--s-3) var(--s-2);
186
- border-top: 1px solid var(--border);
187
- }
188
-
189
- .sidebar-utility {
190
- display: flex;
191
- flex-direction: column;
192
- gap: 2px;
193
- }
194
-
195
- .sidebar-foot {
196
- margin-top: auto;
197
- padding-top: 2px;
198
- display: flex;
199
- flex-direction: row;
200
- justify-content: flex-end;
201
- align-items: center;
202
- gap: 2px;
203
- }
204
- /* Brand block at the bottom of the sidebar. Clickable: navigates to
205
- About. Matches nav-item height so collapsed sidebar reads as a
206
- uniform column of equally-sized icons. */
207
- .sidebar-brand-button {
208
- appearance: none;
209
- background: transparent;
210
- border: 0;
211
- display: flex;
212
- align-items: center;
213
- gap: 8px;
214
- width: 100%;
215
- padding: 0 var(--s-2);
216
- min-height: 28px;
217
- height: 28px;
218
- cursor: pointer;
219
- border-radius: 4px;
220
- text-align: left;
221
- font: inherit;
222
- color: var(--ink);
223
- transition: background .12s;
224
- }
225
- .sidebar-brand-button[aria-selected="true"],
226
- .sidebar-brand-button[aria-selected="true"]:hover {
227
- background: var(--sidebar-active);
228
- font-weight: 500;
229
- }
230
- .sidebar-brand-button:hover { background: var(--sidebar-hover); }
231
- .sidebar-foot .brand-mark {
232
- width: 14px;
233
- height: 14px;
234
- flex: 0 0 14px;
235
- display: inline-flex;
236
- align-items: center;
237
- justify-content: center;
238
- /* Lock the SVG box exactly to 14×14 so flex centering doesn't fight
239
- the SVG's intrinsic dimensions; otherwise the mark drifts a couple
240
- pixels low because <svg width=32 height=32> wants more vertical
241
- space than the 14px row gives it. */
242
- line-height: 0;
243
- }
244
- .sidebar-foot .brand-mark svg {
245
- width: 14px;
246
- height: 14px;
247
- display: block;
248
- }
249
- .sidebar-foot .brand-name {
250
- font-size: 13px;
251
- font-weight: 400;
252
- line-height: 1;
253
- }
254
- .brand-version {
255
- margin-left: auto;
256
- font-family: var(--mono);
257
- font-size: 10px;
258
- color: var(--ink-muted);
259
- font-variant-numeric: tabular-nums;
260
- }
261
-
262
- /* Top strip · single row, currently hosts only the collapse toggle on
263
- the right. Matches the page-title-bar height so the topmost row of
264
- the window reads as one unified band. */
265
- .sidebar-top {
266
- display: flex;
267
- align-items: center;
268
- padding: 0;
269
- min-height: 40px;
270
- /* Sit flush above the first nav item — no extra breathing room
271
- between the brand strip and the nav list. */
272
- margin-bottom: 0;
273
- }
274
- .collapse-toggle {
275
- appearance: none;
276
- background: transparent;
277
- border: 0;
278
- display: inline-flex;
279
- align-items: center;
280
- justify-content: center;
281
- width: 40px;
282
- height: 40px;
283
- padding: 0;
284
- border-radius: 4px;
285
- cursor: pointer;
286
- color: var(--ink);
287
- transition: background .12s;
288
- min-height: 0;
289
- flex: 0 0 40px;
290
- }
291
- .collapse-toggle:hover { background: var(--sidebar-hover); }
292
- .collapse-toggle .nav-icon {
293
- width: 14px;
294
- height: 14px;
295
- flex: 0 0 14px;
296
- }
297
- .sidebar[data-collapsed="true"] .sidebar-top {
298
- justify-content: center;
299
- }
300
-
301
- /* chevron flips when the sidebar is collapsed; the rest is util-item styling */
302
- .collapse-toggle .nav-icon {
303
- transition: transform .25s cubic-bezier(.4, 0, .2, 1);
304
- }
305
- .sidebar[data-collapsed="true"] .collapse-toggle .nav-icon {
306
- transform: rotate(180deg);
307
- }
308
-
309
- /* Drag-to-resize handle. Sits absolutely against the sidebar's right
310
- border so the cursor target spans the full height. 6px wide hit area
311
- centered on the visible 1px border — easy to grab without bumping
312
- into adjacent layout. */
313
- .sidebar-resize-handle {
314
- position: absolute;
315
- top: 0;
316
- right: -3px;
317
- width: 6px;
318
- height: 100%;
319
- cursor: col-resize;
320
- z-index: 5;
321
- touch-action: none;
322
- /* Subtle hover indicator: deepens the border-right color on hover so the
323
- user knows the edge is interactive. */
324
- background: transparent;
325
- transition: background .12s ease;
326
- }
327
- .sidebar-resize-handle:hover,
328
- body.is-resizing-sidebar .sidebar-resize-handle {
329
- /* Wash the hit area in the accent color so the user knows the edge is
330
- live and so the theme follows their accent choice. */
331
- background: var(--accent-soft);
332
- }
333
- /* While dragging, freeze global cursor + suppress text selection so the
334
- whole page tracks resize cleanly even if pointer leaves the handle. */
335
- body.is-resizing-sidebar {
336
- cursor: col-resize !important;
337
- user-select: none;
338
- }
339
- body.is-resizing-sidebar * {
340
- cursor: col-resize !important;
341
- }
342
-
343
- /* === v1.0 codex-style sidebar tree === */
344
-
345
- /* Compact top nav: smaller height + smaller font, so the folder tree
346
- below dominates. */
347
- /* Match the dimensions of .tree-folder-head + .tree-session below so
348
- the top nav, the folder head, and the session rows form one visually
349
- continuous column: same left padding (icon at x=8), same icon-label
350
- gap, same row height, same corner radius. Without this the nav
351
- icons sit 2px further right than the folder icons and the rows are
352
- noticeably taller. */
353
- .sidebar-nav.compact .nav-item {
354
- font-size: 13px;
355
- padding: 4px 8px;
356
- min-height: 28px;
357
- gap: 8px;
358
- border-radius: 4px;
359
- position: relative;
360
- letter-spacing: -0.005em;
361
- transition: background .14s ease, color .14s ease;
362
- }
363
- .sidebar-nav.compact .nav-item:hover { background: var(--sidebar-hover); }
364
- .sidebar-nav.compact .nav-icon {
365
- width: 14px;
366
- height: 14px;
367
- flex: 0 0 14px;
368
- }
369
- .sidebar[data-collapsed="true"] .sidebar-nav.compact .nav-item {
370
- justify-content: center;
371
- gap: 0;
372
- padding: 8px 0;
373
- }
374
- .sidebar-nav.compact .nav-item.is-active {
375
- background: var(--sidebar-active);
376
- color: var(--ink);
377
- font-weight: 500;
378
- }
379
-
380
- /* Tree section header. Looks like codex: uppercase label, small +
381
- button on hover. */
382
- .tree {
383
- margin-top: var(--sidebar-section-gap);
384
- display: flex;
385
- flex-direction: column;
386
- gap: 2px;
387
- /* Only vertical scroll when the tree is taller than the rail.
388
- overflow-y alone leaves overflow-x at the default `auto`, which
389
- gives us an unwanted horizontal scrollbar whenever a session
390
- title (or its hover row padding) brushes the viewport edge.
391
- overflow-x:hidden truncates with ellipsis instead — labels
392
- already do `text-overflow: ellipsis`. */
393
- overflow-y: auto;
394
- overflow-x: hidden;
395
- flex: 1;
396
- min-height: 0;
397
- padding-bottom: var(--s-3);
398
- }
399
- .tree-head {
400
- display: flex;
401
- justify-content: space-between;
402
- align-items: center;
403
- /* Right padding matches .tree-session so the +-button glyph
404
- right-aligns with the per-row tree-meta timestamp below. */
405
- padding: 0 8px;
406
- min-height: 24px;
407
- height: 24px;
408
- font-size: 12px;
409
- font-weight: 500;
410
- letter-spacing: 0;
411
- color: var(--ink-mid);
412
- /* No margin-bottom — let the parent .tree's `gap: 2px` carry the
413
- space below, matching what sits between folder rows. The big gap
414
- above "Sessions" still comes from .tree margin-top. */
415
- }
416
- .tree-head-action {
417
- appearance: none;
418
- background: transparent;
419
- border: 0;
420
- /* Zero right padding so the glyph itself lands flush against the
421
- row's 8px right inset — same column as the tree-meta text on
422
- each session row. */
423
- padding: 2px 0 2px 4px;
424
- border-radius: 4px;
425
- cursor: pointer;
426
- color: var(--ink);
427
- display: inline-flex;
428
- align-items: center;
429
- opacity: 0;
430
- transition: opacity .12s, background .12s;
431
- }
432
- .tree-head:hover .tree-head-action,
433
- .tree-head-action:focus-visible { opacity: 0.7; }
434
- .tree-head:hover .tree-head-action:hover { opacity: 1; background: var(--sidebar-hover); }
435
- .tree-head-action svg { width: 14px; height: 14px; }
436
-
437
- /* Folder grouping. Chevron rotates on expand. */
438
- .tree-folder {
439
- display: flex;
440
- flex-direction: column;
441
- }
442
- .tree-folder[data-dnd-over="true"] > .tree-folder-head {
443
- box-shadow: 0 -2px 0 var(--accent) inset;
444
- }
445
- /* Session being dragged → folder is a drop target. Tint the folder
446
- head + outline the whole folder so the user knows where it'll land,
447
- independent of whether the folder is expanded or collapsed. */
448
- .tree-folder.is-session-drop-target {
449
- border-radius: 6px;
450
- outline: 1px dashed var(--ink-mid);
451
- outline-offset: -2px;
452
- background: var(--sidebar-hover);
453
- }
454
- .tree-session[draggable="true"] { cursor: pointer; }
455
- .tree-session[draggable="true"]:active { cursor: grabbing; }
456
- .tree-folder-head[draggable="true"] {
457
- cursor: grab;
458
- }
459
- .tree-folder-head[draggable="true"]:active {
460
- cursor: grabbing;
461
- }
462
- .tree-folder-head {
463
- appearance: none;
464
- background: transparent;
465
- border: 0;
466
- width: 100%;
467
- text-align: left;
468
- padding: 4px 8px;
469
- display: flex;
470
- align-items: center;
471
- gap: 8px;
472
- font-size: 13px;
473
- font-weight: 400;
474
- color: var(--ink);
475
- cursor: pointer;
476
- border-radius: 4px;
477
- font-family: var(--body);
478
- min-height: 28px;
479
- }
480
- .tree-folder-head:hover { background: var(--sidebar-hover); }
481
- .tree-folder-icon {
482
- display: inline-flex;
483
- width: 14px;
484
- height: 14px;
485
- flex: 0 0 14px;
486
- color: var(--ink);
487
- }
488
- .tree-folder-icon svg { width: 100%; height: 100%; }
489
- .tree-folder-head.is-open .tree-folder-icon { color: var(--ink); }
490
- .tree-folder-name {
491
- flex: 1;
492
- white-space: nowrap;
493
- overflow: hidden;
494
- text-overflow: ellipsis;
495
- }
496
- .tree-folder-count {
497
- font-size: 13px;
498
- color: var(--ink);
499
- font-variant-numeric: tabular-nums;
500
- padding: 0 5px;
501
- background: transparent;
502
- border-radius: 999px;
503
- opacity: 0.6;
504
- }
505
- .tree-folder-actions {
506
- display: none;
507
- gap: 2px;
508
- }
509
- /* Same touch carve-out as session-actions above — don't reveal folder
510
- rename/delete on a tap-emulated hover. */
511
- @media (hover: hover) and (pointer: fine) {
512
- .tree-folder-head:hover .tree-folder-actions { display: inline-flex; }
513
- }
514
- .tree-folder-action {
515
- appearance: none;
516
- background: transparent;
517
- border: 0;
518
- display: inline-flex;
519
- align-items: center;
520
- justify-content: center;
521
- width: 18px;
522
- height: 18px;
523
- padding: 0;
524
- font-size: 13px;
525
- color: var(--ink);
526
- cursor: pointer;
527
- opacity: 0.6;
528
- border-radius: 3px;
529
- }
530
- .tree-folder-action:hover { opacity: 1; background: var(--sidebar-hover); }
531
- .tree-folder-action svg { width: 12px; height: 12px; }
532
-
533
- .tree-folder-body {
534
- display: flex;
535
- flex-direction: column;
536
- /* No left padding here — the session rows themselves get padding-left
537
- that lines their label up under the folder name. Keeping the bg
538
- extend across the full sidebar width when selected/hovered. */
539
- padding-left: 0;
540
- }
541
- .tree-empty {
542
- font-size: 13px;
543
- color: var(--ink);
544
- padding: 3px 8px;
545
- font-style: italic;
546
- opacity: 0.5;
547
- }
548
-
549
- /* Session rows. Codex uses a colored dot + truncated label + tiny
550
- timestamp on the right. */
551
- .tree-session {
552
- display: flex;
553
- align-items: center;
554
- gap: 8px;
555
- /* Match the folder head: 8px left padding, then a 14px icon column
556
- (the colored dot lives here, sized 8px and centered), then 8px gap
557
- before the label — lines the label up exactly under the folder name. */
558
- padding: 4px 8px;
559
- border-radius: 4px;
560
- cursor: pointer;
561
- font-size: 13px;
562
- color: var(--ink);
563
- font-family: var(--body);
564
- user-select: none;
565
- transition: background .1s;
566
- min-height: 28px;
567
- }
568
- .tree-session:hover { background: var(--sidebar-hover); }
569
- .tree-session.is-active {
570
- background: var(--sidebar-active);
571
- font-weight: 500;
572
- }
573
- /* Drop-line shown above the row when a sibling session is being
574
- dragged over it for within-folder reorder. Top-only inset shadow
575
- reads as a 2px insertion mark without shifting layout. */
576
- .tree-session.is-reorder-target {
577
- position: relative;
578
- }
579
- .tree-session.is-reorder-target::before {
580
- content: "";
581
- position: absolute;
582
- top: -1px;
583
- left: 4px;
584
- right: 4px;
585
- height: 2px;
586
- background: var(--ink-mid);
587
- border-radius: 1px;
588
- pointer-events: none;
589
- }
590
- /* Status dot · deliberately understated. The earlier version had a
591
- green dot + soft glow + expanding halo pulse; in a sidebar with
592
- eight running sessions it read as a row of strobing alerts. Now:
593
- one 5px dot, no halo, no shadow, no animation. Color alone carries
594
- running vs stopped. */
595
- .tree-dot {
596
- width: 14px;
597
- height: 14px;
598
- flex: 0 0 14px;
599
- display: inline-flex;
600
- align-items: center;
601
- justify-content: center;
602
- }
603
- .tree-dot::after {
604
- content: "";
605
- width: 7px;
606
- height: 7px;
607
- border-radius: 50%;
608
- background: var(--ink-faint);
609
- transition: background .15s ease;
610
- }
611
- .tree-session.is-running .tree-dot::after {
612
- background: var(--green);
613
- /* Soft halo so the dot reads as "alive" even from across the sidebar.
614
- Box-shadow uses currentColor isn't ideal here (the dot itself uses
615
- background, not color), so we hardcode each state's halo color
616
- below. */
617
- box-shadow: 0 0 0 0 rgba(74, 138, 74, 0.55);
618
- animation: tree-dot-breathe-idle 2.8s ease-in-out infinite;
619
- }
620
- /* Working = CLI is actively writing to its transcript (i.e. thinking
621
- or printing tokens). Idle stays green + slow breathe; working flips
622
- to blue + faster, more obvious breathe. */
623
- .tree-session.is-running.is-working .tree-dot::after {
624
- background: var(--blue, #4a73a5);
625
- box-shadow: 0 0 0 0 rgba(74, 115, 165, 0.65);
626
- animation: tree-dot-breathe-working 1.4s ease-in-out infinite;
627
- }
628
-
629
- @keyframes tree-dot-breathe-idle {
630
- 0%, 100% {
631
- box-shadow: 0 0 0 0 rgba(74, 138, 74, 0.45);
632
- opacity: 0.85;
633
- }
634
- 50% {
635
- box-shadow: 0 0 0 4px rgba(74, 138, 74, 0);
636
- opacity: 1;
637
- }
638
- }
639
- @keyframes tree-dot-breathe-working {
640
- 0%, 100% {
641
- box-shadow: 0 0 0 0 rgba(74, 115, 165, 0.65);
642
- opacity: 0.9;
643
- transform: scale(1);
644
- }
645
- 50% {
646
- box-shadow: 0 0 0 5px rgba(74, 115, 165, 0);
647
- opacity: 1;
648
- transform: scale(1.15);
649
- }
650
- }
651
- /* Respect users who've asked for less motion — keep the color signal
652
- but drop the pulse. */
653
- @media (prefers-reduced-motion: reduce) {
654
- .tree-session.is-running .tree-dot::after,
655
- .tree-session.is-running.is-working .tree-dot::after {
656
- animation: none;
657
- box-shadow: none;
658
- }
659
- }
660
- .tree-label {
661
- flex: 1;
662
- white-space: nowrap;
663
- overflow: hidden;
664
- text-overflow: ellipsis;
665
- }
666
- .tree-session-actions {
667
- display: none;
668
- gap: 2px;
669
- flex-shrink: 0;
670
- }
671
- /* Hover-only on real-pointer devices. On touch devices the first tap
672
- emulates :hover (revealing rename/delete), which means the user has
673
- to tap a second time to actually open the session. (hover: hover)
674
- matches a real mouse / trackpad; (pointer: fine) further requires
675
- precise cursor (so hybrid touch+mouse laptops with a stylus get
676
- hover but pure-touch phones / tablets do not.) Phones keep the
677
- timestamp visible and never reveal the action buttons here — they
678
- can use the kebab in the session pane's top bar instead. */
679
- @media (hover: hover) and (pointer: fine) {
680
- .tree-session:hover .tree-session-actions { display: inline-flex; }
681
- .tree-session:hover .tree-meta { display: none; }
682
- }
683
- .tree-session-action {
684
- appearance: none;
685
- background: transparent;
686
- border: 0;
687
- display: inline-flex;
688
- align-items: center;
689
- justify-content: center;
690
- width: 18px;
691
- height: 18px;
692
- padding: 0;
693
- color: var(--ink);
694
- cursor: pointer;
695
- opacity: 0.6;
696
- border-radius: 3px;
697
- }
698
- .tree-session-action:hover { opacity: 1; background: var(--sidebar-hover); }
699
- .tree-session-action svg { width: 12px; height: 12px; }
700
- .tree-meta {
701
- font-size: 12px;
702
- color: var(--ink);
703
- font-variant-numeric: tabular-nums;
704
- flex-shrink: 0;
705
- opacity: 0.55;
706
- letter-spacing: 0.01em;
707
- }
1
+ /* Left collapsible sidebar nav · brand mark · util items · collapse toggle */
2
+
3
+ .sidebar {
4
+ /* One value drives all three "section break" gaps in the sidebar
5
+ column: brand-strip → first nav item, last nav item → "Sessions"
6
+ header, and "Sessions" header → first folder. Bump or shrink to
7
+ adjust how breathy the rail feels. */
8
+ --sidebar-section-gap: var(--s-2);
9
+ position: sticky;
10
+ top: 0;
11
+ height: 100vh;
12
+ background: var(--ui-bg);
13
+ border-right: 1px solid var(--ui-border);
14
+ display: flex;
15
+ flex-direction: column;
16
+ padding: 0 var(--s-2);
17
+ overflow: visible;
18
+ transition: padding .25s cubic-bezier(.4, 0, .2, 1);
19
+ }
20
+ .sidebar[data-collapsed="true"] {
21
+ padding: 0;
22
+ }
23
+ /* Collapsed: every clickable row becomes a 28x28 square, centered in
24
+ the narrow sidebar column. Width auto + margin auto so the square
25
+ sits centered regardless of what padding the parent has. */
26
+ .sidebar[data-collapsed="true"] .nav-item,
27
+ .sidebar[data-collapsed="true"] .util-item,
28
+ .sidebar[data-collapsed="true"] .sidebar-brand-button,
29
+ .sidebar[data-collapsed="true"] .tree-folder-head,
30
+ .sidebar[data-collapsed="true"] .tree-session {
31
+ width: 28px;
32
+ height: 28px;
33
+ min-height: 28px;
34
+ padding: 0;
35
+ margin: 0 auto;
36
+ justify-content: center;
37
+ gap: 0;
38
+ }
39
+ .sidebar[data-collapsed="true"] .sidebar-top {
40
+ padding: 0;
41
+ min-height: 40px;
42
+ justify-content: center;
43
+ }
44
+ .sidebar[data-collapsed="true"] .sidebar-top .collapse-toggle {
45
+ width: 40px;
46
+ height: 40px;
47
+ min-height: 40px;
48
+ margin: 0;
49
+ }
50
+
51
+ .sidebar-brand {
52
+ display: flex;
53
+ align-items: center;
54
+ gap: var(--s-2);
55
+ padding: 0 var(--s-2);
56
+ min-height: 28px;
57
+ height: 28px;
58
+ }
59
+ .brand-mark {
60
+ display: inline-flex;
61
+ align-items: center;
62
+ justify-content: center;
63
+ width: 20px;
64
+ height: 20px;
65
+ flex: 0 0 20px;
66
+ background: transparent;
67
+ color: var(--accent);
68
+ }
69
+ .brand-mark svg { display: block; width: 20px; height: 20px; }
70
+ .brand-name {
71
+ font-size: 14px;
72
+ font-weight: 600;
73
+ letter-spacing: -0.02em;
74
+ color: var(--ink);
75
+ white-space: nowrap;
76
+ opacity: 1;
77
+ line-height: 1;
78
+ transition: opacity .15s ease;
79
+ }
80
+ .brand-dot { color: var(--accent); }
81
+ .sidebar[data-collapsed="true"] .brand-name { display: none; }
82
+ .sidebar[data-collapsed="true"] .brand-version { display: none; }
83
+ .sidebar[data-collapsed="true"] .sidebar-brand { justify-content: center; padding-left: 0; padding-right: 0; }
84
+
85
+ .sidebar-nav {
86
+ display: flex;
87
+ flex-direction: column;
88
+ gap: 2px;
89
+ flex: 0 0 auto;
90
+ }
91
+
92
+ .nav-item, .util-item {
93
+ appearance: none;
94
+ background: transparent;
95
+ border: 0;
96
+ display: flex;
97
+ align-items: center;
98
+ gap: var(--s-3);
99
+ width: 100%;
100
+ padding: 0 12px;
101
+ min-height: 42px;
102
+ border-radius: var(--r-sm);
103
+ cursor: pointer;
104
+ color: var(--ink);
105
+ font-family: var(--body);
106
+ font-size: 13px;
107
+ font-weight: 400;
108
+ text-align: left;
109
+ transition: background .12s ease, color .12s ease;
110
+ position: relative;
111
+ }
112
+ .nav-item:hover, .util-item:hover {
113
+ background: var(--sidebar-hover);
114
+ }
115
+ .nav-item[aria-selected="true"] {
116
+ background: var(--sidebar-active);
117
+ color: var(--ink);
118
+ }
119
+
120
+ /* Unsaved-changes dot next to nav label */
121
+ .nav-item.has-changes::after {
122
+ content: "";
123
+ position: absolute;
124
+ right: 10px;
125
+ top: 50%;
126
+ transform: translateY(-50%);
127
+ width: 7px;
128
+ height: 7px;
129
+ border-radius: 50%;
130
+ background: var(--ink);
131
+ box-shadow: 0 0 0 0 rgba(26, 24, 21, 0.30);
132
+ animation: dirty-pulse 2s ease-in-out infinite;
133
+ }
134
+ @keyframes dirty-pulse {
135
+ 0%, 100% { box-shadow: 0 0 0 0 rgba(26, 24, 21, 0.22); }
136
+ 50% { box-shadow: 0 0 0 4px rgba(26, 24, 21, 0); }
137
+ }
138
+ .sidebar[data-collapsed="true"] .nav-item.has-changes::after {
139
+ right: auto;
140
+ top: 6px;
141
+ left: 28px;
142
+ }
143
+
144
+ .nav-icon {
145
+ display: inline-flex;
146
+ width: 20px;
147
+ height: 20px;
148
+ flex: 0 0 20px;
149
+ color: currentColor;
150
+ }
151
+ .nav-icon svg { width: 100%; height: 100%; }
152
+ .nav-label {
153
+ white-space: nowrap;
154
+ opacity: 1;
155
+ transition: opacity .15s ease;
156
+ flex: 1;
157
+ }
158
+ .sidebar[data-collapsed="true"] .nav-label { opacity: 0; pointer-events: none; }
159
+ /* Collapsed sidebar (60px wide): hide the label entirely so the icon
160
+ centers in the narrow column instead of being pushed off-screen by
161
+ the still-laid-out (but invisible) label text. Same for the badge. */
162
+ .sidebar[data-collapsed="true"] .nav-label,
163
+ .sidebar[data-collapsed="true"] .nav-badge {
164
+ display: none;
165
+ }
166
+
167
+ .nav-badge {
168
+ font-family: var(--mono);
169
+ font-size: 10.5px;
170
+ background: var(--border-soft);
171
+ color: var(--ink-muted);
172
+ padding: 1px 6px;
173
+ border-radius: 4px;
174
+ font-variant-numeric: tabular-nums;
175
+ opacity: 1;
176
+ transition: opacity .15s ease;
177
+ }
178
+ .sidebar[data-collapsed="true"] .nav-badge { opacity: 0; }
179
+ .nav-item[aria-selected="true"] .nav-badge {
180
+ background: var(--bg-elev);
181
+ color: var(--ink-mid);
182
+ }
183
+
184
+ .sidebar-divider {
185
+ margin: var(--s-3) var(--s-2);
186
+ border-top: 1px solid var(--border);
187
+ }
188
+
189
+ .sidebar-utility {
190
+ display: flex;
191
+ flex-direction: column;
192
+ gap: 2px;
193
+ }
194
+
195
+ .sidebar-foot {
196
+ margin-top: auto;
197
+ padding-top: 2px;
198
+ display: flex;
199
+ flex-direction: row;
200
+ justify-content: flex-end;
201
+ align-items: center;
202
+ gap: 2px;
203
+ }
204
+ /* Brand block at the bottom of the sidebar. Clickable: navigates to
205
+ About. Matches nav-item height so collapsed sidebar reads as a
206
+ uniform column of equally-sized icons. */
207
+ .sidebar-brand-button {
208
+ appearance: none;
209
+ background: transparent;
210
+ border: 0;
211
+ display: flex;
212
+ align-items: center;
213
+ gap: 8px;
214
+ width: 100%;
215
+ padding: 0 var(--s-2);
216
+ min-height: 28px;
217
+ height: 28px;
218
+ cursor: pointer;
219
+ border-radius: 4px;
220
+ text-align: left;
221
+ font: inherit;
222
+ color: var(--ink);
223
+ transition: background .12s;
224
+ }
225
+ .sidebar-brand-button[aria-selected="true"],
226
+ .sidebar-brand-button[aria-selected="true"]:hover {
227
+ background: var(--sidebar-active);
228
+ font-weight: 500;
229
+ }
230
+ .sidebar-brand-button:hover { background: var(--sidebar-hover); }
231
+ .sidebar-foot .brand-mark {
232
+ width: 14px;
233
+ height: 14px;
234
+ flex: 0 0 14px;
235
+ display: inline-flex;
236
+ align-items: center;
237
+ justify-content: center;
238
+ /* Lock the SVG box exactly to 14×14 so flex centering doesn't fight
239
+ the SVG's intrinsic dimensions; otherwise the mark drifts a couple
240
+ pixels low because <svg width=32 height=32> wants more vertical
241
+ space than the 14px row gives it. */
242
+ line-height: 0;
243
+ }
244
+ .sidebar-foot .brand-mark svg {
245
+ width: 14px;
246
+ height: 14px;
247
+ display: block;
248
+ }
249
+ .sidebar-foot .brand-name {
250
+ font-size: 13px;
251
+ font-weight: 400;
252
+ line-height: 1;
253
+ }
254
+ .brand-version {
255
+ margin-left: auto;
256
+ font-family: var(--mono);
257
+ font-size: 10px;
258
+ color: var(--ink-muted);
259
+ font-variant-numeric: tabular-nums;
260
+ }
261
+
262
+ /* Top strip · single row, currently hosts only the collapse toggle on
263
+ the right. Matches the page-title-bar height so the topmost row of
264
+ the window reads as one unified band. */
265
+ .sidebar-top {
266
+ display: flex;
267
+ align-items: center;
268
+ padding: 0;
269
+ min-height: 40px;
270
+ /* Sit flush above the first nav item — no extra breathing room
271
+ between the brand strip and the nav list. */
272
+ margin-bottom: 0;
273
+ }
274
+ .collapse-toggle {
275
+ appearance: none;
276
+ background: transparent;
277
+ border: 0;
278
+ display: inline-flex;
279
+ align-items: center;
280
+ justify-content: center;
281
+ width: 40px;
282
+ height: 40px;
283
+ padding: 0;
284
+ border-radius: 4px;
285
+ cursor: pointer;
286
+ color: var(--ink);
287
+ transition: background .12s;
288
+ min-height: 0;
289
+ flex: 0 0 40px;
290
+ }
291
+ .collapse-toggle:hover { background: var(--sidebar-hover); }
292
+ .collapse-toggle .nav-icon {
293
+ width: 14px;
294
+ height: 14px;
295
+ flex: 0 0 14px;
296
+ }
297
+ .sidebar[data-collapsed="true"] .sidebar-top {
298
+ justify-content: center;
299
+ }
300
+
301
+ /* chevron flips when the sidebar is collapsed; the rest is util-item styling */
302
+ .collapse-toggle .nav-icon {
303
+ transition: transform .25s cubic-bezier(.4, 0, .2, 1);
304
+ }
305
+ .sidebar[data-collapsed="true"] .collapse-toggle .nav-icon {
306
+ transform: rotate(180deg);
307
+ }
308
+
309
+ /* Drag-to-resize handle. Sits absolutely against the sidebar's right
310
+ border so the cursor target spans the full height. 6px wide hit area
311
+ centered on the visible 1px border — easy to grab without bumping
312
+ into adjacent layout. */
313
+ .sidebar-resize-handle {
314
+ position: absolute;
315
+ top: 0;
316
+ right: -3px;
317
+ width: 6px;
318
+ height: 100%;
319
+ cursor: col-resize;
320
+ z-index: 5;
321
+ touch-action: none;
322
+ /* Subtle hover indicator: deepens the border-right color on hover so the
323
+ user knows the edge is interactive. */
324
+ background: transparent;
325
+ transition: background .12s ease;
326
+ }
327
+ .sidebar-resize-handle:hover,
328
+ body.is-resizing-sidebar .sidebar-resize-handle {
329
+ /* Wash the hit area in the accent color so the user knows the edge is
330
+ live and so the theme follows their accent choice. */
331
+ background: var(--accent-soft);
332
+ }
333
+ /* While dragging, freeze global cursor + suppress text selection so the
334
+ whole page tracks resize cleanly even if pointer leaves the handle. */
335
+ body.is-resizing-sidebar {
336
+ cursor: col-resize !important;
337
+ user-select: none;
338
+ }
339
+ body.is-resizing-sidebar * {
340
+ cursor: col-resize !important;
341
+ }
342
+
343
+ /* === v1.0 codex-style sidebar tree === */
344
+
345
+ /* Compact top nav: smaller height + smaller font, so the folder tree
346
+ below dominates. */
347
+ /* Match the dimensions of .tree-folder-head + .tree-session below so
348
+ the top nav, the folder head, and the session rows form one visually
349
+ continuous column: same left padding (icon at x=8), same icon-label
350
+ gap, same row height, same corner radius. Without this the nav
351
+ icons sit 2px further right than the folder icons and the rows are
352
+ noticeably taller. */
353
+ .sidebar-nav.compact .nav-item {
354
+ font-size: 13px;
355
+ padding: 4px 8px;
356
+ min-height: 28px;
357
+ gap: 8px;
358
+ border-radius: 4px;
359
+ position: relative;
360
+ letter-spacing: -0.005em;
361
+ transition: background .14s ease, color .14s ease;
362
+ }
363
+ .sidebar-nav.compact .nav-item:hover { background: var(--sidebar-hover); }
364
+ .sidebar-nav.compact .nav-icon {
365
+ width: 14px;
366
+ height: 14px;
367
+ flex: 0 0 14px;
368
+ }
369
+ .sidebar[data-collapsed="true"] .sidebar-nav.compact .nav-item {
370
+ justify-content: center;
371
+ gap: 0;
372
+ padding: 8px 0;
373
+ }
374
+ .sidebar-nav.compact .nav-item.is-active {
375
+ background: var(--sidebar-active);
376
+ color: var(--ink);
377
+ font-weight: 500;
378
+ }
379
+
380
+ /* Tree section header. Looks like codex: uppercase label, small +
381
+ button on hover. */
382
+ .tree {
383
+ margin-top: var(--sidebar-section-gap);
384
+ display: flex;
385
+ flex-direction: column;
386
+ gap: 2px;
387
+ /* Only vertical scroll when the tree is taller than the rail.
388
+ overflow-y alone leaves overflow-x at the default `auto`, which
389
+ gives us an unwanted horizontal scrollbar whenever a session
390
+ title (or its hover row padding) brushes the viewport edge.
391
+ overflow-x:hidden truncates with ellipsis instead — labels
392
+ already do `text-overflow: ellipsis`. */
393
+ overflow-y: auto;
394
+ overflow-x: hidden;
395
+ flex: 1;
396
+ min-height: 0;
397
+ padding-bottom: var(--s-3);
398
+ }
399
+ .tree-head {
400
+ display: flex;
401
+ justify-content: space-between;
402
+ align-items: center;
403
+ /* Right padding matches .tree-session so the +-button glyph
404
+ right-aligns with the per-row tree-meta timestamp below. */
405
+ padding: 0 8px;
406
+ min-height: 24px;
407
+ height: 24px;
408
+ font-size: 12px;
409
+ font-weight: 500;
410
+ letter-spacing: 0;
411
+ color: var(--ink-mid);
412
+ /* No margin-bottom — let the parent .tree's `gap: 2px` carry the
413
+ space below, matching what sits between folder rows. The big gap
414
+ above "Sessions" still comes from .tree margin-top. */
415
+ }
416
+ .tree-head-action {
417
+ appearance: none;
418
+ background: transparent;
419
+ border: 0;
420
+ /* Zero right padding so the glyph itself lands flush against the
421
+ row's 8px right inset — same column as the tree-meta text on
422
+ each session row. */
423
+ padding: 2px 0 2px 4px;
424
+ border-radius: 4px;
425
+ cursor: pointer;
426
+ color: var(--ink);
427
+ display: inline-flex;
428
+ align-items: center;
429
+ opacity: 0;
430
+ transition: opacity .12s, background .12s;
431
+ }
432
+ .tree-head:hover .tree-head-action,
433
+ .tree-head-action:focus-visible { opacity: 0.7; }
434
+ .tree-head:hover .tree-head-action:hover { opacity: 1; background: var(--sidebar-hover); }
435
+ .tree-head-action svg { width: 14px; height: 14px; }
436
+
437
+ /* Folder grouping. Chevron rotates on expand. */
438
+ .tree-folder {
439
+ display: flex;
440
+ flex-direction: column;
441
+ }
442
+ .tree-folder[data-dnd-over="true"] > .tree-folder-head {
443
+ box-shadow: 0 -2px 0 var(--accent) inset;
444
+ }
445
+ /* Session being dragged → folder is a drop target. Tint the folder
446
+ head + outline the whole folder so the user knows where it'll land,
447
+ independent of whether the folder is expanded or collapsed. */
448
+ .tree-folder.is-session-drop-target {
449
+ border-radius: 6px;
450
+ outline: 1px dashed var(--ink-mid);
451
+ outline-offset: -2px;
452
+ background: var(--sidebar-hover);
453
+ }
454
+ .tree-session[draggable="true"] { cursor: pointer; }
455
+ .tree-session[draggable="true"]:active { cursor: grabbing; }
456
+ .tree-folder-head[draggable="true"] {
457
+ cursor: grab;
458
+ }
459
+ .tree-folder-head[draggable="true"]:active {
460
+ cursor: grabbing;
461
+ }
462
+ .tree-folder-head {
463
+ appearance: none;
464
+ background: transparent;
465
+ border: 0;
466
+ width: 100%;
467
+ text-align: left;
468
+ padding: 4px 8px;
469
+ display: flex;
470
+ align-items: center;
471
+ gap: 8px;
472
+ font-size: 13px;
473
+ font-weight: 400;
474
+ color: var(--ink);
475
+ cursor: pointer;
476
+ border-radius: 4px;
477
+ font-family: var(--body);
478
+ min-height: 28px;
479
+ }
480
+ .tree-folder-head:hover { background: var(--sidebar-hover); }
481
+ .tree-folder-icon {
482
+ display: inline-flex;
483
+ width: 14px;
484
+ height: 14px;
485
+ flex: 0 0 14px;
486
+ color: var(--ink);
487
+ }
488
+ .tree-folder-icon svg { width: 100%; height: 100%; }
489
+ .tree-folder-head.is-open .tree-folder-icon { color: var(--ink); }
490
+ .tree-folder-name {
491
+ flex: 1;
492
+ white-space: nowrap;
493
+ overflow: hidden;
494
+ text-overflow: ellipsis;
495
+ }
496
+ .tree-folder-count {
497
+ font-size: 13px;
498
+ color: var(--ink);
499
+ font-variant-numeric: tabular-nums;
500
+ padding: 0 5px;
501
+ background: transparent;
502
+ border-radius: 999px;
503
+ opacity: 0.6;
504
+ }
505
+ .tree-folder-actions {
506
+ display: none;
507
+ gap: 2px;
508
+ }
509
+ /* Same touch carve-out as session-actions above — don't reveal folder
510
+ rename/delete on a tap-emulated hover. */
511
+ @media (hover: hover) and (pointer: fine) {
512
+ .tree-folder-head:hover .tree-folder-actions { display: inline-flex; }
513
+ }
514
+ .tree-folder-action {
515
+ appearance: none;
516
+ background: transparent;
517
+ border: 0;
518
+ display: inline-flex;
519
+ align-items: center;
520
+ justify-content: center;
521
+ width: 18px;
522
+ height: 18px;
523
+ padding: 0;
524
+ font-size: 13px;
525
+ color: var(--ink);
526
+ cursor: pointer;
527
+ opacity: 0.6;
528
+ border-radius: 3px;
529
+ }
530
+ .tree-folder-action:hover { opacity: 1; background: var(--sidebar-hover); }
531
+ .tree-folder-action svg { width: 12px; height: 12px; }
532
+
533
+ .tree-folder-body {
534
+ display: flex;
535
+ flex-direction: column;
536
+ /* No left padding here — the session rows themselves get padding-left
537
+ that lines their label up under the folder name. Keeping the bg
538
+ extend across the full sidebar width when selected/hovered. */
539
+ padding-left: 0;
540
+ }
541
+ .tree-empty {
542
+ font-size: 13px;
543
+ color: var(--ink);
544
+ padding: 3px 8px;
545
+ font-style: italic;
546
+ opacity: 0.5;
547
+ }
548
+
549
+ /* Session rows. Codex uses a colored dot + truncated label + tiny
550
+ timestamp on the right. */
551
+ .tree-session {
552
+ display: flex;
553
+ align-items: center;
554
+ gap: 8px;
555
+ /* Match the folder head: 8px left padding, then a 14px icon column
556
+ (the colored dot lives here, sized 8px and centered), then 8px gap
557
+ before the label — lines the label up exactly under the folder name. */
558
+ padding: 4px 8px;
559
+ border-radius: 4px;
560
+ cursor: pointer;
561
+ font-size: 13px;
562
+ color: var(--ink);
563
+ font-family: var(--body);
564
+ user-select: none;
565
+ transition: background .1s;
566
+ min-height: 28px;
567
+ }
568
+ .tree-session:hover { background: var(--sidebar-hover); }
569
+ .tree-session.is-active {
570
+ background: var(--sidebar-active);
571
+ font-weight: 500;
572
+ }
573
+ /* Drop-line shown above the row when a sibling session is being
574
+ dragged over it for within-folder reorder. Top-only inset shadow
575
+ reads as a 2px insertion mark without shifting layout. */
576
+ .tree-session.is-reorder-target {
577
+ position: relative;
578
+ }
579
+ .tree-session.is-reorder-target::before {
580
+ content: "";
581
+ position: absolute;
582
+ top: -1px;
583
+ left: 4px;
584
+ right: 4px;
585
+ height: 2px;
586
+ background: var(--ink-mid);
587
+ border-radius: 1px;
588
+ pointer-events: none;
589
+ }
590
+ /* Status dot · deliberately understated. The earlier version had a
591
+ green dot + soft glow + expanding halo pulse; in a sidebar with
592
+ eight running sessions it read as a row of strobing alerts. Now:
593
+ one 5px dot, no halo, no shadow, no animation. Color alone carries
594
+ running vs stopped. */
595
+ .tree-dot {
596
+ width: 14px;
597
+ height: 14px;
598
+ flex: 0 0 14px;
599
+ display: inline-flex;
600
+ align-items: center;
601
+ justify-content: center;
602
+ }
603
+ .tree-dot::after {
604
+ content: "";
605
+ width: 7px;
606
+ height: 7px;
607
+ border-radius: 50%;
608
+ background: var(--ink-faint);
609
+ transition: background .15s ease;
610
+ }
611
+ .tree-session.is-running .tree-dot::after {
612
+ background: var(--green);
613
+ /* Soft halo so the dot reads as "alive" even from across the sidebar.
614
+ Box-shadow uses currentColor isn't ideal here (the dot itself uses
615
+ background, not color), so we hardcode each state's halo color
616
+ below. */
617
+ box-shadow: 0 0 0 0 rgba(74, 138, 74, 0.55);
618
+ animation: tree-dot-breathe-idle 2.8s ease-in-out infinite;
619
+ }
620
+ /* Working = CLI is actively writing to its transcript (i.e. thinking
621
+ or printing tokens). Idle stays green + slow breathe; working flips
622
+ to blue + faster, more obvious breathe. */
623
+ .tree-session.is-running.is-working .tree-dot::after {
624
+ background: var(--blue, #4a73a5);
625
+ box-shadow: 0 0 0 0 rgba(74, 115, 165, 0.65);
626
+ animation: tree-dot-breathe-working 1.4s ease-in-out infinite;
627
+ }
628
+
629
+ @keyframes tree-dot-breathe-idle {
630
+ 0%, 100% {
631
+ box-shadow: 0 0 0 0 rgba(74, 138, 74, 0.45);
632
+ opacity: 0.85;
633
+ }
634
+ 50% {
635
+ box-shadow: 0 0 0 4px rgba(74, 138, 74, 0);
636
+ opacity: 1;
637
+ }
638
+ }
639
+ @keyframes tree-dot-breathe-working {
640
+ 0%, 100% {
641
+ box-shadow: 0 0 0 0 rgba(74, 115, 165, 0.65);
642
+ opacity: 0.9;
643
+ transform: scale(1);
644
+ }
645
+ 50% {
646
+ box-shadow: 0 0 0 5px rgba(74, 115, 165, 0);
647
+ opacity: 1;
648
+ transform: scale(1.15);
649
+ }
650
+ }
651
+ /* Respect users who've asked for less motion — keep the color signal
652
+ but drop the pulse. */
653
+ @media (prefers-reduced-motion: reduce) {
654
+ .tree-session.is-running .tree-dot::after,
655
+ .tree-session.is-running.is-working .tree-dot::after {
656
+ animation: none;
657
+ box-shadow: none;
658
+ }
659
+ }
660
+ .tree-label {
661
+ flex: 1;
662
+ white-space: nowrap;
663
+ overflow: hidden;
664
+ text-overflow: ellipsis;
665
+ }
666
+ .tree-session-actions {
667
+ display: none;
668
+ gap: 2px;
669
+ flex-shrink: 0;
670
+ }
671
+ /* Hover-only on real-pointer devices. On touch devices the first tap
672
+ emulates :hover (revealing rename/delete), which means the user has
673
+ to tap a second time to actually open the session. (hover: hover)
674
+ matches a real mouse / trackpad; (pointer: fine) further requires
675
+ precise cursor (so hybrid touch+mouse laptops with a stylus get
676
+ hover but pure-touch phones / tablets do not.) Phones keep the
677
+ timestamp visible and never reveal the action buttons here — they
678
+ can use the kebab in the session pane's top bar instead. */
679
+ @media (hover: hover) and (pointer: fine) {
680
+ .tree-session:hover .tree-session-actions { display: inline-flex; }
681
+ .tree-session:hover .tree-meta { display: none; }
682
+ }
683
+ .tree-session-action {
684
+ appearance: none;
685
+ background: transparent;
686
+ border: 0;
687
+ display: inline-flex;
688
+ align-items: center;
689
+ justify-content: center;
690
+ width: 18px;
691
+ height: 18px;
692
+ padding: 0;
693
+ color: var(--ink);
694
+ cursor: pointer;
695
+ opacity: 0.6;
696
+ border-radius: 3px;
697
+ }
698
+ .tree-session-action:hover { opacity: 1; background: var(--sidebar-hover); }
699
+ .tree-session-action svg { width: 12px; height: 12px; }
700
+ .tree-meta {
701
+ font-size: 12px;
702
+ color: var(--ink);
703
+ font-variant-numeric: tabular-nums;
704
+ flex-shrink: 0;
705
+ opacity: 0.55;
706
+ letter-spacing: 0.01em;
707
+ }