@cluesmith/codev 1.4.2 → 1.4.4
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.
- package/dist/agent-farm/servers/dashboard-server.js +1 -5
- package/dist/agent-farm/servers/dashboard-server.js.map +1 -1
- package/dist/agent-farm/servers/tower-server.js +141 -40
- package/dist/agent-farm/servers/tower-server.js.map +1 -1
- package/dist/agent-farm/utils/port-registry.d.ts.map +1 -1
- package/dist/agent-farm/utils/port-registry.js +19 -5
- package/dist/agent-farm/utils/port-registry.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/adopt.d.ts.map +1 -1
- package/dist/commands/adopt.js +38 -4
- package/dist/commands/adopt.js.map +1 -1
- package/dist/commands/consult/index.d.ts +1 -0
- package/dist/commands/consult/index.d.ts.map +1 -1
- package/dist/commands/consult/index.js +56 -8
- package/dist/commands/consult/index.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +75 -1
- package/dist/commands/update.js.map +1 -1
- package/dist/lib/templates.d.ts.map +1 -1
- package/dist/lib/templates.js +5 -1
- package/dist/lib/templates.js.map +1 -1
- package/package.json +1 -1
- package/skeleton/resources/commands/consult.md +50 -0
- package/skeleton/templates/AGENTS.md +28 -0
- package/skeleton/templates/CLAUDE.md +28 -0
- package/templates/dashboard/index.html +1 -1
- package/templates/tower.html +172 -4
- package/dist/commands/eject.d.ts +0 -18
- package/dist/commands/eject.d.ts.map +0 -1
- package/dist/commands/eject.js +0 -149
- package/dist/commands/eject.js.map +0 -1
- package/templates/dashboard-split.html +0 -4741
- package/templates/dashboard.html +0 -149
|
@@ -1,4741 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>AF: {{PROJECT_NAME}}</title>
|
|
7
|
-
<style>
|
|
8
|
-
* {
|
|
9
|
-
box-sizing: border-box;
|
|
10
|
-
margin: 0;
|
|
11
|
-
padding: 0;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
:root {
|
|
15
|
-
--bg-primary: #1a1a1a;
|
|
16
|
-
--bg-secondary: #252525;
|
|
17
|
-
--bg-tertiary: #2a2a2a;
|
|
18
|
-
--border: #333;
|
|
19
|
-
--text-primary: #fff;
|
|
20
|
-
--text-secondary: #ccc;
|
|
21
|
-
--text-muted: #666;
|
|
22
|
-
--accent: #3b82f6;
|
|
23
|
-
--tab-active: #333;
|
|
24
|
-
--tab-hover: #2a2a2a;
|
|
25
|
-
/* Status indicator colors per spec 0019 */
|
|
26
|
-
--status-active: #22c55e; /* Green: spawning, implementing */
|
|
27
|
-
--status-waiting: #eab308; /* Yellow: pr-ready (waiting for review) */
|
|
28
|
-
--status-error: #ef4444; /* Red: blocked */
|
|
29
|
-
--status-complete: #9e9e9e; /* Gray: complete */
|
|
30
|
-
/* Project lifecycle status colors per spec 0045 */
|
|
31
|
-
--project-conceived: #eab308; /* Yellow */
|
|
32
|
-
--project-specified: #3b82f6; /* Blue */
|
|
33
|
-
--project-planned: #3b82f6; /* Blue */
|
|
34
|
-
--project-implementing: #f97316; /* Orange */
|
|
35
|
-
--project-implemented: #a855f7; /* Purple */
|
|
36
|
-
--project-committed: #22c55e; /* Green */
|
|
37
|
-
--project-integrated: #9e9e9e; /* Gray */
|
|
38
|
-
--project-abandoned: #ef4444; /* Red */
|
|
39
|
-
--project-on-hold: #9e9e9e; /* Gray */
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
body {
|
|
43
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
44
|
-
background: var(--bg-primary);
|
|
45
|
-
color: var(--text-primary);
|
|
46
|
-
height: 100vh;
|
|
47
|
-
display: flex;
|
|
48
|
-
flex-direction: column;
|
|
49
|
-
overflow: hidden;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/* Header */
|
|
53
|
-
.header {
|
|
54
|
-
display: flex;
|
|
55
|
-
justify-content: space-between;
|
|
56
|
-
align-items: center;
|
|
57
|
-
padding: 12px 16px;
|
|
58
|
-
background: var(--bg-secondary);
|
|
59
|
-
border-bottom: 1px solid var(--border);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
.header h1 {
|
|
63
|
-
font-size: 16px;
|
|
64
|
-
font-weight: 600;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
.header-actions {
|
|
68
|
-
display: flex;
|
|
69
|
-
gap: 8px;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
.btn {
|
|
73
|
-
padding: 6px 12px;
|
|
74
|
-
border-radius: 4px;
|
|
75
|
-
border: 1px solid var(--border);
|
|
76
|
-
background: var(--bg-tertiary);
|
|
77
|
-
color: var(--text-secondary);
|
|
78
|
-
cursor: pointer;
|
|
79
|
-
font-size: 13px;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
.btn:hover {
|
|
83
|
-
background: var(--tab-active);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
.btn-danger {
|
|
87
|
-
border-color: #ef4444;
|
|
88
|
-
color: #ef4444;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
.btn-danger:hover {
|
|
92
|
-
background: rgba(239, 68, 68, 0.1);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/* Main content area */
|
|
96
|
-
.main {
|
|
97
|
-
display: flex;
|
|
98
|
-
flex: 1;
|
|
99
|
-
overflow: hidden;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/* Left pane - Architect */
|
|
103
|
-
.left-pane {
|
|
104
|
-
width: 50%;
|
|
105
|
-
min-width: 20%;
|
|
106
|
-
max-width: 80%;
|
|
107
|
-
resize: horizontal;
|
|
108
|
-
overflow: auto;
|
|
109
|
-
border-right: 1px solid var(--border);
|
|
110
|
-
display: flex;
|
|
111
|
-
flex-direction: column;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
.pane-header {
|
|
115
|
-
padding: 8px 12px;
|
|
116
|
-
background: var(--bg-secondary);
|
|
117
|
-
border-bottom: 1px solid var(--border);
|
|
118
|
-
font-size: 12px;
|
|
119
|
-
color: var(--text-muted);
|
|
120
|
-
text-transform: uppercase;
|
|
121
|
-
letter-spacing: 0.5px;
|
|
122
|
-
display: flex;
|
|
123
|
-
align-items: center;
|
|
124
|
-
gap: 6px;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
.pane-header .status-dot {
|
|
128
|
-
width: 8px;
|
|
129
|
-
height: 8px;
|
|
130
|
-
border-radius: 50%;
|
|
131
|
-
background: var(--status-active);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
.pane-header .status-dot.inactive {
|
|
135
|
-
background: var(--text-muted);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
#architect-content {
|
|
139
|
-
flex: 1;
|
|
140
|
-
display: flex;
|
|
141
|
-
flex-direction: column;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
.left-pane iframe {
|
|
145
|
-
flex: 1;
|
|
146
|
-
width: 100%;
|
|
147
|
-
border: none;
|
|
148
|
-
background: #000;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
.architect-placeholder {
|
|
152
|
-
flex: 1;
|
|
153
|
-
display: flex;
|
|
154
|
-
flex-direction: column;
|
|
155
|
-
align-items: center;
|
|
156
|
-
justify-content: center;
|
|
157
|
-
color: var(--text-muted);
|
|
158
|
-
gap: 16px;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
.architect-placeholder code {
|
|
162
|
-
background: var(--bg-tertiary);
|
|
163
|
-
padding: 4px 8px;
|
|
164
|
-
border-radius: 4px;
|
|
165
|
-
font-size: 13px;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/* Right pane - Tabs */
|
|
169
|
-
.right-pane {
|
|
170
|
-
width: 50%;
|
|
171
|
-
display: flex;
|
|
172
|
-
flex-direction: column;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/* Tab bar */
|
|
176
|
-
.tab-bar {
|
|
177
|
-
display: flex;
|
|
178
|
-
align-items: center;
|
|
179
|
-
background: var(--bg-secondary);
|
|
180
|
-
border-bottom: 1px solid var(--border);
|
|
181
|
-
min-height: 40px;
|
|
182
|
-
overflow: visible; /* Allow overflow menu dropdown to be visible */
|
|
183
|
-
position: relative; /* Position context for overflow menu */
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
.tabs-scroll {
|
|
187
|
-
display: flex;
|
|
188
|
-
overflow-x: auto;
|
|
189
|
-
flex: 1;
|
|
190
|
-
scrollbar-width: none;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
.tabs-scroll::-webkit-scrollbar {
|
|
194
|
-
display: none;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
.tab {
|
|
198
|
-
display: flex;
|
|
199
|
-
align-items: center;
|
|
200
|
-
gap: 6px;
|
|
201
|
-
padding: 8px 12px;
|
|
202
|
-
cursor: pointer;
|
|
203
|
-
border-right: 1px solid var(--border);
|
|
204
|
-
border-bottom: 2px solid transparent; /* Reserve space for active indicator */
|
|
205
|
-
white-space: nowrap;
|
|
206
|
-
flex-shrink: 0;
|
|
207
|
-
position: relative;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
.tab:hover {
|
|
211
|
-
background: var(--tab-hover);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
.tab.active {
|
|
215
|
-
background: var(--bg-tertiary);
|
|
216
|
-
border-bottom: 2px solid var(--accent); /* Blue accent line */
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
.tab.new-tab {
|
|
220
|
-
animation: tab-pulse 0.5s ease-out;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
@keyframes tab-pulse {
|
|
224
|
-
0% { background: var(--accent); }
|
|
225
|
-
100% { background: var(--tab-active); }
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
.tab .icon {
|
|
229
|
-
font-size: 14px;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
.tab .name {
|
|
233
|
-
font-size: 13px;
|
|
234
|
-
max-width: 120px;
|
|
235
|
-
overflow: hidden;
|
|
236
|
-
text-overflow: ellipsis;
|
|
237
|
-
color: var(--text-secondary);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
.tab.active .name {
|
|
241
|
-
color: var(--text-primary);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
.tab .status-dot {
|
|
245
|
-
width: 6px;
|
|
246
|
-
height: 6px;
|
|
247
|
-
border-radius: 50%;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/* Shape modifiers for accessibility (not just color) */
|
|
251
|
-
.tab .status-dot--diamond {
|
|
252
|
-
border-radius: 1px;
|
|
253
|
-
transform: rotate(45deg);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/* Ring shape for pr-ready (accessibility: distinct from circle) */
|
|
257
|
-
.tab .status-dot--ring {
|
|
258
|
-
box-shadow: inset 0 0 0 1.5px currentColor;
|
|
259
|
-
background: transparent !important;
|
|
260
|
-
color: var(--status-waiting);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/* Distinct animations per status category (spec 0019) */
|
|
264
|
-
@keyframes status-pulse {
|
|
265
|
-
/* Pulsing: Active/working (spawning, implementing) */
|
|
266
|
-
0%, 100% { opacity: 1; transform: scale(1); }
|
|
267
|
-
50% { opacity: 0.7; transform: scale(0.9); }
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
@keyframes status-blink-slow {
|
|
271
|
-
/* Slow blink: Idle/waiting (pr-ready) */
|
|
272
|
-
0%, 100% { opacity: 1; }
|
|
273
|
-
50% { opacity: 0.3; }
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
@keyframes status-blink-fast {
|
|
277
|
-
/* Fast blink: Error/blocked */
|
|
278
|
-
0%, 100% { opacity: 1; }
|
|
279
|
-
50% { opacity: 0.2; }
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
.tab .status-dot--pulse {
|
|
283
|
-
animation: status-pulse 2s ease-in-out infinite;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
.tab .status-dot--blink-slow {
|
|
287
|
-
animation: status-blink-slow 3s ease-in-out infinite;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
.tab .status-dot--blink-fast {
|
|
291
|
-
animation: status-blink-fast 0.8s ease-in-out infinite;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/* Respect reduced motion preference (WCAG 2.3.3) */
|
|
295
|
-
/* Motion-independent differentiators remain: diamond for blocked, ring for pr-ready */
|
|
296
|
-
@media (prefers-reduced-motion: reduce) {
|
|
297
|
-
.tab .status-dot--pulse,
|
|
298
|
-
.tab .status-dot--blink-slow,
|
|
299
|
-
.tab .status-dot--blink-fast {
|
|
300
|
-
animation: none;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
.tab .close {
|
|
305
|
-
opacity: 0.6; /* Always clearly visible */
|
|
306
|
-
margin-left: 6px;
|
|
307
|
-
font-size: 16px;
|
|
308
|
-
font-weight: 500;
|
|
309
|
-
color: var(--text-secondary);
|
|
310
|
-
padding: 4px 8px;
|
|
311
|
-
border-radius: 4px;
|
|
312
|
-
cursor: pointer;
|
|
313
|
-
line-height: 1;
|
|
314
|
-
min-width: 24px;
|
|
315
|
-
min-height: 24px;
|
|
316
|
-
display: flex;
|
|
317
|
-
align-items: center;
|
|
318
|
-
justify-content: center;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
.tab:hover .close {
|
|
322
|
-
opacity: 0.9;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
.tab .close:hover {
|
|
326
|
-
opacity: 1;
|
|
327
|
-
background: rgba(239, 68, 68, 0.2); /* Red tint on hover */
|
|
328
|
-
color: #ef4444;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/* Add buttons */
|
|
332
|
-
.add-buttons {
|
|
333
|
-
display: flex;
|
|
334
|
-
gap: 4px;
|
|
335
|
-
padding: 0 8px;
|
|
336
|
-
flex-shrink: 0;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
.add-btn {
|
|
340
|
-
padding: 4px 8px;
|
|
341
|
-
border-radius: 4px;
|
|
342
|
-
border: 1px dashed var(--border);
|
|
343
|
-
background: transparent;
|
|
344
|
-
color: var(--text-muted);
|
|
345
|
-
cursor: pointer;
|
|
346
|
-
font-size: 12px;
|
|
347
|
-
display: flex;
|
|
348
|
-
align-items: center;
|
|
349
|
-
gap: 4px;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
.add-btn:hover {
|
|
353
|
-
border-style: solid;
|
|
354
|
-
color: var(--text-secondary);
|
|
355
|
-
background: var(--bg-tertiary);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
/* Overflow indicator */
|
|
359
|
-
.overflow-btn {
|
|
360
|
-
padding: 8px 12px;
|
|
361
|
-
background: var(--bg-tertiary);
|
|
362
|
-
border: none;
|
|
363
|
-
border-left: 1px solid var(--border);
|
|
364
|
-
color: var(--text-secondary);
|
|
365
|
-
cursor: pointer;
|
|
366
|
-
display: none; /* Hidden by default, shown via JS */
|
|
367
|
-
align-items: center;
|
|
368
|
-
gap: 4px;
|
|
369
|
-
flex-shrink: 0;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
.overflow-btn:hover {
|
|
373
|
-
background: var(--tab-hover);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
.overflow-btn:focus {
|
|
377
|
-
outline: 2px solid var(--accent);
|
|
378
|
-
outline-offset: -2px;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
.overflow-count {
|
|
382
|
-
font-size: 11px;
|
|
383
|
-
background: var(--accent);
|
|
384
|
-
color: white;
|
|
385
|
-
padding: 1px 5px;
|
|
386
|
-
border-radius: 8px;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
/* Overflow menu dropdown */
|
|
390
|
-
.overflow-menu {
|
|
391
|
-
position: absolute;
|
|
392
|
-
right: 0;
|
|
393
|
-
top: 100%;
|
|
394
|
-
background: var(--bg-secondary);
|
|
395
|
-
border: 1px solid var(--border);
|
|
396
|
-
border-radius: 4px;
|
|
397
|
-
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
|
398
|
-
max-height: 300px;
|
|
399
|
-
overflow-y: auto;
|
|
400
|
-
min-width: 200px;
|
|
401
|
-
z-index: 100;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
.overflow-menu.hidden {
|
|
405
|
-
display: none;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
.overflow-menu-item {
|
|
409
|
-
padding: 8px 12px;
|
|
410
|
-
cursor: pointer;
|
|
411
|
-
display: flex;
|
|
412
|
-
align-items: center;
|
|
413
|
-
gap: 8px;
|
|
414
|
-
font-size: 13px;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
.overflow-menu-item:hover,
|
|
418
|
-
.overflow-menu-item:focus {
|
|
419
|
-
background: var(--tab-hover);
|
|
420
|
-
outline: none;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
.overflow-menu-item.active {
|
|
424
|
-
background: var(--tab-active);
|
|
425
|
-
border-left: 2px solid var(--accent);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
.overflow-menu-item .icon {
|
|
429
|
-
font-size: 14px;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
.overflow-menu-item .name {
|
|
433
|
-
flex: 1;
|
|
434
|
-
overflow: hidden;
|
|
435
|
-
text-overflow: ellipsis;
|
|
436
|
-
white-space: nowrap;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
.overflow-menu-item .open-external {
|
|
440
|
-
opacity: 0.5;
|
|
441
|
-
cursor: pointer;
|
|
442
|
-
padding: 2px 6px;
|
|
443
|
-
font-size: 12px;
|
|
444
|
-
border-radius: 3px;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
.overflow-menu-item .open-external:hover {
|
|
448
|
-
opacity: 1;
|
|
449
|
-
background: rgba(255, 255, 255, 0.1);
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
/* Tab content */
|
|
453
|
-
.tab-content {
|
|
454
|
-
flex: 1;
|
|
455
|
-
display: flex;
|
|
456
|
-
flex-direction: column;
|
|
457
|
-
overflow: hidden;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
.tab-content iframe {
|
|
461
|
-
flex: 1;
|
|
462
|
-
width: 100%;
|
|
463
|
-
border: none;
|
|
464
|
-
background: #000;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
.empty-state {
|
|
468
|
-
flex: 1;
|
|
469
|
-
display: flex;
|
|
470
|
-
flex-direction: column;
|
|
471
|
-
align-items: center;
|
|
472
|
-
justify-content: center;
|
|
473
|
-
color: var(--text-muted);
|
|
474
|
-
gap: 12px;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
.empty-state .hint {
|
|
478
|
-
font-size: 13px;
|
|
479
|
-
text-align: center;
|
|
480
|
-
max-width: 300px;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
/* Status bar */
|
|
484
|
-
.status-bar {
|
|
485
|
-
padding: 8px 16px;
|
|
486
|
-
background: var(--bg-secondary);
|
|
487
|
-
border-top: 1px solid var(--border);
|
|
488
|
-
font-size: 12px;
|
|
489
|
-
color: var(--text-muted);
|
|
490
|
-
display: flex;
|
|
491
|
-
gap: 16px;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
.status-item {
|
|
495
|
-
display: flex;
|
|
496
|
-
align-items: center;
|
|
497
|
-
gap: 6px;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
.status-item .dot {
|
|
501
|
-
width: 6px;
|
|
502
|
-
height: 6px;
|
|
503
|
-
border-radius: 50%;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
/* Dialogs */
|
|
507
|
-
.dialog-overlay {
|
|
508
|
-
position: fixed;
|
|
509
|
-
top: 0;
|
|
510
|
-
left: 0;
|
|
511
|
-
right: 0;
|
|
512
|
-
bottom: 0;
|
|
513
|
-
background: rgba(0, 0, 0, 0.6);
|
|
514
|
-
display: flex;
|
|
515
|
-
align-items: center;
|
|
516
|
-
justify-content: center;
|
|
517
|
-
z-index: 1000;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
.dialog-overlay.hidden {
|
|
521
|
-
display: none;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
.dialog {
|
|
525
|
-
background: var(--bg-secondary);
|
|
526
|
-
border: 1px solid var(--border);
|
|
527
|
-
border-radius: 8px;
|
|
528
|
-
padding: 20px;
|
|
529
|
-
min-width: 320px;
|
|
530
|
-
max-width: 90%;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
.dialog h3 {
|
|
534
|
-
margin-bottom: 16px;
|
|
535
|
-
font-size: 16px;
|
|
536
|
-
font-weight: 500;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
.dialog input {
|
|
540
|
-
width: 100%;
|
|
541
|
-
padding: 8px 12px;
|
|
542
|
-
border-radius: 4px;
|
|
543
|
-
border: 1px solid var(--border);
|
|
544
|
-
background: var(--bg-tertiary);
|
|
545
|
-
color: var(--text-primary);
|
|
546
|
-
font-size: 14px;
|
|
547
|
-
margin-bottom: 16px;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
.dialog input:focus {
|
|
551
|
-
outline: none;
|
|
552
|
-
border-color: var(--accent);
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
.dialog-actions {
|
|
556
|
-
display: flex;
|
|
557
|
-
justify-content: flex-end;
|
|
558
|
-
gap: 8px;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
.quick-paths {
|
|
562
|
-
display: flex;
|
|
563
|
-
flex-wrap: wrap;
|
|
564
|
-
gap: 8px;
|
|
565
|
-
margin-bottom: 12px;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
.quick-path {
|
|
569
|
-
padding: 4px 8px;
|
|
570
|
-
border-radius: 4px;
|
|
571
|
-
background: var(--bg-tertiary);
|
|
572
|
-
border: 1px solid var(--border);
|
|
573
|
-
color: var(--text-secondary);
|
|
574
|
-
cursor: pointer;
|
|
575
|
-
font-size: 12px;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
.quick-path:hover {
|
|
579
|
-
background: var(--tab-hover);
|
|
580
|
-
border-color: var(--accent);
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
/* Toast notifications */
|
|
584
|
-
.toast-container {
|
|
585
|
-
position: fixed;
|
|
586
|
-
bottom: 60px;
|
|
587
|
-
right: 16px;
|
|
588
|
-
z-index: 2000;
|
|
589
|
-
display: flex;
|
|
590
|
-
flex-direction: column;
|
|
591
|
-
gap: 8px;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
.toast {
|
|
595
|
-
padding: 12px 16px;
|
|
596
|
-
background: var(--bg-secondary);
|
|
597
|
-
border: 1px solid var(--border);
|
|
598
|
-
border-radius: 6px;
|
|
599
|
-
font-size: 13px;
|
|
600
|
-
display: flex;
|
|
601
|
-
align-items: center;
|
|
602
|
-
gap: 8px;
|
|
603
|
-
animation: toast-in 0.3s ease-out;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
.toast.error {
|
|
607
|
-
border-color: #ef4444;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
.toast.success {
|
|
611
|
-
border-color: #22c55e;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
@keyframes toast-in {
|
|
615
|
-
from {
|
|
616
|
-
opacity: 0;
|
|
617
|
-
transform: translateY(10px);
|
|
618
|
-
}
|
|
619
|
-
to {
|
|
620
|
-
opacity: 1;
|
|
621
|
-
transform: translateY(0);
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
/* Context menu */
|
|
626
|
-
.context-menu {
|
|
627
|
-
position: fixed;
|
|
628
|
-
background: var(--bg-secondary);
|
|
629
|
-
border: 1px solid var(--border);
|
|
630
|
-
border-radius: 4px;
|
|
631
|
-
padding: 4px 0;
|
|
632
|
-
min-width: 150px;
|
|
633
|
-
z-index: 1000;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
.context-menu.hidden {
|
|
637
|
-
display: none;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
.context-menu-item {
|
|
641
|
-
padding: 8px 12px;
|
|
642
|
-
cursor: pointer;
|
|
643
|
-
font-size: 13px;
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
.context-menu-item:hover {
|
|
647
|
-
background: var(--tab-hover);
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
.context-menu-item.danger {
|
|
651
|
-
color: #ef4444;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
/* Activity Summary Modal (Spec 0059) */
|
|
655
|
-
.activity-dialog {
|
|
656
|
-
width: 600px;
|
|
657
|
-
max-width: 90vw;
|
|
658
|
-
max-height: 80vh;
|
|
659
|
-
display: flex;
|
|
660
|
-
flex-direction: column;
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
.activity-dialog-header {
|
|
664
|
-
display: flex;
|
|
665
|
-
justify-content: space-between;
|
|
666
|
-
align-items: center;
|
|
667
|
-
margin-bottom: 16px;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
.activity-dialog-header h3 {
|
|
671
|
-
margin: 0;
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
.activity-close-btn {
|
|
675
|
-
background: none;
|
|
676
|
-
border: none;
|
|
677
|
-
font-size: 24px;
|
|
678
|
-
color: var(--text-muted);
|
|
679
|
-
cursor: pointer;
|
|
680
|
-
padding: 0 8px;
|
|
681
|
-
line-height: 1;
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
.activity-close-btn:hover {
|
|
685
|
-
color: var(--text-primary);
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
.activity-dialog-content {
|
|
689
|
-
flex: 1;
|
|
690
|
-
overflow-y: auto;
|
|
691
|
-
max-height: 50vh;
|
|
692
|
-
margin-bottom: 16px;
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
.activity-loading {
|
|
696
|
-
display: flex;
|
|
697
|
-
align-items: center;
|
|
698
|
-
justify-content: center;
|
|
699
|
-
gap: 12px;
|
|
700
|
-
padding: 40px 20px;
|
|
701
|
-
color: var(--text-muted);
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
.activity-spinner {
|
|
705
|
-
width: 20px;
|
|
706
|
-
height: 20px;
|
|
707
|
-
border: 2px solid var(--border);
|
|
708
|
-
border-top-color: var(--accent);
|
|
709
|
-
border-radius: 50%;
|
|
710
|
-
animation: spin 1s linear infinite;
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
@keyframes spin {
|
|
714
|
-
to { transform: rotate(360deg); }
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
.activity-empty {
|
|
718
|
-
text-align: center;
|
|
719
|
-
padding: 40px 20px;
|
|
720
|
-
color: var(--text-muted);
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
.activity-error {
|
|
724
|
-
text-align: center;
|
|
725
|
-
padding: 40px 20px;
|
|
726
|
-
color: #ef4444;
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
.activity-summary {
|
|
730
|
-
line-height: 1.6;
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
.activity-ai-summary {
|
|
734
|
-
background: var(--bg-tertiary);
|
|
735
|
-
border-left: 3px solid var(--accent);
|
|
736
|
-
padding: 12px 16px;
|
|
737
|
-
margin-bottom: 20px;
|
|
738
|
-
font-style: italic;
|
|
739
|
-
color: var(--text-secondary);
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
.activity-section {
|
|
743
|
-
margin-bottom: 16px;
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
.activity-section h4 {
|
|
747
|
-
font-size: 13px;
|
|
748
|
-
text-transform: uppercase;
|
|
749
|
-
color: var(--text-muted);
|
|
750
|
-
margin: 0 0 8px 0;
|
|
751
|
-
letter-spacing: 0.5px;
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
.activity-section ul {
|
|
755
|
-
margin: 0;
|
|
756
|
-
padding-left: 20px;
|
|
757
|
-
color: var(--text-secondary);
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
.activity-section li {
|
|
761
|
-
margin-bottom: 4px;
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
.activity-section p {
|
|
765
|
-
margin: 4px 0;
|
|
766
|
-
color: var(--text-secondary);
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
.activity-time-value {
|
|
770
|
-
font-size: 18px;
|
|
771
|
-
font-weight: 500;
|
|
772
|
-
color: var(--text-primary);
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
.activity-dialog-footer {
|
|
776
|
-
display: flex;
|
|
777
|
-
justify-content: flex-end;
|
|
778
|
-
gap: 8px;
|
|
779
|
-
padding-top: 12px;
|
|
780
|
-
border-top: 1px solid var(--border);
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
/* Activity Tab Styles (Spec 0059) */
|
|
784
|
-
.activity-tab-container {
|
|
785
|
-
padding: 24px;
|
|
786
|
-
max-width: 700px;
|
|
787
|
-
margin: 0 auto;
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
.activity-tab-container .activity-summary {
|
|
791
|
-
background: var(--bg-secondary);
|
|
792
|
-
border-radius: 8px;
|
|
793
|
-
padding: 20px;
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
.activity-tab-container .activity-actions {
|
|
797
|
-
margin-top: 20px;
|
|
798
|
-
padding-top: 16px;
|
|
799
|
-
border-top: 1px solid var(--border);
|
|
800
|
-
display: flex;
|
|
801
|
-
justify-content: flex-end;
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
/* Projects Tab Styles (Spec 0045) */
|
|
805
|
-
.projects-container {
|
|
806
|
-
flex: 1;
|
|
807
|
-
overflow-y: auto;
|
|
808
|
-
padding: 16px;
|
|
809
|
-
display: flex;
|
|
810
|
-
flex-direction: column;
|
|
811
|
-
gap: 16px;
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
/* Welcome Screen */
|
|
815
|
-
.projects-welcome {
|
|
816
|
-
max-width: 600px;
|
|
817
|
-
margin: 40px auto;
|
|
818
|
-
text-align: center;
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
.projects-welcome h2 {
|
|
822
|
-
font-size: 24px;
|
|
823
|
-
margin-bottom: 16px;
|
|
824
|
-
color: var(--text-primary);
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
.projects-welcome p {
|
|
828
|
-
color: var(--text-secondary);
|
|
829
|
-
line-height: 1.6;
|
|
830
|
-
margin-bottom: 16px;
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
.projects-welcome ol {
|
|
834
|
-
text-align: left;
|
|
835
|
-
margin: 24px 0;
|
|
836
|
-
padding-left: 24px;
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
.projects-welcome li {
|
|
840
|
-
margin-bottom: 8px;
|
|
841
|
-
color: var(--text-secondary);
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
.projects-welcome li strong {
|
|
845
|
-
color: var(--text-primary);
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
.projects-welcome .quick-tip {
|
|
849
|
-
margin-top: 24px;
|
|
850
|
-
padding: 12px;
|
|
851
|
-
background: var(--bg-tertiary);
|
|
852
|
-
border-radius: 6px;
|
|
853
|
-
border-left: 3px solid var(--accent);
|
|
854
|
-
color: var(--text-secondary);
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
.projects-welcome hr {
|
|
858
|
-
border: none;
|
|
859
|
-
border-top: 1px solid var(--border);
|
|
860
|
-
margin: 24px 0;
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
/* Status Summary */
|
|
864
|
-
.status-summary {
|
|
865
|
-
background: var(--bg-secondary);
|
|
866
|
-
border: 1px solid var(--border);
|
|
867
|
-
border-radius: 6px;
|
|
868
|
-
padding: 12px 16px;
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
.status-summary-header {
|
|
872
|
-
display: flex;
|
|
873
|
-
justify-content: space-between;
|
|
874
|
-
align-items: center;
|
|
875
|
-
margin-bottom: 8px;
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
.status-summary-header span {
|
|
879
|
-
font-size: 11px;
|
|
880
|
-
text-transform: uppercase;
|
|
881
|
-
letter-spacing: 0.5px;
|
|
882
|
-
color: var(--text-muted);
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
.status-summary-header button {
|
|
886
|
-
padding: 4px 8px;
|
|
887
|
-
border-radius: 4px;
|
|
888
|
-
border: 1px solid var(--border);
|
|
889
|
-
background: var(--bg-tertiary);
|
|
890
|
-
color: var(--text-secondary);
|
|
891
|
-
cursor: pointer;
|
|
892
|
-
font-size: 14px;
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
.status-summary-header button:hover {
|
|
896
|
-
background: var(--tab-hover);
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
.status-summary .active-projects {
|
|
900
|
-
margin-bottom: 8px;
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
.status-summary .active-count {
|
|
904
|
-
font-size: 14px;
|
|
905
|
-
color: var(--text-primary);
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
.status-summary .active-list {
|
|
909
|
-
margin-top: 4px;
|
|
910
|
-
padding-left: 16px;
|
|
911
|
-
font-size: 13px;
|
|
912
|
-
color: var(--text-secondary);
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
.status-summary .active-list li {
|
|
916
|
-
margin: 2px 0;
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
.status-summary .completed {
|
|
920
|
-
font-size: 13px;
|
|
921
|
-
color: var(--text-muted);
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
/* Kanban Grid */
|
|
925
|
-
.kanban-grid {
|
|
926
|
-
width: 100%;
|
|
927
|
-
border-collapse: collapse;
|
|
928
|
-
font-size: 13px;
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
.kanban-grid th,
|
|
932
|
-
.kanban-grid td {
|
|
933
|
-
padding: 8px 6px;
|
|
934
|
-
text-align: center;
|
|
935
|
-
border-bottom: 1px solid var(--border);
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
.kanban-grid th {
|
|
939
|
-
background: var(--bg-secondary);
|
|
940
|
-
font-size: 10px;
|
|
941
|
-
text-transform: uppercase;
|
|
942
|
-
letter-spacing: 0.5px;
|
|
943
|
-
color: var(--text-muted);
|
|
944
|
-
position: sticky;
|
|
945
|
-
top: 0;
|
|
946
|
-
z-index: 1;
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
.kanban-grid th:first-child,
|
|
950
|
-
.kanban-grid td:first-child {
|
|
951
|
-
text-align: left;
|
|
952
|
-
padding-left: 12px;
|
|
953
|
-
width: 40%;
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
.kanban-grid th:not(:first-child),
|
|
957
|
-
.kanban-grid td:not(:first-child) {
|
|
958
|
-
width: 8%;
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
.kanban-grid tbody tr {
|
|
962
|
-
cursor: default;
|
|
963
|
-
transition: background 0.15s;
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
.kanban-grid tbody tr:hover {
|
|
967
|
-
background: var(--bg-secondary);
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
.kanban-grid tbody tr:focus {
|
|
971
|
-
outline: 2px solid var(--accent);
|
|
972
|
-
outline-offset: -2px;
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
.kanban-grid .project-cell {
|
|
976
|
-
display: flex;
|
|
977
|
-
align-items: center;
|
|
978
|
-
gap: 8px;
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
.kanban-grid .project-id {
|
|
982
|
-
font-family: monospace;
|
|
983
|
-
color: var(--text-muted);
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
.kanban-grid .project-title {
|
|
987
|
-
overflow: hidden;
|
|
988
|
-
text-overflow: ellipsis;
|
|
989
|
-
white-space: nowrap;
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
.kanban-grid .project-cell.clickable {
|
|
993
|
-
cursor: pointer;
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
.kanban-grid .project-cell.clickable:hover .project-title {
|
|
997
|
-
text-decoration: underline;
|
|
998
|
-
color: var(--accent);
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
.kanban-grid .tick-badge {
|
|
1002
|
-
font-size: 10px;
|
|
1003
|
-
padding: 1px 4px;
|
|
1004
|
-
background: var(--bg-tertiary);
|
|
1005
|
-
border-radius: 3px;
|
|
1006
|
-
color: var(--text-muted);
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
/* Stage cell styling */
|
|
1010
|
-
.stage-cell {
|
|
1011
|
-
font-size: 12px;
|
|
1012
|
-
position: relative;
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
.stage-cell .checkmark {
|
|
1016
|
-
color: #22c55e;
|
|
1017
|
-
font-weight: bold;
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
.stage-cell .current-indicator {
|
|
1021
|
-
display: inline-block;
|
|
1022
|
-
width: 12px;
|
|
1023
|
-
height: 12px;
|
|
1024
|
-
border: 2px solid #f97316;
|
|
1025
|
-
border-radius: 50%;
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
.stage-cell .celebration {
|
|
1029
|
-
font-size: 16px;
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
.stage-cell a {
|
|
1033
|
-
color: var(--text-primary);
|
|
1034
|
-
text-decoration: underline;
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
/* Arrow between columns */
|
|
1038
|
-
.kanban-grid th:not(:first-child):not(:last-child)::after,
|
|
1039
|
-
.kanban-grid td.stage-cell:not(:last-child)::after {
|
|
1040
|
-
content: '→';
|
|
1041
|
-
position: absolute;
|
|
1042
|
-
right: -8px;
|
|
1043
|
-
color: var(--text-muted);
|
|
1044
|
-
font-size: 10px;
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
/* Projects info header */
|
|
1048
|
-
.projects-info {
|
|
1049
|
-
background: var(--bg-secondary);
|
|
1050
|
-
border: 1px solid var(--border);
|
|
1051
|
-
border-radius: 6px;
|
|
1052
|
-
padding: 12px 16px;
|
|
1053
|
-
margin-bottom: 12px;
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
.projects-info p {
|
|
1057
|
-
color: var(--text-secondary);
|
|
1058
|
-
font-size: 13px;
|
|
1059
|
-
margin: 0 0 8px 0;
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
.projects-info p:last-child {
|
|
1063
|
-
margin-bottom: 0;
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
.projects-info strong {
|
|
1067
|
-
color: var(--text-primary);
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
.projects-info a {
|
|
1071
|
-
color: var(--accent);
|
|
1072
|
-
text-decoration: none;
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
.projects-info a:hover {
|
|
1076
|
-
text-decoration: underline;
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
/* Project details row */
|
|
1080
|
-
.project-details-row td {
|
|
1081
|
-
padding: 0 !important;
|
|
1082
|
-
border-bottom: 1px solid var(--border);
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
.project-details-content {
|
|
1086
|
-
padding: 16px;
|
|
1087
|
-
background: var(--bg-secondary);
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
.project-details-content h3 {
|
|
1091
|
-
font-size: 16px;
|
|
1092
|
-
margin-bottom: 8px;
|
|
1093
|
-
color: var(--text-primary);
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
.project-details-content p {
|
|
1097
|
-
margin-bottom: 8px;
|
|
1098
|
-
color: var(--text-secondary);
|
|
1099
|
-
font-size: 13px;
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
.project-details-content .notes {
|
|
1103
|
-
font-style: italic;
|
|
1104
|
-
color: var(--text-muted);
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
.project-details-links {
|
|
1108
|
-
display: flex;
|
|
1109
|
-
gap: 8px;
|
|
1110
|
-
margin-top: 12px;
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
.project-details-links a {
|
|
1114
|
-
padding: 4px 10px;
|
|
1115
|
-
background: var(--bg-tertiary);
|
|
1116
|
-
border: 1px solid var(--border);
|
|
1117
|
-
border-radius: 4px;
|
|
1118
|
-
color: var(--text-secondary);
|
|
1119
|
-
text-decoration: none;
|
|
1120
|
-
font-size: 12px;
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
.project-details-links a:hover {
|
|
1124
|
-
background: var(--tab-hover);
|
|
1125
|
-
color: var(--text-primary);
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
.project-dependencies {
|
|
1129
|
-
margin-top: 8px;
|
|
1130
|
-
font-size: 12px;
|
|
1131
|
-
color: var(--text-muted);
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
.project-ticks {
|
|
1135
|
-
margin-top: 8px;
|
|
1136
|
-
font-size: 12px;
|
|
1137
|
-
display: flex;
|
|
1138
|
-
align-items: center;
|
|
1139
|
-
gap: 6px;
|
|
1140
|
-
flex-wrap: wrap;
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
.project-ticks .tick-badge {
|
|
1144
|
-
background: #238636;
|
|
1145
|
-
color: white;
|
|
1146
|
-
padding: 2px 6px;
|
|
1147
|
-
border-radius: 3px;
|
|
1148
|
-
font-size: 11px;
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
/* Collapsible project sections */
|
|
1152
|
-
.project-section {
|
|
1153
|
-
border: 1px solid var(--border);
|
|
1154
|
-
border-radius: 6px;
|
|
1155
|
-
background: var(--bg-secondary);
|
|
1156
|
-
margin-bottom: 12px;
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
.project-section summary {
|
|
1160
|
-
padding: 12px 16px;
|
|
1161
|
-
cursor: pointer;
|
|
1162
|
-
font-size: 14px;
|
|
1163
|
-
font-weight: 500;
|
|
1164
|
-
color: var(--text-primary);
|
|
1165
|
-
display: flex;
|
|
1166
|
-
align-items: center;
|
|
1167
|
-
gap: 8px;
|
|
1168
|
-
user-select: none;
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
.project-section summary:hover {
|
|
1172
|
-
background: var(--bg-tertiary);
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
.project-section summary::marker {
|
|
1176
|
-
content: '';
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
.project-section summary::before {
|
|
1180
|
-
content: '▶';
|
|
1181
|
-
font-size: 10px;
|
|
1182
|
-
transition: transform 0.2s;
|
|
1183
|
-
color: var(--text-muted);
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
.project-section[open] summary::before {
|
|
1187
|
-
transform: rotate(90deg);
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
.project-section .section-count {
|
|
1191
|
-
font-size: 12px;
|
|
1192
|
-
color: var(--text-muted);
|
|
1193
|
-
font-weight: normal;
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
.project-section .kanban-grid {
|
|
1197
|
-
margin: 0;
|
|
1198
|
-
border-radius: 0 0 6px 6px;
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
/* Terminal projects section */
|
|
1202
|
-
.terminal-projects {
|
|
1203
|
-
margin-top: 16px;
|
|
1204
|
-
border: 1px solid var(--border);
|
|
1205
|
-
border-radius: 6px;
|
|
1206
|
-
background: var(--bg-secondary);
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
.terminal-projects summary {
|
|
1210
|
-
padding: 12px 16px;
|
|
1211
|
-
cursor: pointer;
|
|
1212
|
-
font-size: 13px;
|
|
1213
|
-
color: var(--text-muted);
|
|
1214
|
-
display: flex;
|
|
1215
|
-
align-items: center;
|
|
1216
|
-
gap: 8px;
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
.terminal-projects summary:hover {
|
|
1220
|
-
background: var(--bg-tertiary);
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
.terminal-projects summary::marker {
|
|
1224
|
-
content: '';
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
.terminal-projects summary::before {
|
|
1228
|
-
content: '▶';
|
|
1229
|
-
font-size: 10px;
|
|
1230
|
-
transition: transform 0.2s;
|
|
1231
|
-
color: var(--text-muted);
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
.terminal-projects[open] summary::before {
|
|
1235
|
-
transform: rotate(90deg);
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
.terminal-projects ul {
|
|
1239
|
-
list-style: none;
|
|
1240
|
-
padding: 0 16px 16px;
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
.terminal-projects li {
|
|
1244
|
-
padding: 8px 0;
|
|
1245
|
-
border-bottom: 1px solid var(--border);
|
|
1246
|
-
display: flex;
|
|
1247
|
-
gap: 8px;
|
|
1248
|
-
align-items: center;
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
.terminal-projects li:last-child {
|
|
1252
|
-
border-bottom: none;
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
.terminal-projects .project-abandoned {
|
|
1256
|
-
color: var(--project-abandoned);
|
|
1257
|
-
text-decoration: line-through;
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
.terminal-projects .project-on-hold {
|
|
1261
|
-
color: var(--project-on-hold);
|
|
1262
|
-
font-style: italic;
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
/* Error banner */
|
|
1266
|
-
.projects-error {
|
|
1267
|
-
padding: 16px;
|
|
1268
|
-
background: rgba(239, 68, 68, 0.1);
|
|
1269
|
-
border: 1px solid var(--status-error);
|
|
1270
|
-
border-radius: 6px;
|
|
1271
|
-
display: flex;
|
|
1272
|
-
align-items: center;
|
|
1273
|
-
gap: 12px;
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
.projects-error-message {
|
|
1277
|
-
flex: 1;
|
|
1278
|
-
color: var(--text-secondary);
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
|
-
.projects-error button {
|
|
1282
|
-
padding: 6px 12px;
|
|
1283
|
-
background: var(--bg-tertiary);
|
|
1284
|
-
border: 1px solid var(--border);
|
|
1285
|
-
border-radius: 4px;
|
|
1286
|
-
color: var(--text-secondary);
|
|
1287
|
-
cursor: pointer;
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
.projects-error button:hover {
|
|
1291
|
-
background: var(--tab-hover);
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
/* Stage link styling */
|
|
1295
|
-
.stage-link {
|
|
1296
|
-
text-decoration: none;
|
|
1297
|
-
color: inherit;
|
|
1298
|
-
cursor: pointer;
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
.stage-link:hover .stage-indicator {
|
|
1302
|
-
transform: scale(1.2);
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
/* Projects tab without close button */
|
|
1306
|
-
.tab.tab-uncloseable .close {
|
|
1307
|
-
display: none;
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
/* Tree Styles (used by dashboard file browser) */
|
|
1311
|
-
.tree-item {
|
|
1312
|
-
display: flex;
|
|
1313
|
-
align-items: center;
|
|
1314
|
-
padding: 4px 8px;
|
|
1315
|
-
cursor: pointer;
|
|
1316
|
-
user-select: none;
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
.tree-item:hover {
|
|
1320
|
-
background: var(--bg-secondary);
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
.tree-item.selected {
|
|
1324
|
-
background: var(--tab-active);
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
.tree-item-icon {
|
|
1328
|
-
width: 16px;
|
|
1329
|
-
height: 16px;
|
|
1330
|
-
margin-right: 4px;
|
|
1331
|
-
display: flex;
|
|
1332
|
-
align-items: center;
|
|
1333
|
-
justify-content: center;
|
|
1334
|
-
font-size: 10px;
|
|
1335
|
-
color: var(--text-muted);
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
.tree-item-icon.folder-toggle {
|
|
1339
|
-
cursor: pointer;
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
.tree-item-icon.folder-toggle:hover {
|
|
1343
|
-
color: var(--text-secondary);
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
.tree-item-name {
|
|
1347
|
-
font-size: 13px;
|
|
1348
|
-
color: var(--text-secondary);
|
|
1349
|
-
overflow: hidden;
|
|
1350
|
-
text-overflow: ellipsis;
|
|
1351
|
-
white-space: nowrap;
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
.tree-item:hover .tree-item-name {
|
|
1355
|
-
color: var(--text-primary);
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
.tree-item[data-type="dir"] .tree-item-name {
|
|
1359
|
-
color: var(--text-primary);
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
|
-
.tree-item[data-type="file"]:hover .tree-item-name {
|
|
1363
|
-
color: var(--accent);
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
.tree-children {
|
|
1367
|
-
overflow: hidden;
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
.tree-children.collapsed {
|
|
1371
|
-
display: none;
|
|
1372
|
-
}
|
|
1373
|
-
|
|
1374
|
-
/* Dashboard Tab Styles (Spec 0057) */
|
|
1375
|
-
.dashboard-container {
|
|
1376
|
-
flex: 1;
|
|
1377
|
-
overflow-y: auto;
|
|
1378
|
-
display: flex;
|
|
1379
|
-
flex-direction: column;
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
.dashboard-header {
|
|
1383
|
-
display: flex;
|
|
1384
|
-
gap: 16px;
|
|
1385
|
-
padding: 16px;
|
|
1386
|
-
flex-shrink: 0;
|
|
1387
|
-
}
|
|
1388
|
-
|
|
1389
|
-
@media (max-width: 900px) {
|
|
1390
|
-
.dashboard-header {
|
|
1391
|
-
flex-direction: column;
|
|
1392
|
-
}
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
/* Collapsible section styles */
|
|
1396
|
-
.dashboard-section {
|
|
1397
|
-
background: var(--bg-secondary);
|
|
1398
|
-
border: 1px solid var(--border);
|
|
1399
|
-
border-radius: 8px;
|
|
1400
|
-
overflow: hidden;
|
|
1401
|
-
display: flex;
|
|
1402
|
-
flex-direction: column;
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
|
-
.dashboard-section.section-tabs,
|
|
1406
|
-
.dashboard-section.section-files {
|
|
1407
|
-
flex: 1;
|
|
1408
|
-
max-height: 280px;
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
|
-
.dashboard-section.section-projects {
|
|
1412
|
-
flex: 0 0 auto;
|
|
1413
|
-
margin: 0 16px 16px 16px;
|
|
1414
|
-
max-height: 50%;
|
|
1415
|
-
overflow-y: auto;
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
.dashboard-section.section-projects .dashboard-section-content {
|
|
1419
|
-
flex: 0 0 auto;
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
/* Tabs/Files expand to fill remaining space above Projects */
|
|
1423
|
-
.dashboard-header {
|
|
1424
|
-
flex: 1;
|
|
1425
|
-
min-height: 0;
|
|
1426
|
-
}
|
|
1427
|
-
|
|
1428
|
-
.dashboard-section.section-tabs,
|
|
1429
|
-
.dashboard-section.section-files {
|
|
1430
|
-
max-height: none;
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
.dashboard-section-header {
|
|
1434
|
-
display: flex;
|
|
1435
|
-
justify-content: space-between;
|
|
1436
|
-
align-items: center;
|
|
1437
|
-
padding: 8px 12px;
|
|
1438
|
-
cursor: pointer;
|
|
1439
|
-
user-select: none;
|
|
1440
|
-
flex-shrink: 0;
|
|
1441
|
-
border-bottom: 1px solid var(--border);
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
.dashboard-section-header:hover {
|
|
1445
|
-
background: var(--bg-tertiary);
|
|
1446
|
-
}
|
|
1447
|
-
|
|
1448
|
-
.dashboard-section-header h3 {
|
|
1449
|
-
font-size: 12px;
|
|
1450
|
-
text-transform: uppercase;
|
|
1451
|
-
color: var(--text-muted);
|
|
1452
|
-
letter-spacing: 0.5px;
|
|
1453
|
-
margin: 0;
|
|
1454
|
-
display: flex;
|
|
1455
|
-
align-items: center;
|
|
1456
|
-
gap: 6px;
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
.dashboard-section-header .collapse-icon {
|
|
1460
|
-
font-size: 10px;
|
|
1461
|
-
transition: transform 0.2s;
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
.dashboard-section.collapsed .collapse-icon {
|
|
1465
|
-
transform: rotate(-90deg);
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
.dashboard-section.collapsed .dashboard-section-header {
|
|
1469
|
-
border-bottom: none;
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
.dashboard-section-header .header-actions {
|
|
1473
|
-
display: flex;
|
|
1474
|
-
gap: 4px;
|
|
1475
|
-
}
|
|
1476
|
-
|
|
1477
|
-
.dashboard-section-header .header-actions button {
|
|
1478
|
-
padding: 4px 8px;
|
|
1479
|
-
border-radius: 4px;
|
|
1480
|
-
border: 1px solid var(--border);
|
|
1481
|
-
background: var(--bg-tertiary);
|
|
1482
|
-
color: var(--text-secondary);
|
|
1483
|
-
cursor: pointer;
|
|
1484
|
-
font-size: 11px;
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
.dashboard-section-header .header-actions button:hover {
|
|
1488
|
-
background: var(--tab-hover);
|
|
1489
|
-
color: var(--text-primary);
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
.dashboard-section-content {
|
|
1493
|
-
flex: 1;
|
|
1494
|
-
overflow-y: auto;
|
|
1495
|
-
padding: 8px 12px;
|
|
1496
|
-
}
|
|
1497
|
-
|
|
1498
|
-
.dashboard-section.collapsed .dashboard-section-content {
|
|
1499
|
-
display: none;
|
|
1500
|
-
}
|
|
1501
|
-
|
|
1502
|
-
/* Legacy support */
|
|
1503
|
-
.dashboard-column {
|
|
1504
|
-
background: var(--bg-secondary);
|
|
1505
|
-
border: 1px solid var(--border);
|
|
1506
|
-
border-radius: 8px;
|
|
1507
|
-
padding: 12px;
|
|
1508
|
-
overflow: hidden;
|
|
1509
|
-
display: flex;
|
|
1510
|
-
flex-direction: column;
|
|
1511
|
-
max-height: 280px;
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
|
-
.dashboard-column-header {
|
|
1515
|
-
display: flex;
|
|
1516
|
-
justify-content: space-between;
|
|
1517
|
-
align-items: center;
|
|
1518
|
-
margin-bottom: 8px;
|
|
1519
|
-
flex-shrink: 0;
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
.dashboard-column-header h3 {
|
|
1523
|
-
font-size: 12px;
|
|
1524
|
-
text-transform: uppercase;
|
|
1525
|
-
color: var(--text-muted);
|
|
1526
|
-
letter-spacing: 0.5px;
|
|
1527
|
-
margin: 0;
|
|
1528
|
-
}
|
|
1529
|
-
|
|
1530
|
-
.dashboard-column-header .header-actions {
|
|
1531
|
-
display: flex;
|
|
1532
|
-
gap: 4px;
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
.dashboard-column-header .header-actions button {
|
|
1536
|
-
padding: 4px 8px;
|
|
1537
|
-
border-radius: 4px;
|
|
1538
|
-
border: 1px solid var(--border);
|
|
1539
|
-
background: var(--bg-tertiary);
|
|
1540
|
-
color: var(--text-secondary);
|
|
1541
|
-
cursor: pointer;
|
|
1542
|
-
font-size: 11px;
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
|
-
.dashboard-column-header .header-actions button:hover {
|
|
1546
|
-
background: var(--tab-hover);
|
|
1547
|
-
color: var(--text-primary);
|
|
1548
|
-
}
|
|
1549
|
-
|
|
1550
|
-
.dashboard-tabs-list {
|
|
1551
|
-
flex: 1;
|
|
1552
|
-
overflow-y: auto;
|
|
1553
|
-
margin-bottom: 8px;
|
|
1554
|
-
}
|
|
1555
|
-
|
|
1556
|
-
.dashboard-tab-item {
|
|
1557
|
-
display: flex;
|
|
1558
|
-
align-items: center;
|
|
1559
|
-
gap: 8px;
|
|
1560
|
-
padding: 6px 8px;
|
|
1561
|
-
border-radius: 4px;
|
|
1562
|
-
cursor: pointer;
|
|
1563
|
-
font-size: 13px;
|
|
1564
|
-
color: var(--text-secondary);
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
|
-
.dashboard-tab-item:hover {
|
|
1568
|
-
background: var(--bg-tertiary);
|
|
1569
|
-
}
|
|
1570
|
-
|
|
1571
|
-
.dashboard-tab-item.active {
|
|
1572
|
-
background: var(--accent);
|
|
1573
|
-
color: white;
|
|
1574
|
-
}
|
|
1575
|
-
|
|
1576
|
-
.dashboard-tab-item .tab-icon {
|
|
1577
|
-
font-size: 14px;
|
|
1578
|
-
flex-shrink: 0;
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
|
-
.dashboard-tab-item .tab-name {
|
|
1582
|
-
flex: 1;
|
|
1583
|
-
overflow: hidden;
|
|
1584
|
-
text-overflow: ellipsis;
|
|
1585
|
-
white-space: nowrap;
|
|
1586
|
-
}
|
|
1587
|
-
|
|
1588
|
-
.dashboard-actions {
|
|
1589
|
-
flex-shrink: 0;
|
|
1590
|
-
display: flex;
|
|
1591
|
-
gap: 8px;
|
|
1592
|
-
}
|
|
1593
|
-
|
|
1594
|
-
.dashboard-actions .btn-action {
|
|
1595
|
-
flex: 1;
|
|
1596
|
-
padding: 8px 12px;
|
|
1597
|
-
border-radius: 4px;
|
|
1598
|
-
border: 1px dashed var(--border);
|
|
1599
|
-
background: transparent;
|
|
1600
|
-
color: var(--text-muted);
|
|
1601
|
-
cursor: pointer;
|
|
1602
|
-
font-size: 12px;
|
|
1603
|
-
display: flex;
|
|
1604
|
-
align-items: center;
|
|
1605
|
-
justify-content: center;
|
|
1606
|
-
gap: 4px;
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
|
-
.dashboard-actions .btn-action:hover {
|
|
1610
|
-
border-style: solid;
|
|
1611
|
-
color: var(--text-secondary);
|
|
1612
|
-
background: var(--bg-tertiary);
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
.dashboard-files-list {
|
|
1616
|
-
flex: 1;
|
|
1617
|
-
overflow-y: auto;
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
|
-
.dashboard-files-list .tree-item {
|
|
1621
|
-
padding: 3px 6px;
|
|
1622
|
-
font-size: 12px;
|
|
1623
|
-
}
|
|
1624
|
-
|
|
1625
|
-
.dashboard-files-list .tree-item-name {
|
|
1626
|
-
font-size: 12px;
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
|
-
/* File search styles (Spec 0058) */
|
|
1630
|
-
.files-search-container {
|
|
1631
|
-
display: flex;
|
|
1632
|
-
align-items: center;
|
|
1633
|
-
padding: 6px 8px;
|
|
1634
|
-
gap: 6px;
|
|
1635
|
-
border-bottom: 1px solid var(--border);
|
|
1636
|
-
}
|
|
1637
|
-
|
|
1638
|
-
.files-search-input {
|
|
1639
|
-
flex: 1;
|
|
1640
|
-
background: var(--bg-tertiary);
|
|
1641
|
-
border: 1px solid var(--border);
|
|
1642
|
-
border-radius: 4px;
|
|
1643
|
-
padding: 6px 10px;
|
|
1644
|
-
font-size: 12px;
|
|
1645
|
-
color: var(--text-primary);
|
|
1646
|
-
outline: none;
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
|
-
.files-search-input:focus {
|
|
1650
|
-
border-color: var(--accent);
|
|
1651
|
-
}
|
|
1652
|
-
|
|
1653
|
-
.files-search-input::placeholder {
|
|
1654
|
-
color: var(--text-muted);
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
|
-
.files-search-clear {
|
|
1658
|
-
background: transparent;
|
|
1659
|
-
border: none;
|
|
1660
|
-
color: var(--text-muted);
|
|
1661
|
-
cursor: pointer;
|
|
1662
|
-
font-size: 14px;
|
|
1663
|
-
padding: 2px 6px;
|
|
1664
|
-
border-radius: 4px;
|
|
1665
|
-
line-height: 1;
|
|
1666
|
-
}
|
|
1667
|
-
|
|
1668
|
-
.files-search-clear:hover {
|
|
1669
|
-
color: var(--text-primary);
|
|
1670
|
-
background: var(--bg-tertiary);
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1673
|
-
.files-search-clear.hidden {
|
|
1674
|
-
display: none;
|
|
1675
|
-
}
|
|
1676
|
-
|
|
1677
|
-
.files-search-results {
|
|
1678
|
-
flex: 1;
|
|
1679
|
-
overflow-y: auto;
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
|
-
.files-search-result {
|
|
1683
|
-
padding: 6px 12px;
|
|
1684
|
-
cursor: pointer;
|
|
1685
|
-
display: flex;
|
|
1686
|
-
flex-direction: column;
|
|
1687
|
-
gap: 2px;
|
|
1688
|
-
}
|
|
1689
|
-
|
|
1690
|
-
.files-search-result:hover,
|
|
1691
|
-
.files-search-result.selected {
|
|
1692
|
-
background: var(--bg-tertiary);
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
|
-
.files-search-result-name {
|
|
1696
|
-
font-size: 12px;
|
|
1697
|
-
color: var(--text-primary);
|
|
1698
|
-
}
|
|
1699
|
-
|
|
1700
|
-
.files-search-result-path {
|
|
1701
|
-
font-size: 11px;
|
|
1702
|
-
color: var(--text-muted);
|
|
1703
|
-
overflow: hidden;
|
|
1704
|
-
text-overflow: ellipsis;
|
|
1705
|
-
white-space: nowrap;
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1708
|
-
.files-search-highlight {
|
|
1709
|
-
color: var(--accent);
|
|
1710
|
-
font-weight: 500;
|
|
1711
|
-
}
|
|
1712
|
-
|
|
1713
|
-
/* Cmd+P Palette styles (Spec 0058) */
|
|
1714
|
-
.file-palette {
|
|
1715
|
-
position: fixed;
|
|
1716
|
-
inset: 0;
|
|
1717
|
-
z-index: 1000;
|
|
1718
|
-
display: flex;
|
|
1719
|
-
justify-content: center;
|
|
1720
|
-
padding-top: 80px;
|
|
1721
|
-
}
|
|
1722
|
-
|
|
1723
|
-
.file-palette.hidden {
|
|
1724
|
-
display: none;
|
|
1725
|
-
}
|
|
1726
|
-
|
|
1727
|
-
.file-palette-backdrop {
|
|
1728
|
-
position: absolute;
|
|
1729
|
-
inset: 0;
|
|
1730
|
-
background: rgba(0, 0, 0, 0.5);
|
|
1731
|
-
}
|
|
1732
|
-
|
|
1733
|
-
.file-palette-container {
|
|
1734
|
-
position: relative;
|
|
1735
|
-
width: 500px;
|
|
1736
|
-
max-width: 90vw;
|
|
1737
|
-
max-height: 450px;
|
|
1738
|
-
background: var(--bg-secondary);
|
|
1739
|
-
border: 1px solid var(--border);
|
|
1740
|
-
border-radius: 8px;
|
|
1741
|
-
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
|
1742
|
-
display: flex;
|
|
1743
|
-
flex-direction: column;
|
|
1744
|
-
overflow: hidden;
|
|
1745
|
-
}
|
|
1746
|
-
|
|
1747
|
-
.file-palette-input {
|
|
1748
|
-
width: 100%;
|
|
1749
|
-
padding: 14px 16px;
|
|
1750
|
-
background: var(--bg-tertiary);
|
|
1751
|
-
border: none;
|
|
1752
|
-
border-bottom: 1px solid var(--border);
|
|
1753
|
-
font-size: 14px;
|
|
1754
|
-
color: var(--text-primary);
|
|
1755
|
-
outline: none;
|
|
1756
|
-
}
|
|
1757
|
-
|
|
1758
|
-
.file-palette-input::placeholder {
|
|
1759
|
-
color: var(--text-muted);
|
|
1760
|
-
}
|
|
1761
|
-
|
|
1762
|
-
.file-palette-results {
|
|
1763
|
-
flex: 1;
|
|
1764
|
-
overflow-y: auto;
|
|
1765
|
-
max-height: 380px;
|
|
1766
|
-
}
|
|
1767
|
-
|
|
1768
|
-
.file-palette-result {
|
|
1769
|
-
padding: 10px 16px;
|
|
1770
|
-
cursor: pointer;
|
|
1771
|
-
display: flex;
|
|
1772
|
-
flex-direction: column;
|
|
1773
|
-
gap: 2px;
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
.file-palette-result:hover,
|
|
1777
|
-
.file-palette-result.selected {
|
|
1778
|
-
background: var(--bg-tertiary);
|
|
1779
|
-
}
|
|
1780
|
-
|
|
1781
|
-
.file-palette-result-name {
|
|
1782
|
-
font-size: 13px;
|
|
1783
|
-
color: var(--text-primary);
|
|
1784
|
-
}
|
|
1785
|
-
|
|
1786
|
-
.file-palette-result-path {
|
|
1787
|
-
font-size: 12px;
|
|
1788
|
-
color: var(--text-muted);
|
|
1789
|
-
overflow: hidden;
|
|
1790
|
-
text-overflow: ellipsis;
|
|
1791
|
-
white-space: nowrap;
|
|
1792
|
-
}
|
|
1793
|
-
|
|
1794
|
-
.file-palette-empty {
|
|
1795
|
-
padding: 16px;
|
|
1796
|
-
text-align: center;
|
|
1797
|
-
color: var(--text-muted);
|
|
1798
|
-
font-size: 13px;
|
|
1799
|
-
}
|
|
1800
|
-
|
|
1801
|
-
.dashboard-empty-state {
|
|
1802
|
-
color: var(--text-muted);
|
|
1803
|
-
font-size: 13px;
|
|
1804
|
-
padding: 12px;
|
|
1805
|
-
text-align: center;
|
|
1806
|
-
}
|
|
1807
|
-
|
|
1808
|
-
/* Status indicators in dashboard tab list */
|
|
1809
|
-
.dashboard-status-indicator {
|
|
1810
|
-
width: 8px;
|
|
1811
|
-
height: 8px;
|
|
1812
|
-
border-radius: 50%;
|
|
1813
|
-
flex-shrink: 0;
|
|
1814
|
-
}
|
|
1815
|
-
|
|
1816
|
-
.dashboard-status-working {
|
|
1817
|
-
background: var(--status-active);
|
|
1818
|
-
animation: status-pulse 2s ease-in-out infinite;
|
|
1819
|
-
}
|
|
1820
|
-
|
|
1821
|
-
.dashboard-status-idle {
|
|
1822
|
-
background: var(--status-waiting);
|
|
1823
|
-
animation: status-blink-slow 3s ease-in-out infinite;
|
|
1824
|
-
}
|
|
1825
|
-
|
|
1826
|
-
.dashboard-status-blocked {
|
|
1827
|
-
background: var(--status-error);
|
|
1828
|
-
animation: status-blink-fast 0.8s ease-in-out infinite;
|
|
1829
|
-
}
|
|
1830
|
-
|
|
1831
|
-
@media (prefers-reduced-motion: reduce) {
|
|
1832
|
-
.dashboard-status-working,
|
|
1833
|
-
.dashboard-status-idle,
|
|
1834
|
-
.dashboard-status-blocked {
|
|
1835
|
-
animation: none;
|
|
1836
|
-
}
|
|
1837
|
-
}
|
|
1838
|
-
</style>
|
|
1839
|
-
</head>
|
|
1840
|
-
<body>
|
|
1841
|
-
<header class="header">
|
|
1842
|
-
<h1>Agent Farm - {{PROJECT_NAME}}</h1>
|
|
1843
|
-
<div class="header-actions">
|
|
1844
|
-
<button class="btn activity-summary-btn" onclick="showActivitySummary()" title="What did I do today?">
|
|
1845
|
-
🕐 Today
|
|
1846
|
-
</button>
|
|
1847
|
-
</div>
|
|
1848
|
-
</header>
|
|
1849
|
-
|
|
1850
|
-
<main class="main">
|
|
1851
|
-
<!-- Left pane: Architect terminal -->
|
|
1852
|
-
<div class="left-pane">
|
|
1853
|
-
<div class="pane-header">
|
|
1854
|
-
<span class="status-dot" id="architect-status"></span>
|
|
1855
|
-
<span>Architect</span>
|
|
1856
|
-
</div>
|
|
1857
|
-
<div id="architect-content"></div>
|
|
1858
|
-
</div>
|
|
1859
|
-
|
|
1860
|
-
<!-- Right pane: Tabbed interface -->
|
|
1861
|
-
<div class="right-pane">
|
|
1862
|
-
<div class="tab-bar">
|
|
1863
|
-
<div class="tabs-scroll" id="tabs-container"></div>
|
|
1864
|
-
<button class="overflow-btn" id="overflow-btn" onclick="toggleOverflowMenu()" aria-haspopup="true" aria-expanded="false" title="Show all tabs">
|
|
1865
|
-
<span>...</span>
|
|
1866
|
-
<span class="overflow-count" id="overflow-count">+0</span>
|
|
1867
|
-
</button>
|
|
1868
|
-
<div class="overflow-menu hidden" id="overflow-menu" role="menu"></div>
|
|
1869
|
-
</div>
|
|
1870
|
-
<div class="tab-content" id="tab-content"></div>
|
|
1871
|
-
</div>
|
|
1872
|
-
</main>
|
|
1873
|
-
|
|
1874
|
-
<footer class="status-bar">
|
|
1875
|
-
<div class="status-item" id="status-architect">
|
|
1876
|
-
<span class="dot" style="background: var(--text-muted)"></span>
|
|
1877
|
-
<span>Architect: stopped</span>
|
|
1878
|
-
</div>
|
|
1879
|
-
<div class="status-item" id="status-builders">
|
|
1880
|
-
<span>0 builders</span>
|
|
1881
|
-
</div>
|
|
1882
|
-
<div class="status-item" id="status-shells">
|
|
1883
|
-
<span>0 shells</span>
|
|
1884
|
-
</div>
|
|
1885
|
-
<div class="status-item" id="status-files">
|
|
1886
|
-
<span>0 files</span>
|
|
1887
|
-
</div>
|
|
1888
|
-
</footer>
|
|
1889
|
-
|
|
1890
|
-
<!-- File picker dialog -->
|
|
1891
|
-
<div class="dialog-overlay hidden" id="file-dialog">
|
|
1892
|
-
<div class="dialog">
|
|
1893
|
-
<h3>Open File</h3>
|
|
1894
|
-
<div class="quick-paths">
|
|
1895
|
-
<button class="quick-path" onclick="setFilePath('codev/specs/')">codev/specs/</button>
|
|
1896
|
-
<button class="quick-path" onclick="setFilePath('codev/plans/')">codev/plans/</button>
|
|
1897
|
-
<button class="quick-path" onclick="setFilePath('codev/reviews/')">codev/reviews/</button>
|
|
1898
|
-
</div>
|
|
1899
|
-
<input type="text" id="file-path-input" placeholder="Enter file path..." />
|
|
1900
|
-
<div class="dialog-actions">
|
|
1901
|
-
<button class="btn" onclick="hideFileDialog()">Cancel</button>
|
|
1902
|
-
<button class="btn" onclick="openFile()">Open</button>
|
|
1903
|
-
</div>
|
|
1904
|
-
</div>
|
|
1905
|
-
</div>
|
|
1906
|
-
|
|
1907
|
-
<!-- Close confirmation dialog -->
|
|
1908
|
-
<div class="dialog-overlay hidden" id="close-dialog">
|
|
1909
|
-
<div class="dialog">
|
|
1910
|
-
<h3 id="close-dialog-title">Close tab?</h3>
|
|
1911
|
-
<p id="close-dialog-message" style="color: var(--text-secondary); margin-bottom: 16px; font-size: 14px;"></p>
|
|
1912
|
-
<div class="dialog-actions">
|
|
1913
|
-
<button class="btn" onclick="hideCloseDialog()">Cancel</button>
|
|
1914
|
-
<button class="btn btn-danger" onclick="confirmClose()">Close</button>
|
|
1915
|
-
</div>
|
|
1916
|
-
</div>
|
|
1917
|
-
</div>
|
|
1918
|
-
|
|
1919
|
-
<!-- Context menu -->
|
|
1920
|
-
<div class="context-menu hidden" id="context-menu" role="menu">
|
|
1921
|
-
<div class="context-menu-item" role="menuitem" tabindex="0" data-action="openContextTab" onclick="openContextTab()" onkeydown="handleContextMenuKeydown(event)">Open in New Tab</div>
|
|
1922
|
-
<div class="context-menu-item" role="menuitem" tabindex="-1" data-action="reloadContextTab" id="context-reload" onclick="reloadContextTab()" onkeydown="handleContextMenuKeydown(event)">Reload</div>
|
|
1923
|
-
<div class="context-menu-item" role="menuitem" tabindex="-1" data-action="closeActiveTab" onclick="closeActiveTab()" onkeydown="handleContextMenuKeydown(event)">Close</div>
|
|
1924
|
-
<div class="context-menu-item" role="menuitem" tabindex="-1" data-action="closeOtherTabs" onclick="closeOtherTabs()" onkeydown="handleContextMenuKeydown(event)">Close Others</div>
|
|
1925
|
-
<div class="context-menu-item danger" role="menuitem" tabindex="-1" data-action="closeAllTabs" onclick="closeAllTabs()" onkeydown="handleContextMenuKeydown(event)">Close All</div>
|
|
1926
|
-
</div>
|
|
1927
|
-
|
|
1928
|
-
<!-- Toast container -->
|
|
1929
|
-
<div class="toast-container" id="toast-container"></div>
|
|
1930
|
-
|
|
1931
|
-
<!-- Activity Summary Modal (Spec 0059) -->
|
|
1932
|
-
<div class="dialog-overlay hidden" id="activity-modal">
|
|
1933
|
-
<div class="dialog activity-dialog">
|
|
1934
|
-
<div class="activity-dialog-header">
|
|
1935
|
-
<h3>Today's Summary</h3>
|
|
1936
|
-
<button class="activity-close-btn" onclick="closeActivityModal()" title="Close (Esc)">×</button>
|
|
1937
|
-
</div>
|
|
1938
|
-
<div class="activity-dialog-content" id="activity-content">
|
|
1939
|
-
<div class="activity-loading">
|
|
1940
|
-
<span class="activity-spinner"></span>
|
|
1941
|
-
Loading activity...
|
|
1942
|
-
</div>
|
|
1943
|
-
</div>
|
|
1944
|
-
<div class="activity-dialog-footer">
|
|
1945
|
-
<button class="btn" onclick="copyActivitySummary()">📋 Copy to Clipboard</button>
|
|
1946
|
-
<button class="btn" onclick="closeActivityModal()">Close</button>
|
|
1947
|
-
</div>
|
|
1948
|
-
</div>
|
|
1949
|
-
</div>
|
|
1950
|
-
|
|
1951
|
-
<!-- File search palette (Cmd+P) - Spec 0058 -->
|
|
1952
|
-
<div id="file-palette" class="file-palette hidden">
|
|
1953
|
-
<div class="file-palette-backdrop" onclick="closePalette()"></div>
|
|
1954
|
-
<div class="file-palette-container">
|
|
1955
|
-
<input type="text"
|
|
1956
|
-
id="palette-input"
|
|
1957
|
-
class="file-palette-input"
|
|
1958
|
-
placeholder="Search files by name..."
|
|
1959
|
-
oninput="onPaletteInput(this.value)"
|
|
1960
|
-
onkeydown="onPaletteKeydown(event)" />
|
|
1961
|
-
<div id="palette-results" class="file-palette-results"></div>
|
|
1962
|
-
</div>
|
|
1963
|
-
</div>
|
|
1964
|
-
|
|
1965
|
-
<script>
|
|
1966
|
-
// STATE_INJECTION_POINT
|
|
1967
|
-
|
|
1968
|
-
// State management
|
|
1969
|
-
const state = window.INITIAL_STATE || {
|
|
1970
|
-
architect: null,
|
|
1971
|
-
builders: [],
|
|
1972
|
-
utils: [],
|
|
1973
|
-
annotations: []
|
|
1974
|
-
};
|
|
1975
|
-
|
|
1976
|
-
// Tab state
|
|
1977
|
-
let tabs = [];
|
|
1978
|
-
let activeTabId = null;
|
|
1979
|
-
let pendingCloseTabId = null;
|
|
1980
|
-
let contextMenuTabId = null;
|
|
1981
|
-
|
|
1982
|
-
// Collapsible section state (persisted to localStorage)
|
|
1983
|
-
const SECTION_STATE_KEY = 'codev-dashboard-sections';
|
|
1984
|
-
let sectionState = loadSectionState();
|
|
1985
|
-
|
|
1986
|
-
function loadSectionState() {
|
|
1987
|
-
try {
|
|
1988
|
-
const saved = localStorage.getItem(SECTION_STATE_KEY);
|
|
1989
|
-
if (saved) return JSON.parse(saved);
|
|
1990
|
-
} catch (e) { /* ignore */ }
|
|
1991
|
-
return { tabs: true, files: true, projects: true };
|
|
1992
|
-
}
|
|
1993
|
-
|
|
1994
|
-
function saveSectionState() {
|
|
1995
|
-
try {
|
|
1996
|
-
localStorage.setItem(SECTION_STATE_KEY, JSON.stringify(sectionState));
|
|
1997
|
-
} catch (e) { /* ignore */ }
|
|
1998
|
-
}
|
|
1999
|
-
|
|
2000
|
-
function toggleSection(section) {
|
|
2001
|
-
sectionState[section] = !sectionState[section];
|
|
2002
|
-
saveSectionState();
|
|
2003
|
-
renderDashboardTabContent();
|
|
2004
|
-
}
|
|
2005
|
-
|
|
2006
|
-
// Initialize
|
|
2007
|
-
function init() {
|
|
2008
|
-
buildTabsFromState();
|
|
2009
|
-
renderArchitect();
|
|
2010
|
-
renderTabs();
|
|
2011
|
-
renderTabContent();
|
|
2012
|
-
updateStatusBar();
|
|
2013
|
-
startPolling();
|
|
2014
|
-
setupBroadcastChannel();
|
|
2015
|
-
setupOverflowDetection();
|
|
2016
|
-
}
|
|
2017
|
-
|
|
2018
|
-
// Set up overflow detection for the tab bar
|
|
2019
|
-
function setupOverflowDetection() {
|
|
2020
|
-
const container = document.getElementById('tabs-container');
|
|
2021
|
-
|
|
2022
|
-
// Check on load
|
|
2023
|
-
checkTabOverflow();
|
|
2024
|
-
|
|
2025
|
-
// Check on window resize (debounced)
|
|
2026
|
-
let resizeTimeout;
|
|
2027
|
-
window.addEventListener('resize', () => {
|
|
2028
|
-
clearTimeout(resizeTimeout);
|
|
2029
|
-
resizeTimeout = setTimeout(checkTabOverflow, 100);
|
|
2030
|
-
});
|
|
2031
|
-
|
|
2032
|
-
// Check on scroll (debounced) - updates +N count when user scrolls tabs
|
|
2033
|
-
if (container) {
|
|
2034
|
-
let scrollTimeout;
|
|
2035
|
-
container.addEventListener('scroll', () => {
|
|
2036
|
-
clearTimeout(scrollTimeout);
|
|
2037
|
-
scrollTimeout = setTimeout(checkTabOverflow, 50);
|
|
2038
|
-
});
|
|
2039
|
-
}
|
|
2040
|
-
|
|
2041
|
-
// Also use ResizeObserver for the tabs container if available
|
|
2042
|
-
if (typeof ResizeObserver !== 'undefined') {
|
|
2043
|
-
if (container) {
|
|
2044
|
-
const observer = new ResizeObserver(() => {
|
|
2045
|
-
checkTabOverflow();
|
|
2046
|
-
});
|
|
2047
|
-
observer.observe(container);
|
|
2048
|
-
}
|
|
2049
|
-
}
|
|
2050
|
-
}
|
|
2051
|
-
|
|
2052
|
-
// Set up BroadcastChannel for cross-tab communication
|
|
2053
|
-
// This allows terminal file clicks to open files in the dashboard
|
|
2054
|
-
function setupBroadcastChannel() {
|
|
2055
|
-
const channel = new BroadcastChannel('agent-farm');
|
|
2056
|
-
channel.onmessage = async (event) => {
|
|
2057
|
-
const { type, path, line } = event.data;
|
|
2058
|
-
if (type === 'openFile' && path) {
|
|
2059
|
-
await openFileFromMessage(path, line);
|
|
2060
|
-
}
|
|
2061
|
-
};
|
|
2062
|
-
}
|
|
2063
|
-
|
|
2064
|
-
// Open a file from a BroadcastChannel message
|
|
2065
|
-
async function openFileFromMessage(filePath, lineNumber) {
|
|
2066
|
-
try {
|
|
2067
|
-
// Check if file is already open
|
|
2068
|
-
const existingTab = tabs.find(t => t.type === 'file' && t.path === filePath);
|
|
2069
|
-
if (existingTab) {
|
|
2070
|
-
// Switch to the existing tab and refresh content
|
|
2071
|
-
selectTab(existingTab.id);
|
|
2072
|
-
refreshFileTab(existingTab.id); // Refresh content in case file changed
|
|
2073
|
-
showToast(`Switched to ${getFileName(filePath)}`, 'success');
|
|
2074
|
-
// TODO: scroll to line if lineNumber provided
|
|
2075
|
-
return;
|
|
2076
|
-
}
|
|
2077
|
-
|
|
2078
|
-
// Open the file via API
|
|
2079
|
-
const response = await fetch('/api/tabs/file', {
|
|
2080
|
-
method: 'POST',
|
|
2081
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2082
|
-
body: JSON.stringify({ path: filePath })
|
|
2083
|
-
});
|
|
2084
|
-
|
|
2085
|
-
if (!response.ok) {
|
|
2086
|
-
throw new Error(await response.text());
|
|
2087
|
-
}
|
|
2088
|
-
|
|
2089
|
-
const result = await response.json();
|
|
2090
|
-
|
|
2091
|
-
// Refresh state and switch to the new tab
|
|
2092
|
-
await refresh();
|
|
2093
|
-
|
|
2094
|
-
// Find and select the new file tab
|
|
2095
|
-
const newTab = tabs.find(t => t.type === 'file' && (t.path === filePath || t.annotationId === result.id));
|
|
2096
|
-
if (newTab) {
|
|
2097
|
-
selectTab(newTab.id);
|
|
2098
|
-
}
|
|
2099
|
-
|
|
2100
|
-
showToast(`Opened ${getFileName(filePath)}${lineNumber ? ':' + lineNumber : ''}`, 'success');
|
|
2101
|
-
} catch (err) {
|
|
2102
|
-
showToast('Failed to open file: ' + err.message, 'error');
|
|
2103
|
-
}
|
|
2104
|
-
}
|
|
2105
|
-
|
|
2106
|
-
// Track known tab IDs to detect new tabs
|
|
2107
|
-
let knownTabIds = new Set();
|
|
2108
|
-
|
|
2109
|
-
// Projects tab state
|
|
2110
|
-
let projectsData = [];
|
|
2111
|
-
let projectlistHash = null;
|
|
2112
|
-
let expandedProjectId = null;
|
|
2113
|
-
let projectlistError = null;
|
|
2114
|
-
let projectlistDebounce = null;
|
|
2115
|
-
|
|
2116
|
-
// Files tab state (Spec 0055)
|
|
2117
|
-
let filesTreeData = [];
|
|
2118
|
-
let filesTreeExpanded = new Set(); // Set of expanded folder paths
|
|
2119
|
-
let filesTreeError = null;
|
|
2120
|
-
let filesTreeLoaded = false;
|
|
2121
|
-
|
|
2122
|
-
// File search state (Spec 0058)
|
|
2123
|
-
let filesTreeFlat = []; // Flattened array of {name, path} objects for searching
|
|
2124
|
-
let filesSearchQuery = '';
|
|
2125
|
-
let filesSearchResults = [];
|
|
2126
|
-
let filesSearchIndex = 0;
|
|
2127
|
-
let filesSearchDebounceTimer = null;
|
|
2128
|
-
|
|
2129
|
-
// Cmd+P palette state (Spec 0058)
|
|
2130
|
-
let paletteOpen = false;
|
|
2131
|
-
let paletteQuery = '';
|
|
2132
|
-
let paletteResults = [];
|
|
2133
|
-
let paletteIndex = 0;
|
|
2134
|
-
let paletteDebounceTimer = null;
|
|
2135
|
-
|
|
2136
|
-
// Build tabs from initial state
|
|
2137
|
-
function buildTabsFromState() {
|
|
2138
|
-
const previousTabIds = new Set(tabs.map(t => t.id));
|
|
2139
|
-
// Preserve client-side-only tabs (like activity)
|
|
2140
|
-
const clientSideTabs = tabs.filter(t => t.type === 'activity');
|
|
2141
|
-
tabs = [];
|
|
2142
|
-
|
|
2143
|
-
// Dashboard tab is ALWAYS first and uncloseable (Spec 0045, 0057)
|
|
2144
|
-
tabs.push({
|
|
2145
|
-
id: 'dashboard',
|
|
2146
|
-
type: 'dashboard',
|
|
2147
|
-
name: 'Dashboard',
|
|
2148
|
-
closeable: false
|
|
2149
|
-
});
|
|
2150
|
-
|
|
2151
|
-
// Add file tabs from annotations
|
|
2152
|
-
for (const annotation of state.annotations || []) {
|
|
2153
|
-
tabs.push({
|
|
2154
|
-
id: `file-${annotation.id}`,
|
|
2155
|
-
type: 'file',
|
|
2156
|
-
name: getFileName(annotation.file),
|
|
2157
|
-
path: annotation.file,
|
|
2158
|
-
port: annotation.port,
|
|
2159
|
-
annotationId: annotation.id
|
|
2160
|
-
});
|
|
2161
|
-
}
|
|
2162
|
-
|
|
2163
|
-
// Add builder tabs
|
|
2164
|
-
for (const builder of state.builders || []) {
|
|
2165
|
-
tabs.push({
|
|
2166
|
-
id: `builder-${builder.id}`,
|
|
2167
|
-
type: 'builder',
|
|
2168
|
-
name: builder.name || `Builder ${builder.id}`,
|
|
2169
|
-
projectId: builder.id,
|
|
2170
|
-
port: builder.port,
|
|
2171
|
-
status: builder.status
|
|
2172
|
-
});
|
|
2173
|
-
}
|
|
2174
|
-
|
|
2175
|
-
// Add shell tabs
|
|
2176
|
-
for (const util of state.utils || []) {
|
|
2177
|
-
tabs.push({
|
|
2178
|
-
id: `shell-${util.id}`,
|
|
2179
|
-
type: 'shell',
|
|
2180
|
-
name: util.name,
|
|
2181
|
-
port: util.port,
|
|
2182
|
-
utilId: util.id
|
|
2183
|
-
});
|
|
2184
|
-
}
|
|
2185
|
-
|
|
2186
|
-
// Re-add preserved client-side tabs
|
|
2187
|
-
for (const tab of clientSideTabs) {
|
|
2188
|
-
tabs.push(tab);
|
|
2189
|
-
}
|
|
2190
|
-
|
|
2191
|
-
// Detect new tabs and auto-switch to them (skip projects tab)
|
|
2192
|
-
for (const tab of tabs) {
|
|
2193
|
-
if (tab.id !== 'dashboard' && tab.id !== 'files' && !knownTabIds.has(tab.id) && previousTabIds.size > 0) {
|
|
2194
|
-
// This is a new tab - switch to it
|
|
2195
|
-
activeTabId = tab.id;
|
|
2196
|
-
break;
|
|
2197
|
-
}
|
|
2198
|
-
}
|
|
2199
|
-
|
|
2200
|
-
// Update known tab IDs
|
|
2201
|
-
knownTabIds = new Set(tabs.map(t => t.id));
|
|
2202
|
-
|
|
2203
|
-
// Set active tab to Dashboard on first load if none selected
|
|
2204
|
-
if (!activeTabId) {
|
|
2205
|
-
activeTabId = 'dashboard';
|
|
2206
|
-
}
|
|
2207
|
-
}
|
|
2208
|
-
|
|
2209
|
-
// Get filename from path (includes parent dir for context)
|
|
2210
|
-
function getFileName(path) {
|
|
2211
|
-
const parts = path.split('/').filter(p => p);
|
|
2212
|
-
if (parts.length >= 2) {
|
|
2213
|
-
return parts.slice(-2).join('/');
|
|
2214
|
-
}
|
|
2215
|
-
return parts[parts.length - 1] || path;
|
|
2216
|
-
}
|
|
2217
|
-
|
|
2218
|
-
// Track current architect port to avoid re-rendering iframe unnecessarily
|
|
2219
|
-
let currentArchitectPort = null;
|
|
2220
|
-
|
|
2221
|
-
// Render architect pane
|
|
2222
|
-
function renderArchitect() {
|
|
2223
|
-
const content = document.getElementById('architect-content');
|
|
2224
|
-
const statusDot = document.getElementById('architect-status');
|
|
2225
|
-
|
|
2226
|
-
if (state.architect && state.architect.port) {
|
|
2227
|
-
statusDot.classList.remove('inactive');
|
|
2228
|
-
// Only update iframe if port changed (avoid flashing on poll)
|
|
2229
|
-
if (currentArchitectPort !== state.architect.port) {
|
|
2230
|
-
currentArchitectPort = state.architect.port;
|
|
2231
|
-
content.innerHTML = `<iframe src="http://localhost:${state.architect.port}" title="Architect Terminal" allow="clipboard-read; clipboard-write"></iframe>`;
|
|
2232
|
-
}
|
|
2233
|
-
} else {
|
|
2234
|
-
if (currentArchitectPort !== null) {
|
|
2235
|
-
currentArchitectPort = null;
|
|
2236
|
-
content.innerHTML = `
|
|
2237
|
-
<div class="architect-placeholder">
|
|
2238
|
-
<p>Architect not running</p>
|
|
2239
|
-
<p>Run <code>agent-farm start</code> to begin</p>
|
|
2240
|
-
</div>
|
|
2241
|
-
`;
|
|
2242
|
-
}
|
|
2243
|
-
statusDot.classList.add('inactive');
|
|
2244
|
-
}
|
|
2245
|
-
}
|
|
2246
|
-
|
|
2247
|
-
// Render tabs
|
|
2248
|
-
function renderTabs() {
|
|
2249
|
-
const container = document.getElementById('tabs-container');
|
|
2250
|
-
|
|
2251
|
-
if (tabs.length === 0) {
|
|
2252
|
-
container.innerHTML = '';
|
|
2253
|
-
checkTabOverflow(); // Update overflow state when tabs cleared
|
|
2254
|
-
return;
|
|
2255
|
-
}
|
|
2256
|
-
|
|
2257
|
-
container.innerHTML = tabs.map(tab => {
|
|
2258
|
-
const isActive = tab.id === activeTabId;
|
|
2259
|
-
const icon = getTabIcon(tab.type);
|
|
2260
|
-
const statusDot = tab.type === 'builder' ? getStatusDot(tab.status) : '';
|
|
2261
|
-
const tooltip = getTabTooltip(tab);
|
|
2262
|
-
const isUncloseable = tab.closeable === false;
|
|
2263
|
-
|
|
2264
|
-
return `
|
|
2265
|
-
<div class="tab ${isActive ? 'active' : ''} ${isUncloseable ? 'tab-uncloseable' : ''}"
|
|
2266
|
-
onclick="selectTab('${tab.id}')"
|
|
2267
|
-
oncontextmenu="showContextMenu(event, '${tab.id}')"
|
|
2268
|
-
data-tab-id="${tab.id}"
|
|
2269
|
-
title="${tooltip}">
|
|
2270
|
-
<span class="icon">${icon}</span>
|
|
2271
|
-
<span class="name">${tab.name}</span>
|
|
2272
|
-
${statusDot}
|
|
2273
|
-
${!isUncloseable ? `<span class="close"
|
|
2274
|
-
onclick="event.stopPropagation(); closeTab('${tab.id}', event)"
|
|
2275
|
-
role="button"
|
|
2276
|
-
tabindex="0"
|
|
2277
|
-
aria-label="Close ${tab.name}"
|
|
2278
|
-
onkeydown="if(event.key==='Enter'||event.key===' '){event.stopPropagation();closeTab('${tab.id}',event)}">×</span>` : ''}
|
|
2279
|
-
</div>
|
|
2280
|
-
`;
|
|
2281
|
-
}).join('');
|
|
2282
|
-
|
|
2283
|
-
// Check overflow after tabs are rendered
|
|
2284
|
-
checkTabOverflow();
|
|
2285
|
-
}
|
|
2286
|
-
|
|
2287
|
-
// Get tab icon
|
|
2288
|
-
function getTabIcon(type) {
|
|
2289
|
-
switch (type) {
|
|
2290
|
-
case 'dashboard': return '🏠';
|
|
2291
|
-
case 'files': return '📁';
|
|
2292
|
-
case 'file': return '📄';
|
|
2293
|
-
case 'builder': return '🔨';
|
|
2294
|
-
case 'shell': return '>_';
|
|
2295
|
-
default: return '?';
|
|
2296
|
-
}
|
|
2297
|
-
}
|
|
2298
|
-
|
|
2299
|
-
// Status configuration - hoisted for performance (per Codex review)
|
|
2300
|
-
// Colors per spec 0019: green=active, yellow=waiting, red=blocked, gray=complete
|
|
2301
|
-
// Animations per spec 0019: pulse=active, blink-slow=waiting, blink-fast=blocked, static=complete
|
|
2302
|
-
// Shapes for accessibility: circle=default, diamond=blocked, ring=waiting
|
|
2303
|
-
const STATUS_CONFIG = {
|
|
2304
|
-
'spawning': { color: 'var(--status-active)', label: 'Spawning', shape: 'circle', animation: 'pulse' },
|
|
2305
|
-
'implementing': { color: 'var(--status-active)', label: 'Implementing', shape: 'circle', animation: 'pulse' },
|
|
2306
|
-
'blocked': { color: 'var(--status-error)', label: 'Blocked', shape: 'diamond', animation: 'blink-fast' },
|
|
2307
|
-
'pr-ready': { color: 'var(--status-waiting)', label: 'PR Ready', shape: 'ring', animation: 'blink-slow' },
|
|
2308
|
-
'complete': { color: 'var(--status-complete)', label: 'Complete', shape: 'circle', animation: null }
|
|
2309
|
-
};
|
|
2310
|
-
const DEFAULT_STATUS_CONFIG = { color: 'var(--text-muted)', label: 'Unknown', shape: 'circle', animation: null };
|
|
2311
|
-
|
|
2312
|
-
// Get status dot HTML with accessibility support
|
|
2313
|
-
// Accessibility: distinct animations per status, shapes for reduced-motion users
|
|
2314
|
-
// Uses role="img" instead of role="status" to avoid screen reader chatter on poll (per Codex review)
|
|
2315
|
-
function getStatusDot(status) {
|
|
2316
|
-
const config = STATUS_CONFIG[status] || { ...DEFAULT_STATUS_CONFIG, label: status || 'Unknown' };
|
|
2317
|
-
// Build CSS classes for accessibility
|
|
2318
|
-
const classes = ['status-dot'];
|
|
2319
|
-
if (config.shape === 'diamond') classes.push('status-dot--diamond');
|
|
2320
|
-
if (config.shape === 'ring') classes.push('status-dot--ring');
|
|
2321
|
-
if (config.animation === 'pulse') classes.push('status-dot--pulse');
|
|
2322
|
-
if (config.animation === 'blink-slow') classes.push('status-dot--blink-slow');
|
|
2323
|
-
if (config.animation === 'blink-fast') classes.push('status-dot--blink-fast');
|
|
2324
|
-
return `<span class="${classes.join(' ')}" style="background: ${config.color}" title="${config.label}" role="img" aria-label="${config.label}"></span>`;
|
|
2325
|
-
}
|
|
2326
|
-
|
|
2327
|
-
// Escape HTML special characters to prevent XSS
|
|
2328
|
-
function escapeHtml(text) {
|
|
2329
|
-
return String(text)
|
|
2330
|
-
.replace(/&/g, '&')
|
|
2331
|
-
.replace(/</g, '<')
|
|
2332
|
-
.replace(/>/g, '>')
|
|
2333
|
-
.replace(/"/g, '"')
|
|
2334
|
-
.replace(/'/g, ''');
|
|
2335
|
-
}
|
|
2336
|
-
|
|
2337
|
-
// Generate tooltip text for tab hover
|
|
2338
|
-
function getTabTooltip(tab) {
|
|
2339
|
-
const lines = [tab.name];
|
|
2340
|
-
|
|
2341
|
-
if (tab.type === 'builder') {
|
|
2342
|
-
if (tab.port) lines.push(`Port: ${tab.port}`);
|
|
2343
|
-
lines.push(`Status: ${tab.status || 'unknown'}`);
|
|
2344
|
-
// Extract project ID from tab id (e.g., "builder-0037" -> "0037")
|
|
2345
|
-
const projectId = tab.id.replace('builder-', '');
|
|
2346
|
-
lines.push(`Worktree: .builders/${projectId}`);
|
|
2347
|
-
} else if (tab.type === 'file') {
|
|
2348
|
-
lines.push(`Path: ${tab.path}`);
|
|
2349
|
-
if (tab.port) lines.push(`Port: ${tab.port}`);
|
|
2350
|
-
} else if (tab.type === 'shell') {
|
|
2351
|
-
if (tab.port) lines.push(`Port: ${tab.port}`);
|
|
2352
|
-
}
|
|
2353
|
-
|
|
2354
|
-
return escapeHtml(lines.join('\n'));
|
|
2355
|
-
}
|
|
2356
|
-
|
|
2357
|
-
// Track current tab content to avoid re-rendering iframe unnecessarily
|
|
2358
|
-
let currentTabPort = null;
|
|
2359
|
-
let currentTabType = null;
|
|
2360
|
-
|
|
2361
|
-
// Render tab content
|
|
2362
|
-
function renderTabContent() {
|
|
2363
|
-
const content = document.getElementById('tab-content');
|
|
2364
|
-
|
|
2365
|
-
if (!activeTabId || tabs.length === 0) {
|
|
2366
|
-
if (currentTabPort !== null || currentTabType !== null) {
|
|
2367
|
-
currentTabPort = null;
|
|
2368
|
-
currentTabType = null;
|
|
2369
|
-
content.innerHTML = `
|
|
2370
|
-
<div class="empty-state">
|
|
2371
|
-
<p>No tabs open</p>
|
|
2372
|
-
<p class="hint">Click the + buttons above or ask the architect to open files/builders</p>
|
|
2373
|
-
</div>
|
|
2374
|
-
`;
|
|
2375
|
-
}
|
|
2376
|
-
return;
|
|
2377
|
-
}
|
|
2378
|
-
|
|
2379
|
-
const tab = tabs.find(t => t.id === activeTabId);
|
|
2380
|
-
if (!tab) {
|
|
2381
|
-
if (currentTabPort !== null || currentTabType !== null) {
|
|
2382
|
-
currentTabPort = null;
|
|
2383
|
-
currentTabType = null;
|
|
2384
|
-
content.innerHTML = '<div class="empty-state"><p>Tab not found</p></div>';
|
|
2385
|
-
}
|
|
2386
|
-
return;
|
|
2387
|
-
}
|
|
2388
|
-
|
|
2389
|
-
// Handle dashboard tab specially (no iframe, inline content)
|
|
2390
|
-
if (tab.type === 'dashboard') {
|
|
2391
|
-
if (currentTabType !== 'dashboard') {
|
|
2392
|
-
currentTabType = 'dashboard';
|
|
2393
|
-
currentTabPort = null;
|
|
2394
|
-
renderDashboardTab();
|
|
2395
|
-
}
|
|
2396
|
-
return;
|
|
2397
|
-
}
|
|
2398
|
-
|
|
2399
|
-
// Handle activity tab specially (no iframe, inline content)
|
|
2400
|
-
if (tab.type === 'activity') {
|
|
2401
|
-
if (currentTabType !== 'activity') {
|
|
2402
|
-
currentTabType = 'activity';
|
|
2403
|
-
currentTabPort = null;
|
|
2404
|
-
renderActivityTab();
|
|
2405
|
-
}
|
|
2406
|
-
return;
|
|
2407
|
-
}
|
|
2408
|
-
|
|
2409
|
-
// For other tabs, only update iframe if port changed (avoid flashing on poll)
|
|
2410
|
-
if (currentTabPort !== tab.port || currentTabType !== tab.type) {
|
|
2411
|
-
currentTabPort = tab.port;
|
|
2412
|
-
currentTabType = tab.type;
|
|
2413
|
-
content.innerHTML = `<iframe src="http://localhost:${tab.port}" title="${tab.name}" allow="clipboard-read; clipboard-write"></iframe>`;
|
|
2414
|
-
}
|
|
2415
|
-
}
|
|
2416
|
-
|
|
2417
|
-
// Force refresh the iframe for a file tab (reloads content from server)
|
|
2418
|
-
function refreshFileTab(tabId) {
|
|
2419
|
-
const tab = tabs.find(t => t.id === tabId);
|
|
2420
|
-
if (!tab || tab.type !== 'file' || !tab.port) return;
|
|
2421
|
-
|
|
2422
|
-
// If this tab is currently active, force iframe reload
|
|
2423
|
-
if (activeTabId === tabId) {
|
|
2424
|
-
const content = document.getElementById('tab-content');
|
|
2425
|
-
const iframe = content.querySelector('iframe');
|
|
2426
|
-
if (iframe) {
|
|
2427
|
-
// Add cache-busting query param to force reload
|
|
2428
|
-
iframe.src = `http://localhost:${tab.port}?t=${Date.now()}`;
|
|
2429
|
-
}
|
|
2430
|
-
}
|
|
2431
|
-
}
|
|
2432
|
-
|
|
2433
|
-
// Update status bar
|
|
2434
|
-
function updateStatusBar() {
|
|
2435
|
-
// Architect status
|
|
2436
|
-
const archStatus = document.getElementById('status-architect');
|
|
2437
|
-
if (state.architect) {
|
|
2438
|
-
archStatus.innerHTML = `
|
|
2439
|
-
<span class="dot" style="background: var(--status-active)"></span>
|
|
2440
|
-
<span>Architect: running</span>
|
|
2441
|
-
`;
|
|
2442
|
-
} else {
|
|
2443
|
-
archStatus.innerHTML = `
|
|
2444
|
-
<span class="dot" style="background: var(--text-muted)"></span>
|
|
2445
|
-
<span>Architect: stopped</span>
|
|
2446
|
-
`;
|
|
2447
|
-
}
|
|
2448
|
-
|
|
2449
|
-
// Counts
|
|
2450
|
-
const builderCount = (state.builders || []).length;
|
|
2451
|
-
const shellCount = (state.utils || []).length;
|
|
2452
|
-
const fileCount = (state.annotations || []).length;
|
|
2453
|
-
|
|
2454
|
-
document.getElementById('status-builders').innerHTML = `<span>${builderCount} builder${builderCount !== 1 ? 's' : ''}</span>`;
|
|
2455
|
-
document.getElementById('status-shells').innerHTML = `<span>${shellCount} shell${shellCount !== 1 ? 's' : ''}</span>`;
|
|
2456
|
-
document.getElementById('status-files').innerHTML = `<span>${fileCount} file${fileCount !== 1 ? 's' : ''}</span>`;
|
|
2457
|
-
}
|
|
2458
|
-
|
|
2459
|
-
// Select tab
|
|
2460
|
-
function selectTab(tabId) {
|
|
2461
|
-
activeTabId = tabId;
|
|
2462
|
-
renderTabs();
|
|
2463
|
-
renderTabContent();
|
|
2464
|
-
// Scroll the active tab into view if needed
|
|
2465
|
-
scrollActiveTabIntoView();
|
|
2466
|
-
}
|
|
2467
|
-
|
|
2468
|
-
// Scroll the active tab into view
|
|
2469
|
-
function scrollActiveTabIntoView() {
|
|
2470
|
-
const container = document.getElementById('tabs-container');
|
|
2471
|
-
const activeTab = container.querySelector('.tab.active');
|
|
2472
|
-
if (activeTab) {
|
|
2473
|
-
activeTab.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
|
|
2474
|
-
}
|
|
2475
|
-
}
|
|
2476
|
-
|
|
2477
|
-
// Check if tabs are overflowing and update the overflow button
|
|
2478
|
-
function checkTabOverflow() {
|
|
2479
|
-
const container = document.getElementById('tabs-container');
|
|
2480
|
-
const overflowBtn = document.getElementById('overflow-btn');
|
|
2481
|
-
const overflowCount = document.getElementById('overflow-count');
|
|
2482
|
-
|
|
2483
|
-
if (!container || !overflowBtn) return;
|
|
2484
|
-
|
|
2485
|
-
const isOverflowing = container.scrollWidth > container.clientWidth;
|
|
2486
|
-
overflowBtn.style.display = isOverflowing ? 'flex' : 'none';
|
|
2487
|
-
|
|
2488
|
-
if (isOverflowing) {
|
|
2489
|
-
// Count hidden tabs (those partially or fully outside visible area - both sides)
|
|
2490
|
-
const tabElements = container.querySelectorAll('.tab');
|
|
2491
|
-
const containerRect = container.getBoundingClientRect();
|
|
2492
|
-
let hiddenCount = 0;
|
|
2493
|
-
|
|
2494
|
-
tabElements.forEach(tab => {
|
|
2495
|
-
const rect = tab.getBoundingClientRect();
|
|
2496
|
-
// Tab is hidden if scrolled off the right edge
|
|
2497
|
-
if (rect.right > containerRect.right + 1) {
|
|
2498
|
-
hiddenCount++;
|
|
2499
|
-
}
|
|
2500
|
-
// Tab is hidden if scrolled off the left edge
|
|
2501
|
-
else if (rect.left < containerRect.left - 1) {
|
|
2502
|
-
hiddenCount++;
|
|
2503
|
-
}
|
|
2504
|
-
});
|
|
2505
|
-
|
|
2506
|
-
overflowCount.textContent = `+${hiddenCount}`;
|
|
2507
|
-
}
|
|
2508
|
-
}
|
|
2509
|
-
|
|
2510
|
-
// Toggle the overflow menu
|
|
2511
|
-
function toggleOverflowMenu() {
|
|
2512
|
-
const menu = document.getElementById('overflow-menu');
|
|
2513
|
-
const btn = document.getElementById('overflow-btn');
|
|
2514
|
-
const isHidden = menu.classList.contains('hidden');
|
|
2515
|
-
|
|
2516
|
-
if (isHidden) {
|
|
2517
|
-
showOverflowMenu();
|
|
2518
|
-
} else {
|
|
2519
|
-
hideOverflowMenu();
|
|
2520
|
-
}
|
|
2521
|
-
}
|
|
2522
|
-
|
|
2523
|
-
// Show the overflow menu
|
|
2524
|
-
function showOverflowMenu() {
|
|
2525
|
-
const menu = document.getElementById('overflow-menu');
|
|
2526
|
-
const btn = document.getElementById('overflow-btn');
|
|
2527
|
-
|
|
2528
|
-
// Build menu items for all tabs
|
|
2529
|
-
menu.innerHTML = tabs.map((tab, index) => {
|
|
2530
|
-
const icon = getTabIcon(tab.type);
|
|
2531
|
-
const isActive = tab.id === activeTabId;
|
|
2532
|
-
return `
|
|
2533
|
-
<div class="overflow-menu-item ${isActive ? 'active' : ''}"
|
|
2534
|
-
role="menuitem"
|
|
2535
|
-
tabindex="${index === 0 ? 0 : -1}"
|
|
2536
|
-
data-tab-id="${tab.id}"
|
|
2537
|
-
onclick="selectTabFromMenu('${tab.id}')"
|
|
2538
|
-
onkeydown="handleOverflowMenuKeydown(event, '${tab.id}')">
|
|
2539
|
-
<span class="icon">${icon}</span>
|
|
2540
|
-
<span class="name">${tab.name}</span>
|
|
2541
|
-
<span class="open-external"
|
|
2542
|
-
onclick="event.stopPropagation(); openInNewTabFromMenu('${tab.id}')"
|
|
2543
|
-
onkeydown="if(event.key==='Enter'||event.key===' '){event.stopPropagation();openInNewTabFromMenu('${tab.id}')}"
|
|
2544
|
-
title="Open in new tab"
|
|
2545
|
-
role="button"
|
|
2546
|
-
tabindex="0"
|
|
2547
|
-
aria-label="Open ${tab.name} in new tab">↗</span>
|
|
2548
|
-
</div>
|
|
2549
|
-
`;
|
|
2550
|
-
}).join('');
|
|
2551
|
-
|
|
2552
|
-
menu.classList.remove('hidden');
|
|
2553
|
-
btn.setAttribute('aria-expanded', 'true');
|
|
2554
|
-
|
|
2555
|
-
// Focus the first item
|
|
2556
|
-
const firstItem = menu.querySelector('.overflow-menu-item');
|
|
2557
|
-
if (firstItem) firstItem.focus();
|
|
2558
|
-
|
|
2559
|
-
// Close on click outside (after a small delay to avoid immediate close)
|
|
2560
|
-
setTimeout(() => {
|
|
2561
|
-
document.addEventListener('click', handleOverflowClickOutside);
|
|
2562
|
-
}, 0);
|
|
2563
|
-
}
|
|
2564
|
-
|
|
2565
|
-
// Hide the overflow menu
|
|
2566
|
-
function hideOverflowMenu() {
|
|
2567
|
-
const menu = document.getElementById('overflow-menu');
|
|
2568
|
-
const btn = document.getElementById('overflow-btn');
|
|
2569
|
-
menu.classList.add('hidden');
|
|
2570
|
-
btn.setAttribute('aria-expanded', 'false');
|
|
2571
|
-
document.removeEventListener('click', handleOverflowClickOutside);
|
|
2572
|
-
}
|
|
2573
|
-
|
|
2574
|
-
// Handle click outside overflow menu
|
|
2575
|
-
function handleOverflowClickOutside(event) {
|
|
2576
|
-
const menu = document.getElementById('overflow-menu');
|
|
2577
|
-
const btn = document.getElementById('overflow-btn');
|
|
2578
|
-
if (!menu.contains(event.target) && !btn.contains(event.target)) {
|
|
2579
|
-
hideOverflowMenu();
|
|
2580
|
-
}
|
|
2581
|
-
}
|
|
2582
|
-
|
|
2583
|
-
// Select tab from overflow menu
|
|
2584
|
-
function selectTabFromMenu(tabId) {
|
|
2585
|
-
hideOverflowMenu();
|
|
2586
|
-
selectTab(tabId);
|
|
2587
|
-
}
|
|
2588
|
-
|
|
2589
|
-
// Open tab in new window from overflow menu
|
|
2590
|
-
function openInNewTabFromMenu(tabId) {
|
|
2591
|
-
hideOverflowMenu();
|
|
2592
|
-
openInNewTab(tabId);
|
|
2593
|
-
}
|
|
2594
|
-
|
|
2595
|
-
// Handle keyboard navigation in overflow menu
|
|
2596
|
-
function handleOverflowMenuKeydown(event, tabId) {
|
|
2597
|
-
const menu = document.getElementById('overflow-menu');
|
|
2598
|
-
const items = Array.from(menu.querySelectorAll('.overflow-menu-item'));
|
|
2599
|
-
const currentIndex = items.findIndex(item => item === document.activeElement);
|
|
2600
|
-
|
|
2601
|
-
switch (event.key) {
|
|
2602
|
-
case 'ArrowDown':
|
|
2603
|
-
event.preventDefault();
|
|
2604
|
-
const nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
|
|
2605
|
-
items[nextIndex].focus();
|
|
2606
|
-
break;
|
|
2607
|
-
case 'ArrowUp':
|
|
2608
|
-
event.preventDefault();
|
|
2609
|
-
const prevIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
|
|
2610
|
-
items[prevIndex].focus();
|
|
2611
|
-
break;
|
|
2612
|
-
case 'Enter':
|
|
2613
|
-
case ' ':
|
|
2614
|
-
event.preventDefault();
|
|
2615
|
-
selectTabFromMenu(tabId);
|
|
2616
|
-
break;
|
|
2617
|
-
case 'Escape':
|
|
2618
|
-
event.preventDefault();
|
|
2619
|
-
hideOverflowMenu();
|
|
2620
|
-
document.getElementById('overflow-btn').focus();
|
|
2621
|
-
break;
|
|
2622
|
-
case 'Tab':
|
|
2623
|
-
// Allow Tab to close menu and move focus
|
|
2624
|
-
hideOverflowMenu();
|
|
2625
|
-
break;
|
|
2626
|
-
}
|
|
2627
|
-
}
|
|
2628
|
-
|
|
2629
|
-
// Close tab
|
|
2630
|
-
function closeTab(tabId, event) {
|
|
2631
|
-
const tab = tabs.find(t => t.id === tabId);
|
|
2632
|
-
if (!tab) return;
|
|
2633
|
-
|
|
2634
|
-
// Shift+click bypasses confirmation
|
|
2635
|
-
if (event && event.shiftKey) {
|
|
2636
|
-
doCloseTab(tabId);
|
|
2637
|
-
return;
|
|
2638
|
-
}
|
|
2639
|
-
|
|
2640
|
-
// Files don't need confirmation
|
|
2641
|
-
if (tab.type === 'file') {
|
|
2642
|
-
doCloseTab(tabId);
|
|
2643
|
-
return;
|
|
2644
|
-
}
|
|
2645
|
-
|
|
2646
|
-
// Show confirmation for builders and shells
|
|
2647
|
-
pendingCloseTabId = tabId;
|
|
2648
|
-
const dialog = document.getElementById('close-dialog');
|
|
2649
|
-
const title = document.getElementById('close-dialog-title');
|
|
2650
|
-
const message = document.getElementById('close-dialog-message');
|
|
2651
|
-
|
|
2652
|
-
if (tab.type === 'builder') {
|
|
2653
|
-
title.textContent = `Stop builder ${tab.name}?`;
|
|
2654
|
-
message.textContent = 'This will terminate the builder process.';
|
|
2655
|
-
} else {
|
|
2656
|
-
title.textContent = `Close shell ${tab.name}?`;
|
|
2657
|
-
message.textContent = 'This will terminate the shell process.';
|
|
2658
|
-
}
|
|
2659
|
-
|
|
2660
|
-
dialog.classList.remove('hidden');
|
|
2661
|
-
}
|
|
2662
|
-
|
|
2663
|
-
// Actually close the tab
|
|
2664
|
-
async function doCloseTab(tabId) {
|
|
2665
|
-
const tab = tabs.find(t => t.id === tabId);
|
|
2666
|
-
if (!tab) return;
|
|
2667
|
-
|
|
2668
|
-
try {
|
|
2669
|
-
// Call API to close the tab
|
|
2670
|
-
await fetch(`/api/tabs/${encodeURIComponent(tabId)}`, { method: 'DELETE' });
|
|
2671
|
-
|
|
2672
|
-
// Remove from local state
|
|
2673
|
-
tabs = tabs.filter(t => t.id !== tabId);
|
|
2674
|
-
|
|
2675
|
-
// If closing active tab, switch to another
|
|
2676
|
-
if (activeTabId === tabId) {
|
|
2677
|
-
activeTabId = tabs.length > 0 ? tabs[tabs.length - 1].id : null;
|
|
2678
|
-
}
|
|
2679
|
-
|
|
2680
|
-
renderTabs();
|
|
2681
|
-
renderTabContent();
|
|
2682
|
-
showToast('Tab closed', 'success');
|
|
2683
|
-
} catch (err) {
|
|
2684
|
-
showToast('Failed to close tab: ' + err.message, 'error');
|
|
2685
|
-
}
|
|
2686
|
-
}
|
|
2687
|
-
|
|
2688
|
-
// Confirm close from dialog
|
|
2689
|
-
function confirmClose() {
|
|
2690
|
-
if (pendingCloseTabId) {
|
|
2691
|
-
doCloseTab(pendingCloseTabId);
|
|
2692
|
-
hideCloseDialog();
|
|
2693
|
-
}
|
|
2694
|
-
}
|
|
2695
|
-
|
|
2696
|
-
function hideCloseDialog() {
|
|
2697
|
-
document.getElementById('close-dialog').classList.add('hidden');
|
|
2698
|
-
pendingCloseTabId = null;
|
|
2699
|
-
}
|
|
2700
|
-
|
|
2701
|
-
// Context menu
|
|
2702
|
-
function showContextMenu(event, tabId) {
|
|
2703
|
-
event.preventDefault();
|
|
2704
|
-
contextMenuTabId = tabId;
|
|
2705
|
-
|
|
2706
|
-
const menu = document.getElementById('context-menu');
|
|
2707
|
-
menu.style.left = event.clientX + 'px';
|
|
2708
|
-
menu.style.top = event.clientY + 'px';
|
|
2709
|
-
menu.classList.remove('hidden');
|
|
2710
|
-
|
|
2711
|
-
// Show/hide reload option based on tab type
|
|
2712
|
-
const tab = tabs.find(t => t.id === tabId);
|
|
2713
|
-
const reloadItem = document.getElementById('context-reload');
|
|
2714
|
-
if (reloadItem) {
|
|
2715
|
-
reloadItem.style.display = (tab && tab.type === 'file') ? 'block' : 'none';
|
|
2716
|
-
}
|
|
2717
|
-
|
|
2718
|
-
// Focus first item for keyboard navigation
|
|
2719
|
-
const firstItem = menu.querySelector('.context-menu-item');
|
|
2720
|
-
if (firstItem) firstItem.focus();
|
|
2721
|
-
|
|
2722
|
-
// Close on click outside
|
|
2723
|
-
setTimeout(() => {
|
|
2724
|
-
document.addEventListener('click', hideContextMenu, { once: true });
|
|
2725
|
-
}, 0);
|
|
2726
|
-
}
|
|
2727
|
-
|
|
2728
|
-
// Reload file tab content
|
|
2729
|
-
function reloadContextTab() {
|
|
2730
|
-
if (contextMenuTabId) {
|
|
2731
|
-
refreshFileTab(contextMenuTabId);
|
|
2732
|
-
showToast('Reloaded', 'success');
|
|
2733
|
-
}
|
|
2734
|
-
hideContextMenu();
|
|
2735
|
-
}
|
|
2736
|
-
|
|
2737
|
-
function hideContextMenu() {
|
|
2738
|
-
document.getElementById('context-menu').classList.add('hidden');
|
|
2739
|
-
contextMenuTabId = null;
|
|
2740
|
-
}
|
|
2741
|
-
|
|
2742
|
-
// Handle keyboard navigation in context menu
|
|
2743
|
-
function handleContextMenuKeydown(event) {
|
|
2744
|
-
const menu = document.getElementById('context-menu');
|
|
2745
|
-
const items = Array.from(menu.querySelectorAll('.context-menu-item'));
|
|
2746
|
-
const currentIndex = items.findIndex(item => item === document.activeElement);
|
|
2747
|
-
|
|
2748
|
-
switch (event.key) {
|
|
2749
|
-
case 'ArrowDown':
|
|
2750
|
-
event.preventDefault();
|
|
2751
|
-
const nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
|
|
2752
|
-
items[nextIndex].focus();
|
|
2753
|
-
break;
|
|
2754
|
-
case 'ArrowUp':
|
|
2755
|
-
event.preventDefault();
|
|
2756
|
-
const prevIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
|
|
2757
|
-
items[prevIndex].focus();
|
|
2758
|
-
break;
|
|
2759
|
-
case 'Enter':
|
|
2760
|
-
case ' ':
|
|
2761
|
-
event.preventDefault();
|
|
2762
|
-
const actionName = event.target.dataset.action;
|
|
2763
|
-
if (actionName && typeof window[actionName] === 'function') {
|
|
2764
|
-
window[actionName]();
|
|
2765
|
-
}
|
|
2766
|
-
break;
|
|
2767
|
-
case 'Escape':
|
|
2768
|
-
event.preventDefault();
|
|
2769
|
-
hideContextMenu();
|
|
2770
|
-
break;
|
|
2771
|
-
case 'Tab':
|
|
2772
|
-
hideContextMenu();
|
|
2773
|
-
break;
|
|
2774
|
-
}
|
|
2775
|
-
}
|
|
2776
|
-
|
|
2777
|
-
function closeActiveTab() {
|
|
2778
|
-
if (contextMenuTabId) {
|
|
2779
|
-
closeTab(contextMenuTabId);
|
|
2780
|
-
}
|
|
2781
|
-
hideContextMenu();
|
|
2782
|
-
}
|
|
2783
|
-
|
|
2784
|
-
function closeOtherTabs() {
|
|
2785
|
-
if (contextMenuTabId) {
|
|
2786
|
-
// Skip uncloseable tabs (Projects tab)
|
|
2787
|
-
const otherTabs = tabs.filter(t => t.id !== contextMenuTabId && t.closeable !== false);
|
|
2788
|
-
otherTabs.forEach(t => doCloseTab(t.id));
|
|
2789
|
-
}
|
|
2790
|
-
hideContextMenu();
|
|
2791
|
-
}
|
|
2792
|
-
|
|
2793
|
-
function closeAllTabs() {
|
|
2794
|
-
// Skip uncloseable tabs (Projects tab)
|
|
2795
|
-
tabs.filter(t => t.closeable !== false).forEach(t => doCloseTab(t.id));
|
|
2796
|
-
hideContextMenu();
|
|
2797
|
-
}
|
|
2798
|
-
|
|
2799
|
-
// Open tab content in a new browser tab
|
|
2800
|
-
function openInNewTab(tabId) {
|
|
2801
|
-
const tab = tabs.find(t => t.id === tabId);
|
|
2802
|
-
if (!tab) return;
|
|
2803
|
-
|
|
2804
|
-
let url;
|
|
2805
|
-
if (tab.type === 'file') {
|
|
2806
|
-
// File tabs use the annotation port
|
|
2807
|
-
if (!tab.port) {
|
|
2808
|
-
showToast('Tab not ready', 'error');
|
|
2809
|
-
return;
|
|
2810
|
-
}
|
|
2811
|
-
url = `http://localhost:${tab.port}`;
|
|
2812
|
-
} else {
|
|
2813
|
-
// Builder or shell - direct port access
|
|
2814
|
-
if (!tab.port) {
|
|
2815
|
-
showToast('Tab not ready', 'error');
|
|
2816
|
-
return;
|
|
2817
|
-
}
|
|
2818
|
-
url = `http://localhost:${tab.port}`;
|
|
2819
|
-
}
|
|
2820
|
-
|
|
2821
|
-
window.open(url, '_blank', 'noopener,noreferrer');
|
|
2822
|
-
}
|
|
2823
|
-
|
|
2824
|
-
// Open context menu tab in new tab
|
|
2825
|
-
function openContextTab() {
|
|
2826
|
-
if (contextMenuTabId) {
|
|
2827
|
-
openInNewTab(contextMenuTabId);
|
|
2828
|
-
}
|
|
2829
|
-
hideContextMenu();
|
|
2830
|
-
}
|
|
2831
|
-
|
|
2832
|
-
// File dialog
|
|
2833
|
-
function showFileDialog() {
|
|
2834
|
-
document.getElementById('file-dialog').classList.remove('hidden');
|
|
2835
|
-
document.getElementById('file-path-input').focus();
|
|
2836
|
-
}
|
|
2837
|
-
|
|
2838
|
-
function hideFileDialog() {
|
|
2839
|
-
document.getElementById('file-dialog').classList.add('hidden');
|
|
2840
|
-
document.getElementById('file-path-input').value = '';
|
|
2841
|
-
}
|
|
2842
|
-
|
|
2843
|
-
function setFilePath(path) {
|
|
2844
|
-
document.getElementById('file-path-input').value = path;
|
|
2845
|
-
document.getElementById('file-path-input').focus();
|
|
2846
|
-
}
|
|
2847
|
-
|
|
2848
|
-
async function openFile() {
|
|
2849
|
-
const path = document.getElementById('file-path-input').value.trim();
|
|
2850
|
-
if (!path) return;
|
|
2851
|
-
|
|
2852
|
-
try {
|
|
2853
|
-
const response = await fetch('/api/tabs/file', {
|
|
2854
|
-
method: 'POST',
|
|
2855
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2856
|
-
body: JSON.stringify({ path })
|
|
2857
|
-
});
|
|
2858
|
-
|
|
2859
|
-
if (!response.ok) {
|
|
2860
|
-
throw new Error(await response.text());
|
|
2861
|
-
}
|
|
2862
|
-
|
|
2863
|
-
hideFileDialog();
|
|
2864
|
-
await refresh();
|
|
2865
|
-
showToast(`Opened ${path}`, 'success');
|
|
2866
|
-
} catch (err) {
|
|
2867
|
-
showToast('Failed to open file: ' + err.message, 'error');
|
|
2868
|
-
}
|
|
2869
|
-
}
|
|
2870
|
-
|
|
2871
|
-
// Spawn worktree builder (no dialog - spawns with random ID)
|
|
2872
|
-
async function spawnBuilder() {
|
|
2873
|
-
try {
|
|
2874
|
-
const response = await fetch('/api/tabs/builder', {
|
|
2875
|
-
method: 'POST',
|
|
2876
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2877
|
-
body: JSON.stringify({})
|
|
2878
|
-
});
|
|
2879
|
-
|
|
2880
|
-
if (!response.ok) {
|
|
2881
|
-
throw new Error(await response.text());
|
|
2882
|
-
}
|
|
2883
|
-
|
|
2884
|
-
const result = await response.json();
|
|
2885
|
-
|
|
2886
|
-
// Add to local tabs and select it
|
|
2887
|
-
const newTab = {
|
|
2888
|
-
id: `builder-${result.id}`,
|
|
2889
|
-
type: 'builder',
|
|
2890
|
-
name: result.name,
|
|
2891
|
-
port: result.port
|
|
2892
|
-
};
|
|
2893
|
-
tabs.push(newTab);
|
|
2894
|
-
activeTabId = newTab.id;
|
|
2895
|
-
renderTabs();
|
|
2896
|
-
renderTabContent();
|
|
2897
|
-
showToast(`Builder ${result.name} spawned`, 'success');
|
|
2898
|
-
} catch (err) {
|
|
2899
|
-
showToast('Failed to spawn builder: ' + err.message, 'error');
|
|
2900
|
-
}
|
|
2901
|
-
}
|
|
2902
|
-
|
|
2903
|
-
// Spawn shell
|
|
2904
|
-
async function spawnShell() {
|
|
2905
|
-
try {
|
|
2906
|
-
const response = await fetch('/api/tabs/shell', {
|
|
2907
|
-
method: 'POST',
|
|
2908
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2909
|
-
body: JSON.stringify({})
|
|
2910
|
-
});
|
|
2911
|
-
|
|
2912
|
-
if (!response.ok) {
|
|
2913
|
-
throw new Error(await response.text());
|
|
2914
|
-
}
|
|
2915
|
-
|
|
2916
|
-
const result = await response.json();
|
|
2917
|
-
|
|
2918
|
-
// Add to local tabs and select it
|
|
2919
|
-
const newTab = {
|
|
2920
|
-
id: `shell-${result.id}`,
|
|
2921
|
-
type: 'shell',
|
|
2922
|
-
name: result.name,
|
|
2923
|
-
port: result.port,
|
|
2924
|
-
utilId: result.id,
|
|
2925
|
-
pendingLoad: true // Mark as pending to delay iframe
|
|
2926
|
-
};
|
|
2927
|
-
tabs.push(newTab);
|
|
2928
|
-
activeTabId = newTab.id;
|
|
2929
|
-
renderTabs();
|
|
2930
|
-
|
|
2931
|
-
// Show loading state, then load iframe after delay
|
|
2932
|
-
const content = document.getElementById('tab-content');
|
|
2933
|
-
content.innerHTML = '<div class="empty-state"><p>Starting shell...</p></div>';
|
|
2934
|
-
|
|
2935
|
-
setTimeout(() => {
|
|
2936
|
-
delete newTab.pendingLoad;
|
|
2937
|
-
currentTabPort = null; // Force re-render
|
|
2938
|
-
renderTabContent();
|
|
2939
|
-
}, 800);
|
|
2940
|
-
|
|
2941
|
-
showToast('Shell spawned', 'success');
|
|
2942
|
-
} catch (err) {
|
|
2943
|
-
showToast('Failed to spawn shell: ' + err.message, 'error');
|
|
2944
|
-
}
|
|
2945
|
-
}
|
|
2946
|
-
|
|
2947
|
-
// Refresh state from API
|
|
2948
|
-
async function refresh() {
|
|
2949
|
-
try {
|
|
2950
|
-
const response = await fetch('/api/state');
|
|
2951
|
-
if (!response.ok) throw new Error('Failed to fetch state');
|
|
2952
|
-
|
|
2953
|
-
const newState = await response.json();
|
|
2954
|
-
Object.assign(state, newState);
|
|
2955
|
-
|
|
2956
|
-
buildTabsFromState();
|
|
2957
|
-
renderArchitect();
|
|
2958
|
-
renderTabs();
|
|
2959
|
-
renderTabContent();
|
|
2960
|
-
updateStatusBar();
|
|
2961
|
-
} catch (err) {
|
|
2962
|
-
console.error('Refresh error:', err);
|
|
2963
|
-
}
|
|
2964
|
-
}
|
|
2965
|
-
|
|
2966
|
-
// Toast notifications
|
|
2967
|
-
function showToast(message, type = 'info') {
|
|
2968
|
-
const container = document.getElementById('toast-container');
|
|
2969
|
-
const toast = document.createElement('div');
|
|
2970
|
-
toast.className = `toast ${type}`;
|
|
2971
|
-
toast.textContent = message;
|
|
2972
|
-
container.appendChild(toast);
|
|
2973
|
-
|
|
2974
|
-
setTimeout(() => {
|
|
2975
|
-
toast.remove();
|
|
2976
|
-
}, 3000);
|
|
2977
|
-
}
|
|
2978
|
-
|
|
2979
|
-
// Polling for state updates
|
|
2980
|
-
let pollInterval = null;
|
|
2981
|
-
|
|
2982
|
-
function startPolling() {
|
|
2983
|
-
pollInterval = setInterval(refresh, 1000);
|
|
2984
|
-
}
|
|
2985
|
-
|
|
2986
|
-
function stopPolling() {
|
|
2987
|
-
if (pollInterval) {
|
|
2988
|
-
clearInterval(pollInterval);
|
|
2989
|
-
pollInterval = null;
|
|
2990
|
-
}
|
|
2991
|
-
}
|
|
2992
|
-
|
|
2993
|
-
// Keyboard shortcuts
|
|
2994
|
-
document.addEventListener('keydown', (e) => {
|
|
2995
|
-
// Escape to close dialogs and menus
|
|
2996
|
-
if (e.key === 'Escape') {
|
|
2997
|
-
hideFileDialog();
|
|
2998
|
-
hideCloseDialog();
|
|
2999
|
-
hideContextMenu();
|
|
3000
|
-
hideOverflowMenu();
|
|
3001
|
-
// Activity modal (Spec 0059)
|
|
3002
|
-
const activityModal = document.getElementById('activity-modal');
|
|
3003
|
-
if (activityModal && !activityModal.classList.contains('hidden')) {
|
|
3004
|
-
closeActivityModal();
|
|
3005
|
-
}
|
|
3006
|
-
}
|
|
3007
|
-
|
|
3008
|
-
// Enter in dialogs
|
|
3009
|
-
if (e.key === 'Enter') {
|
|
3010
|
-
if (!document.getElementById('file-dialog').classList.contains('hidden')) {
|
|
3011
|
-
openFile();
|
|
3012
|
-
}
|
|
3013
|
-
}
|
|
3014
|
-
|
|
3015
|
-
// Ctrl+Tab / Ctrl+Shift+Tab to switch tabs
|
|
3016
|
-
if (e.ctrlKey && e.key === 'Tab') {
|
|
3017
|
-
e.preventDefault();
|
|
3018
|
-
if (tabs.length < 2) return;
|
|
3019
|
-
|
|
3020
|
-
const currentIndex = tabs.findIndex(t => t.id === activeTabId);
|
|
3021
|
-
let newIndex;
|
|
3022
|
-
|
|
3023
|
-
if (e.shiftKey) {
|
|
3024
|
-
newIndex = currentIndex <= 0 ? tabs.length - 1 : currentIndex - 1;
|
|
3025
|
-
} else {
|
|
3026
|
-
newIndex = currentIndex >= tabs.length - 1 ? 0 : currentIndex + 1;
|
|
3027
|
-
}
|
|
3028
|
-
|
|
3029
|
-
selectTab(tabs[newIndex].id);
|
|
3030
|
-
}
|
|
3031
|
-
|
|
3032
|
-
// Ctrl+W to close current tab
|
|
3033
|
-
if (e.ctrlKey && e.key === 'w') {
|
|
3034
|
-
e.preventDefault();
|
|
3035
|
-
if (activeTabId) {
|
|
3036
|
-
closeTab(activeTabId, e);
|
|
3037
|
-
}
|
|
3038
|
-
}
|
|
3039
|
-
});
|
|
3040
|
-
|
|
3041
|
-
// ============================================
|
|
3042
|
-
// Projects Tab Functions (Spec 0045)
|
|
3043
|
-
// ============================================
|
|
3044
|
-
|
|
3045
|
-
// XSS-safe HTML escaping (used by escapeHtml above, same implementation)
|
|
3046
|
-
function escapeProjectHtml(text) {
|
|
3047
|
-
if (!text) return '';
|
|
3048
|
-
const div = document.createElement('div');
|
|
3049
|
-
div.textContent = String(text);
|
|
3050
|
-
return div.innerHTML;
|
|
3051
|
-
}
|
|
3052
|
-
|
|
3053
|
-
// Simple DJB2 hash for change detection
|
|
3054
|
-
function hashString(str) {
|
|
3055
|
-
let hash = 5381;
|
|
3056
|
-
for (let i = 0; i < str.length; i++) {
|
|
3057
|
-
hash = ((hash << 5) + hash) + str.charCodeAt(i);
|
|
3058
|
-
}
|
|
3059
|
-
return hash >>> 0;
|
|
3060
|
-
}
|
|
3061
|
-
|
|
3062
|
-
// Parse a single project entry from YAML-like text
|
|
3063
|
-
function parseProjectEntry(text) {
|
|
3064
|
-
const project = {};
|
|
3065
|
-
const lines = text.split('\n');
|
|
3066
|
-
|
|
3067
|
-
for (const line of lines) {
|
|
3068
|
-
// Match key: value or key: "value"
|
|
3069
|
-
// Also handle "- id:" YAML list format
|
|
3070
|
-
const match = line.match(/^\s*-?\s*(\w+):\s*(.*)$/);
|
|
3071
|
-
if (!match) continue;
|
|
3072
|
-
|
|
3073
|
-
const [, key, rawValue] = match;
|
|
3074
|
-
// Remove quotes if present
|
|
3075
|
-
let value = rawValue.trim();
|
|
3076
|
-
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
3077
|
-
(value.startsWith("'") && value.endsWith("'"))) {
|
|
3078
|
-
value = value.slice(1, -1);
|
|
3079
|
-
}
|
|
3080
|
-
|
|
3081
|
-
// Handle nested files object
|
|
3082
|
-
if (key === 'files') {
|
|
3083
|
-
project.files = {};
|
|
3084
|
-
continue;
|
|
3085
|
-
}
|
|
3086
|
-
if (key === 'spec' || key === 'plan' || key === 'review') {
|
|
3087
|
-
if (!project.files) project.files = {};
|
|
3088
|
-
project.files[key] = value === 'null' ? null : value;
|
|
3089
|
-
continue;
|
|
3090
|
-
}
|
|
3091
|
-
|
|
3092
|
-
// Handle nested timestamps object
|
|
3093
|
-
if (key === 'timestamps') {
|
|
3094
|
-
project.timestamps = {};
|
|
3095
|
-
continue;
|
|
3096
|
-
}
|
|
3097
|
-
const timestampFields = ['conceived_at', 'specified_at', 'planned_at',
|
|
3098
|
-
'implementing_at', 'implemented_at', 'committed_at', 'integrated_at'];
|
|
3099
|
-
if (timestampFields.includes(key)) {
|
|
3100
|
-
if (!project.timestamps) project.timestamps = {};
|
|
3101
|
-
project.timestamps[key] = value === 'null' ? null : value;
|
|
3102
|
-
continue;
|
|
3103
|
-
}
|
|
3104
|
-
|
|
3105
|
-
// Handle arrays (simple inline format)
|
|
3106
|
-
if (key === 'dependencies' || key === 'tags' || key === 'ticks') {
|
|
3107
|
-
if (value.startsWith('[') && value.endsWith(']')) {
|
|
3108
|
-
const inner = value.slice(1, -1);
|
|
3109
|
-
if (inner.trim() === '') {
|
|
3110
|
-
project[key] = [];
|
|
3111
|
-
} else {
|
|
3112
|
-
project[key] = inner.split(',').map(s => s.trim().replace(/^["']|["']$/g, ''));
|
|
3113
|
-
}
|
|
3114
|
-
} else {
|
|
3115
|
-
project[key] = [];
|
|
3116
|
-
}
|
|
3117
|
-
continue;
|
|
3118
|
-
}
|
|
3119
|
-
|
|
3120
|
-
// Regular string values
|
|
3121
|
-
if (value !== 'null') {
|
|
3122
|
-
project[key] = value;
|
|
3123
|
-
}
|
|
3124
|
-
}
|
|
3125
|
-
|
|
3126
|
-
return project;
|
|
3127
|
-
}
|
|
3128
|
-
|
|
3129
|
-
// Validate that a project entry is valid
|
|
3130
|
-
function isValidProject(project) {
|
|
3131
|
-
// Must have id (4-digit string, not "NNNN")
|
|
3132
|
-
if (!project.id || project.id === 'NNNN' || !/^\d{4}$/.test(project.id)) {
|
|
3133
|
-
return false;
|
|
3134
|
-
}
|
|
3135
|
-
|
|
3136
|
-
// Must have status
|
|
3137
|
-
const validStatuses = ['conceived', 'specified', 'planned', 'implementing',
|
|
3138
|
-
'implemented', 'committed', 'integrated', 'abandoned', 'on-hold'];
|
|
3139
|
-
if (!project.status || !validStatuses.includes(project.status)) {
|
|
3140
|
-
return false;
|
|
3141
|
-
}
|
|
3142
|
-
|
|
3143
|
-
// Must have title
|
|
3144
|
-
if (!project.title) {
|
|
3145
|
-
return false;
|
|
3146
|
-
}
|
|
3147
|
-
|
|
3148
|
-
// Filter out example entries
|
|
3149
|
-
if (project.tags && project.tags.includes('example')) {
|
|
3150
|
-
return false;
|
|
3151
|
-
}
|
|
3152
|
-
|
|
3153
|
-
return true;
|
|
3154
|
-
}
|
|
3155
|
-
|
|
3156
|
-
// Parse projectlist.md content into array of projects
|
|
3157
|
-
function parseProjectlist(content) {
|
|
3158
|
-
const projects = [];
|
|
3159
|
-
|
|
3160
|
-
try {
|
|
3161
|
-
// Extract YAML code blocks
|
|
3162
|
-
const yamlBlockRegex = /```yaml\n([\s\S]*?)```/g;
|
|
3163
|
-
let match;
|
|
3164
|
-
|
|
3165
|
-
while ((match = yamlBlockRegex.exec(content)) !== null) {
|
|
3166
|
-
const block = match[1];
|
|
3167
|
-
|
|
3168
|
-
// Split by project entries (lines starting with " - id:")
|
|
3169
|
-
// Handle both top-level and indented entries
|
|
3170
|
-
const projectMatches = block.split(/\n(?=\s*- id:)/);
|
|
3171
|
-
|
|
3172
|
-
for (const projectText of projectMatches) {
|
|
3173
|
-
if (!projectText.trim() || !projectText.includes('id:')) continue;
|
|
3174
|
-
|
|
3175
|
-
const project = parseProjectEntry(projectText);
|
|
3176
|
-
if (isValidProject(project)) {
|
|
3177
|
-
projects.push(project);
|
|
3178
|
-
}
|
|
3179
|
-
}
|
|
3180
|
-
}
|
|
3181
|
-
} catch (err) {
|
|
3182
|
-
console.error('Error parsing projectlist:', err);
|
|
3183
|
-
return [];
|
|
3184
|
-
}
|
|
3185
|
-
|
|
3186
|
-
return projects;
|
|
3187
|
-
}
|
|
3188
|
-
|
|
3189
|
-
// Render the welcome screen for new users
|
|
3190
|
-
function renderWelcomeScreen() {
|
|
3191
|
-
return `
|
|
3192
|
-
<div class="projects-welcome">
|
|
3193
|
-
<h2>Welcome to Codev</h2>
|
|
3194
|
-
<p>Codev helps you build software with AI assistance. Projects flow through 7 stages from idea to production:</p>
|
|
3195
|
-
<ol>
|
|
3196
|
-
<li><strong>Conceived</strong> - Tell the architect what you want to build</li>
|
|
3197
|
-
<li><strong>Specified</strong> - AI writes a spec, you approve it</li>
|
|
3198
|
-
<li><strong>Planned</strong> - AI creates an implementation plan</li>
|
|
3199
|
-
<li><strong>Implementing</strong> - Builder AI writes the code</li>
|
|
3200
|
-
<li><strong>Implemented</strong> - Code complete, PR ready for review</li>
|
|
3201
|
-
<li><strong>Committed</strong> - PR merged to main</li>
|
|
3202
|
-
<li><strong>Integrated</strong> - Validated in production</li>
|
|
3203
|
-
</ol>
|
|
3204
|
-
<hr>
|
|
3205
|
-
<p class="quick-tip">
|
|
3206
|
-
<strong>Quick tip:</strong> Say "I want to build a [feature]" and the architect will guide you through the process.
|
|
3207
|
-
</p>
|
|
3208
|
-
</div>
|
|
3209
|
-
`;
|
|
3210
|
-
}
|
|
3211
|
-
|
|
3212
|
-
// Render the error banner
|
|
3213
|
-
function renderErrorBanner(message) {
|
|
3214
|
-
return `
|
|
3215
|
-
<div class="projects-error">
|
|
3216
|
-
<span class="projects-error-message">${escapeProjectHtml(message)}</span>
|
|
3217
|
-
<button onclick="reloadProjectlist()">Retry</button>
|
|
3218
|
-
</div>
|
|
3219
|
-
`;
|
|
3220
|
-
}
|
|
3221
|
-
|
|
3222
|
-
// Group projects by status for summary
|
|
3223
|
-
function groupByStatus(projects, statuses) {
|
|
3224
|
-
const groups = {};
|
|
3225
|
-
for (const status of statuses) {
|
|
3226
|
-
groups[status] = projects.filter(p => p.status === status);
|
|
3227
|
-
}
|
|
3228
|
-
return groups;
|
|
3229
|
-
}
|
|
3230
|
-
|
|
3231
|
-
// Render the status summary section
|
|
3232
|
-
function renderStatusSummary(projects) {
|
|
3233
|
-
const activeStatuses = ['conceived', 'specified', 'planned', 'implementing', 'implemented', 'committed'];
|
|
3234
|
-
const active = projects.filter(p => activeStatuses.includes(p.status));
|
|
3235
|
-
const completed = projects.filter(p => p.status === 'integrated');
|
|
3236
|
-
const byStatus = groupByStatus(active, activeStatuses);
|
|
3237
|
-
|
|
3238
|
-
const activeListItems = [];
|
|
3239
|
-
for (const status of activeStatuses) {
|
|
3240
|
-
const statusProjects = byStatus[status] || [];
|
|
3241
|
-
if (statusProjects.length > 0) {
|
|
3242
|
-
const names = statusProjects.slice(0, 3).map(p => `${p.id} ${p.title}`).join(', ');
|
|
3243
|
-
const more = statusProjects.length > 3 ? ` (+${statusProjects.length - 3} more)` : '';
|
|
3244
|
-
activeListItems.push(`<li>${statusProjects.length} ${status}: ${escapeProjectHtml(names)}${more}</li>`);
|
|
3245
|
-
}
|
|
3246
|
-
}
|
|
3247
|
-
|
|
3248
|
-
return `
|
|
3249
|
-
<div class="status-summary">
|
|
3250
|
-
<div class="status-summary-header">
|
|
3251
|
-
<span>Status Summary</span>
|
|
3252
|
-
<button onclick="reloadProjectlist()" title="Reload">↻</button>
|
|
3253
|
-
</div>
|
|
3254
|
-
<div class="active-projects">
|
|
3255
|
-
<span class="active-count">Active: ${active.length} project${active.length !== 1 ? 's' : ''}</span>
|
|
3256
|
-
${activeListItems.length > 0 ? `<ul class="active-list">${activeListItems.join('')}</ul>` : ''}
|
|
3257
|
-
</div>
|
|
3258
|
-
<div class="completed">Completed: ${completed.length} integrated</div>
|
|
3259
|
-
</div>
|
|
3260
|
-
`;
|
|
3261
|
-
}
|
|
3262
|
-
|
|
3263
|
-
// Get the lifecycle stages in order
|
|
3264
|
-
const LIFECYCLE_STAGES = ['conceived', 'specified', 'planned', 'implementing', 'implemented', 'committed', 'integrated'];
|
|
3265
|
-
|
|
3266
|
-
// Abbreviated column headers
|
|
3267
|
-
const STAGE_HEADERS = {
|
|
3268
|
-
'conceived': "CONC'D",
|
|
3269
|
-
'specified': "SPEC'D",
|
|
3270
|
-
'planned': 'PLANNED',
|
|
3271
|
-
'implementing': 'IMPLING',
|
|
3272
|
-
'implemented': 'IMPLED',
|
|
3273
|
-
'committed': 'CMTD',
|
|
3274
|
-
'integrated': "INTGR'D"
|
|
3275
|
-
};
|
|
3276
|
-
|
|
3277
|
-
// Stage tooltips explaining purpose and exit criteria
|
|
3278
|
-
const STAGE_TOOLTIPS = {
|
|
3279
|
-
'conceived': "CONCEIVED: Idea has been captured.\nExit: Human approves the specification.",
|
|
3280
|
-
'specified': "SPECIFIED: Human approved the spec.\nExit: Architect creates an implementation plan.",
|
|
3281
|
-
'planned': "PLANNED: Implementation plan is ready.\nExit: Architect spawns a Builder.",
|
|
3282
|
-
'implementing': "IMPLEMENTING: Builder is working on the code.\nExit: Builder creates a PR.",
|
|
3283
|
-
'implemented': "IMPLEMENTED: PR is ready for review.\nExit: Builder merges after Architect review.",
|
|
3284
|
-
'committed': "COMMITTED: PR has been merged.\nExit: Human validates in production.",
|
|
3285
|
-
'integrated': "INTEGRATED: Validated in production.\nThis is the goal state."
|
|
3286
|
-
};
|
|
3287
|
-
|
|
3288
|
-
// Get stage index (for comparison)
|
|
3289
|
-
function getStageIndex(status) {
|
|
3290
|
-
return LIFECYCLE_STAGES.indexOf(status);
|
|
3291
|
-
}
|
|
3292
|
-
|
|
3293
|
-
// Get the label and link for a stage cell
|
|
3294
|
-
function getStageCellContent(project, stage) {
|
|
3295
|
-
switch (stage) {
|
|
3296
|
-
case 'specified':
|
|
3297
|
-
if (project.files && project.files.spec) {
|
|
3298
|
-
return { label: 'Spec', link: project.files.spec };
|
|
3299
|
-
}
|
|
3300
|
-
return { label: '', link: null };
|
|
3301
|
-
case 'planned':
|
|
3302
|
-
if (project.files && project.files.plan) {
|
|
3303
|
-
return { label: 'Plan', link: project.files.plan };
|
|
3304
|
-
}
|
|
3305
|
-
return { label: '', link: null };
|
|
3306
|
-
case 'implemented':
|
|
3307
|
-
if (project.files && project.files.review) {
|
|
3308
|
-
return { label: 'Revw', link: project.files.review };
|
|
3309
|
-
}
|
|
3310
|
-
return { label: '', link: null };
|
|
3311
|
-
case 'committed':
|
|
3312
|
-
// PR link from notes (format: "PR #N merged")
|
|
3313
|
-
if (project.notes) {
|
|
3314
|
-
const prMatch = project.notes.match(/PR\s*#?(\d+)/i);
|
|
3315
|
-
if (prMatch) {
|
|
3316
|
-
return { label: 'PR', link: `https://github.com/cluesmith/codev/pull/${prMatch[1]}`, external: true };
|
|
3317
|
-
}
|
|
3318
|
-
}
|
|
3319
|
-
return { label: '', link: null };
|
|
3320
|
-
default:
|
|
3321
|
-
return { label: '', link: null };
|
|
3322
|
-
}
|
|
3323
|
-
}
|
|
3324
|
-
|
|
3325
|
-
// Render a stage cell with appropriate styling
|
|
3326
|
-
function renderStageCell(project, stage) {
|
|
3327
|
-
const currentIndex = getStageIndex(project.status);
|
|
3328
|
-
const stageIndex = getStageIndex(stage);
|
|
3329
|
-
|
|
3330
|
-
let cellClass = 'stage-cell';
|
|
3331
|
-
let content = '';
|
|
3332
|
-
let ariaLabel = '';
|
|
3333
|
-
|
|
3334
|
-
if (stageIndex < currentIndex) {
|
|
3335
|
-
// Completed stage - green checkmark
|
|
3336
|
-
ariaLabel = `${stage}: completed`;
|
|
3337
|
-
|
|
3338
|
-
const cellContent = getStageCellContent(project, stage);
|
|
3339
|
-
if (cellContent.label && cellContent.link) {
|
|
3340
|
-
if (cellContent.external) {
|
|
3341
|
-
content = `<span class="checkmark">✓</span> <a href="${cellContent.link}" target="_blank" rel="noopener">${cellContent.label}</a>`;
|
|
3342
|
-
} else {
|
|
3343
|
-
content = `<span class="checkmark">✓</span> <a href="#" onclick="openProjectFile('${cellContent.link}'); return false;">${cellContent.label}</a>`;
|
|
3344
|
-
}
|
|
3345
|
-
} else {
|
|
3346
|
-
content = '<span class="checkmark">✓</span>';
|
|
3347
|
-
}
|
|
3348
|
-
} else if (stageIndex === currentIndex) {
|
|
3349
|
-
// Current stage - hollow orange circle (or confetti if recently integrated)
|
|
3350
|
-
if (stage === 'integrated' && isRecentlyIntegrated(project)) {
|
|
3351
|
-
ariaLabel = `${stage}: recently completed!`;
|
|
3352
|
-
content = '<span class="celebration">🎉</span>';
|
|
3353
|
-
} else {
|
|
3354
|
-
ariaLabel = `${stage}: in progress`;
|
|
3355
|
-
|
|
3356
|
-
const cellContent = getStageCellContent(project, stage);
|
|
3357
|
-
if (cellContent.label && cellContent.link) {
|
|
3358
|
-
if (cellContent.external) {
|
|
3359
|
-
content = `<span class="current-indicator"></span> <a href="${cellContent.link}" target="_blank" rel="noopener">${cellContent.label}</a>`;
|
|
3360
|
-
} else {
|
|
3361
|
-
content = `<span class="current-indicator"></span> <a href="#" onclick="openProjectFile('${cellContent.link}'); return false;">${cellContent.label}</a>`;
|
|
3362
|
-
}
|
|
3363
|
-
} else {
|
|
3364
|
-
content = '<span class="current-indicator"></span>';
|
|
3365
|
-
}
|
|
3366
|
-
}
|
|
3367
|
-
} else {
|
|
3368
|
-
// Future stage - empty
|
|
3369
|
-
ariaLabel = `${stage}: pending`;
|
|
3370
|
-
}
|
|
3371
|
-
|
|
3372
|
-
return `<td role="gridcell" class="${cellClass}" aria-label="${ariaLabel}">${content}</td>`;
|
|
3373
|
-
}
|
|
3374
|
-
|
|
3375
|
-
// Get URL for stage-specific artifact
|
|
3376
|
-
function getStageLinkUrl(project, stage) {
|
|
3377
|
-
if (!project.files) return null;
|
|
3378
|
-
|
|
3379
|
-
switch (stage) {
|
|
3380
|
-
case 'specified':
|
|
3381
|
-
return project.files.spec || null;
|
|
3382
|
-
case 'planned':
|
|
3383
|
-
return project.files.plan || null;
|
|
3384
|
-
case 'integrated':
|
|
3385
|
-
return project.files.review || null;
|
|
3386
|
-
default:
|
|
3387
|
-
return null;
|
|
3388
|
-
}
|
|
3389
|
-
}
|
|
3390
|
-
|
|
3391
|
-
// Open a project file in a new annotation tab
|
|
3392
|
-
async function openProjectFile(path) {
|
|
3393
|
-
try {
|
|
3394
|
-
const response = await fetch('/api/tabs/file', {
|
|
3395
|
-
method: 'POST',
|
|
3396
|
-
headers: { 'Content-Type': 'application/json' },
|
|
3397
|
-
body: JSON.stringify({ path })
|
|
3398
|
-
});
|
|
3399
|
-
|
|
3400
|
-
if (!response.ok) {
|
|
3401
|
-
throw new Error(await response.text());
|
|
3402
|
-
}
|
|
3403
|
-
|
|
3404
|
-
await refresh();
|
|
3405
|
-
showToast(`Opened ${path}`, 'success');
|
|
3406
|
-
} catch (err) {
|
|
3407
|
-
showToast('Failed to open file: ' + err.message, 'error');
|
|
3408
|
-
}
|
|
3409
|
-
}
|
|
3410
|
-
|
|
3411
|
-
// Render a single project row
|
|
3412
|
-
function renderProjectRow(project) {
|
|
3413
|
-
const isExpanded = expandedProjectId === project.id;
|
|
3414
|
-
|
|
3415
|
-
const row = `
|
|
3416
|
-
<tr class="status-${project.status}"
|
|
3417
|
-
role="row"
|
|
3418
|
-
tabindex="0"
|
|
3419
|
-
aria-expanded="${isExpanded}"
|
|
3420
|
-
onkeydown="handleProjectRowKeydown(event, '${project.id}')">
|
|
3421
|
-
<td role="gridcell">
|
|
3422
|
-
<div class="project-cell clickable" onclick="toggleProjectDetails('${project.id}'); event.stopPropagation();">
|
|
3423
|
-
<span class="project-id">${escapeProjectHtml(project.id)}</span>
|
|
3424
|
-
<span class="project-title" title="${escapeProjectHtml(project.title)}">${escapeProjectHtml(project.title)}</span>
|
|
3425
|
-
</div>
|
|
3426
|
-
</td>
|
|
3427
|
-
${LIFECYCLE_STAGES.map(stage => renderStageCell(project, stage)).join('')}
|
|
3428
|
-
</tr>
|
|
3429
|
-
`;
|
|
3430
|
-
|
|
3431
|
-
if (isExpanded) {
|
|
3432
|
-
return row + renderProjectDetailsRow(project);
|
|
3433
|
-
}
|
|
3434
|
-
return row;
|
|
3435
|
-
}
|
|
3436
|
-
|
|
3437
|
-
// Render the details row when expanded
|
|
3438
|
-
function renderProjectDetailsRow(project) {
|
|
3439
|
-
const links = [];
|
|
3440
|
-
if (project.files && project.files.review) {
|
|
3441
|
-
links.push(`<a href="#" onclick="openProjectFile('${project.files.review}'); return false;">Review</a>`);
|
|
3442
|
-
}
|
|
3443
|
-
|
|
3444
|
-
const dependencies = project.dependencies && project.dependencies.length > 0
|
|
3445
|
-
? `<div class="project-dependencies">Dependencies: ${project.dependencies.map(d => escapeProjectHtml(d)).join(', ')}</div>`
|
|
3446
|
-
: '';
|
|
3447
|
-
|
|
3448
|
-
// Render TICKs if present
|
|
3449
|
-
const ticks = project.ticks && project.ticks.length > 0
|
|
3450
|
-
? `<div class="project-ticks">TICKs: ${project.ticks.map(t => `<span class="tick-badge">TICK-${escapeProjectHtml(t)}</span>`).join(' ')}</div>`
|
|
3451
|
-
: '';
|
|
3452
|
-
|
|
3453
|
-
return `
|
|
3454
|
-
<tr class="project-details-row" role="row">
|
|
3455
|
-
<td colspan="8">
|
|
3456
|
-
<div class="project-details-content">
|
|
3457
|
-
<h3>${escapeProjectHtml(project.title)}</h3>
|
|
3458
|
-
${project.summary ? `<p>${escapeProjectHtml(project.summary)}</p>` : ''}
|
|
3459
|
-
${project.notes ? `<p class="notes">${escapeProjectHtml(project.notes)}</p>` : ''}
|
|
3460
|
-
${ticks}
|
|
3461
|
-
${links.length > 0 ? `<div class="project-details-links">${links.join('')}</div>` : ''}
|
|
3462
|
-
${dependencies}
|
|
3463
|
-
</div>
|
|
3464
|
-
</td>
|
|
3465
|
-
</tr>
|
|
3466
|
-
`;
|
|
3467
|
-
}
|
|
3468
|
-
|
|
3469
|
-
// Handle keyboard navigation on project rows
|
|
3470
|
-
function handleProjectRowKeydown(event, projectId) {
|
|
3471
|
-
if (event.key === 'Enter' || event.key === ' ') {
|
|
3472
|
-
event.preventDefault();
|
|
3473
|
-
toggleProjectDetails(projectId);
|
|
3474
|
-
} else if (event.key === 'ArrowDown') {
|
|
3475
|
-
event.preventDefault();
|
|
3476
|
-
const currentRow = event.target.closest('tr');
|
|
3477
|
-
let nextRow = currentRow.nextElementSibling;
|
|
3478
|
-
// Skip details rows
|
|
3479
|
-
while (nextRow && nextRow.classList.contains('project-details-row')) {
|
|
3480
|
-
nextRow = nextRow.nextElementSibling;
|
|
3481
|
-
}
|
|
3482
|
-
if (nextRow) nextRow.focus();
|
|
3483
|
-
} else if (event.key === 'ArrowUp') {
|
|
3484
|
-
event.preventDefault();
|
|
3485
|
-
const currentRow = event.target.closest('tr');
|
|
3486
|
-
let prevRow = currentRow.previousElementSibling;
|
|
3487
|
-
// Skip details rows
|
|
3488
|
-
while (prevRow && prevRow.classList.contains('project-details-row')) {
|
|
3489
|
-
prevRow = prevRow.previousElementSibling;
|
|
3490
|
-
}
|
|
3491
|
-
if (prevRow && prevRow.getAttribute('tabindex') === '0') prevRow.focus();
|
|
3492
|
-
}
|
|
3493
|
-
}
|
|
3494
|
-
|
|
3495
|
-
// Toggle project details expansion
|
|
3496
|
-
function toggleProjectDetails(projectId) {
|
|
3497
|
-
if (expandedProjectId === projectId) {
|
|
3498
|
-
expandedProjectId = null;
|
|
3499
|
-
} else {
|
|
3500
|
-
expandedProjectId = projectId;
|
|
3501
|
-
}
|
|
3502
|
-
// Re-render the projects tab to update expansion state
|
|
3503
|
-
renderProjectsTabContent();
|
|
3504
|
-
}
|
|
3505
|
-
|
|
3506
|
-
// Render a table for a list of projects
|
|
3507
|
-
function renderProjectTable(projectList) {
|
|
3508
|
-
if (projectList.length === 0) {
|
|
3509
|
-
return '<p style="color: var(--text-muted); text-align: center; padding: 20px;">No projects</p>';
|
|
3510
|
-
}
|
|
3511
|
-
|
|
3512
|
-
return `
|
|
3513
|
-
<table class="kanban-grid" role="grid" aria-label="Project status grid">
|
|
3514
|
-
<thead>
|
|
3515
|
-
<tr role="row">
|
|
3516
|
-
<th role="columnheader">Project</th>
|
|
3517
|
-
${LIFECYCLE_STAGES.map(stage => `<th role="columnheader" title="${STAGE_TOOLTIPS[stage]}">${STAGE_HEADERS[stage]}</th>`).join('')}
|
|
3518
|
-
</tr>
|
|
3519
|
-
</thead>
|
|
3520
|
-
<tbody>
|
|
3521
|
-
${projectList.map(p => renderProjectRow(p)).join('')}
|
|
3522
|
-
</tbody>
|
|
3523
|
-
</table>
|
|
3524
|
-
`;
|
|
3525
|
-
}
|
|
3526
|
-
|
|
3527
|
-
// Check if a project was integrated in the last 24 hours
|
|
3528
|
-
function isRecentlyIntegrated(project) {
|
|
3529
|
-
if (project.status !== 'integrated') return false;
|
|
3530
|
-
|
|
3531
|
-
// Look in timestamps.integrated_at (new format)
|
|
3532
|
-
const integratedAt = project.timestamps?.integrated_at;
|
|
3533
|
-
if (!integratedAt) return false;
|
|
3534
|
-
|
|
3535
|
-
const integratedDate = new Date(integratedAt);
|
|
3536
|
-
if (isNaN(integratedDate.getTime())) return false;
|
|
3537
|
-
|
|
3538
|
-
const now = new Date();
|
|
3539
|
-
const hoursDiff = (now - integratedDate) / (1000 * 60 * 60);
|
|
3540
|
-
|
|
3541
|
-
return hoursDiff <= 24;
|
|
3542
|
-
}
|
|
3543
|
-
|
|
3544
|
-
// Render the Kanban grid with Active/Inactive sections
|
|
3545
|
-
function renderKanbanGrid(projects) {
|
|
3546
|
-
// Separate active (conceived through committed) from inactive (integrated)
|
|
3547
|
-
const activeStatuses = ['conceived', 'specified', 'planned', 'implementing', 'implemented', 'committed'];
|
|
3548
|
-
|
|
3549
|
-
// Status order for sorting (higher index = further along)
|
|
3550
|
-
const statusOrder = {
|
|
3551
|
-
'conceived': 0,
|
|
3552
|
-
'specified': 1,
|
|
3553
|
-
'planned': 2,
|
|
3554
|
-
'implementing': 3,
|
|
3555
|
-
'implemented': 4,
|
|
3556
|
-
'committed': 5,
|
|
3557
|
-
'integrated': 6
|
|
3558
|
-
};
|
|
3559
|
-
|
|
3560
|
-
// Include recently integrated projects in Active section
|
|
3561
|
-
const activeProjects = projects.filter(p =>
|
|
3562
|
-
activeStatuses.includes(p.status) || isRecentlyIntegrated(p)
|
|
3563
|
-
);
|
|
3564
|
-
|
|
3565
|
-
// Sort active projects by completion (most complete first)
|
|
3566
|
-
activeProjects.sort((a, b) => {
|
|
3567
|
-
const orderA = statusOrder[a.status] || 0;
|
|
3568
|
-
const orderB = statusOrder[b.status] || 0;
|
|
3569
|
-
// Higher status first (descending), then by ID (ascending) for tie-breaker
|
|
3570
|
-
if (orderB !== orderA) return orderB - orderA;
|
|
3571
|
-
return a.id.localeCompare(b.id);
|
|
3572
|
-
});
|
|
3573
|
-
|
|
3574
|
-
const inactiveProjects = projects.filter(p =>
|
|
3575
|
-
p.status === 'integrated' && !isRecentlyIntegrated(p)
|
|
3576
|
-
);
|
|
3577
|
-
|
|
3578
|
-
let html = '';
|
|
3579
|
-
|
|
3580
|
-
// Active section - expanded by default
|
|
3581
|
-
if (activeProjects.length > 0 || inactiveProjects.length === 0) {
|
|
3582
|
-
html += `
|
|
3583
|
-
<details class="project-section" open>
|
|
3584
|
-
<summary>Active <span class="section-count">(${activeProjects.length})</span></summary>
|
|
3585
|
-
${renderProjectTable(activeProjects)}
|
|
3586
|
-
</details>
|
|
3587
|
-
`;
|
|
3588
|
-
}
|
|
3589
|
-
|
|
3590
|
-
// Inactive section - collapsed by default
|
|
3591
|
-
if (inactiveProjects.length > 0) {
|
|
3592
|
-
html += `
|
|
3593
|
-
<details class="project-section">
|
|
3594
|
-
<summary>Completed <span class="section-count">(${inactiveProjects.length})</span></summary>
|
|
3595
|
-
${renderProjectTable(inactiveProjects)}
|
|
3596
|
-
</details>
|
|
3597
|
-
`;
|
|
3598
|
-
}
|
|
3599
|
-
|
|
3600
|
-
return html;
|
|
3601
|
-
}
|
|
3602
|
-
|
|
3603
|
-
// Render the terminal projects section (abandoned, on-hold)
|
|
3604
|
-
function renderTerminalProjects(projects) {
|
|
3605
|
-
const terminal = projects.filter(p => ['abandoned', 'on-hold'].includes(p.status));
|
|
3606
|
-
|
|
3607
|
-
if (terminal.length === 0) return '';
|
|
3608
|
-
|
|
3609
|
-
const items = terminal.map(p => {
|
|
3610
|
-
const className = p.status === 'abandoned' ? 'project-abandoned' : 'project-on-hold';
|
|
3611
|
-
const statusText = p.status === 'on-hold' ? ' (on-hold)' : '';
|
|
3612
|
-
return `
|
|
3613
|
-
<li>
|
|
3614
|
-
<span class="${className}">
|
|
3615
|
-
<span class="project-id">${escapeProjectHtml(p.id)}</span>
|
|
3616
|
-
${escapeProjectHtml(p.title)}${statusText}
|
|
3617
|
-
</span>
|
|
3618
|
-
</li>
|
|
3619
|
-
`;
|
|
3620
|
-
}).join('');
|
|
3621
|
-
|
|
3622
|
-
return `
|
|
3623
|
-
<details class="terminal-projects">
|
|
3624
|
-
<summary>Terminal Projects (${terminal.length})</summary>
|
|
3625
|
-
<ul>${items}</ul>
|
|
3626
|
-
</details>
|
|
3627
|
-
`;
|
|
3628
|
-
}
|
|
3629
|
-
|
|
3630
|
-
// ========================================
|
|
3631
|
-
// Files Tab Functions (Spec 0055)
|
|
3632
|
-
// ========================================
|
|
3633
|
-
|
|
3634
|
-
// Load the file tree from the API
|
|
3635
|
-
async function loadFilesTree() {
|
|
3636
|
-
try {
|
|
3637
|
-
const response = await fetch('/api/files');
|
|
3638
|
-
if (!response.ok) {
|
|
3639
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
3640
|
-
}
|
|
3641
|
-
filesTreeData = await response.json();
|
|
3642
|
-
filesTreeError = null;
|
|
3643
|
-
filesTreeLoaded = true;
|
|
3644
|
-
// Flatten tree for search (Spec 0058)
|
|
3645
|
-
filesTreeFlat = flattenFilesTree(filesTreeData);
|
|
3646
|
-
} catch (err) {
|
|
3647
|
-
console.error('Failed to load files tree:', err);
|
|
3648
|
-
filesTreeError = 'Could not load file tree: ' + err.message;
|
|
3649
|
-
filesTreeData = [];
|
|
3650
|
-
filesTreeFlat = [];
|
|
3651
|
-
}
|
|
3652
|
-
}
|
|
3653
|
-
|
|
3654
|
-
// Flatten the file tree into a searchable array (Spec 0058)
|
|
3655
|
-
function flattenFilesTree(nodes, result = []) {
|
|
3656
|
-
for (const node of nodes) {
|
|
3657
|
-
if (node.type === 'file') {
|
|
3658
|
-
result.push({ name: node.name, path: node.path });
|
|
3659
|
-
} else if (node.children) {
|
|
3660
|
-
flattenFilesTree(node.children, result);
|
|
3661
|
-
}
|
|
3662
|
-
}
|
|
3663
|
-
return result;
|
|
3664
|
-
}
|
|
3665
|
-
|
|
3666
|
-
// Search files with relevance sorting (Spec 0058)
|
|
3667
|
-
function searchFiles(query) {
|
|
3668
|
-
if (!query) return [];
|
|
3669
|
-
const q = query.toLowerCase();
|
|
3670
|
-
|
|
3671
|
-
const matches = filesTreeFlat.filter(f =>
|
|
3672
|
-
f.path.toLowerCase().includes(q)
|
|
3673
|
-
);
|
|
3674
|
-
|
|
3675
|
-
// Sort by relevance: exact filename > filename prefix > filename contains > path
|
|
3676
|
-
matches.sort((a, b) => {
|
|
3677
|
-
const aName = a.name.toLowerCase();
|
|
3678
|
-
const bName = b.name.toLowerCase();
|
|
3679
|
-
const aPath = a.path.toLowerCase();
|
|
3680
|
-
const bPath = b.path.toLowerCase();
|
|
3681
|
-
|
|
3682
|
-
// Exact filename match first
|
|
3683
|
-
if (aName === q && bName !== q) return -1;
|
|
3684
|
-
if (bName === q && aName !== q) return 1;
|
|
3685
|
-
|
|
3686
|
-
// Filename starts with query
|
|
3687
|
-
if (aName.startsWith(q) && !bName.startsWith(q)) return -1;
|
|
3688
|
-
if (bName.startsWith(q) && !aName.startsWith(q)) return 1;
|
|
3689
|
-
|
|
3690
|
-
// Filename contains query
|
|
3691
|
-
if (aName.includes(q) && !bName.includes(q)) return -1;
|
|
3692
|
-
if (bName.includes(q) && !aName.includes(q)) return 1;
|
|
3693
|
-
|
|
3694
|
-
// Alphabetical by path
|
|
3695
|
-
return aPath.localeCompare(bPath);
|
|
3696
|
-
});
|
|
3697
|
-
|
|
3698
|
-
return matches.slice(0, 15);
|
|
3699
|
-
}
|
|
3700
|
-
|
|
3701
|
-
// Escape a string for use inside a JavaScript string literal in onclick handlers
|
|
3702
|
-
// This handles quotes, backslashes, and other special characters
|
|
3703
|
-
function escapeJsString(str) {
|
|
3704
|
-
return str
|
|
3705
|
-
.replace(/\\/g, '\\\\')
|
|
3706
|
-
.replace(/'/g, "\\'")
|
|
3707
|
-
.replace(/"/g, '\\"')
|
|
3708
|
-
.replace(/\n/g, '\\n')
|
|
3709
|
-
.replace(/\r/g, '\\r');
|
|
3710
|
-
}
|
|
3711
|
-
|
|
3712
|
-
// Render tree nodes recursively
|
|
3713
|
-
function renderTreeNodes(nodes, depth) {
|
|
3714
|
-
if (!nodes || nodes.length === 0) return '';
|
|
3715
|
-
|
|
3716
|
-
return nodes.map(node => {
|
|
3717
|
-
const indent = depth * 16;
|
|
3718
|
-
const isExpanded = filesTreeExpanded.has(node.path);
|
|
3719
|
-
// Use escapeJsString for onclick handlers (handles quotes correctly)
|
|
3720
|
-
// Use escapeHtml for data attributes and display text (handles XSS)
|
|
3721
|
-
const jsPath = escapeJsString(node.path);
|
|
3722
|
-
|
|
3723
|
-
if (node.type === 'dir') {
|
|
3724
|
-
const icon = isExpanded ? '▼' : '▶';
|
|
3725
|
-
const childrenHtml = node.children && node.children.length > 0
|
|
3726
|
-
? `<div class="tree-children ${isExpanded ? '' : 'collapsed'}" data-path="${escapeHtml(node.path)}">${renderTreeNodes(node.children, depth + 1)}</div>`
|
|
3727
|
-
: '';
|
|
3728
|
-
|
|
3729
|
-
return `
|
|
3730
|
-
<div class="tree-item" data-type="dir" data-path="${escapeHtml(node.path)}" style="padding-left: ${indent + 8}px;" onclick="toggleFolder('${jsPath}')">
|
|
3731
|
-
<span class="tree-item-icon folder-toggle">${icon}</span>
|
|
3732
|
-
<span class="tree-item-name">${escapeHtml(node.name)}</span>
|
|
3733
|
-
</div>
|
|
3734
|
-
${childrenHtml}
|
|
3735
|
-
`;
|
|
3736
|
-
} else {
|
|
3737
|
-
return `
|
|
3738
|
-
<div class="tree-item" data-type="file" data-path="${escapeHtml(node.path)}" style="padding-left: ${indent + 8}px;" onclick="openFileFromTree('${jsPath}')">
|
|
3739
|
-
<span class="tree-item-icon">${getFileIcon(node.name)}</span>
|
|
3740
|
-
<span class="tree-item-name">${escapeHtml(node.name)}</span>
|
|
3741
|
-
</div>
|
|
3742
|
-
`;
|
|
3743
|
-
}
|
|
3744
|
-
}).join('');
|
|
3745
|
-
}
|
|
3746
|
-
|
|
3747
|
-
// Get file icon based on extension
|
|
3748
|
-
function getFileIcon(filename) {
|
|
3749
|
-
const ext = filename.split('.').pop().toLowerCase();
|
|
3750
|
-
const iconMap = {
|
|
3751
|
-
'js': '📜',
|
|
3752
|
-
'ts': '📜',
|
|
3753
|
-
'jsx': '⚛️',
|
|
3754
|
-
'tsx': '⚛️',
|
|
3755
|
-
'json': '{}',
|
|
3756
|
-
'md': '📝',
|
|
3757
|
-
'html': '🌐',
|
|
3758
|
-
'css': '🎨',
|
|
3759
|
-
'py': '🐍',
|
|
3760
|
-
'sh': '⚙️',
|
|
3761
|
-
'bash': '⚙️',
|
|
3762
|
-
'yml': '⚙️',
|
|
3763
|
-
'yaml': '⚙️',
|
|
3764
|
-
'png': '🖼️',
|
|
3765
|
-
'jpg': '🖼️',
|
|
3766
|
-
'jpeg': '🖼️',
|
|
3767
|
-
'gif': '🖼️',
|
|
3768
|
-
'svg': '🖼️',
|
|
3769
|
-
};
|
|
3770
|
-
return iconMap[ext] || '📄';
|
|
3771
|
-
}
|
|
3772
|
-
|
|
3773
|
-
// Toggle folder expanded/collapsed state
|
|
3774
|
-
function toggleFolder(path) {
|
|
3775
|
-
if (filesTreeExpanded.has(path)) {
|
|
3776
|
-
filesTreeExpanded.delete(path);
|
|
3777
|
-
} else {
|
|
3778
|
-
filesTreeExpanded.add(path);
|
|
3779
|
-
}
|
|
3780
|
-
rerenderFilesBrowser();
|
|
3781
|
-
}
|
|
3782
|
-
|
|
3783
|
-
// Re-render file browser in current context (dashboard or files tab)
|
|
3784
|
-
function rerenderFilesBrowser() {
|
|
3785
|
-
if (activeTabId === 'dashboard') {
|
|
3786
|
-
// Re-render just the files content in dashboard
|
|
3787
|
-
const filesContentEl = document.getElementById('dashboard-files-content');
|
|
3788
|
-
if (filesContentEl) {
|
|
3789
|
-
filesContentEl.innerHTML = filesSearchQuery
|
|
3790
|
-
? renderFilesSearchResults()
|
|
3791
|
-
: renderDashboardFilesBrowserWithWrapper();
|
|
3792
|
-
}
|
|
3793
|
-
}
|
|
3794
|
-
}
|
|
3795
|
-
|
|
3796
|
-
// Wrapper for file browser that includes the list element ID (Spec 0058)
|
|
3797
|
-
function renderDashboardFilesBrowserWithWrapper() {
|
|
3798
|
-
return `<div class="dashboard-files-list" id="dashboard-files-list">${renderDashboardFilesBrowser()}</div>`;
|
|
3799
|
-
}
|
|
3800
|
-
|
|
3801
|
-
// ========================================
|
|
3802
|
-
// File Search Functions (Spec 0058)
|
|
3803
|
-
// ========================================
|
|
3804
|
-
|
|
3805
|
-
// Debounced search input handler for Files column
|
|
3806
|
-
function onFilesSearchInput(value) {
|
|
3807
|
-
clearTimeout(filesSearchDebounceTimer);
|
|
3808
|
-
filesSearchDebounceTimer = setTimeout(() => {
|
|
3809
|
-
filesSearchQuery = value;
|
|
3810
|
-
filesSearchResults = searchFiles(value);
|
|
3811
|
-
filesSearchIndex = 0;
|
|
3812
|
-
rerenderFilesSearch();
|
|
3813
|
-
}, 100);
|
|
3814
|
-
}
|
|
3815
|
-
|
|
3816
|
-
// Clear files search and restore tree view
|
|
3817
|
-
function clearFilesSearch() {
|
|
3818
|
-
filesSearchQuery = '';
|
|
3819
|
-
filesSearchResults = [];
|
|
3820
|
-
filesSearchIndex = 0;
|
|
3821
|
-
const input = document.getElementById('files-search-input');
|
|
3822
|
-
if (input) {
|
|
3823
|
-
input.value = '';
|
|
3824
|
-
}
|
|
3825
|
-
rerenderFilesSearch();
|
|
3826
|
-
}
|
|
3827
|
-
|
|
3828
|
-
// Re-render the files search area (results or tree)
|
|
3829
|
-
function rerenderFilesSearch() {
|
|
3830
|
-
const filesContentEl = document.getElementById('dashboard-files-content');
|
|
3831
|
-
if (filesContentEl) {
|
|
3832
|
-
filesContentEl.innerHTML = filesSearchQuery
|
|
3833
|
-
? renderFilesSearchResults()
|
|
3834
|
-
: renderDashboardFilesBrowserWithWrapper();
|
|
3835
|
-
}
|
|
3836
|
-
// Update clear button visibility
|
|
3837
|
-
const clearBtn = document.querySelector('.files-search-clear');
|
|
3838
|
-
if (clearBtn) {
|
|
3839
|
-
clearBtn.classList.toggle('hidden', !filesSearchQuery);
|
|
3840
|
-
}
|
|
3841
|
-
}
|
|
3842
|
-
|
|
3843
|
-
// Render search results for Files column
|
|
3844
|
-
function renderFilesSearchResults() {
|
|
3845
|
-
if (!filesSearchResults.length) {
|
|
3846
|
-
return '<div class="dashboard-empty-state">No files found</div>';
|
|
3847
|
-
}
|
|
3848
|
-
|
|
3849
|
-
return `<div class="files-search-results">${filesSearchResults.map((file, index) =>
|
|
3850
|
-
renderSearchResult(file, index, index === filesSearchIndex, filesSearchQuery, 'files')
|
|
3851
|
-
).join('')}</div>`;
|
|
3852
|
-
}
|
|
3853
|
-
|
|
3854
|
-
// Highlight matching text in search results
|
|
3855
|
-
function highlightMatch(text, query) {
|
|
3856
|
-
if (!query) return escapeHtml(text);
|
|
3857
|
-
const q = query.toLowerCase();
|
|
3858
|
-
const t = text.toLowerCase();
|
|
3859
|
-
const idx = t.indexOf(q);
|
|
3860
|
-
if (idx === -1) return escapeHtml(text);
|
|
3861
|
-
|
|
3862
|
-
return escapeHtml(text.substring(0, idx)) +
|
|
3863
|
-
'<span class="files-search-highlight">' + escapeHtml(text.substring(idx, idx + query.length)) + '</span>' +
|
|
3864
|
-
escapeHtml(text.substring(idx + query.length));
|
|
3865
|
-
}
|
|
3866
|
-
|
|
3867
|
-
// Render a single search result (shared by Files column and palette)
|
|
3868
|
-
function renderSearchResult(file, index, isSelected, query, context) {
|
|
3869
|
-
const classPrefix = context === 'palette' ? 'file-palette' : 'files-search';
|
|
3870
|
-
const jsPath = escapeJsString(file.path);
|
|
3871
|
-
|
|
3872
|
-
return `
|
|
3873
|
-
<div class="${classPrefix}-result ${isSelected ? 'selected' : ''}"
|
|
3874
|
-
data-index="${index}"
|
|
3875
|
-
onclick="openFileFromSearch('${jsPath}', '${context}')">
|
|
3876
|
-
<div class="${classPrefix}-result-name">${highlightMatch(file.name, query)}</div>
|
|
3877
|
-
<div class="${classPrefix}-result-path">${highlightMatch(file.path, query)}</div>
|
|
3878
|
-
</div>
|
|
3879
|
-
`;
|
|
3880
|
-
}
|
|
3881
|
-
|
|
3882
|
-
// Keyboard handler for Files search input
|
|
3883
|
-
function onFilesSearchKeydown(event) {
|
|
3884
|
-
if (!filesSearchResults.length) {
|
|
3885
|
-
if (event.key === 'Escape') {
|
|
3886
|
-
clearFilesSearch();
|
|
3887
|
-
event.target.blur();
|
|
3888
|
-
}
|
|
3889
|
-
return;
|
|
3890
|
-
}
|
|
3891
|
-
|
|
3892
|
-
if (event.key === 'ArrowDown') {
|
|
3893
|
-
event.preventDefault();
|
|
3894
|
-
filesSearchIndex = Math.min(filesSearchIndex + 1, filesSearchResults.length - 1);
|
|
3895
|
-
rerenderFilesSearch();
|
|
3896
|
-
scrollSelectedIntoView('files');
|
|
3897
|
-
} else if (event.key === 'ArrowUp') {
|
|
3898
|
-
event.preventDefault();
|
|
3899
|
-
filesSearchIndex = Math.max(filesSearchIndex - 1, 0);
|
|
3900
|
-
rerenderFilesSearch();
|
|
3901
|
-
scrollSelectedIntoView('files');
|
|
3902
|
-
} else if (event.key === 'Enter') {
|
|
3903
|
-
event.preventDefault();
|
|
3904
|
-
if (filesSearchResults[filesSearchIndex]) {
|
|
3905
|
-
openFileFromSearch(filesSearchResults[filesSearchIndex].path, 'files');
|
|
3906
|
-
}
|
|
3907
|
-
} else if (event.key === 'Escape') {
|
|
3908
|
-
clearFilesSearch();
|
|
3909
|
-
event.target.blur();
|
|
3910
|
-
}
|
|
3911
|
-
}
|
|
3912
|
-
|
|
3913
|
-
// Scroll selected result into view
|
|
3914
|
-
function scrollSelectedIntoView(context) {
|
|
3915
|
-
const selector = context === 'palette'
|
|
3916
|
-
? '.file-palette-result.selected'
|
|
3917
|
-
: '.files-search-result.selected';
|
|
3918
|
-
const selected = document.querySelector(selector);
|
|
3919
|
-
if (selected) {
|
|
3920
|
-
selected.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
3921
|
-
}
|
|
3922
|
-
}
|
|
3923
|
-
|
|
3924
|
-
// Open file from search result (shared by Files column and palette)
|
|
3925
|
-
function openFileFromSearch(filePath, context) {
|
|
3926
|
-
// Check if file is already open
|
|
3927
|
-
const existingTab = tabs.find(t => t.type === 'file' && t.path === filePath);
|
|
3928
|
-
if (existingTab) {
|
|
3929
|
-
selectTab(existingTab.id);
|
|
3930
|
-
refreshFileTab(existingTab.id); // Refresh content in case file changed
|
|
3931
|
-
} else {
|
|
3932
|
-
openFileFromTree(filePath);
|
|
3933
|
-
}
|
|
3934
|
-
|
|
3935
|
-
// Clear search / close palette
|
|
3936
|
-
if (context === 'palette') {
|
|
3937
|
-
closePalette();
|
|
3938
|
-
} else {
|
|
3939
|
-
clearFilesSearch();
|
|
3940
|
-
}
|
|
3941
|
-
}
|
|
3942
|
-
|
|
3943
|
-
// ========================================
|
|
3944
|
-
// Cmd+P Palette Functions (Spec 0058)
|
|
3945
|
-
// ========================================
|
|
3946
|
-
|
|
3947
|
-
// Open the file search palette
|
|
3948
|
-
function openPalette() {
|
|
3949
|
-
paletteOpen = true;
|
|
3950
|
-
paletteQuery = '';
|
|
3951
|
-
paletteResults = [];
|
|
3952
|
-
paletteIndex = 0;
|
|
3953
|
-
document.getElementById('file-palette').classList.remove('hidden');
|
|
3954
|
-
const input = document.getElementById('palette-input');
|
|
3955
|
-
input.value = '';
|
|
3956
|
-
input.focus();
|
|
3957
|
-
rerenderPaletteResults();
|
|
3958
|
-
}
|
|
3959
|
-
|
|
3960
|
-
// Close the file search palette
|
|
3961
|
-
function closePalette() {
|
|
3962
|
-
paletteOpen = false;
|
|
3963
|
-
paletteQuery = '';
|
|
3964
|
-
paletteResults = [];
|
|
3965
|
-
paletteIndex = 0;
|
|
3966
|
-
document.getElementById('file-palette').classList.add('hidden');
|
|
3967
|
-
}
|
|
3968
|
-
|
|
3969
|
-
// Debounced palette input handler
|
|
3970
|
-
function onPaletteInput(value) {
|
|
3971
|
-
clearTimeout(paletteDebounceTimer);
|
|
3972
|
-
paletteDebounceTimer = setTimeout(() => {
|
|
3973
|
-
paletteQuery = value;
|
|
3974
|
-
paletteResults = searchFiles(value);
|
|
3975
|
-
paletteIndex = 0;
|
|
3976
|
-
rerenderPaletteResults();
|
|
3977
|
-
}, 100);
|
|
3978
|
-
}
|
|
3979
|
-
|
|
3980
|
-
// Re-render palette results
|
|
3981
|
-
function rerenderPaletteResults() {
|
|
3982
|
-
const resultsEl = document.getElementById('palette-results');
|
|
3983
|
-
if (!resultsEl) return;
|
|
3984
|
-
|
|
3985
|
-
if (!paletteQuery) {
|
|
3986
|
-
resultsEl.innerHTML = '<div class="file-palette-empty">Type to search files...</div>';
|
|
3987
|
-
return;
|
|
3988
|
-
}
|
|
3989
|
-
|
|
3990
|
-
if (!paletteResults.length) {
|
|
3991
|
-
resultsEl.innerHTML = '<div class="file-palette-empty">No files found</div>';
|
|
3992
|
-
return;
|
|
3993
|
-
}
|
|
3994
|
-
|
|
3995
|
-
resultsEl.innerHTML = paletteResults.map((file, index) =>
|
|
3996
|
-
renderSearchResult(file, index, index === paletteIndex, paletteQuery, 'palette')
|
|
3997
|
-
).join('');
|
|
3998
|
-
}
|
|
3999
|
-
|
|
4000
|
-
// Keyboard handler for palette input
|
|
4001
|
-
function onPaletteKeydown(event) {
|
|
4002
|
-
if (event.key === 'Escape') {
|
|
4003
|
-
closePalette();
|
|
4004
|
-
return;
|
|
4005
|
-
}
|
|
4006
|
-
|
|
4007
|
-
if (!paletteResults.length) return;
|
|
4008
|
-
|
|
4009
|
-
if (event.key === 'ArrowDown') {
|
|
4010
|
-
event.preventDefault();
|
|
4011
|
-
paletteIndex = Math.min(paletteIndex + 1, paletteResults.length - 1);
|
|
4012
|
-
rerenderPaletteResults();
|
|
4013
|
-
scrollSelectedIntoView('palette');
|
|
4014
|
-
} else if (event.key === 'ArrowUp') {
|
|
4015
|
-
event.preventDefault();
|
|
4016
|
-
paletteIndex = Math.max(paletteIndex - 1, 0);
|
|
4017
|
-
rerenderPaletteResults();
|
|
4018
|
-
scrollSelectedIntoView('palette');
|
|
4019
|
-
} else if (event.key === 'Enter') {
|
|
4020
|
-
event.preventDefault();
|
|
4021
|
-
if (paletteResults[paletteIndex]) {
|
|
4022
|
-
openFileFromSearch(paletteResults[paletteIndex].path, 'palette');
|
|
4023
|
-
}
|
|
4024
|
-
}
|
|
4025
|
-
}
|
|
4026
|
-
|
|
4027
|
-
// Global keyboard handler for Cmd+P / Ctrl+P and Escape
|
|
4028
|
-
document.addEventListener('keydown', (e) => {
|
|
4029
|
-
// Global Escape handler for palette (works even if input loses focus)
|
|
4030
|
-
if (e.key === 'Escape' && paletteOpen) {
|
|
4031
|
-
closePalette();
|
|
4032
|
-
return;
|
|
4033
|
-
}
|
|
4034
|
-
|
|
4035
|
-
// Cmd+P (macOS) or Ctrl+P (Windows/Linux)
|
|
4036
|
-
if ((e.metaKey || e.ctrlKey) && e.key === 'p') {
|
|
4037
|
-
// Skip if user is typing in an input/textarea (except our search inputs)
|
|
4038
|
-
const active = document.activeElement;
|
|
4039
|
-
const isOurInput = active?.id === 'palette-input' || active?.id === 'files-search-input';
|
|
4040
|
-
const isEditable = active?.tagName === 'INPUT' || active?.tagName === 'TEXTAREA' || active?.isContentEditable;
|
|
4041
|
-
|
|
4042
|
-
if (!isOurInput && isEditable) return; // Let native behavior happen
|
|
4043
|
-
|
|
4044
|
-
e.preventDefault(); // Prevent browser Print dialog
|
|
4045
|
-
if (paletteOpen) {
|
|
4046
|
-
closePalette();
|
|
4047
|
-
} else {
|
|
4048
|
-
openPalette();
|
|
4049
|
-
}
|
|
4050
|
-
}
|
|
4051
|
-
});
|
|
4052
|
-
|
|
4053
|
-
// Collapse all folders
|
|
4054
|
-
function collapseAllFolders() {
|
|
4055
|
-
filesTreeExpanded.clear();
|
|
4056
|
-
rerenderFilesBrowser();
|
|
4057
|
-
}
|
|
4058
|
-
|
|
4059
|
-
// Expand all folders
|
|
4060
|
-
function expandAllFolders() {
|
|
4061
|
-
function collectPaths(nodes) {
|
|
4062
|
-
for (const node of nodes) {
|
|
4063
|
-
if (node.type === 'dir') {
|
|
4064
|
-
filesTreeExpanded.add(node.path);
|
|
4065
|
-
if (node.children) {
|
|
4066
|
-
collectPaths(node.children);
|
|
4067
|
-
}
|
|
4068
|
-
}
|
|
4069
|
-
}
|
|
4070
|
-
}
|
|
4071
|
-
collectPaths(filesTreeData);
|
|
4072
|
-
rerenderFilesBrowser();
|
|
4073
|
-
}
|
|
4074
|
-
|
|
4075
|
-
// Refresh files tree
|
|
4076
|
-
async function refreshFilesTree() {
|
|
4077
|
-
await loadFilesTree();
|
|
4078
|
-
rerenderFilesBrowser();
|
|
4079
|
-
showToast('Files refreshed', 'success');
|
|
4080
|
-
}
|
|
4081
|
-
|
|
4082
|
-
// Open file from tree click
|
|
4083
|
-
async function openFileFromTree(filePath) {
|
|
4084
|
-
try {
|
|
4085
|
-
// Check if file is already open
|
|
4086
|
-
const existingTab = tabs.find(t => t.type === 'file' && t.path === filePath);
|
|
4087
|
-
if (existingTab) {
|
|
4088
|
-
selectTab(existingTab.id);
|
|
4089
|
-
refreshFileTab(existingTab.id); // Refresh content in case file changed
|
|
4090
|
-
return;
|
|
4091
|
-
}
|
|
4092
|
-
|
|
4093
|
-
// Open the file via API
|
|
4094
|
-
const response = await fetch('/api/tabs/file', {
|
|
4095
|
-
method: 'POST',
|
|
4096
|
-
headers: { 'Content-Type': 'application/json' },
|
|
4097
|
-
body: JSON.stringify({ path: filePath })
|
|
4098
|
-
});
|
|
4099
|
-
|
|
4100
|
-
if (!response.ok) {
|
|
4101
|
-
throw new Error(await response.text());
|
|
4102
|
-
}
|
|
4103
|
-
|
|
4104
|
-
// Refresh state and switch to the new tab
|
|
4105
|
-
await refresh();
|
|
4106
|
-
|
|
4107
|
-
// Find and select the new file tab
|
|
4108
|
-
const newTab = tabs.find(t => t.type === 'file' && t.path === filePath);
|
|
4109
|
-
if (newTab) {
|
|
4110
|
-
selectTab(newTab.id);
|
|
4111
|
-
}
|
|
4112
|
-
|
|
4113
|
-
showToast(`Opened ${getFileName(filePath)}`, 'success');
|
|
4114
|
-
} catch (err) {
|
|
4115
|
-
showToast('Failed to open file: ' + err.message, 'error');
|
|
4116
|
-
}
|
|
4117
|
-
}
|
|
4118
|
-
|
|
4119
|
-
// ========================================
|
|
4120
|
-
// Projects Tab Functions (Spec 0045)
|
|
4121
|
-
// ========================================
|
|
4122
|
-
|
|
4123
|
-
// Render the info header with helpful links
|
|
4124
|
-
function renderInfoHeader() {
|
|
4125
|
-
return `
|
|
4126
|
-
<div class="projects-info">
|
|
4127
|
-
<h1 style="font-size: 20px; margin-bottom: 12px; color: var(--text-primary);">Agent Farm Dashboard</h1>
|
|
4128
|
-
<p>Coordinate AI builders working on your codebase. The left panel shows the Architect terminal – tell it what you want to build. <strong>Tabs</strong> shows open terminals (Architect, Builders, utility shells). <strong>Files</strong> lets you browse and open project files. <strong>Projects</strong> tracks work as it moves from conception to integration.</p>
|
|
4129
|
-
<p>Docs: <a href="#" onclick="openProjectFile('codev/resources/cheatsheet.md'); return false;">Cheatsheet</a> · <a href="#" onclick="openProjectFile('codev/resources/lifecycle.md'); return false;">Lifecycle</a> · <a href="#" onclick="openProjectFile('codev/resources/commands/overview.md'); return false;">CLI Reference</a> · <a href="#" onclick="openProjectFile('codev/protocols/spider/protocol.md'); return false;">SPIDER Protocol</a> · <a href="https://github.com/cluesmith/codev#readme" target="_blank">README</a> · <a href="https://discord.gg/mJ92DhDa6n" target="_blank">Discord</a></p>
|
|
4130
|
-
</div>
|
|
4131
|
-
`;
|
|
4132
|
-
}
|
|
4133
|
-
|
|
4134
|
-
// Render the dashboard tab content (internal - called after data is loaded)
|
|
4135
|
-
function renderDashboardTabContent() {
|
|
4136
|
-
const content = document.getElementById('tab-content');
|
|
4137
|
-
|
|
4138
|
-
content.innerHTML = `
|
|
4139
|
-
<div class="dashboard-container">
|
|
4140
|
-
${renderInfoHeader()}
|
|
4141
|
-
<div class="dashboard-header">
|
|
4142
|
-
<!-- Tabs Section -->
|
|
4143
|
-
<div class="dashboard-section section-tabs ${sectionState.tabs ? '' : 'collapsed'}">
|
|
4144
|
-
<div class="dashboard-section-header" onclick="toggleSection('tabs')">
|
|
4145
|
-
<h3><span class="collapse-icon">▼</span> Tabs</h3>
|
|
4146
|
-
<div class="header-actions" onclick="event.stopPropagation()">
|
|
4147
|
-
<button onclick="spawnBuilder()" title="New Worktree">+ Worktree</button>
|
|
4148
|
-
<button onclick="spawnShell()" title="New Shell">+ Shell</button>
|
|
4149
|
-
</div>
|
|
4150
|
-
</div>
|
|
4151
|
-
<div class="dashboard-section-content">
|
|
4152
|
-
<div class="dashboard-tabs-list" id="dashboard-tabs-list">
|
|
4153
|
-
${renderDashboardTabsList()}
|
|
4154
|
-
</div>
|
|
4155
|
-
</div>
|
|
4156
|
-
</div>
|
|
4157
|
-
<!-- Files Section -->
|
|
4158
|
-
<div class="dashboard-section section-files ${sectionState.files ? '' : 'collapsed'}">
|
|
4159
|
-
<div class="dashboard-section-header" onclick="toggleSection('files')">
|
|
4160
|
-
<h3><span class="collapse-icon">▼</span> Files</h3>
|
|
4161
|
-
<div class="header-actions" onclick="event.stopPropagation()">
|
|
4162
|
-
<button onclick="refreshFilesTree()" title="Refresh">↻</button>
|
|
4163
|
-
<button onclick="collapseAllFolders()" title="Collapse All">⊟</button>
|
|
4164
|
-
<button onclick="expandAllFolders()" title="Expand All">⊞</button>
|
|
4165
|
-
</div>
|
|
4166
|
-
</div>
|
|
4167
|
-
<div class="dashboard-section-content">
|
|
4168
|
-
<div class="files-search-container" onclick="event.stopPropagation()">
|
|
4169
|
-
<input type="text"
|
|
4170
|
-
id="files-search-input"
|
|
4171
|
-
class="files-search-input"
|
|
4172
|
-
placeholder="Search files by name..."
|
|
4173
|
-
oninput="onFilesSearchInput(this.value)"
|
|
4174
|
-
onkeydown="onFilesSearchKeydown(event)"
|
|
4175
|
-
value="${escapeHtml(filesSearchQuery)}" />
|
|
4176
|
-
<button class="files-search-clear ${filesSearchQuery ? '' : 'hidden'}"
|
|
4177
|
-
onclick="clearFilesSearch()"
|
|
4178
|
-
title="Clear search">×</button>
|
|
4179
|
-
</div>
|
|
4180
|
-
<div id="dashboard-files-content">
|
|
4181
|
-
${filesSearchQuery ? renderFilesSearchResults() : renderDashboardFilesBrowserWithWrapper()}
|
|
4182
|
-
</div>
|
|
4183
|
-
</div>
|
|
4184
|
-
</div>
|
|
4185
|
-
</div>
|
|
4186
|
-
<!-- Projects Section -->
|
|
4187
|
-
<div class="dashboard-section section-projects ${sectionState.projects ? '' : 'collapsed'}">
|
|
4188
|
-
<div class="dashboard-section-header" onclick="toggleSection('projects')">
|
|
4189
|
-
<h3><span class="collapse-icon">▼</span> Projects</h3>
|
|
4190
|
-
</div>
|
|
4191
|
-
<div class="dashboard-section-content" id="dashboard-projects">
|
|
4192
|
-
${renderDashboardProjectsSection()}
|
|
4193
|
-
</div>
|
|
4194
|
-
</div>
|
|
4195
|
-
</div>
|
|
4196
|
-
`;
|
|
4197
|
-
}
|
|
4198
|
-
|
|
4199
|
-
// Render the tabs list for dashboard
|
|
4200
|
-
function renderDashboardTabsList() {
|
|
4201
|
-
// Filter to show terminal tabs only (not Dashboard/Files tabs)
|
|
4202
|
-
const terminalTabs = tabs.filter(t => t.type !== 'dashboard' && t.type !== 'files');
|
|
4203
|
-
|
|
4204
|
-
if (terminalTabs.length === 0) {
|
|
4205
|
-
return '<div class="dashboard-empty-state">No tabs open</div>';
|
|
4206
|
-
}
|
|
4207
|
-
|
|
4208
|
-
return terminalTabs.map(tab => {
|
|
4209
|
-
const isActive = tab.id === activeTabId;
|
|
4210
|
-
const icon = getTabIcon(tab.type);
|
|
4211
|
-
const statusIndicator = getDashboardStatusIndicator(tab);
|
|
4212
|
-
|
|
4213
|
-
return `
|
|
4214
|
-
<div class="dashboard-tab-item ${isActive ? 'active' : ''}" onclick="selectTab('${tab.id}')">
|
|
4215
|
-
${statusIndicator}
|
|
4216
|
-
<span class="tab-icon">${icon}</span>
|
|
4217
|
-
<span class="tab-name">${escapeHtml(tab.name)}</span>
|
|
4218
|
-
</div>
|
|
4219
|
-
`;
|
|
4220
|
-
}).join('');
|
|
4221
|
-
}
|
|
4222
|
-
|
|
4223
|
-
// Get status indicator for dashboard tab list
|
|
4224
|
-
function getDashboardStatusIndicator(tab) {
|
|
4225
|
-
if (tab.type !== 'builder') return '';
|
|
4226
|
-
|
|
4227
|
-
// Use builder status from state
|
|
4228
|
-
const builderState = (state.builders || []).find(b => `builder-${b.id}` === tab.id);
|
|
4229
|
-
if (!builderState) return '';
|
|
4230
|
-
|
|
4231
|
-
const status = builderState.status;
|
|
4232
|
-
if (['spawning', 'implementing'].includes(status)) {
|
|
4233
|
-
return '<span class="dashboard-status-indicator dashboard-status-working" title="Working"></span>';
|
|
4234
|
-
}
|
|
4235
|
-
if (status === 'blocked') {
|
|
4236
|
-
return '<span class="dashboard-status-indicator dashboard-status-blocked" title="Blocked"></span>';
|
|
4237
|
-
}
|
|
4238
|
-
if (['pr-ready', 'complete'].includes(status)) {
|
|
4239
|
-
return '<span class="dashboard-status-indicator dashboard-status-idle" title="Idle"></span>';
|
|
4240
|
-
}
|
|
4241
|
-
return '';
|
|
4242
|
-
}
|
|
4243
|
-
|
|
4244
|
-
// Render compact file browser for dashboard
|
|
4245
|
-
function renderDashboardFilesBrowser() {
|
|
4246
|
-
if (filesTreeError) {
|
|
4247
|
-
return `<div class="dashboard-empty-state">${escapeHtml(filesTreeError)}</div>`;
|
|
4248
|
-
}
|
|
4249
|
-
|
|
4250
|
-
if (!filesTreeLoaded || filesTreeData.length === 0) {
|
|
4251
|
-
return '<div class="dashboard-empty-state">Loading files...</div>';
|
|
4252
|
-
}
|
|
4253
|
-
|
|
4254
|
-
return renderTreeNodes(filesTreeData, 0);
|
|
4255
|
-
}
|
|
4256
|
-
|
|
4257
|
-
// Render the projects section for dashboard
|
|
4258
|
-
function renderDashboardProjectsSection() {
|
|
4259
|
-
if (projectlistError) {
|
|
4260
|
-
return renderErrorBanner(projectlistError);
|
|
4261
|
-
}
|
|
4262
|
-
|
|
4263
|
-
if (projectsData.length === 0) {
|
|
4264
|
-
// No welcome screen - just a helpful message
|
|
4265
|
-
return `
|
|
4266
|
-
<div class="dashboard-empty-state" style="padding: 24px;">
|
|
4267
|
-
No projects yet. Ask the Architect to create your first project.
|
|
4268
|
-
</div>
|
|
4269
|
-
`;
|
|
4270
|
-
}
|
|
4271
|
-
|
|
4272
|
-
// Render the existing project view
|
|
4273
|
-
return `
|
|
4274
|
-
${renderKanbanGrid(projectsData)}
|
|
4275
|
-
${renderTerminalProjects(projectsData)}
|
|
4276
|
-
`;
|
|
4277
|
-
}
|
|
4278
|
-
|
|
4279
|
-
// Create new utility shell (quick action button)
|
|
4280
|
-
async function createNewShell() {
|
|
4281
|
-
try {
|
|
4282
|
-
const response = await fetch('/api/tabs/shell', { method: 'POST' });
|
|
4283
|
-
const data = await response.json();
|
|
4284
|
-
if (!data.success && data.error) {
|
|
4285
|
-
showToast(data.error || 'Failed to create shell', 'error');
|
|
4286
|
-
return;
|
|
4287
|
-
}
|
|
4288
|
-
await refresh();
|
|
4289
|
-
if (data.id) {
|
|
4290
|
-
selectTab(`shell-${data.id}`);
|
|
4291
|
-
}
|
|
4292
|
-
showToast('Shell created', 'success');
|
|
4293
|
-
} catch (err) {
|
|
4294
|
-
showToast('Network error: ' + err.message, 'error');
|
|
4295
|
-
}
|
|
4296
|
-
}
|
|
4297
|
-
|
|
4298
|
-
// Create new worktree shell (quick action button)
|
|
4299
|
-
async function createNewWorktreeShell() {
|
|
4300
|
-
const branch = prompt('Branch name (leave empty for temp worktree):');
|
|
4301
|
-
if (branch === null) return; // User cancelled
|
|
4302
|
-
|
|
4303
|
-
try {
|
|
4304
|
-
const response = await fetch('/api/tabs/shell', {
|
|
4305
|
-
method: 'POST',
|
|
4306
|
-
headers: { 'Content-Type': 'application/json' },
|
|
4307
|
-
body: JSON.stringify({ worktree: true, branch: branch || undefined })
|
|
4308
|
-
});
|
|
4309
|
-
const data = await response.json();
|
|
4310
|
-
if (!data.success && data.error) {
|
|
4311
|
-
showToast(data.error || 'Failed to create worktree shell', 'error');
|
|
4312
|
-
return;
|
|
4313
|
-
}
|
|
4314
|
-
await refresh();
|
|
4315
|
-
// Auto-select the newly created tab (consistent with createNewShell behavior)
|
|
4316
|
-
if (data.id) {
|
|
4317
|
-
selectTab(`shell-${data.id}`);
|
|
4318
|
-
}
|
|
4319
|
-
showToast('Worktree shell created', 'success');
|
|
4320
|
-
} catch (err) {
|
|
4321
|
-
showToast('Network error: ' + err.message, 'error');
|
|
4322
|
-
}
|
|
4323
|
-
}
|
|
4324
|
-
|
|
4325
|
-
// Render the dashboard tab (entry point - loads data first)
|
|
4326
|
-
async function renderDashboardTab() {
|
|
4327
|
-
const content = document.getElementById('tab-content');
|
|
4328
|
-
content.innerHTML = '<div class="dashboard-container"><p style="color: var(--text-muted); padding: 16px;">Loading dashboard...</p></div>';
|
|
4329
|
-
|
|
4330
|
-
// Load both projectlist and files tree in parallel
|
|
4331
|
-
await Promise.all([
|
|
4332
|
-
loadProjectlist(),
|
|
4333
|
-
loadFilesTreeIfNeeded()
|
|
4334
|
-
]);
|
|
4335
|
-
|
|
4336
|
-
renderDashboardTabContent();
|
|
4337
|
-
checkStarterMode(); // Update polling state after initial load
|
|
4338
|
-
}
|
|
4339
|
-
|
|
4340
|
-
// Load files tree if not already loaded
|
|
4341
|
-
async function loadFilesTreeIfNeeded() {
|
|
4342
|
-
if (!filesTreeLoaded) {
|
|
4343
|
-
await loadFilesTree();
|
|
4344
|
-
}
|
|
4345
|
-
}
|
|
4346
|
-
|
|
4347
|
-
// Legacy function for backward compatibility (still used by polling)
|
|
4348
|
-
function renderProjectsTabContent() {
|
|
4349
|
-
// If dashboard tab is active, re-render dashboard instead
|
|
4350
|
-
if (activeTabId === 'dashboard') {
|
|
4351
|
-
renderDashboardTabContent();
|
|
4352
|
-
}
|
|
4353
|
-
}
|
|
4354
|
-
|
|
4355
|
-
// Legacy function alias
|
|
4356
|
-
async function renderProjectsTab() {
|
|
4357
|
-
await renderDashboardTab();
|
|
4358
|
-
}
|
|
4359
|
-
|
|
4360
|
-
// Load projectlist.md from disk
|
|
4361
|
-
async function loadProjectlist() {
|
|
4362
|
-
try {
|
|
4363
|
-
const response = await fetch('/file?path=codev/projectlist.md');
|
|
4364
|
-
|
|
4365
|
-
if (!response.ok) {
|
|
4366
|
-
if (response.status === 404) {
|
|
4367
|
-
// File not found - show welcome screen
|
|
4368
|
-
projectsData = [];
|
|
4369
|
-
projectlistError = null;
|
|
4370
|
-
return;
|
|
4371
|
-
}
|
|
4372
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
4373
|
-
}
|
|
4374
|
-
|
|
4375
|
-
const text = await response.text();
|
|
4376
|
-
const newHash = hashString(text);
|
|
4377
|
-
|
|
4378
|
-
// Only re-parse if content changed
|
|
4379
|
-
if (newHash !== projectlistHash) {
|
|
4380
|
-
projectlistHash = newHash;
|
|
4381
|
-
projectsData = parseProjectlist(text);
|
|
4382
|
-
projectlistError = null;
|
|
4383
|
-
}
|
|
4384
|
-
} catch (err) {
|
|
4385
|
-
console.error('Failed to load projectlist:', err);
|
|
4386
|
-
projectlistError = 'Could not load projectlist.md: ' + err.message;
|
|
4387
|
-
// Preserve last good state if available
|
|
4388
|
-
if (projectsData.length === 0) {
|
|
4389
|
-
projectsData = [];
|
|
4390
|
-
}
|
|
4391
|
-
}
|
|
4392
|
-
}
|
|
4393
|
-
|
|
4394
|
-
// Reload projectlist (manual refresh button)
|
|
4395
|
-
async function reloadProjectlist() {
|
|
4396
|
-
projectlistHash = null; // Force re-parse
|
|
4397
|
-
await loadProjectlist();
|
|
4398
|
-
renderProjectsTabContent();
|
|
4399
|
-
checkStarterMode(); // Update polling state after reload
|
|
4400
|
-
}
|
|
4401
|
-
|
|
4402
|
-
// Poll projectlist for changes (every 5 seconds)
|
|
4403
|
-
async function pollProjectlist() {
|
|
4404
|
-
// Only poll if dashboard tab is active
|
|
4405
|
-
if (activeTabId !== 'dashboard') return;
|
|
4406
|
-
|
|
4407
|
-
try {
|
|
4408
|
-
const response = await fetch('/file?path=codev/projectlist.md');
|
|
4409
|
-
if (!response.ok) return;
|
|
4410
|
-
|
|
4411
|
-
const text = await response.text();
|
|
4412
|
-
const newHash = hashString(text);
|
|
4413
|
-
|
|
4414
|
-
if (newHash !== projectlistHash) {
|
|
4415
|
-
// Content changed - debounce to avoid reading mid-write
|
|
4416
|
-
clearTimeout(projectlistDebounce);
|
|
4417
|
-
projectlistDebounce = setTimeout(async () => {
|
|
4418
|
-
projectlistHash = newHash;
|
|
4419
|
-
projectsData = parseProjectlist(text);
|
|
4420
|
-
projectlistError = null;
|
|
4421
|
-
renderProjectsTabContent();
|
|
4422
|
-
checkStarterMode(); // Update polling state after content change
|
|
4423
|
-
}, 500);
|
|
4424
|
-
}
|
|
4425
|
-
} catch (err) {
|
|
4426
|
-
// Silently ignore polling errors
|
|
4427
|
-
}
|
|
4428
|
-
}
|
|
4429
|
-
|
|
4430
|
-
// Poll for projectlist.md creation when in starter mode (every 15 seconds)
|
|
4431
|
-
let starterModePollingInterval = null;
|
|
4432
|
-
|
|
4433
|
-
async function pollForProjectlistCreation() {
|
|
4434
|
-
try {
|
|
4435
|
-
const response = await fetch('/api/projectlist-exists');
|
|
4436
|
-
if (!response.ok) return;
|
|
4437
|
-
|
|
4438
|
-
const { exists } = await response.json();
|
|
4439
|
-
if (exists) {
|
|
4440
|
-
// projectlist.md was created - stop polling and reload
|
|
4441
|
-
if (starterModePollingInterval) {
|
|
4442
|
-
clearInterval(starterModePollingInterval);
|
|
4443
|
-
starterModePollingInterval = null;
|
|
4444
|
-
}
|
|
4445
|
-
window.location.reload();
|
|
4446
|
-
}
|
|
4447
|
-
} catch (err) {
|
|
4448
|
-
// Silently ignore polling errors
|
|
4449
|
-
}
|
|
4450
|
-
}
|
|
4451
|
-
|
|
4452
|
-
// Check if we should start starter mode polling
|
|
4453
|
-
function checkStarterMode() {
|
|
4454
|
-
// We're in starter mode ONLY if:
|
|
4455
|
-
// 1. projectsData is empty (no projects loaded)
|
|
4456
|
-
// 2. No error occurred
|
|
4457
|
-
// 3. projectlistHash is null (file was not found, not just empty)
|
|
4458
|
-
// This prevents infinite reload loop when file exists but is empty
|
|
4459
|
-
const isStarterMode = projectsData.length === 0 && !projectlistError && projectlistHash === null;
|
|
4460
|
-
|
|
4461
|
-
if (isStarterMode && !starterModePollingInterval) {
|
|
4462
|
-
// Start polling for projectlist.md creation
|
|
4463
|
-
starterModePollingInterval = setInterval(pollForProjectlistCreation, 15000);
|
|
4464
|
-
} else if (!isStarterMode && starterModePollingInterval) {
|
|
4465
|
-
// Stop polling - file exists now (even if empty)
|
|
4466
|
-
clearInterval(starterModePollingInterval);
|
|
4467
|
-
starterModePollingInterval = null;
|
|
4468
|
-
}
|
|
4469
|
-
}
|
|
4470
|
-
|
|
4471
|
-
// Start projectlist polling (separate from main state polling)
|
|
4472
|
-
setInterval(pollProjectlist, 5000);
|
|
4473
|
-
|
|
4474
|
-
// ========================================
|
|
4475
|
-
// Activity Summary (Spec 0059)
|
|
4476
|
-
// ========================================
|
|
4477
|
-
|
|
4478
|
-
let activityData = null;
|
|
4479
|
-
|
|
4480
|
-
// Show activity summary modal
|
|
4481
|
-
async function showActivitySummary() {
|
|
4482
|
-
// Check if activity tab already exists
|
|
4483
|
-
let activityTab = tabs.find(t => t.type === 'activity');
|
|
4484
|
-
|
|
4485
|
-
if (!activityTab) {
|
|
4486
|
-
// Create new activity tab
|
|
4487
|
-
activityTab = {
|
|
4488
|
-
id: 'activity-today',
|
|
4489
|
-
type: 'activity',
|
|
4490
|
-
name: 'Today'
|
|
4491
|
-
};
|
|
4492
|
-
tabs.push(activityTab);
|
|
4493
|
-
}
|
|
4494
|
-
|
|
4495
|
-
// Switch to activity tab
|
|
4496
|
-
activeTabId = activityTab.id;
|
|
4497
|
-
currentTabType = null; // Force re-render
|
|
4498
|
-
renderTabs();
|
|
4499
|
-
renderTabContent();
|
|
4500
|
-
}
|
|
4501
|
-
|
|
4502
|
-
// Render the activity tab content
|
|
4503
|
-
async function renderActivityTab() {
|
|
4504
|
-
const content = document.getElementById('tab-content');
|
|
4505
|
-
|
|
4506
|
-
// Show loading state
|
|
4507
|
-
content.innerHTML = `
|
|
4508
|
-
<div class="activity-tab-container">
|
|
4509
|
-
<div class="activity-loading">
|
|
4510
|
-
<span class="activity-spinner"></span>
|
|
4511
|
-
Loading activity...
|
|
4512
|
-
</div>
|
|
4513
|
-
</div>
|
|
4514
|
-
`;
|
|
4515
|
-
|
|
4516
|
-
try {
|
|
4517
|
-
const response = await fetch('/api/activity-summary');
|
|
4518
|
-
if (!response.ok) {
|
|
4519
|
-
throw new Error(await response.text());
|
|
4520
|
-
}
|
|
4521
|
-
activityData = await response.json();
|
|
4522
|
-
renderActivityTabContent(activityData);
|
|
4523
|
-
} catch (err) {
|
|
4524
|
-
content.innerHTML = `
|
|
4525
|
-
<div class="activity-tab-container">
|
|
4526
|
-
<div class="activity-error">
|
|
4527
|
-
Failed to load activity: ${escapeHtml(err.message)}
|
|
4528
|
-
</div>
|
|
4529
|
-
</div>
|
|
4530
|
-
`;
|
|
4531
|
-
}
|
|
4532
|
-
}
|
|
4533
|
-
|
|
4534
|
-
// Render activity tab content (similar to modal but in tab)
|
|
4535
|
-
function renderActivityTabContent(data) {
|
|
4536
|
-
const content = document.getElementById('tab-content');
|
|
4537
|
-
|
|
4538
|
-
// Check for zero activity
|
|
4539
|
-
if (data.commits.length === 0 && data.prs.length === 0 && data.builders.length === 0) {
|
|
4540
|
-
content.innerHTML = `
|
|
4541
|
-
<div class="activity-tab-container">
|
|
4542
|
-
<div class="activity-empty">
|
|
4543
|
-
<p>No activity recorded today</p>
|
|
4544
|
-
<p style="font-size: 12px; margin-top: 8px;">Make some commits or create PRs to see your daily summary!</p>
|
|
4545
|
-
</div>
|
|
4546
|
-
</div>
|
|
4547
|
-
`;
|
|
4548
|
-
return;
|
|
4549
|
-
}
|
|
4550
|
-
|
|
4551
|
-
const hours = Math.floor(data.timeTracking.activeMinutes / 60);
|
|
4552
|
-
const mins = data.timeTracking.activeMinutes % 60;
|
|
4553
|
-
const uniqueBranches = new Set(data.commits.map(c => c.branch)).size;
|
|
4554
|
-
const mergedPrs = data.prs.filter(p => p.state === 'MERGED').length;
|
|
4555
|
-
|
|
4556
|
-
// Format time strings
|
|
4557
|
-
const formatTime = (isoString) => {
|
|
4558
|
-
if (!isoString) return '--';
|
|
4559
|
-
const date = new Date(isoString);
|
|
4560
|
-
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
4561
|
-
};
|
|
4562
|
-
|
|
4563
|
-
let html = '<div class="activity-tab-container"><div class="activity-summary">';
|
|
4564
|
-
|
|
4565
|
-
// AI Summary (if available)
|
|
4566
|
-
if (data.aiSummary) {
|
|
4567
|
-
html += `<div class="activity-ai-summary">${escapeHtml(data.aiSummary)}</div>`;
|
|
4568
|
-
}
|
|
4569
|
-
|
|
4570
|
-
// Activity section
|
|
4571
|
-
html += `
|
|
4572
|
-
<div class="activity-section">
|
|
4573
|
-
<h4>Activity</h4>
|
|
4574
|
-
<ul>
|
|
4575
|
-
<li>${data.commits.length} commits across ${uniqueBranches} branch${uniqueBranches !== 1 ? 'es' : ''}</li>
|
|
4576
|
-
<li>${data.files.length} files modified</li>
|
|
4577
|
-
<li>${data.prs.length} PR${data.prs.length !== 1 ? 's' : ''} created${mergedPrs > 0 ? `, ${mergedPrs} merged` : ''}</li>
|
|
4578
|
-
</ul>
|
|
4579
|
-
</div>
|
|
4580
|
-
`;
|
|
4581
|
-
|
|
4582
|
-
// Projects section (if any status changes)
|
|
4583
|
-
if (data.projectChanges && data.projectChanges.length > 0) {
|
|
4584
|
-
html += `
|
|
4585
|
-
<div class="activity-section">
|
|
4586
|
-
<h4>Projects Touched</h4>
|
|
4587
|
-
<ul>
|
|
4588
|
-
${data.projectChanges.map(p => `<li>${escapeHtml(p.id)}: ${escapeHtml(p.title)} (${escapeHtml(p.oldStatus)} → ${escapeHtml(p.newStatus)})</li>`).join('')}
|
|
4589
|
-
</ul>
|
|
4590
|
-
</div>
|
|
4591
|
-
`;
|
|
4592
|
-
}
|
|
4593
|
-
|
|
4594
|
-
// Time section
|
|
4595
|
-
html += `
|
|
4596
|
-
<div class="activity-section">
|
|
4597
|
-
<h4>Time</h4>
|
|
4598
|
-
<p><span class="activity-time-value">~${hours}h ${mins}m</span> active time</p>
|
|
4599
|
-
<p>First activity: ${formatTime(data.timeTracking.firstActivity)}</p>
|
|
4600
|
-
<p>Last activity: ${formatTime(data.timeTracking.lastActivity)}</p>
|
|
4601
|
-
</div>
|
|
4602
|
-
`;
|
|
4603
|
-
|
|
4604
|
-
// Copy button
|
|
4605
|
-
html += `
|
|
4606
|
-
<div class="activity-actions">
|
|
4607
|
-
<button class="btn" onclick="copyActivityToClipboard()">Copy to Clipboard</button>
|
|
4608
|
-
</div>
|
|
4609
|
-
`;
|
|
4610
|
-
|
|
4611
|
-
html += '</div></div>';
|
|
4612
|
-
content.innerHTML = html;
|
|
4613
|
-
}
|
|
4614
|
-
|
|
4615
|
-
// Render activity summary content
|
|
4616
|
-
function renderActivitySummary(data) {
|
|
4617
|
-
const content = document.getElementById('activity-content');
|
|
4618
|
-
|
|
4619
|
-
// Check for zero activity
|
|
4620
|
-
if (data.commits.length === 0 && data.prs.length === 0 && data.builders.length === 0) {
|
|
4621
|
-
content.innerHTML = `
|
|
4622
|
-
<div class="activity-empty">
|
|
4623
|
-
<p>No activity recorded today</p>
|
|
4624
|
-
<p style="font-size: 12px; margin-top: 8px;">Make some commits or create PRs to see your daily summary!</p>
|
|
4625
|
-
</div>
|
|
4626
|
-
`;
|
|
4627
|
-
return;
|
|
4628
|
-
}
|
|
4629
|
-
|
|
4630
|
-
const hours = Math.floor(data.timeTracking.activeMinutes / 60);
|
|
4631
|
-
const mins = data.timeTracking.activeMinutes % 60;
|
|
4632
|
-
const uniqueBranches = new Set(data.commits.map(c => c.branch)).size;
|
|
4633
|
-
const mergedPrs = data.prs.filter(p => p.state === 'MERGED').length;
|
|
4634
|
-
|
|
4635
|
-
// Format time strings
|
|
4636
|
-
const formatTime = (isoString) => {
|
|
4637
|
-
if (!isoString) return '--';
|
|
4638
|
-
const date = new Date(isoString);
|
|
4639
|
-
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
4640
|
-
};
|
|
4641
|
-
|
|
4642
|
-
let html = '<div class="activity-summary">';
|
|
4643
|
-
|
|
4644
|
-
// AI Summary (if available)
|
|
4645
|
-
if (data.aiSummary) {
|
|
4646
|
-
html += `<div class="activity-ai-summary">${escapeHtml(data.aiSummary)}</div>`;
|
|
4647
|
-
}
|
|
4648
|
-
|
|
4649
|
-
// Activity section
|
|
4650
|
-
html += `
|
|
4651
|
-
<div class="activity-section">
|
|
4652
|
-
<h4>Activity</h4>
|
|
4653
|
-
<ul>
|
|
4654
|
-
<li>${data.commits.length} commits across ${uniqueBranches} branch${uniqueBranches !== 1 ? 'es' : ''}</li>
|
|
4655
|
-
<li>${data.files.length} files modified</li>
|
|
4656
|
-
<li>${data.prs.length} PR${data.prs.length !== 1 ? 's' : ''} created${mergedPrs > 0 ? `, ${mergedPrs} merged` : ''}</li>
|
|
4657
|
-
</ul>
|
|
4658
|
-
</div>
|
|
4659
|
-
`;
|
|
4660
|
-
|
|
4661
|
-
// Projects section (if any status changes)
|
|
4662
|
-
if (data.projectChanges && data.projectChanges.length > 0) {
|
|
4663
|
-
html += `
|
|
4664
|
-
<div class="activity-section">
|
|
4665
|
-
<h4>Projects Touched</h4>
|
|
4666
|
-
<ul>
|
|
4667
|
-
${data.projectChanges.map(p => `<li>${escapeHtml(p.id)}: ${escapeHtml(p.title)} (${escapeHtml(p.oldStatus)} → ${escapeHtml(p.newStatus)})</li>`).join('')}
|
|
4668
|
-
</ul>
|
|
4669
|
-
</div>
|
|
4670
|
-
`;
|
|
4671
|
-
}
|
|
4672
|
-
|
|
4673
|
-
// Time section
|
|
4674
|
-
html += `
|
|
4675
|
-
<div class="activity-section">
|
|
4676
|
-
<h4>Time</h4>
|
|
4677
|
-
<p><span class="activity-time-value">~${hours}h ${mins}m</span> active time</p>
|
|
4678
|
-
<p>First activity: ${formatTime(data.timeTracking.firstActivity)}</p>
|
|
4679
|
-
<p>Last activity: ${formatTime(data.timeTracking.lastActivity)}</p>
|
|
4680
|
-
</div>
|
|
4681
|
-
`;
|
|
4682
|
-
|
|
4683
|
-
html += '</div>';
|
|
4684
|
-
content.innerHTML = html;
|
|
4685
|
-
}
|
|
4686
|
-
|
|
4687
|
-
// Close activity modal
|
|
4688
|
-
function closeActivityModal() {
|
|
4689
|
-
document.getElementById('activity-modal').classList.add('hidden');
|
|
4690
|
-
}
|
|
4691
|
-
|
|
4692
|
-
// Copy activity summary to clipboard
|
|
4693
|
-
function copyActivitySummary() {
|
|
4694
|
-
if (!activityData) return;
|
|
4695
|
-
|
|
4696
|
-
const hours = Math.floor(activityData.timeTracking.activeMinutes / 60);
|
|
4697
|
-
const mins = activityData.timeTracking.activeMinutes % 60;
|
|
4698
|
-
const uniqueBranches = new Set(activityData.commits.map(c => c.branch)).size;
|
|
4699
|
-
const mergedPrs = activityData.prs.filter(p => p.state === 'MERGED').length;
|
|
4700
|
-
|
|
4701
|
-
let markdown = `## Today's Summary\n\n`;
|
|
4702
|
-
|
|
4703
|
-
if (activityData.aiSummary) {
|
|
4704
|
-
markdown += `${activityData.aiSummary}\n\n`;
|
|
4705
|
-
}
|
|
4706
|
-
|
|
4707
|
-
markdown += `### Activity\n`;
|
|
4708
|
-
markdown += `- ${activityData.commits.length} commits across ${uniqueBranches} branches\n`;
|
|
4709
|
-
markdown += `- ${activityData.files.length} files modified\n`;
|
|
4710
|
-
markdown += `- ${activityData.prs.length} PRs${mergedPrs > 0 ? ` (${mergedPrs} merged)` : ''}\n\n`;
|
|
4711
|
-
|
|
4712
|
-
if (activityData.projectChanges && activityData.projectChanges.length > 0) {
|
|
4713
|
-
markdown += `### Projects Touched\n`;
|
|
4714
|
-
activityData.projectChanges.forEach(p => {
|
|
4715
|
-
markdown += `- ${p.id}: ${p.title} (${p.oldStatus} → ${p.newStatus})\n`;
|
|
4716
|
-
});
|
|
4717
|
-
markdown += '\n';
|
|
4718
|
-
}
|
|
4719
|
-
|
|
4720
|
-
markdown += `### Time\n`;
|
|
4721
|
-
markdown += `Active time: ~${hours}h ${mins}m\n`;
|
|
4722
|
-
|
|
4723
|
-
navigator.clipboard.writeText(markdown).then(() => {
|
|
4724
|
-
showToast('Copied to clipboard', 'success');
|
|
4725
|
-
}).catch(() => {
|
|
4726
|
-
showToast('Failed to copy', 'error');
|
|
4727
|
-
});
|
|
4728
|
-
}
|
|
4729
|
-
|
|
4730
|
-
// Close activity modal when clicking backdrop
|
|
4731
|
-
document.getElementById('activity-modal').addEventListener('click', (e) => {
|
|
4732
|
-
if (e.target.id === 'activity-modal') {
|
|
4733
|
-
closeActivityModal();
|
|
4734
|
-
}
|
|
4735
|
-
});
|
|
4736
|
-
|
|
4737
|
-
// Initialize on load
|
|
4738
|
-
init();
|
|
4739
|
-
</script>
|
|
4740
|
-
</body>
|
|
4741
|
-
</html>
|