@bakapiano/ccsm 0.14.0 → 0.15.1

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