claude-controller 0.1.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +2 -2
  2. package/bin/autoloop.sh +382 -0
  3. package/bin/ctl +1189 -0
  4. package/bin/native-app.py +6 -3
  5. package/bin/watchdog.sh +357 -0
  6. package/cognitive/__init__.py +14 -0
  7. package/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
  8. package/cognitive/__pycache__/dispatcher.cpython-314.pyc +0 -0
  9. package/cognitive/__pycache__/evaluator.cpython-314.pyc +0 -0
  10. package/cognitive/__pycache__/goal_engine.cpython-314.pyc +0 -0
  11. package/cognitive/__pycache__/learning.cpython-314.pyc +0 -0
  12. package/cognitive/__pycache__/orchestrator.cpython-314.pyc +0 -0
  13. package/cognitive/__pycache__/planner.cpython-314.pyc +0 -0
  14. package/cognitive/dispatcher.py +192 -0
  15. package/cognitive/evaluator.py +289 -0
  16. package/cognitive/goal_engine.py +232 -0
  17. package/cognitive/learning.py +189 -0
  18. package/cognitive/orchestrator.py +303 -0
  19. package/cognitive/planner.py +207 -0
  20. package/cognitive/prompts/analyst.md +31 -0
  21. package/cognitive/prompts/coder.md +22 -0
  22. package/cognitive/prompts/reviewer.md +33 -0
  23. package/cognitive/prompts/tester.md +21 -0
  24. package/cognitive/prompts/writer.md +25 -0
  25. package/config.sh +6 -1
  26. package/dag/__init__.py +5 -0
  27. package/dag/__pycache__/__init__.cpython-314.pyc +0 -0
  28. package/dag/__pycache__/graph.cpython-314.pyc +0 -0
  29. package/dag/graph.py +222 -0
  30. package/lib/jobs.sh +12 -1
  31. package/package.json +11 -5
  32. package/postinstall.sh +1 -1
  33. package/service/controller.sh +43 -11
  34. package/web/audit.py +122 -0
  35. package/web/checkpoint.py +80 -0
  36. package/web/config.py +2 -5
  37. package/web/handler.py +634 -473
  38. package/web/handler_fs.py +153 -0
  39. package/web/handler_goals.py +203 -0
  40. package/web/handler_jobs.py +372 -0
  41. package/web/handler_memory.py +203 -0
  42. package/web/handler_sessions.py +132 -0
  43. package/web/jobs.py +585 -13
  44. package/web/personas.py +419 -0
  45. package/web/pipeline.py +981 -0
  46. package/web/presets.py +506 -0
  47. package/web/projects.py +246 -0
  48. package/web/static/api.js +141 -0
  49. package/web/static/app.js +25 -1937
  50. package/web/static/attachments.js +144 -0
  51. package/web/static/base.css +497 -0
  52. package/web/static/context.js +204 -0
  53. package/web/static/dirs.js +246 -0
  54. package/web/static/form.css +763 -0
  55. package/web/static/goals.css +363 -0
  56. package/web/static/goals.js +300 -0
  57. package/web/static/i18n.js +625 -0
  58. package/web/static/index.html +215 -13
  59. package/web/static/{styles.css → jobs.css} +746 -1141
  60. package/web/static/jobs.js +1270 -0
  61. package/web/static/memoryview.js +117 -0
  62. package/web/static/personas.js +228 -0
  63. package/web/static/pipeline.css +338 -0
  64. package/web/static/pipelines.js +487 -0
  65. package/web/static/presets.js +244 -0
  66. package/web/static/send.js +135 -0
  67. package/web/static/settings-style.css +291 -0
  68. package/web/static/settings.js +81 -0
  69. package/web/static/stream.js +534 -0
  70. package/web/static/utils.js +131 -0
  71. package/web/webhook.py +210 -0
