@cluesmith/codev 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/dist/agent-farm/cli.d.ts.map +1 -1
  2. package/dist/agent-farm/cli.js +19 -0
  3. package/dist/agent-farm/cli.js.map +1 -1
  4. package/dist/agent-farm/commands/cleanup.d.ts.map +1 -1
  5. package/dist/agent-farm/commands/cleanup.js +18 -1
  6. package/dist/agent-farm/commands/cleanup.js.map +1 -1
  7. package/dist/agent-farm/commands/consult.d.ts +16 -0
  8. package/dist/agent-farm/commands/consult.d.ts.map +1 -0
  9. package/dist/agent-farm/commands/consult.js +51 -0
  10. package/dist/agent-farm/commands/consult.js.map +1 -0
  11. package/dist/agent-farm/commands/open.js +6 -6
  12. package/dist/agent-farm/commands/open.js.map +1 -1
  13. package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
  14. package/dist/agent-farm/commands/spawn.js +51 -42
  15. package/dist/agent-farm/commands/spawn.js.map +1 -1
  16. package/dist/agent-farm/commands/start.d.ts.map +1 -1
  17. package/dist/agent-farm/commands/start.js +9 -14
  18. package/dist/agent-farm/commands/start.js.map +1 -1
  19. package/dist/agent-farm/commands/util.js +2 -2
  20. package/dist/agent-farm/commands/util.js.map +1 -1
  21. package/dist/agent-farm/db/errors.d.ts +4 -0
  22. package/dist/agent-farm/db/errors.d.ts.map +1 -1
  23. package/dist/agent-farm/db/errors.js +8 -0
  24. package/dist/agent-farm/db/errors.js.map +1 -1
  25. package/dist/agent-farm/servers/dashboard-server.js +125 -71
  26. package/dist/agent-farm/servers/dashboard-server.js.map +1 -1
  27. package/dist/agent-farm/servers/open-server.d.ts +9 -0
  28. package/dist/agent-farm/servers/open-server.d.ts.map +1 -0
  29. package/dist/agent-farm/servers/{annotate-server.js → open-server.js} +17 -15
  30. package/dist/agent-farm/servers/open-server.js.map +1 -0
  31. package/dist/agent-farm/servers/tower-server.js +4 -7
  32. package/dist/agent-farm/servers/tower-server.js.map +1 -1
  33. package/dist/agent-farm/state.d.ts +5 -0
  34. package/dist/agent-farm/state.d.ts.map +1 -1
  35. package/dist/agent-farm/state.js +17 -0
  36. package/dist/agent-farm/state.js.map +1 -1
  37. package/dist/agent-farm/types.d.ts +1 -1
  38. package/dist/agent-farm/types.d.ts.map +1 -1
  39. package/dist/agent-farm/utils/config.d.ts.map +1 -1
  40. package/dist/agent-farm/utils/config.js +13 -7
  41. package/dist/agent-farm/utils/config.js.map +1 -1
  42. package/dist/agent-farm/utils/port-registry.d.ts +1 -1
  43. package/dist/agent-farm/utils/port-registry.d.ts.map +1 -1
  44. package/dist/agent-farm/utils/port-registry.js +1 -1
  45. package/dist/agent-farm/utils/port-registry.js.map +1 -1
  46. package/dist/agent-farm/utils/shell.d.ts +19 -0
  47. package/dist/agent-farm/utils/shell.d.ts.map +1 -1
  48. package/dist/agent-farm/utils/shell.js +28 -0
  49. package/dist/agent-farm/utils/shell.js.map +1 -1
  50. package/dist/cli.d.ts.map +1 -1
  51. package/dist/cli.js +33 -0
  52. package/dist/cli.js.map +1 -1
  53. package/dist/commands/adopt.d.ts +3 -0
  54. package/dist/commands/adopt.d.ts.map +1 -1
  55. package/dist/commands/adopt.js +31 -25
  56. package/dist/commands/adopt.js.map +1 -1
  57. package/dist/commands/consult/index.d.ts +3 -2
  58. package/dist/commands/consult/index.d.ts.map +1 -1
  59. package/dist/commands/consult/index.js +128 -54
  60. package/dist/commands/consult/index.js.map +1 -1
  61. package/dist/commands/doctor.d.ts.map +1 -1
  62. package/dist/commands/doctor.js +88 -36
  63. package/dist/commands/doctor.js.map +1 -1
  64. package/dist/commands/eject.d.ts +18 -0
  65. package/dist/commands/eject.d.ts.map +1 -0
  66. package/dist/commands/eject.js +149 -0
  67. package/dist/commands/eject.js.map +1 -0
  68. package/dist/commands/import.d.ts +16 -0
  69. package/dist/commands/import.d.ts.map +1 -0
  70. package/dist/commands/import.js +278 -0
  71. package/dist/commands/import.js.map +1 -0
  72. package/dist/commands/init.d.ts +3 -0
  73. package/dist/commands/init.d.ts.map +1 -1
  74. package/dist/commands/init.js +32 -27
  75. package/dist/commands/init.js.map +1 -1
  76. package/dist/lib/projectlist-parser.d.ts +70 -0
  77. package/dist/lib/projectlist-parser.d.ts.map +1 -0
  78. package/dist/lib/projectlist-parser.js +200 -0
  79. package/dist/lib/projectlist-parser.js.map +1 -0
  80. package/dist/lib/skeleton.d.ts +41 -0
  81. package/dist/lib/skeleton.d.ts.map +1 -0
  82. package/dist/lib/skeleton.js +110 -0
  83. package/dist/lib/skeleton.js.map +1 -0
  84. package/dist/lib/templates.d.ts +2 -1
  85. package/dist/lib/templates.d.ts.map +1 -1
  86. package/dist/lib/templates.js +11 -10
  87. package/dist/lib/templates.js.map +1 -1
  88. package/package.json +5 -4
  89. package/{templates → skeleton}/DEPENDENCIES.md +3 -48
  90. package/skeleton/bin/agent-farm +7 -0
  91. package/skeleton/docs/commands/agent-farm.md +469 -0
  92. package/skeleton/docs/commands/codev.md +253 -0
  93. package/skeleton/docs/commands/consult.md +286 -0
  94. package/skeleton/docs/commands/overview.md +108 -0
  95. package/skeleton/maintain/.gitkeep +2 -0
  96. package/{templates → skeleton}/protocols/experiment/protocol.md +2 -2
  97. package/skeleton/protocols/maintain/protocol.md +502 -0
  98. package/skeleton/protocols/maintain/templates/maintenance-run.md +64 -0
  99. package/{templates → skeleton}/protocols/spider/protocol.md +9 -9
  100. package/{templates/protocols/spider-solo → skeleton/protocols/spider}/templates/plan.md +22 -1
  101. package/{templates/protocols/spider-solo → skeleton/protocols/spider}/templates/spec.md +30 -1
  102. package/skeleton/protocols/tick/protocol.md +277 -0
  103. package/skeleton/resources/lessons-learned.md +30 -0
  104. package/skeleton/resources/workflow-reference.md +242 -0
  105. package/skeleton/roles/architect.md +283 -0
  106. package/{templates → skeleton}/roles/builder.md +2 -0
  107. package/skeleton/roles/review-types/impl-review.md +56 -0
  108. package/skeleton/roles/review-types/integration-review.md +68 -0
  109. package/skeleton/roles/review-types/plan-review.md +59 -0
  110. package/skeleton/roles/review-types/pr-ready.md +72 -0
  111. package/skeleton/roles/review-types/spec-review.md +55 -0
  112. package/skeleton/templates/lessons-learned.md +28 -0
  113. package/{templates → skeleton}/templates/projectlist.md +17 -16
  114. package/dist/agent-farm/servers/annotate-server.d.ts +0 -9
  115. package/dist/agent-farm/servers/annotate-server.d.ts.map +0 -1
  116. package/dist/agent-farm/servers/annotate-server.js.map +0 -1
  117. package/templates/agents/architecture-documenter.md +0 -189
  118. package/templates/agents/codev-updater.md +0 -276
  119. package/templates/agents/spider-protocol-updater.md +0 -118
  120. package/templates/annotate.html +0 -903
  121. package/templates/bin/agent-farm +0 -18
  122. package/templates/bin/annotate-server.js +0 -140
  123. package/templates/dashboard-split.html +0 -1679
  124. package/templates/dashboard.html +0 -149
  125. package/templates/protocols/maintain/protocol.md +0 -235
  126. package/templates/protocols/spider/templates/plan.md +0 -169
  127. package/templates/protocols/spider/templates/review.md +0 -207
  128. package/templates/protocols/spider/templates/spec.md +0 -140
  129. package/templates/protocols/spider-solo/protocol.md +0 -619
  130. package/templates/protocols/tick/protocol.md +0 -250
  131. package/templates/roles/architect.md +0 -230
  132. package/templates/tower.html +0 -1032
  133. /package/{templates/AGENTS.md → skeleton/AGENTS.md.template} +0 -0
  134. /package/{templates/CLAUDE.md → skeleton/CLAUDE.md.template} +0 -0
  135. /package/{templates → skeleton}/bin/codev-doctor +0 -0
  136. /package/{templates → skeleton}/builders.md +0 -0
  137. /package/{templates → skeleton}/config.json +0 -0
  138. /package/{templates → skeleton}/plans/.gitkeep +0 -0
  139. /package/{templates → skeleton}/protocols/experiment/templates/notes.md +0 -0
  140. /package/{templates/protocols/spider-solo → skeleton/protocols/spider}/templates/review.md +0 -0
  141. /package/{templates → skeleton}/protocols/tick/templates/plan.md +0 -0
  142. /package/{templates → skeleton}/protocols/tick/templates/review.md +0 -0
  143. /package/{templates → skeleton}/protocols/tick/templates/spec.md +0 -0
  144. /package/{templates → skeleton}/reviews/.gitkeep +0 -0
  145. /package/{templates → skeleton}/roles/consultant.md +0 -0
  146. /package/{templates → skeleton}/specs/.gitkeep +0 -0
