@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.
- package/dist/agent-farm/cli.d.ts.map +1 -1
- package/dist/agent-farm/cli.js +19 -0
- package/dist/agent-farm/cli.js.map +1 -1
- package/dist/agent-farm/commands/cleanup.d.ts.map +1 -1
- package/dist/agent-farm/commands/cleanup.js +18 -1
- package/dist/agent-farm/commands/cleanup.js.map +1 -1
- package/dist/agent-farm/commands/consult.d.ts +16 -0
- package/dist/agent-farm/commands/consult.d.ts.map +1 -0
- package/dist/agent-farm/commands/consult.js +51 -0
- package/dist/agent-farm/commands/consult.js.map +1 -0
- package/dist/agent-farm/commands/open.js +6 -6
- package/dist/agent-farm/commands/open.js.map +1 -1
- package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
- package/dist/agent-farm/commands/spawn.js +51 -42
- package/dist/agent-farm/commands/spawn.js.map +1 -1
- package/dist/agent-farm/commands/start.d.ts.map +1 -1
- package/dist/agent-farm/commands/start.js +9 -14
- package/dist/agent-farm/commands/start.js.map +1 -1
- package/dist/agent-farm/commands/util.js +2 -2
- package/dist/agent-farm/commands/util.js.map +1 -1
- package/dist/agent-farm/db/errors.d.ts +4 -0
- package/dist/agent-farm/db/errors.d.ts.map +1 -1
- package/dist/agent-farm/db/errors.js +8 -0
- package/dist/agent-farm/db/errors.js.map +1 -1
- package/dist/agent-farm/servers/dashboard-server.js +125 -71
- package/dist/agent-farm/servers/dashboard-server.js.map +1 -1
- package/dist/agent-farm/servers/open-server.d.ts +9 -0
- package/dist/agent-farm/servers/open-server.d.ts.map +1 -0
- package/dist/agent-farm/servers/{annotate-server.js → open-server.js} +17 -15
- package/dist/agent-farm/servers/open-server.js.map +1 -0
- package/dist/agent-farm/servers/tower-server.js +4 -7
- package/dist/agent-farm/servers/tower-server.js.map +1 -1
- package/dist/agent-farm/state.d.ts +5 -0
- package/dist/agent-farm/state.d.ts.map +1 -1
- package/dist/agent-farm/state.js +17 -0
- package/dist/agent-farm/state.js.map +1 -1
- package/dist/agent-farm/types.d.ts +1 -1
- package/dist/agent-farm/types.d.ts.map +1 -1
- package/dist/agent-farm/utils/config.d.ts.map +1 -1
- package/dist/agent-farm/utils/config.js +13 -7
- package/dist/agent-farm/utils/config.js.map +1 -1
- package/dist/agent-farm/utils/port-registry.d.ts +1 -1
- package/dist/agent-farm/utils/port-registry.d.ts.map +1 -1
- package/dist/agent-farm/utils/port-registry.js +1 -1
- package/dist/agent-farm/utils/port-registry.js.map +1 -1
- package/dist/agent-farm/utils/shell.d.ts +19 -0
- package/dist/agent-farm/utils/shell.d.ts.map +1 -1
- package/dist/agent-farm/utils/shell.js +28 -0
- package/dist/agent-farm/utils/shell.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +33 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/adopt.d.ts +3 -0
- package/dist/commands/adopt.d.ts.map +1 -1
- package/dist/commands/adopt.js +31 -25
- package/dist/commands/adopt.js.map +1 -1
- package/dist/commands/consult/index.d.ts +3 -2
- package/dist/commands/consult/index.d.ts.map +1 -1
- package/dist/commands/consult/index.js +128 -54
- package/dist/commands/consult/index.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +88 -36
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/eject.d.ts +18 -0
- package/dist/commands/eject.d.ts.map +1 -0
- package/dist/commands/eject.js +149 -0
- package/dist/commands/eject.js.map +1 -0
- package/dist/commands/import.d.ts +16 -0
- package/dist/commands/import.d.ts.map +1 -0
- package/dist/commands/import.js +278 -0
- package/dist/commands/import.js.map +1 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +32 -27
- package/dist/commands/init.js.map +1 -1
- package/dist/lib/projectlist-parser.d.ts +70 -0
- package/dist/lib/projectlist-parser.d.ts.map +1 -0
- package/dist/lib/projectlist-parser.js +200 -0
- package/dist/lib/projectlist-parser.js.map +1 -0
- package/dist/lib/skeleton.d.ts +41 -0
- package/dist/lib/skeleton.d.ts.map +1 -0
- package/dist/lib/skeleton.js +110 -0
- package/dist/lib/skeleton.js.map +1 -0
- package/dist/lib/templates.d.ts +2 -1
- package/dist/lib/templates.d.ts.map +1 -1
- package/dist/lib/templates.js +11 -10
- package/dist/lib/templates.js.map +1 -1
- package/package.json +5 -4
- package/{templates → skeleton}/DEPENDENCIES.md +3 -48
- package/skeleton/bin/agent-farm +7 -0
- package/skeleton/docs/commands/agent-farm.md +469 -0
- package/skeleton/docs/commands/codev.md +253 -0
- package/skeleton/docs/commands/consult.md +286 -0
- package/skeleton/docs/commands/overview.md +108 -0
- package/skeleton/maintain/.gitkeep +2 -0
- package/{templates → skeleton}/protocols/experiment/protocol.md +2 -2
- package/skeleton/protocols/maintain/protocol.md +502 -0
- package/skeleton/protocols/maintain/templates/maintenance-run.md +64 -0
- package/{templates → skeleton}/protocols/spider/protocol.md +9 -9
- package/{templates/protocols/spider-solo → skeleton/protocols/spider}/templates/plan.md +22 -1
- package/{templates/protocols/spider-solo → skeleton/protocols/spider}/templates/spec.md +30 -1
- package/skeleton/protocols/tick/protocol.md +277 -0
- package/skeleton/resources/lessons-learned.md +30 -0
- package/skeleton/resources/workflow-reference.md +242 -0
- package/skeleton/roles/architect.md +283 -0
- package/{templates → skeleton}/roles/builder.md +2 -0
- package/skeleton/roles/review-types/impl-review.md +56 -0
- package/skeleton/roles/review-types/integration-review.md +68 -0
- package/skeleton/roles/review-types/plan-review.md +59 -0
- package/skeleton/roles/review-types/pr-ready.md +72 -0
- package/skeleton/roles/review-types/spec-review.md +55 -0
- package/skeleton/templates/lessons-learned.md +28 -0
- package/{templates → skeleton}/templates/projectlist.md +17 -16
- package/dist/agent-farm/servers/annotate-server.d.ts +0 -9
- package/dist/agent-farm/servers/annotate-server.d.ts.map +0 -1
- package/dist/agent-farm/servers/annotate-server.js.map +0 -1
- package/templates/agents/architecture-documenter.md +0 -189
- package/templates/agents/codev-updater.md +0 -276
- package/templates/agents/spider-protocol-updater.md +0 -118
- package/templates/annotate.html +0 -903
- package/templates/bin/agent-farm +0 -18
- package/templates/bin/annotate-server.js +0 -140
- package/templates/dashboard-split.html +0 -1679
- package/templates/dashboard.html +0 -149
- package/templates/protocols/maintain/protocol.md +0 -235
- package/templates/protocols/spider/templates/plan.md +0 -169
- package/templates/protocols/spider/templates/review.md +0 -207
- package/templates/protocols/spider/templates/spec.md +0 -140
- package/templates/protocols/spider-solo/protocol.md +0 -619
- package/templates/protocols/tick/protocol.md +0 -250
- package/templates/roles/architect.md +0 -230
- package/templates/tower.html +0 -1032
- /package/{templates/AGENTS.md → skeleton/AGENTS.md.template} +0 -0
- /package/{templates/CLAUDE.md → skeleton/CLAUDE.md.template} +0 -0
- /package/{templates → skeleton}/bin/codev-doctor +0 -0
- /package/{templates → skeleton}/builders.md +0 -0
- /package/{templates → skeleton}/config.json +0 -0
- /package/{templates → skeleton}/plans/.gitkeep +0 -0
- /package/{templates → skeleton}/protocols/experiment/templates/notes.md +0 -0
- /package/{templates/protocols/spider-solo → skeleton/protocols/spider}/templates/review.md +0 -0
- /package/{templates → skeleton}/protocols/tick/templates/plan.md +0 -0
- /package/{templates → skeleton}/protocols/tick/templates/review.md +0 -0
- /package/{templates → skeleton}/protocols/tick/templates/spec.md +0 -0
- /package/{templates → skeleton}/reviews/.gitkeep +0 -0
- /package/{templates → skeleton}/roles/consultant.md +0 -0
- /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)}">×</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, '&')
|
|
1028
|
-
.replace(/</g, '<')
|
|
1029
|
-
.replace(/>/g, '>')
|
|
1030
|
-
.replace(/"/g, '"')
|
|
1031
|
-
.replace(/'/g, ''');
|
|
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>
|