@@ -0,0 +1,144 @@
1
+ /* ═══════════════════════════════════════════════
2
+ Attachments — 파일 업로드, 첨부 관리
3
+ ═══════════════════════════════════════════════ */
4
+
5
+ const attachments = [];
6
+
7
+ function updateAttachBadge() {
8
+ document.getElementById('imgCountBadge').textContent = '';
9
+ }
10
+
11
+ function insertAtCursor(textarea, text) {
12
+ const start = textarea.selectionStart;
13
+ const end = textarea.selectionEnd;
14
+ const before = textarea.value.substring(0, start);
15
+ const after = textarea.value.substring(end);
16
+ const space = (before.length > 0 && !before.endsWith(' ') && !before.endsWith('\n')) ? ' ' : '';
17
+ textarea.value = before + space + text + ' ' + after;
18
+ const newPos = start + space.length + text.length + 1;
19
+ textarea.selectionStart = textarea.selectionEnd = newPos;
20
+ textarea.focus();
21
+ updatePromptMirror();
22
+ }
23
+
24
+ function updatePromptMirror() {
25
+ const ta = document.getElementById('promptInput');
26
+ const mirror = document.getElementById('promptMirror');
27
+ if (!ta || !mirror) return;
28
+ const val = ta.value;
29
+ if (!val) {
30
+ mirror.innerHTML = '';
31
+ syncAttachmentsFromText('');
32
+ return;
33
+ }
34
+ const escaped = escapeHtml(val);
35
+ mirror.innerHTML = escaped.replace(/@(\/[^\s,]+|image\d+)/g, (match) => {
36
+ return `<span class="prompt-at-ref">${escapeHtml(match)}</span>`;
37
+ }) + '\n';
38
+ mirror.scrollTop = ta.scrollTop;
39
+ syncAttachmentsFromText(val);
40
+ }
41
+
42
+ function syncAttachmentsFromText(text) {
43
+ const container = document.getElementById('attachmentPreviews');
44
+ if (!container) return;
45
+ let changed = false;
46
+ attachments.forEach((att, idx) => {
47
+ if (!att) return;
48
+ const ref = `@image${idx}`;
49
+ if (!text.includes(ref)) {
50
+ attachments[idx] = null;
51
+ const thumb = container.querySelector(`[data-idx="${idx}"]`);
52
+ if (thumb) thumb.remove();
53
+ changed = true;
54
+ }
55
+ });
56
+ if (changed) updateAttachBadge();
57
+ }
58
+
59
+ async function uploadFile(file) {
60
+ return new Promise((resolve, reject) => {
61
+ const reader = new FileReader();
62
+ reader.onload = async () => {
63
+ try {
64
+ const data = await apiFetch('/api/upload', {
65
+ method: 'POST',
66
+ body: JSON.stringify({ filename: file.name, data: reader.result }),
67
+ });
68
+ resolve(data);
69
+ } catch (err) {
70
+ reject(err);
71
+ }
72
+ };
73
+ reader.onerror = () => reject(new Error(t('msg_file_read_failed')));
74
+ reader.readAsDataURL(file);
75
+ });
76
+ }
77
+
78
+ function removeAttachment(idx) {
79
+ attachments[idx] = null;
80
+ const container = document.getElementById('attachmentPreviews');
81
+ const thumb = container.querySelector(`[data-idx="${idx}"]`);
82
+ if (thumb) thumb.remove();
83
+ const ta = document.getElementById('promptInput');
84
+ ta.value = ta.value.replace(new RegExp(`\\s*@image${idx}\\b`, 'g'), '');
85
+ updateAttachBadge();
86
+ updatePromptMirror();
87
+ }
88
+
89
+ function clearAttachments() {
90
+ attachments.length = 0;
91
+ document.getElementById('attachmentPreviews').innerHTML = '';
92
+ updateAttachBadge();
93
+ updatePromptMirror();
94
+ }
95
+
96
+ function openFilePicker() {
97
+ document.getElementById('filePickerInput').click();
98
+ }
99
+
100
+ async function handleFiles(files) {
101
+ const container = document.getElementById('attachmentPreviews');
102
+ for (const file of files) {
103
+ const isImage = file.type.startsWith('image/');
104
+ const localUrl = isImage ? URL.createObjectURL(file) : null;
105
+
106
+ const tempIdx = attachments.length;
107
+ attachments.push({ localUrl, serverPath: null, filename: file.name, isImage, size: file.size });
108
+
109
+ const thumb = document.createElement('div');
110
+ thumb.dataset.idx = tempIdx;
111
+
112
+ if (isImage) {
113
+ thumb.className = 'img-thumb uploading';
114
+ thumb.innerHTML = `<img src="${localUrl}" alt="${escapeHtml(file.name)}">
115
+ <button class="img-remove" onclick="removeAttachment(${tempIdx})" title="제거">&times;</button>`;
116
+ } else {
117
+ thumb.className = 'file-thumb uploading';
118
+ thumb.innerHTML = `
119
+ <div class="file-icon">${escapeHtml(getFileExt(file.name))}</div>
120
+ <div class="file-info">
121
+ <div class="file-name" title="${escapeHtml(file.name)}">${escapeHtml(file.name)}</div>
122
+ <div class="file-size">${formatFileSize(file.size)}</div>
123
+ </div>
124
+ <button class="file-remove" onclick="removeAttachment(${tempIdx})" title="제거">&times;</button>`;
125
+ }
126
+ container.appendChild(thumb);
127
+ updateAttachBadge();
128
+
129
+ try {
130
+ const data = await uploadFile(file);
131
+ if (!attachments[tempIdx]) continue;
132
+ attachments[tempIdx].serverPath = data.path;
133
+ attachments[tempIdx].filename = data.filename || file.name;
134
+ thumb.classList.remove('uploading');
135
+ const ta = document.getElementById('promptInput');
136
+ insertAtCursor(ta, `@image${tempIdx}`);
137
+ } catch (err) {
138
+ showToast(`${t('msg_upload_failed')}: ${escapeHtml(file.name)} — ${err.message}`, 'error');
139
+ if (attachments[tempIdx]) attachments[tempIdx] = null;
140
+ if (thumb.parentNode) thumb.remove();
141
+ updateAttachBadge();
142
+ }
143
+ }
144
+ }
@@ -0,0 +1,497 @@
1
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
2
+
3
+ :root {
4
+ --bg: #0f1117;
5
+ --surface: #1a1d27;
6
+ --surface-hover: #222531;
7
+ --surface-active: #2a2d3a;
8
+ --border: #2e3140;
9
+ --border-light: #3a3d4a;
10
+ --text: #e4e6ed;
11
+ --text-secondary: #8b8fa3;
12
+ --text-muted: #5c5f73;
13
+ --accent: #6c8cff;
14
+ --accent-hover: #8aa4ff;
15
+ --accent-glow: rgba(108, 140, 255, 0.15);
16
+ --green: #34d399;
17
+ --green-dim: rgba(52, 211, 153, 0.15);
18
+ --red: #f87171;
19
+ --red-dim: rgba(248, 113, 113, 0.15);
20
+ --yellow: #fbbf24;
21
+ --yellow-dim: rgba(251, 191, 36, 0.15);
22
+ --blue: #60a5fa;
23
+ --blue-dim: rgba(96, 165, 250, 0.15);
24
+ --radius: 8px;
25
+ --radius-lg: 12px;
26
+ --font: 'Noto Sans KR', system-ui, -apple-system, sans-serif;
27
+ --font-mono: 'JetBrains Mono', 'SF Mono', 'Consolas', monospace;
28
+ --transition: 0.2s ease;
29
+ --shadow: 0 2px 8px rgba(0,0,0,0.3);
30
+ }
31
+
32
+ /* ── Light Theme ── */
33
+ [data-theme="light"] {
34
+ --bg: #f5f6fa;
35
+ --surface: #ffffff;
36
+ --surface-hover: #f0f1f5;
37
+ --surface-active: #e8e9ef;
38
+ --border: #d8dae3;
39
+ --border-light: #c5c8d4;
40
+ --text: #1a1d27;
41
+ --text-secondary: #4a4e63;
42
+ --text-muted: #8b8fa3;
43
+ --accent: #4f6cf7;
44
+ --accent-hover: #3d5ce5;
45
+ --accent-glow: rgba(79, 108, 247, 0.12);
46
+ --green: #0d9668;
47
+ --green-dim: rgba(13, 150, 104, 0.1);
48
+ --red: #dc3545;
49
+ --red-dim: rgba(220, 53, 69, 0.08);
50
+ --yellow: #c07d10;
51
+ --yellow-dim: rgba(192, 125, 16, 0.08);
52
+ --blue: #2563eb;
53
+ --blue-dim: rgba(37, 99, 235, 0.08);
54
+ --shadow: 0 2px 8px rgba(0,0,0,0.08);
55
+ --stream-bg: #f0f1f5;
56
+ }
57
+
58
+ html { font-size: 14px; }
59
+
60
+ body {
61
+ font-family: var(--font);
62
+ background: var(--bg);
63
+ color: var(--text);
64
+ min-height: 100vh;
65
+ line-height: 1.6;
66
+ -webkit-font-smoothing: antialiased;
67
+ max-width: 100vw;
68
+ overflow-x: hidden;
69
+ }
70
+
71
+ /* ── Scrollbar ── */
72
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
73
+ ::-webkit-scrollbar-track { background: transparent; }
74
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
75
+ ::-webkit-scrollbar-thumb:hover { background: var(--border-light); }
76
+
77
+ /* ── Buttons ── */
78
+ button {
79
+ font-family: var(--font);
80
+ cursor: pointer;
81
+ border: none;
82
+ outline: none;
83
+ transition: all var(--transition);
84
+ }
85
+
86
+ .btn {
87
+ display: inline-flex;
88
+ align-items: center;
89
+ gap: 6px;
90
+ padding: 7px 14px;
91
+ border-radius: var(--radius);
92
+ font-size: 0.82rem;
93
+ font-weight: 500;
94
+ background: var(--surface-hover);
95
+ color: var(--text-secondary);
96
+ border: 1px solid var(--border);
97
+ }
98
+
99
+ .btn:hover {
100
+ background: var(--surface-active);
101
+ color: var(--text);
102
+ border-color: var(--border-light);
103
+ }
104
+
105
+ .btn:active { transform: scale(0.97); }
106
+
107
+ .btn-primary {
108
+ background: var(--accent);
109
+ color: #fff;
110
+ border-color: var(--accent);
111
+ }
112
+
113
+ .btn-primary:hover {
114
+ background: var(--accent-hover);
115
+ border-color: var(--accent-hover);
116
+ box-shadow: 0 0 20px var(--accent-glow);
117
+ }
118
+
119
+ .btn-danger {
120
+ color: var(--red);
121
+ }
122
+
123
+ .btn-danger:hover {
124
+ background: var(--red-dim);
125
+ border-color: var(--red);
126
+ }
127
+
128
+ .btn-warning {
129
+ color: var(--yellow);
130
+ }
131
+
132
+ .btn-warning:hover {
133
+ background: var(--yellow-dim);
134
+ border-color: var(--yellow);
135
+ }
136
+
137
+ .btn-success {
138
+ color: var(--green);
139
+ }
140
+
141
+ .btn-success:hover {
142
+ background: var(--green-dim);
143
+ border-color: var(--green);
144
+ }
145
+
146
+ .btn-sm {
147
+ padding: 4px 10px;
148
+ font-size: 0.75rem;
149
+ }
150
+
151
+ .btn-icon {
152
+ padding: 7px;
153
+ display: inline-flex;
154
+ align-items: center;
155
+ justify-content: center;
156
+ }
157
+
158
+ .btn-collapse svg {
159
+ transition: transform 0.2s ease;
160
+ }
161
+ .btn-collapse.collapsed svg {
162
+ transform: rotate(-90deg);
163
+ }
164
+
165
+ .table-wrap.collapsed {
166
+ display: none;
167
+ }
168
+
169
+ /* ── Context Toolbar ── */
170
+ .ctx-toolbar-wrap {
171
+ position: relative;
172
+ flex-shrink: 0;
173
+ }
174
+
175
+ .ctx-toolbar {
176
+ display: flex;
177
+ align-items: center;
178
+ gap: 2px;
179
+ flex-shrink: 0;
180
+ }
181
+
182
+ .ctx-btn {
183
+ padding: 4px 10px;
184
+ border-radius: var(--radius);
185
+ font-size: 0.72rem;
186
+ font-weight: 600;
187
+ font-family: var(--font-mono);
188
+ background: transparent;
189
+ color: var(--text-muted);
190
+ border: 1px solid transparent;
191
+ letter-spacing: 0.02em;
192
+ text-transform: lowercase;
193
+ transition: all 0.15s ease;
194
+ }
195
+
196
+ .ctx-btn:hover {
197
+ background: var(--surface-hover);
198
+ color: var(--text-secondary);
199
+ border-color: var(--border);
200
+ }
201
+
202
+ .ctx-btn.active {
203
+ background: var(--accent-glow);
204
+ color: var(--accent);
205
+ border-color: var(--accent);
206
+ }
207
+
208
+ .ctx-btn.active[id="ctxResume"],
209
+ .ctx-btn.active[id="ctxFork"] {
210
+ background: var(--blue);
211
+ color: #fff;
212
+ border-color: var(--blue);
213
+ box-shadow: 0 1px 4px rgba(0,0,0,0.15);
214
+ }
215
+
216
+ .ctx-btn-x {
217
+ padding: 4px 6px;
218
+ border-radius: var(--radius);
219
+ font-size: 0.78rem;
220
+ font-weight: 500;
221
+ background: transparent;
222
+ color: var(--text-muted);
223
+ border: 1px solid transparent;
224
+ line-height: 1;
225
+ display: inline-flex;
226
+ align-items: center;
227
+ justify-content: center;
228
+ }
229
+
230
+ .ctx-btn-x:hover {
231
+ background: var(--red-dim);
232
+ color: var(--red);
233
+ border-color: var(--red);
234
+ }
235
+
236
+ .ctx-session-label {
237
+ display: none;
238
+ padding: 3px 10px;
239
+ border-radius: 12px;
240
+ font-size: 0.75rem;
241
+ font-weight: 600;
242
+ font-family: var(--font-mono);
243
+ background: var(--blue);
244
+ color: #fff;
245
+ overflow: hidden;
246
+ text-overflow: ellipsis;
247
+ white-space: nowrap;
248
+ line-height: 1.6;
249
+ min-width: 0;
250
+ max-width: 200px;
251
+ box-shadow: 0 0 0 2px var(--blue-dim), 0 1px 4px rgba(0,0,0,0.15);
252
+ animation: ctx-label-pop 0.2s ease-out;
253
+ }
254
+
255
+ @keyframes ctx-label-pop {
256
+ from { opacity: 0; transform: scale(0.9); }
257
+ to { opacity: 1; transform: scale(1); }
258
+ }
259
+
260
+ .ctx-session-label.visible {
261
+ display: inline-flex;
262
+ align-items: center;
263
+ gap: 4px;
264
+ }
265
+
266
+ .session-picker {
267
+ display: none;
268
+ flex-direction: column;
269
+ position: absolute;
270
+ top: 100%;
271
+ right: 0;
272
+ margin-top: 4px;
273
+ width: 520px;
274
+ max-width: calc(100vw - 32px);
275
+ max-height: 420px;
276
+ background: var(--surface);
277
+ border: 1px solid var(--border);
278
+ border-radius: var(--radius-lg);
279
+ box-shadow: 0 8px 32px rgba(0,0,0,0.4);
280
+ z-index: 200;
281
+ }
282
+
283
+ .session-picker.open { display: flex; }
284
+
285
+ .session-picker-header {
286
+ display: flex;
287
+ align-items: center;
288
+ justify-content: space-between;
289
+ padding: 10px 14px;
290
+ border-bottom: 1px solid var(--border);
291
+ flex-shrink: 0;
292
+ }
293
+
294
+ .session-picker-title {
295
+ font-size: 0.78rem;
296
+ font-weight: 600;
297
+ color: var(--text-secondary);
298
+ }
299
+
300
+ .session-picker-search {
301
+ padding: 8px 14px;
302
+ border-bottom: 1px solid var(--border);
303
+ flex-shrink: 0;
304
+ }
305
+
306
+ .session-picker-search input {
307
+ width: 100%;
308
+ padding: 6px 10px;
309
+ border: 1px solid var(--border);
310
+ border-radius: var(--radius);
311
+ background: var(--bg);
312
+ color: var(--text);
313
+ font-size: 0.78rem;
314
+ font-family: var(--font);
315
+ outline: none;
316
+ transition: border-color var(--transition);
317
+ }
318
+
319
+ .session-picker-search input:focus {
320
+ border-color: var(--accent);
321
+ }
322
+
323
+ .session-picker-search input::placeholder {
324
+ color: var(--text-muted);
325
+ }
326
+
327
+ .session-picker-list {
328
+ overflow-y: auto;
329
+ flex: 1;
330
+ }
331
+
332
+ .session-item {
333
+ display: flex;
334
+ flex-direction: column;
335
+ gap: 4px;
336
+ padding: 10px 14px;
337
+ cursor: pointer;
338
+ border-bottom: 1px solid var(--border);
339
+ transition: background var(--transition);
340
+ }
341
+
342
+ .session-item:hover { background: var(--surface-hover); }
343
+ .session-item:last-child { border-bottom: none; }
344
+
345
+ .session-item-row {
346
+ display: flex;
347
+ align-items: center;
348
+ gap: 8px;
349
+ }
350
+
351
+ .session-item-status {
352
+ display: inline-block;
353
+ width: 7px;
354
+ height: 7px;
355
+ border-radius: 50%;
356
+ flex-shrink: 0;
357
+ }
358
+ .session-item-status.done { background: var(--green); }
359
+ .session-item-status.failed { background: var(--red); }
360
+ .session-item-status.running { background: var(--yellow); animation: pulse 1.5s infinite; }
361
+ .session-item-status.unknown { background: var(--text-muted); }
362
+
363
+ @keyframes pulse {
364
+ 0%, 100% { opacity: 1; }
365
+ 50% { opacity: 0.4; }
366
+ }
367
+
368
+ .session-item-id {
369
+ font-family: var(--font-mono);
370
+ font-size: 0.7rem;
371
+ color: var(--accent);
372
+ flex-shrink: 0;
373
+ }
374
+
375
+ .session-item-slug {
376
+ font-size: 0.7rem;
377
+ color: var(--text-muted);
378
+ flex-shrink: 0;
379
+ font-style: italic;
380
+ }
381
+
382
+ .session-item-job {
383
+ font-family: var(--font-mono);
384
+ font-size: 0.65rem;
385
+ color: var(--text-muted);
386
+ flex-shrink: 0;
387
+ }
388
+
389
+ .session-item-prompt {
390
+ font-size: 0.78rem;
391
+ color: var(--text);
392
+ flex: 1;
393
+ overflow: hidden;
394
+ text-overflow: ellipsis;
395
+ white-space: nowrap;
396
+ }
397
+
398
+ .session-item-meta {
399
+ display: flex;
400
+ align-items: center;
401
+ gap: 12px;
402
+ padding-left: 15px;
403
+ }
404
+
405
+ .session-item-time {
406
+ font-size: 0.68rem;
407
+ color: var(--text-muted);
408
+ flex-shrink: 0;
409
+ }
410
+
411
+ .session-item-cwd {
412
+ font-size: 0.65rem;
413
+ color: var(--text-muted);
414
+ font-family: var(--font-mono);
415
+ overflow: hidden;
416
+ text-overflow: ellipsis;
417
+ white-space: nowrap;
418
+ }
419
+
420
+ .session-filter-bar {
421
+ display: flex;
422
+ align-items: center;
423
+ gap: 6px;
424
+ padding: 6px 14px;
425
+ border-bottom: 1px solid var(--border);
426
+ flex-shrink: 0;
427
+ }
428
+
429
+ .session-filter-toggle {
430
+ display: inline-flex;
431
+ align-items: center;
432
+ gap: 4px;
433
+ padding: 3px 8px;
434
+ border: 1px solid var(--border);
435
+ border-radius: 4px;
436
+ background: transparent;
437
+ color: var(--text-muted);
438
+ font-size: 0.68rem;
439
+ font-family: var(--font);
440
+ cursor: pointer;
441
+ transition: all var(--transition);
442
+ white-space: nowrap;
443
+ }
444
+
445
+ .session-filter-toggle:hover { border-color: var(--accent); color: var(--text-secondary); }
446
+ .session-filter-toggle.active { background: var(--accent-glow); border-color: var(--accent); color: var(--accent); }
447
+
448
+ .session-filter-project {
449
+ font-size: 0.68rem;
450
+ color: var(--text-muted);
451
+ overflow: hidden;
452
+ text-overflow: ellipsis;
453
+ white-space: nowrap;
454
+ flex: 1;
455
+ }
456
+
457
+ .session-group-header {
458
+ display: flex;
459
+ align-items: center;
460
+ gap: 6px;
461
+ padding: 6px 14px;
462
+ background: var(--bg);
463
+ font-size: 0.68rem;
464
+ font-weight: 600;
465
+ color: var(--text-secondary);
466
+ border-bottom: 1px solid var(--border);
467
+ position: sticky;
468
+ top: 0;
469
+ z-index: 1;
470
+ }
471
+
472
+ .session-group-header svg {
473
+ width: 12px;
474
+ height: 12px;
475
+ flex-shrink: 0;
476
+ opacity: 0.6;
477
+ }
478
+
479
+ .session-group-count {
480
+ font-weight: 400;
481
+ color: var(--text-muted);
482
+ margin-left: auto;
483
+ }
484
+
485
+ .session-empty {
486
+ padding: 24px 14px;
487
+ text-align: center;
488
+ font-size: 0.78rem;
489
+ color: var(--text-muted);
490
+ }
491
+
492
+ .session-empty-icon {
493
+ font-size: 1.5rem;
494
+ margin-bottom: 8px;
495
+ opacity: 0.5;
496
+ }
497
+