@@ -1,1679 +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
- }
31
-
32
- body {
33
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
34
- background: var(--bg-primary);
35
- color: var(--text-primary);
36
- height: 100vh;
37
- display: flex;
38
- flex-direction: column;
39
- overflow: hidden;
40
- }
41
-
42
- /* Header */
43
- .header {
44
- display: flex;
45
- justify-content: space-between;
46
- align-items: center;
47
- padding: 12px 16px;
48
- background: var(--bg-secondary);
49
- border-bottom: 1px solid var(--border);
50
- }
51
-
52
- .header h1 {
53
- font-size: 16px;
54
- font-weight: 600;
55
- }
56
-
57
- .header-actions {
58
- display: flex;
59
- gap: 8px;
60
- }
61
-
62
- .btn {
63
- padding: 6px 12px;
64
- border-radius: 4px;
65
- border: 1px solid var(--border);
66
- background: var(--bg-tertiary);
67
- color: var(--text-secondary);
68
- cursor: pointer;
69
- font-size: 13px;
70
- }
71
-
72
- .btn:hover {
73
- background: var(--tab-active);
74
- }
75
-
76
- .btn-danger {
77
- border-color: #ef4444;
78
- color: #ef4444;
79
- }
80
-
81
- .btn-danger:hover {
82
- background: rgba(239, 68, 68, 0.1);
83
- }
84
-
85
- /* Main content area */
86
- .main {
87
- display: flex;
88
- flex: 1;
89
- overflow: hidden;
90
- }
91
-
92
- /* Left pane - Architect */
93
- .left-pane {
94
- width: 50%;
95
- min-width: 20%;
96
- max-width: 80%;
97
- resize: horizontal;
98
- overflow: auto;
99
- border-right: 1px solid var(--border);
100
- display: flex;
101
- flex-direction: column;
102
- }
103
-
104
- .pane-header {
105
- padding: 8px 12px;
106
- background: var(--bg-secondary);
107
- border-bottom: 1px solid var(--border);
108
- font-size: 12px;
109
- color: var(--text-muted);
110
- text-transform: uppercase;
111
- letter-spacing: 0.5px;
112
- display: flex;
113
- align-items: center;
114
- gap: 6px;
115
- }
116
-
117
- .pane-header .status-dot {
118
- width: 8px;
119
- height: 8px;
120
- border-radius: 50%;
121
- background: var(--status-active);
122
- }
123
-
124
- .pane-header .status-dot.inactive {
125
- background: var(--text-muted);
126
- }
127
-
128
- #architect-content {
129
- flex: 1;
130
- display: flex;
131
- flex-direction: column;
132
- }
133
-
134
- .left-pane iframe {
135
- flex: 1;
136
- width: 100%;
137
- border: none;
138
- background: #000;
139
- }
140
-
141
- .architect-placeholder {
142
- flex: 1;
143
- display: flex;
144
- flex-direction: column;
145
- align-items: center;
146
- justify-content: center;
147
- color: var(--text-muted);
148
- gap: 16px;
149
- }
150
-
151
- .architect-placeholder code {
152
- background: var(--bg-tertiary);
153
- padding: 4px 8px;
154
- border-radius: 4px;
155
- font-size: 13px;
156
- }
157
-
158
- /* Right pane - Tabs */
159
- .right-pane {
160
- width: 50%;
161
- display: flex;
162
- flex-direction: column;
163
- }
164
-
165
- /* Tab bar */
166
- .tab-bar {
167
- display: flex;
168
- align-items: center;
169
- background: var(--bg-secondary);
170
- border-bottom: 1px solid var(--border);
171
- min-height: 40px;
172
- overflow: visible; /* Allow overflow menu dropdown to be visible */
173
- position: relative; /* Position context for overflow menu */
174
- }
175
-
176
- .tabs-scroll {
177
- display: flex;
178
- overflow-x: auto;
179
- flex: 1;
180
- scrollbar-width: none;
181
- }
182
-
183
- .tabs-scroll::-webkit-scrollbar {
184
- display: none;
185
- }
186
-
187
- .tab {
188
- display: flex;
189
- align-items: center;
190
- gap: 6px;
191
- padding: 8px 12px;
192
- cursor: pointer;
193
- border-right: 1px solid var(--border);
194
- border-bottom: 2px solid transparent; /* Reserve space for active indicator */
195
- white-space: nowrap;
196
- flex-shrink: 0;
197
- position: relative;
198
- }
199
-
200
- .tab:hover {
201
- background: var(--tab-hover);
202
- }
203
-
204
- .tab.active {
205
- background: var(--bg-tertiary);
206
- border-bottom: 2px solid var(--accent); /* Blue accent line */
207
- }
208
-
209
- .tab.new-tab {
210
- animation: tab-pulse 0.5s ease-out;
211
- }
212
-
213
- @keyframes tab-pulse {
214
- 0% { background: var(--accent); }
215
- 100% { background: var(--tab-active); }
216
- }
217
-
218
- .tab .icon {
219
- font-size: 14px;
220
- }
221
-
222
- .tab .name {
223
- font-size: 13px;
224
- max-width: 120px;
225
- overflow: hidden;
226
- text-overflow: ellipsis;
227
- color: var(--text-secondary);
228
- }
229
-
230
- .tab.active .name {
231
- color: var(--text-primary);
232
- }
233
-
234
- .tab .status-dot {
235
- width: 6px;
236
- height: 6px;
237
- border-radius: 50%;
238
- }
239
-
240
- /* Shape modifiers for accessibility (not just color) */
241
- .tab .status-dot--diamond {
242
- border-radius: 1px;
243
- transform: rotate(45deg);
244
- }
245
-
246
- /* Ring shape for pr-ready (accessibility: distinct from circle) */
247
- .tab .status-dot--ring {
248
- box-shadow: inset 0 0 0 1.5px currentColor;
249
- background: transparent !important;
250
- color: var(--status-waiting);
251
- }
252
-
253
- /* Distinct animations per status category (spec 0019) */
254
- @keyframes status-pulse {
255
- /* Pulsing: Active/working (spawning, implementing) */
256
- 0%, 100% { opacity: 1; transform: scale(1); }
257
- 50% { opacity: 0.7; transform: scale(0.9); }
258
- }
259
-
260
- @keyframes status-blink-slow {
261
- /* Slow blink: Idle/waiting (pr-ready) */
262
- 0%, 100% { opacity: 1; }
263
- 50% { opacity: 0.3; }
264
- }
265
-
266
- @keyframes status-blink-fast {
267
- /* Fast blink: Error/blocked */
268
- 0%, 100% { opacity: 1; }
269
- 50% { opacity: 0.2; }
270
- }
271
-
272
- .tab .status-dot--pulse {
273
- animation: status-pulse 2s ease-in-out infinite;
274
- }
275
-
276
- .tab .status-dot--blink-slow {
277
- animation: status-blink-slow 3s ease-in-out infinite;
278
- }
279
-
280
- .tab .status-dot--blink-fast {
281
- animation: status-blink-fast 0.8s ease-in-out infinite;
282
- }
283
-
284
- /* Respect reduced motion preference (WCAG 2.3.3) */
285
- /* Motion-independent differentiators remain: diamond for blocked, ring for pr-ready */
286
- @media (prefers-reduced-motion: reduce) {
287
- .tab .status-dot--pulse,
288
- .tab .status-dot--blink-slow,
289
- .tab .status-dot--blink-fast {
290
- animation: none;
291
- }
292
- }
293
-
294
- .tab .close {
295
- opacity: 0.6; /* Always clearly visible */
296
- margin-left: 6px;
297
- font-size: 16px;
298
- font-weight: 500;
299
- color: var(--text-secondary);
300
- padding: 4px 8px;
301
- border-radius: 4px;
302
- cursor: pointer;
303
- line-height: 1;
304
- min-width: 24px;
305
- min-height: 24px;
306
- display: flex;
307
- align-items: center;
308
- justify-content: center;
309
- }
310
-
311
- .tab:hover .close {
312
- opacity: 0.9;
313
- }
314
-
315
- .tab .close:hover {
316
- opacity: 1;
317
- background: rgba(239, 68, 68, 0.2); /* Red tint on hover */
318
- color: #ef4444;
319
- }
320
-
321
- /* Add buttons */
322
- .add-buttons {
323
- display: flex;
324
- gap: 4px;
325
- padding: 0 8px;
326
- flex-shrink: 0;
327
- }
328
-
329
- .add-btn {
330
- padding: 4px 8px;
331
- border-radius: 4px;
332
- border: 1px dashed var(--border);
333
- background: transparent;
334
- color: var(--text-muted);
335
- cursor: pointer;
336
- font-size: 12px;
337
- display: flex;
338
- align-items: center;
339
- gap: 4px;
340
- }
341
-
342
- .add-btn:hover {
343
- border-style: solid;
344
- color: var(--text-secondary);
345
- background: var(--bg-tertiary);
346
- }
347
-
348
- /* Overflow indicator */
349
- .overflow-btn {
350
- padding: 8px 12px;
351
- background: var(--bg-tertiary);
352
- border: none;
353
- border-left: 1px solid var(--border);
354
- color: var(--text-secondary);
355
- cursor: pointer;
356
- display: none; /* Hidden by default, shown via JS */
357
- align-items: center;
358
- gap: 4px;
359
- flex-shrink: 0;
360
- }
361
-
362
- .overflow-btn:hover {
363
- background: var(--tab-hover);
364
- }
365
-
366
- .overflow-btn:focus {
367
- outline: 2px solid var(--accent);
368
- outline-offset: -2px;
369
- }
370
-
371
- .overflow-count {
372
- font-size: 11px;
373
- background: var(--accent);
374
- color: white;
375
- padding: 1px 5px;
376
- border-radius: 8px;
377
- }
378
-
379
- /* Overflow menu dropdown */
380
- .overflow-menu {
381
- position: absolute;
382
- right: 0;
383
- top: 100%;
384
- background: var(--bg-secondary);
385
- border: 1px solid var(--border);
386
- border-radius: 4px;
387
- box-shadow: 0 4px 12px rgba(0,0,0,0.3);
388
- max-height: 300px;
389
- overflow-y: auto;
390
- min-width: 200px;
391
- z-index: 100;
392
- }
393
-
394
- .overflow-menu.hidden {
395
- display: none;
396
- }
397
-
398
- .overflow-menu-item {
399
- padding: 8px 12px;
400
- cursor: pointer;
401
- display: flex;
402
- align-items: center;
403
- gap: 8px;
404
- font-size: 13px;
405
- }
406
-
407
- .overflow-menu-item:hover,
408
- .overflow-menu-item:focus {
409
- background: var(--tab-hover);
410
- outline: none;
411
- }
412
-
413
- .overflow-menu-item.active {
414
- background: var(--tab-active);
415
- border-left: 2px solid var(--accent);
416
- }
417
-
418
- .overflow-menu-item .icon {
419
- font-size: 14px;
420
- }
421
-
422
- .overflow-menu-item .name {
423
- flex: 1;
424
- overflow: hidden;
425
- text-overflow: ellipsis;
426
- white-space: nowrap;
427
- }
428
-
429
- .overflow-menu-item .open-external {
430
- opacity: 0.5;
431
- cursor: pointer;
432
- padding: 2px 6px;
433
- font-size: 12px;
434
- border-radius: 3px;
435
- }
436
-
437
- .overflow-menu-item .open-external:hover {
438
- opacity: 1;
439
- background: rgba(255, 255, 255, 0.1);
440
- }
441
-
442
- /* Tab content */
443
- .tab-content {
444
- flex: 1;
445
- display: flex;
446
- flex-direction: column;
447
- overflow: hidden;
448
- }
449
-
450
- .tab-content iframe {
451
- flex: 1;
452
- width: 100%;
453
- border: none;
454
- background: #000;
455
- }
456
-
457
- .empty-state {
458
- flex: 1;
459
- display: flex;
460
- flex-direction: column;
461
- align-items: center;
462
- justify-content: center;
463
- color: var(--text-muted);
464
- gap: 12px;
465
- }
466
-
467
- .empty-state .hint {
468
- font-size: 13px;
469
- text-align: center;
470
- max-width: 300px;
471
- }
472
-
473
- /* Status bar */
474
- .status-bar {
475
- padding: 8px 16px;
476
- background: var(--bg-secondary);
477
- border-top: 1px solid var(--border);
478
- font-size: 12px;
479
- color: var(--text-muted);
480
- display: flex;
481
- gap: 16px;
482
- }
483
-
484
- .status-item {
485
- display: flex;
486
- align-items: center;
487
- gap: 6px;
488
- }
489
-
490
- .status-item .dot {
491
- width: 6px;
492
- height: 6px;
493
- border-radius: 50%;
494
- }
495
-
496
- /* Dialogs */
497
- .dialog-overlay {
498
- position: fixed;
499
- top: 0;
500
- left: 0;
501
- right: 0;
502
- bottom: 0;
503
- background: rgba(0, 0, 0, 0.6);
504
- display: flex;
505
- align-items: center;
506
- justify-content: center;
507
- z-index: 1000;
508
- }
509
-
510
- .dialog-overlay.hidden {
511
- display: none;
512
- }
513
-
514
- .dialog {
515
- background: var(--bg-secondary);
516
- border: 1px solid var(--border);
517
- border-radius: 8px;
518
- padding: 20px;
519
- min-width: 320px;
520
- max-width: 90%;
521
- }
522
-
523
- .dialog h3 {
524
- margin-bottom: 16px;
525
- font-size: 16px;
526
- font-weight: 500;
527
- }
528
-
529
- .dialog input {
530
- width: 100%;
531
- padding: 8px 12px;
532
- border-radius: 4px;
533
- border: 1px solid var(--border);
534
- background: var(--bg-tertiary);
535
- color: var(--text-primary);
536
- font-size: 14px;
537
- margin-bottom: 16px;
538
- }
539
-
540
- .dialog input:focus {
541
- outline: none;
542
- border-color: var(--accent);
543
- }
544
-
545
- .dialog-actions {
546
- display: flex;
547
- justify-content: flex-end;
548
- gap: 8px;
549
- }
550
-
551
- .quick-paths {
552
- display: flex;
553
- flex-wrap: wrap;
554
- gap: 8px;
555
- margin-bottom: 12px;
556
- }
557
-
558
- .quick-path {
559
- padding: 4px 8px;
560
- border-radius: 4px;
561
- background: var(--bg-tertiary);
562
- border: 1px solid var(--border);
563
- color: var(--text-secondary);
564
- cursor: pointer;
565
- font-size: 12px;
566
- }
567
-
568
- .quick-path:hover {
569
- background: var(--tab-hover);
570
- border-color: var(--accent);
571
- }
572
-
573
- /* Toast notifications */
574
- .toast-container {
575
- position: fixed;
576
- bottom: 60px;
577
- right: 16px;
578
- z-index: 2000;
579
- display: flex;
580
- flex-direction: column;
581
- gap: 8px;
582
- }
583
-
584
- .toast {
585
- padding: 12px 16px;
586
- background: var(--bg-secondary);
587
- border: 1px solid var(--border);
588
- border-radius: 6px;
589
- font-size: 13px;
590
- display: flex;
591
- align-items: center;
592
- gap: 8px;
593
- animation: toast-in 0.3s ease-out;
594
- }
595
-
596
- .toast.error {
597
- border-color: #ef4444;
598
- }
599
-
600
- .toast.success {
601
- border-color: #22c55e;
602
- }
603
-
604
- @keyframes toast-in {
605
- from {
606
- opacity: 0;
607
- transform: translateY(10px);
608
- }
609
- to {
610
- opacity: 1;
611
- transform: translateY(0);
612
- }
613
- }
614
-
615
- /* Context menu */
616
- .context-menu {
617
- position: fixed;
618
- background: var(--bg-secondary);
619
- border: 1px solid var(--border);
620
- border-radius: 4px;
621
- padding: 4px 0;
622
- min-width: 150px;
623
- z-index: 1000;
624
- }
625
-
626
- .context-menu.hidden {
627
- display: none;
628
- }
629
-
630
- .context-menu-item {
631
- padding: 8px 12px;
632
- cursor: pointer;
633
- font-size: 13px;
634
- }
635
-
636
- .context-menu-item:hover {
637
- background: var(--tab-hover);
638
- }
639
-
640
- .context-menu-item.danger {
641
- color: #ef4444;
642
- }
643
- </style>
644
- </head>
645
- <body>
646
- <header class="header">
647
- <h1>Agent Farm - {{PROJECT_NAME}}</h1>
648
- </header>
649
-
650
- <main class="main">
651
- <!-- Left pane: Architect terminal -->
652
- <div class="left-pane">
653
- <div class="pane-header">
654
- <span class="status-dot" id="architect-status"></span>
655
- <span>Architect</span>
656
- </div>
657
- <div id="architect-content"></div>
658
- </div>
659
-
660
- <!-- Right pane: Tabbed interface -->
661
- <div class="right-pane">
662
- <div class="tab-bar">
663
- <div class="tabs-scroll" id="tabs-container"></div>
664
- <button class="overflow-btn" id="overflow-btn" onclick="toggleOverflowMenu()" aria-haspopup="true" aria-expanded="false" title="Show all tabs">
665
- <span>...</span>
666
- <span class="overflow-count" id="overflow-count">+0</span>
667
- </button>
668
- <div class="overflow-menu hidden" id="overflow-menu" role="menu"></div>
669
- <div class="add-buttons">
670
- <button class="add-btn" onclick="showFileDialog()" title="Open file">+ 📄</button>
671
- <button class="add-btn" onclick="spawnBuilder()" title="Spawn worktree builder">+ 🔨</button>
672
- <button class="add-btn" onclick="spawnShell()" title="New shell">+ >_</button>
673
- </div>
674
- </div>
675
- <div class="tab-content" id="tab-content"></div>
676
- </div>
677
- </main>
678
-
679
- <footer class="status-bar">
680
- <div class="status-item" id="status-architect">
681
- <span class="dot" style="background: var(--text-muted)"></span>
682
- <span>Architect: stopped</span>
683
- </div>
684
- <div class="status-item" id="status-builders">
685
- <span>0 builders</span>
686
- </div>
687
- <div class="status-item" id="status-shells">
688
- <span>0 shells</span>
689
- </div>
690
- <div class="status-item" id="status-files">
691
- <span>0 files</span>
692
- </div>
693
- </footer>
694
-
695
- <!-- File picker dialog -->
696
- <div class="dialog-overlay hidden" id="file-dialog">
697
- <div class="dialog">
698
- <h3>Open File</h3>
699
- <div class="quick-paths">
700
- <button class="quick-path" onclick="setFilePath('codev/specs/')">codev/specs/</button>
701
- <button class="quick-path" onclick="setFilePath('codev/plans/')">codev/plans/</button>
702
- <button class="quick-path" onclick="setFilePath('codev/reviews/')">codev/reviews/</button>
703
- </div>
704
- <input type="text" id="file-path-input" placeholder="Enter file path..." />
705
- <div class="dialog-actions">
706
- <button class="btn" onclick="hideFileDialog()">Cancel</button>
707
- <button class="btn" onclick="openFile()">Open</button>
708
- </div>
709
- </div>
710
- </div>
711
-
712
- <!-- Close confirmation dialog -->
713
- <div class="dialog-overlay hidden" id="close-dialog">
714
- <div class="dialog">
715
- <h3 id="close-dialog-title">Close tab?</h3>
716
- <p id="close-dialog-message" style="color: var(--text-secondary); margin-bottom: 16px; font-size: 14px;"></p>
717
- <div class="dialog-actions">
718
- <button class="btn" onclick="hideCloseDialog()">Cancel</button>
719
- <button class="btn btn-danger" onclick="confirmClose()">Close</button>
720
- </div>
721
- </div>
722
- </div>
723
-
724
- <!-- Context menu -->
725
- <div class="context-menu hidden" id="context-menu" role="menu">
726
- <div class="context-menu-item" role="menuitem" tabindex="0" data-action="openContextTab" onclick="openContextTab()" onkeydown="handleContextMenuKeydown(event)">Open in New Tab</div>
727
- <div class="context-menu-item" role="menuitem" tabindex="-1" data-action="closeActiveTab" onclick="closeActiveTab()" onkeydown="handleContextMenuKeydown(event)">Close</div>
728
- <div class="context-menu-item" role="menuitem" tabindex="-1" data-action="closeOtherTabs" onclick="closeOtherTabs()" onkeydown="handleContextMenuKeydown(event)">Close Others</div>
729
- <div class="context-menu-item danger" role="menuitem" tabindex="-1" data-action="closeAllTabs" onclick="closeAllTabs()" onkeydown="handleContextMenuKeydown(event)">Close All</div>
730
- </div>
731
-
732
- <!-- Toast container -->
733
- <div class="toast-container" id="toast-container"></div>
734
-
735
- <script>
736
- // STATE_INJECTION_POINT
737
-
738
- // State management
739
- const state = window.INITIAL_STATE || {
740
- architect: null,
741
- builders: [],
742
- utils: [],
743
- annotations: []
744
- };
745
-
746
- // Tab state
747
- let tabs = [];
748
- let activeTabId = null;
749
- let pendingCloseTabId = null;
750
- let contextMenuTabId = null;
751
-
752
- // Initialize
753
- function init() {
754
- buildTabsFromState();
755
- renderArchitect();
756
- renderTabs();
757
- renderTabContent();
758
- updateStatusBar();
759
- startPolling();
760
- setupBroadcastChannel();
761
- setupOverflowDetection();
762
- }
763
-
764
- // Set up overflow detection for the tab bar
765
- function setupOverflowDetection() {
766
- const container = document.getElementById('tabs-container');
767
-
768
- // Check on load
769
- checkTabOverflow();
770
-
771
- // Check on window resize (debounced)
772
- let resizeTimeout;
773
- window.addEventListener('resize', () => {
774
- clearTimeout(resizeTimeout);
775
- resizeTimeout = setTimeout(checkTabOverflow, 100);
776
- });
777
-
778
- // Check on scroll (debounced) - updates +N count when user scrolls tabs
779
- if (container) {
780
- let scrollTimeout;
781
- container.addEventListener('scroll', () => {
782
- clearTimeout(scrollTimeout);
783
- scrollTimeout = setTimeout(checkTabOverflow, 50);
784
- });
785
- }
786
-
787
- // Also use ResizeObserver for the tabs container if available
788
- if (typeof ResizeObserver !== 'undefined') {
789
- if (container) {
790
- const observer = new ResizeObserver(() => {
791
- checkTabOverflow();
792
- });
793
- observer.observe(container);
794
- }
795
- }
796
- }
797
-
798
- // Set up BroadcastChannel for cross-tab communication
799
- // This allows terminal file clicks to open files in the dashboard
800
- function setupBroadcastChannel() {
801
- const channel = new BroadcastChannel('agent-farm');
802
- channel.onmessage = async (event) => {
803
- const { type, path, line } = event.data;
804
- if (type === 'openFile' && path) {
805
- await openFileFromMessage(path, line);
806
- }
807
- };
808
- }
809
-
810
- // Open a file from a BroadcastChannel message
811
- async function openFileFromMessage(filePath, lineNumber) {
812
- try {
813
- // Check if file is already open
814
- const existingTab = tabs.find(t => t.type === 'file' && t.path === filePath);
815
- if (existingTab) {
816
- // Just switch to the existing tab
817
- selectTab(existingTab.id);
818
- showToast(`Switched to ${getFileName(filePath)}`, 'success');
819
- // TODO: scroll to line if lineNumber provided
820
- return;
821
- }
822
-
823
- // Open the file via API
824
- const response = await fetch('/api/tabs/file', {
825
- method: 'POST',
826
- headers: { 'Content-Type': 'application/json' },
827
- body: JSON.stringify({ path: filePath })
828
- });
829
-
830
- if (!response.ok) {
831
- throw new Error(await response.text());
832
- }
833
-
834
- const result = await response.json();
835
-
836
- // Refresh state and switch to the new tab
837
- await refresh();
838
-
839
- // Find and select the new file tab
840
- const newTab = tabs.find(t => t.type === 'file' && (t.path === filePath || t.annotationId === result.id));
841
- if (newTab) {
842
- selectTab(newTab.id);
843
- }
844
-
845
- showToast(`Opened ${getFileName(filePath)}${lineNumber ? ':' + lineNumber : ''}`, 'success');
846
- } catch (err) {
847
- showToast('Failed to open file: ' + err.message, 'error');
848
- }
849
- }
850
-
851
- // Track known tab IDs to detect new tabs
852
- let knownTabIds = new Set();
853
-
854
- // Build tabs from initial state
855
- function buildTabsFromState() {
856
- const previousTabIds = new Set(tabs.map(t => t.id));
857
- tabs = [];
858
-
859
- // Add file tabs from annotations
860
- for (const annotation of state.annotations || []) {
861
- tabs.push({
862
- id: `file-${annotation.id}`,
863
- type: 'file',
864
- name: getFileName(annotation.file),
865
- path: annotation.file,
866
- port: annotation.port,
867
- annotationId: annotation.id
868
- });
869
- }
870
-
871
- // Add builder tabs
872
- for (const builder of state.builders || []) {
873
- tabs.push({
874
- id: `builder-${builder.id}`,
875
- type: 'builder',
876
- name: builder.name || `Builder ${builder.id}`,
877
- projectId: builder.id,
878
- port: builder.port,
879
- status: builder.status
880
- });
881
- }
882
-
883
- // Add shell tabs
884
- for (const util of state.utils || []) {
885
- tabs.push({
886
- id: `shell-${util.id}`,
887
- type: 'shell',
888
- name: util.name,
889
- port: util.port,
890
- utilId: util.id
891
- });
892
- }
893
-
894
- // Detect new tabs and auto-switch to them
895
- for (const tab of tabs) {
896
- if (!knownTabIds.has(tab.id) && previousTabIds.size > 0) {
897
- // This is a new tab - switch to it
898
- activeTabId = tab.id;
899
- break;
900
- }
901
- }
902
-
903
- // Update known tab IDs
904
- knownTabIds = new Set(tabs.map(t => t.id));
905
-
906
- // Set active tab to first one if none selected
907
- if (tabs.length > 0 && !activeTabId) {
908
- activeTabId = tabs[0].id;
909
- }
910
- }
911
-
912
- // Get filename from path
913
- function getFileName(path) {
914
- const parts = path.split('/');
915
- return parts[parts.length - 1];
916
- }
917
-
918
- // Track current architect port to avoid re-rendering iframe unnecessarily
919
- let currentArchitectPort = null;
920
-
921
- // Render architect pane
922
- function renderArchitect() {
923
- const content = document.getElementById('architect-content');
924
- const statusDot = document.getElementById('architect-status');
925
-
926
- if (state.architect && state.architect.port) {
927
- statusDot.classList.remove('inactive');
928
- // Only update iframe if port changed (avoid flashing on poll)
929
- if (currentArchitectPort !== state.architect.port) {
930
- currentArchitectPort = state.architect.port;
931
- content.innerHTML = `<iframe src="http://localhost:${state.architect.port}" title="Architect Terminal" allow="clipboard-read; clipboard-write"></iframe>`;
932
- }
933
- } else {
934
- if (currentArchitectPort !== null) {
935
- currentArchitectPort = null;
936
- content.innerHTML = `
937
- <div class="architect-placeholder">
938
- <p>Architect not running</p>
939
- <p>Run <code>agent-farm start</code> to begin</p>
940
- </div>
941
- `;
942
- }
943
- statusDot.classList.add('inactive');
944
- }
945
- }
946
-
947
- // Render tabs
948
- function renderTabs() {
949
- const container = document.getElementById('tabs-container');
950
-
951
- if (tabs.length === 0) {
952
- container.innerHTML = '';
953
- checkTabOverflow(); // Update overflow state when tabs cleared
954
- return;
955
- }
956
-
957
- container.innerHTML = tabs.map(tab => {
958
- const isActive = tab.id === activeTabId;
959
- const icon = getTabIcon(tab.type);
960
- const statusDot = tab.type === 'builder' ? getStatusDot(tab.status) : '';
961
- const tooltip = getTabTooltip(tab);
962
-
963
- return `
964
- <div class="tab ${isActive ? 'active' : ''}"
965
- onclick="selectTab('${tab.id}')"
966
- oncontextmenu="showContextMenu(event, '${tab.id}')"
967
- data-tab-id="${tab.id}"
968
- title="${tooltip}">
969
- <span class="icon">${icon}</span>
970
- <span class="name">${tab.name}</span>
971
- ${statusDot}
972
- <span class="close"
973
- onclick="event.stopPropagation(); closeTab('${tab.id}', event)"
974
- role="button"
975
- tabindex="0"
976
- aria-label="Close ${tab.name}"
977
- onkeydown="if(event.key==='Enter'||event.key===' '){event.stopPropagation();closeTab('${tab.id}',event)}">&times;</span>
978
- </div>
979
- `;
980
- }).join('');
981
-
982
- // Check overflow after tabs are rendered
983
- checkTabOverflow();
984
- }
985
-
986
- // Get tab icon
987
- function getTabIcon(type) {
988
- switch (type) {
989
- case 'file': return '📄';
990
- case 'builder': return '🔨';
991
- case 'shell': return '>_';
992
- default: return '?';
993
- }
994
- }
995
-
996
- // Status configuration - hoisted for performance (per Codex review)
997
- // Colors per spec 0019: green=active, yellow=waiting, red=blocked, gray=complete
998
- // Animations per spec 0019: pulse=active, blink-slow=waiting, blink-fast=blocked, static=complete
999
- // Shapes for accessibility: circle=default, diamond=blocked, ring=waiting
1000
- const STATUS_CONFIG = {
1001
- 'spawning': { color: 'var(--status-active)', label: 'Spawning', shape: 'circle', animation: 'pulse' },
1002
- 'implementing': { color: 'var(--status-active)', label: 'Implementing', shape: 'circle', animation: 'pulse' },
1003
- 'blocked': { color: 'var(--status-error)', label: 'Blocked', shape: 'diamond', animation: 'blink-fast' },
1004
- 'pr-ready': { color: 'var(--status-waiting)', label: 'PR Ready', shape: 'ring', animation: 'blink-slow' },
1005
- 'complete': { color: 'var(--status-complete)', label: 'Complete', shape: 'circle', animation: null }
1006
- };
1007
- const DEFAULT_STATUS_CONFIG = { color: 'var(--text-muted)', label: 'Unknown', shape: 'circle', animation: null };
1008
-
1009
- // Get status dot HTML with accessibility support
1010
- // Accessibility: distinct animations per status, shapes for reduced-motion users
1011
- // Uses role="img" instead of role="status" to avoid screen reader chatter on poll (per Codex review)
1012
- function getStatusDot(status) {
1013
- const config = STATUS_CONFIG[status] || { ...DEFAULT_STATUS_CONFIG, label: status || 'Unknown' };
1014
- // Build CSS classes for accessibility
1015
- const classes = ['status-dot'];
1016
- if (config.shape === 'diamond') classes.push('status-dot--diamond');
1017
- if (config.shape === 'ring') classes.push('status-dot--ring');
1018
- if (config.animation === 'pulse') classes.push('status-dot--pulse');
1019
- if (config.animation === 'blink-slow') classes.push('status-dot--blink-slow');
1020
- if (config.animation === 'blink-fast') classes.push('status-dot--blink-fast');
1021
- return `<span class="${classes.join(' ')}" style="background: ${config.color}" title="${config.label}" role="img" aria-label="${config.label}"></span>`;
1022
- }
1023
-
1024
- // Escape HTML special characters to prevent XSS
1025
- function escapeHtml(text) {
1026
- return String(text)
1027
- .replace(/&/g, '&amp;')
1028
- .replace(/</g, '&lt;')
1029
- .replace(/>/g, '&gt;')
1030
- .replace(/"/g, '&quot;')
1031
- .replace(/'/g, '&#39;');
1032
- }
1033
-
1034
- // Generate tooltip text for tab hover
1035
- function getTabTooltip(tab) {
1036
- const lines = [tab.name];
1037
-
1038
- if (tab.type === 'builder') {
1039
- if (tab.port) lines.push(`Port: ${tab.port}`);
1040
- lines.push(`Status: ${tab.status || 'unknown'}`);
1041
- // Extract project ID from tab id (e.g., "builder-0037" -> "0037")
1042
- const projectId = tab.id.replace('builder-', '');
1043
- lines.push(`Worktree: .builders/${projectId}`);
1044
- } else if (tab.type === 'file') {
1045
- lines.push(`Path: ${tab.path}`);
1046
- if (tab.port) lines.push(`Port: ${tab.port}`);
1047
- } else if (tab.type === 'shell') {
1048
- if (tab.port) lines.push(`Port: ${tab.port}`);
1049
- }
1050
-
1051
- return escapeHtml(lines.join('\n'));
1052
- }
1053
-
1054
- // Track current tab content to avoid re-rendering iframe unnecessarily
1055
- let currentTabPort = null;
1056
-
1057
- // Render tab content
1058
- function renderTabContent() {
1059
- const content = document.getElementById('tab-content');
1060
-
1061
- if (!activeTabId || tabs.length === 0) {
1062
- if (currentTabPort !== null) {
1063
- currentTabPort = null;
1064
- content.innerHTML = `
1065
- <div class="empty-state">
1066
- <p>No tabs open</p>
1067
- <p class="hint">Click the + buttons above or ask the architect to open files/builders</p>
1068
- </div>
1069
- `;
1070
- }
1071
- return;
1072
- }
1073
-
1074
- const tab = tabs.find(t => t.id === activeTabId);
1075
- if (!tab) {
1076
- if (currentTabPort !== null) {
1077
- currentTabPort = null;
1078
- content.innerHTML = '<div class="empty-state"><p>Tab not found</p></div>';
1079
- }
1080
- return;
1081
- }
1082
-
1083
- // Only update iframe if port changed (avoid flashing on poll)
1084
- if (currentTabPort !== tab.port) {
1085
- currentTabPort = tab.port;
1086
- content.innerHTML = `<iframe src="http://localhost:${tab.port}" title="${tab.name}" allow="clipboard-read; clipboard-write"></iframe>`;
1087
- }
1088
- }
1089
-
1090
- // Update status bar
1091
- function updateStatusBar() {
1092
- // Architect status
1093
- const archStatus = document.getElementById('status-architect');
1094
- if (state.architect) {
1095
- archStatus.innerHTML = `
1096
- <span class="dot" style="background: var(--status-active)"></span>
1097
- <span>Architect: running</span>
1098
- `;
1099
- } else {
1100
- archStatus.innerHTML = `
1101
- <span class="dot" style="background: var(--text-muted)"></span>
1102
- <span>Architect: stopped</span>
1103
- `;
1104
- }
1105
-
1106
- // Counts
1107
- const builderCount = (state.builders || []).length;
1108
- const shellCount = (state.utils || []).length;
1109
- const fileCount = (state.annotations || []).length;
1110
-
1111
- document.getElementById('status-builders').innerHTML = `<span>${builderCount} builder${builderCount !== 1 ? 's' : ''}</span>`;
1112
- document.getElementById('status-shells').innerHTML = `<span>${shellCount} shell${shellCount !== 1 ? 's' : ''}</span>`;
1113
- document.getElementById('status-files').innerHTML = `<span>${fileCount} file${fileCount !== 1 ? 's' : ''}</span>`;
1114
- }
1115
-
1116
- // Select tab
1117
- function selectTab(tabId) {
1118
- activeTabId = tabId;
1119
- renderTabs();
1120
- renderTabContent();
1121
- // Scroll the active tab into view if needed
1122
- scrollActiveTabIntoView();
1123
- }
1124
-
1125
- // Scroll the active tab into view
1126
- function scrollActiveTabIntoView() {
1127
- const container = document.getElementById('tabs-container');
1128
- const activeTab = container.querySelector('.tab.active');
1129
- if (activeTab) {
1130
- activeTab.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
1131
- }
1132
- }
1133
-
1134
- // Check if tabs are overflowing and update the overflow button
1135
- function checkTabOverflow() {
1136
- const container = document.getElementById('tabs-container');
1137
- const overflowBtn = document.getElementById('overflow-btn');
1138
- const overflowCount = document.getElementById('overflow-count');
1139
-
1140
- if (!container || !overflowBtn) return;
1141
-
1142
- const isOverflowing = container.scrollWidth > container.clientWidth;
1143
- overflowBtn.style.display = isOverflowing ? 'flex' : 'none';
1144
-
1145
- if (isOverflowing) {
1146
- // Count hidden tabs (those partially or fully outside visible area - both sides)
1147
- const tabElements = container.querySelectorAll('.tab');
1148
- const containerRect = container.getBoundingClientRect();
1149
- let hiddenCount = 0;
1150
-
1151
- tabElements.forEach(tab => {
1152
- const rect = tab.getBoundingClientRect();
1153
- // Tab is hidden if scrolled off the right edge
1154
- if (rect.right > containerRect.right + 1) {
1155
- hiddenCount++;
1156
- }
1157
- // Tab is hidden if scrolled off the left edge
1158
- else if (rect.left < containerRect.left - 1) {
1159
- hiddenCount++;
1160
- }
1161
- });
1162
-
1163
- overflowCount.textContent = `+${hiddenCount}`;
1164
- }
1165
- }
1166
-
1167
- // Toggle the overflow menu
1168
- function toggleOverflowMenu() {
1169
- const menu = document.getElementById('overflow-menu');
1170
- const btn = document.getElementById('overflow-btn');
1171
- const isHidden = menu.classList.contains('hidden');
1172
-
1173
- if (isHidden) {
1174
- showOverflowMenu();
1175
- } else {
1176
- hideOverflowMenu();
1177
- }
1178
- }
1179
-
1180
- // Show the overflow menu
1181
- function showOverflowMenu() {
1182
- const menu = document.getElementById('overflow-menu');
1183
- const btn = document.getElementById('overflow-btn');
1184
-
1185
- // Build menu items for all tabs
1186
- menu.innerHTML = tabs.map((tab, index) => {
1187
- const icon = getTabIcon(tab.type);
1188
- const isActive = tab.id === activeTabId;
1189
- return `
1190
- <div class="overflow-menu-item ${isActive ? 'active' : ''}"
1191
- role="menuitem"
1192
- tabindex="${index === 0 ? 0 : -1}"
1193
- data-tab-id="${tab.id}"
1194
- onclick="selectTabFromMenu('${tab.id}')"
1195
- onkeydown="handleOverflowMenuKeydown(event, '${tab.id}')">
1196
- <span class="icon">${icon}</span>
1197
- <span class="name">${tab.name}</span>
1198
- <span class="open-external"
1199
- onclick="event.stopPropagation(); openInNewTabFromMenu('${tab.id}')"
1200
- onkeydown="if(event.key==='Enter'||event.key===' '){event.stopPropagation();openInNewTabFromMenu('${tab.id}')}"
1201
- title="Open in new tab"
1202
- role="button"
1203
- tabindex="0"
1204
- aria-label="Open ${tab.name} in new tab">↗</span>
1205
- </div>
1206
- `;
1207
- }).join('');
1208
-
1209
- menu.classList.remove('hidden');
1210
- btn.setAttribute('aria-expanded', 'true');
1211
-
1212
- // Focus the first item
1213
- const firstItem = menu.querySelector('.overflow-menu-item');
1214
- if (firstItem) firstItem.focus();
1215
-
1216
- // Close on click outside (after a small delay to avoid immediate close)
1217
- setTimeout(() => {
1218
- document.addEventListener('click', handleOverflowClickOutside);
1219
- }, 0);
1220
- }
1221
-
1222
- // Hide the overflow menu
1223
- function hideOverflowMenu() {
1224
- const menu = document.getElementById('overflow-menu');
1225
- const btn = document.getElementById('overflow-btn');
1226
- menu.classList.add('hidden');
1227
- btn.setAttribute('aria-expanded', 'false');
1228
- document.removeEventListener('click', handleOverflowClickOutside);
1229
- }
1230
-
1231
- // Handle click outside overflow menu
1232
- function handleOverflowClickOutside(event) {
1233
- const menu = document.getElementById('overflow-menu');
1234
- const btn = document.getElementById('overflow-btn');
1235
- if (!menu.contains(event.target) && !btn.contains(event.target)) {
1236
- hideOverflowMenu();
1237
- }
1238
- }
1239
-
1240
- // Select tab from overflow menu
1241
- function selectTabFromMenu(tabId) {
1242
- hideOverflowMenu();
1243
- selectTab(tabId);
1244
- }
1245
-
1246
- // Open tab in new window from overflow menu
1247
- function openInNewTabFromMenu(tabId) {
1248
- hideOverflowMenu();
1249
- openInNewTab(tabId);
1250
- }
1251
-
1252
- // Handle keyboard navigation in overflow menu
1253
- function handleOverflowMenuKeydown(event, tabId) {
1254
- const menu = document.getElementById('overflow-menu');
1255
- const items = Array.from(menu.querySelectorAll('.overflow-menu-item'));
1256
- const currentIndex = items.findIndex(item => item === document.activeElement);
1257
-
1258
- switch (event.key) {
1259
- case 'ArrowDown':
1260
- event.preventDefault();
1261
- const nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
1262
- items[nextIndex].focus();
1263
- break;
1264
- case 'ArrowUp':
1265
- event.preventDefault();
1266
- const prevIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
1267
- items[prevIndex].focus();
1268
- break;
1269
- case 'Enter':
1270
- case ' ':
1271
- event.preventDefault();
1272
- selectTabFromMenu(tabId);
1273
- break;
1274
- case 'Escape':
1275
- event.preventDefault();
1276
- hideOverflowMenu();
1277
- document.getElementById('overflow-btn').focus();
1278
- break;
1279
- case 'Tab':
1280
- // Allow Tab to close menu and move focus
1281
- hideOverflowMenu();
1282
- break;
1283
- }
1284
- }
1285
-
1286
- // Close tab
1287
- function closeTab(tabId, event) {
1288
- const tab = tabs.find(t => t.id === tabId);
1289
- if (!tab) return;
1290
-
1291
- // Shift+click bypasses confirmation
1292
- if (event && event.shiftKey) {
1293
- doCloseTab(tabId);
1294
- return;
1295
- }
1296
-
1297
- // Files don't need confirmation
1298
- if (tab.type === 'file') {
1299
- doCloseTab(tabId);
1300
- return;
1301
- }
1302
-
1303
- // Show confirmation for builders and shells
1304
- pendingCloseTabId = tabId;
1305
- const dialog = document.getElementById('close-dialog');
1306
- const title = document.getElementById('close-dialog-title');
1307
- const message = document.getElementById('close-dialog-message');
1308
-
1309
- if (tab.type === 'builder') {
1310
- title.textContent = `Stop builder ${tab.name}?`;
1311
- message.textContent = 'This will terminate the builder process.';
1312
- } else {
1313
- title.textContent = `Close shell ${tab.name}?`;
1314
- message.textContent = 'This will terminate the shell process.';
1315
- }
1316
-
1317
- dialog.classList.remove('hidden');
1318
- }
1319
-
1320
- // Actually close the tab
1321
- async function doCloseTab(tabId) {
1322
- const tab = tabs.find(t => t.id === tabId);
1323
- if (!tab) return;
1324
-
1325
- try {
1326
- // Call API to close the tab
1327
- await fetch(`/api/tabs/${encodeURIComponent(tabId)}`, { method: 'DELETE' });
1328
-
1329
- // Remove from local state
1330
- tabs = tabs.filter(t => t.id !== tabId);
1331
-
1332
- // If closing active tab, switch to another
1333
- if (activeTabId === tabId) {
1334
- activeTabId = tabs.length > 0 ? tabs[tabs.length - 1].id : null;
1335
- }
1336
-
1337
- renderTabs();
1338
- renderTabContent();
1339
- showToast('Tab closed', 'success');
1340
- } catch (err) {
1341
- showToast('Failed to close tab: ' + err.message, 'error');
1342
- }
1343
- }
1344
-
1345
- // Confirm close from dialog
1346
- function confirmClose() {
1347
- if (pendingCloseTabId) {
1348
- doCloseTab(pendingCloseTabId);
1349
- hideCloseDialog();
1350
- }
1351
- }
1352
-
1353
- function hideCloseDialog() {
1354
- document.getElementById('close-dialog').classList.add('hidden');
1355
- pendingCloseTabId = null;
1356
- }
1357
-
1358
- // Context menu
1359
- function showContextMenu(event, tabId) {
1360
- event.preventDefault();
1361
- contextMenuTabId = tabId;
1362
-
1363
- const menu = document.getElementById('context-menu');
1364
- menu.style.left = event.clientX + 'px';
1365
- menu.style.top = event.clientY + 'px';
1366
- menu.classList.remove('hidden');
1367
-
1368
- // Focus first item for keyboard navigation
1369
- const firstItem = menu.querySelector('.context-menu-item');
1370
- if (firstItem) firstItem.focus();
1371
-
1372
- // Close on click outside
1373
- setTimeout(() => {
1374
- document.addEventListener('click', hideContextMenu, { once: true });
1375
- }, 0);
1376
- }
1377
-
1378
- function hideContextMenu() {
1379
- document.getElementById('context-menu').classList.add('hidden');
1380
- contextMenuTabId = null;
1381
- }
1382
-
1383
- // Handle keyboard navigation in context menu
1384
- function handleContextMenuKeydown(event) {
1385
- const menu = document.getElementById('context-menu');
1386
- const items = Array.from(menu.querySelectorAll('.context-menu-item'));
1387
- const currentIndex = items.findIndex(item => item === document.activeElement);
1388
-
1389
- switch (event.key) {
1390
- case 'ArrowDown':
1391
- event.preventDefault();
1392
- const nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
1393
- items[nextIndex].focus();
1394
- break;
1395
- case 'ArrowUp':
1396
- event.preventDefault();
1397
- const prevIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
1398
- items[prevIndex].focus();
1399
- break;
1400
- case 'Enter':
1401
- case ' ':
1402
- event.preventDefault();
1403
- const actionName = event.target.dataset.action;
1404
- if (actionName && typeof window[actionName] === 'function') {
1405
- window[actionName]();
1406
- }
1407
- break;
1408
- case 'Escape':
1409
- event.preventDefault();
1410
- hideContextMenu();
1411
- break;
1412
- case 'Tab':
1413
- hideContextMenu();
1414
- break;
1415
- }
1416
- }
1417
-
1418
- function closeActiveTab() {
1419
- if (contextMenuTabId) {
1420
- closeTab(contextMenuTabId);
1421
- }
1422
- hideContextMenu();
1423
- }
1424
-
1425
- function closeOtherTabs() {
1426
- if (contextMenuTabId) {
1427
- const otherTabs = tabs.filter(t => t.id !== contextMenuTabId);
1428
- otherTabs.forEach(t => doCloseTab(t.id));
1429
- }
1430
- hideContextMenu();
1431
- }
1432
-
1433
- function closeAllTabs() {
1434
- tabs.forEach(t => doCloseTab(t.id));
1435
- hideContextMenu();
1436
- }
1437
-
1438
- // Open tab content in a new browser tab
1439
- function openInNewTab(tabId) {
1440
- const tab = tabs.find(t => t.id === tabId);
1441
- if (!tab) return;
1442
-
1443
- let url;
1444
- if (tab.type === 'file') {
1445
- // File tabs use the annotation port
1446
- if (!tab.port) {
1447
- showToast('Tab not ready', 'error');
1448
- return;
1449
- }
1450
- url = `http://localhost:${tab.port}`;
1451
- } else {
1452
- // Builder or shell - direct port access
1453
- if (!tab.port) {
1454
- showToast('Tab not ready', 'error');
1455
- return;
1456
- }
1457
- url = `http://localhost:${tab.port}`;
1458
- }
1459
-
1460
- window.open(url, '_blank', 'noopener,noreferrer');
1461
- }
1462
-
1463
- // Open context menu tab in new tab
1464
- function openContextTab() {
1465
- if (contextMenuTabId) {
1466
- openInNewTab(contextMenuTabId);
1467
- }
1468
- hideContextMenu();
1469
- }
1470
-
1471
- // File dialog
1472
- function showFileDialog() {
1473
- document.getElementById('file-dialog').classList.remove('hidden');
1474
- document.getElementById('file-path-input').focus();
1475
- }
1476
-
1477
- function hideFileDialog() {
1478
- document.getElementById('file-dialog').classList.add('hidden');
1479
- document.getElementById('file-path-input').value = '';
1480
- }
1481
-
1482
- function setFilePath(path) {
1483
- document.getElementById('file-path-input').value = path;
1484
- document.getElementById('file-path-input').focus();
1485
- }
1486
-
1487
- async function openFile() {
1488
- const path = document.getElementById('file-path-input').value.trim();
1489
- if (!path) return;
1490
-
1491
- try {
1492
- const response = await fetch('/api/tabs/file', {
1493
- method: 'POST',
1494
- headers: { 'Content-Type': 'application/json' },
1495
- body: JSON.stringify({ path })
1496
- });
1497
-
1498
- if (!response.ok) {
1499
- throw new Error(await response.text());
1500
- }
1501
-
1502
- hideFileDialog();
1503
- await refresh();
1504
- showToast(`Opened ${path}`, 'success');
1505
- } catch (err) {
1506
- showToast('Failed to open file: ' + err.message, 'error');
1507
- }
1508
- }
1509
-
1510
- // Spawn worktree builder (no dialog - spawns with random ID)
1511
- async function spawnBuilder() {
1512
- try {
1513
- const response = await fetch('/api/tabs/builder', {
1514
- method: 'POST',
1515
- headers: { 'Content-Type': 'application/json' },
1516
- body: JSON.stringify({})
1517
- });
1518
-
1519
- if (!response.ok) {
1520
- throw new Error(await response.text());
1521
- }
1522
-
1523
- const result = await response.json();
1524
-
1525
- // Add to local tabs and select it
1526
- const newTab = {
1527
- id: `builder-${result.id}`,
1528
- type: 'builder',
1529
- name: result.name,
1530
- port: result.port
1531
- };
1532
- tabs.push(newTab);
1533
- activeTabId = newTab.id;
1534
- renderTabs();
1535
- renderTabContent();
1536
- showToast(`Builder ${result.name} spawned`, 'success');
1537
- } catch (err) {
1538
- showToast('Failed to spawn builder: ' + err.message, 'error');
1539
- }
1540
- }
1541
-
1542
- // Spawn shell
1543
- async function spawnShell() {
1544
- try {
1545
- const response = await fetch('/api/tabs/shell', {
1546
- method: 'POST',
1547
- headers: { 'Content-Type': 'application/json' },
1548
- body: JSON.stringify({})
1549
- });
1550
-
1551
- if (!response.ok) {
1552
- throw new Error(await response.text());
1553
- }
1554
-
1555
- const result = await response.json();
1556
-
1557
- // Add to local tabs and select it
1558
- const newTab = {
1559
- id: `shell-${result.id}`,
1560
- type: 'shell',
1561
- name: result.name,
1562
- port: result.port,
1563
- utilId: result.id,
1564
- pendingLoad: true // Mark as pending to delay iframe
1565
- };
1566
- tabs.push(newTab);
1567
- activeTabId = newTab.id;
1568
- renderTabs();
1569
-
1570
- // Show loading state, then load iframe after delay
1571
- const content = document.getElementById('tab-content');
1572
- content.innerHTML = '<div class="empty-state"><p>Starting shell...</p></div>';
1573
-
1574
- setTimeout(() => {
1575
- delete newTab.pendingLoad;
1576
- currentTabPort = null; // Force re-render
1577
- renderTabContent();
1578
- }, 800);
1579
-
1580
- showToast('Shell spawned', 'success');
1581
- } catch (err) {
1582
- showToast('Failed to spawn shell: ' + err.message, 'error');
1583
- }
1584
- }
1585
-
1586
- // Refresh state from API
1587
- async function refresh() {
1588
- try {
1589
- const response = await fetch('/api/state');
1590
- if (!response.ok) throw new Error('Failed to fetch state');
1591
-
1592
- const newState = await response.json();
1593
- Object.assign(state, newState);
1594
-
1595
- buildTabsFromState();
1596
- renderArchitect();
1597
- renderTabs();
1598
- renderTabContent();
1599
- updateStatusBar();
1600
- } catch (err) {
1601
- console.error('Refresh error:', err);
1602
- }
1603
- }
1604
-
1605
- // Toast notifications
1606
- function showToast(message, type = 'info') {
1607
- const container = document.getElementById('toast-container');
1608
- const toast = document.createElement('div');
1609
- toast.className = `toast ${type}`;
1610
- toast.textContent = message;
1611
- container.appendChild(toast);
1612
-
1613
- setTimeout(() => {
1614
- toast.remove();
1615
- }, 3000);
1616
- }
1617
-
1618
- // Polling for state updates
1619
- let pollInterval = null;
1620
-
1621
- function startPolling() {
1622
- pollInterval = setInterval(refresh, 1000);
1623
- }
1624
-
1625
- function stopPolling() {
1626
- if (pollInterval) {
1627
- clearInterval(pollInterval);
1628
- pollInterval = null;
1629
- }
1630
- }
1631
-
1632
- // Keyboard shortcuts
1633
- document.addEventListener('keydown', (e) => {
1634
- // Escape to close dialogs and menus
1635
- if (e.key === 'Escape') {
1636
- hideFileDialog();
1637
- hideCloseDialog();
1638
- hideContextMenu();
1639
- hideOverflowMenu();
1640
- }
1641
-
1642
- // Enter in dialogs
1643
- if (e.key === 'Enter') {
1644
- if (!document.getElementById('file-dialog').classList.contains('hidden')) {
1645
- openFile();
1646
- }
1647
- }
1648
-
1649
- // Ctrl+Tab / Ctrl+Shift+Tab to switch tabs
1650
- if (e.ctrlKey && e.key === 'Tab') {
1651
- e.preventDefault();
1652
- if (tabs.length < 2) return;
1653
-
1654
- const currentIndex = tabs.findIndex(t => t.id === activeTabId);
1655
- let newIndex;
1656
-
1657
- if (e.shiftKey) {
1658
- newIndex = currentIndex <= 0 ? tabs.length - 1 : currentIndex - 1;
1659
- } else {
1660
- newIndex = currentIndex >= tabs.length - 1 ? 0 : currentIndex + 1;
1661
- }
1662
-
1663
- selectTab(tabs[newIndex].id);
1664
- }
1665
-
1666
- // Ctrl+W to close current tab
1667
- if (e.ctrlKey && e.key === 'w') {
1668
- e.preventDefault();
1669
- if (activeTabId) {
1670
- closeTab(activeTabId, e);
1671
- }
1672
- }
1673
- });
1674
-
1675
- // Initialize on load
1676
- init();
1677
- </script>
1678
- </body>
1679
- </html>