ralphflow 0.2.0 → 0.4.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/README.md +67 -27
- package/dist/chunk-GVOJO5IN.js +274 -0
- package/dist/ralphflow.js +372 -322
- package/dist/server-O6J52DZT.js +323 -0
- package/package.json +7 -2
- package/src/dashboard/ui/index.html +838 -0
- package/src/templates/code-implementation/loops/00-story-loop/prompt.md +19 -11
- package/src/templates/code-implementation/loops/01-tasks-loop/prompt.md +7 -5
- package/src/templates/code-implementation/loops/02-delivery-loop/prompt.md +4 -2
- package/src/templates/research/loops/00-discovery-loop/prompt.md +7 -5
- package/src/templates/research/loops/01-research-loop/prompt.md +7 -5
- package/src/templates/research/loops/02-story-loop/prompt.md +4 -2
- package/src/templates/research/loops/03-document-loop/prompt.md +4 -2
|
@@ -0,0 +1,838 @@
|
|
|
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>RalphFlow Dashboard</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #0d1117;
|
|
10
|
+
--bg-surface: #161b22;
|
|
11
|
+
--bg-hover: #1c2128;
|
|
12
|
+
--bg-active: #21262d;
|
|
13
|
+
--border: #30363d;
|
|
14
|
+
--text: #e6edf3;
|
|
15
|
+
--text-dim: #8b949e;
|
|
16
|
+
--text-muted: #484f58;
|
|
17
|
+
--accent: #58a6ff;
|
|
18
|
+
--green: #3fb950;
|
|
19
|
+
--blue: #58a6ff;
|
|
20
|
+
--yellow: #d29922;
|
|
21
|
+
--red: #f85149;
|
|
22
|
+
--purple: #bc8cff;
|
|
23
|
+
--mono: 'SF Mono', 'Cascadia Code', 'Fira Code', 'JetBrains Mono', monospace;
|
|
24
|
+
--sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
|
25
|
+
--radius: 6px;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
29
|
+
|
|
30
|
+
body {
|
|
31
|
+
font-family: var(--sans);
|
|
32
|
+
background: var(--bg);
|
|
33
|
+
color: var(--text);
|
|
34
|
+
height: 100vh;
|
|
35
|
+
display: flex;
|
|
36
|
+
flex-direction: column;
|
|
37
|
+
overflow: hidden;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* Header */
|
|
41
|
+
.header {
|
|
42
|
+
display: flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
justify-content: space-between;
|
|
45
|
+
padding: 12px 20px;
|
|
46
|
+
border-bottom: 1px solid var(--border);
|
|
47
|
+
background: var(--bg-surface);
|
|
48
|
+
flex-shrink: 0;
|
|
49
|
+
}
|
|
50
|
+
.header h1 {
|
|
51
|
+
font-size: 15px;
|
|
52
|
+
font-weight: 600;
|
|
53
|
+
letter-spacing: -0.3px;
|
|
54
|
+
}
|
|
55
|
+
.header .host {
|
|
56
|
+
font-family: var(--mono);
|
|
57
|
+
font-size: 12px;
|
|
58
|
+
color: var(--text-dim);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* Main layout */
|
|
62
|
+
.main {
|
|
63
|
+
display: flex;
|
|
64
|
+
flex: 1;
|
|
65
|
+
overflow: hidden;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* Sidebar */
|
|
69
|
+
.sidebar {
|
|
70
|
+
width: 240px;
|
|
71
|
+
border-right: 1px solid var(--border);
|
|
72
|
+
background: var(--bg-surface);
|
|
73
|
+
overflow-y: auto;
|
|
74
|
+
flex-shrink: 0;
|
|
75
|
+
}
|
|
76
|
+
.sidebar-section {
|
|
77
|
+
padding: 12px 0;
|
|
78
|
+
}
|
|
79
|
+
.sidebar-label {
|
|
80
|
+
padding: 0 16px;
|
|
81
|
+
font-size: 11px;
|
|
82
|
+
font-weight: 600;
|
|
83
|
+
text-transform: uppercase;
|
|
84
|
+
letter-spacing: 0.5px;
|
|
85
|
+
color: var(--text-dim);
|
|
86
|
+
margin-bottom: 4px;
|
|
87
|
+
}
|
|
88
|
+
.sidebar-item {
|
|
89
|
+
display: block;
|
|
90
|
+
padding: 6px 16px;
|
|
91
|
+
font-size: 13px;
|
|
92
|
+
color: var(--text-dim);
|
|
93
|
+
cursor: pointer;
|
|
94
|
+
border-left: 2px solid transparent;
|
|
95
|
+
transition: background 0.1s;
|
|
96
|
+
}
|
|
97
|
+
.sidebar-item:hover { background: var(--bg-hover); color: var(--text); }
|
|
98
|
+
.sidebar-item.active {
|
|
99
|
+
background: var(--bg-active);
|
|
100
|
+
color: var(--text);
|
|
101
|
+
border-left-color: var(--accent);
|
|
102
|
+
}
|
|
103
|
+
.sidebar-item.app-item {
|
|
104
|
+
font-weight: 600;
|
|
105
|
+
color: var(--text);
|
|
106
|
+
padding: 8px 16px;
|
|
107
|
+
}
|
|
108
|
+
.sidebar-item.loop-item { padding-left: 32px; font-size: 12px; }
|
|
109
|
+
.sidebar-item .badge {
|
|
110
|
+
font-size: 10px;
|
|
111
|
+
font-family: var(--mono);
|
|
112
|
+
padding: 1px 6px;
|
|
113
|
+
border-radius: 10px;
|
|
114
|
+
margin-left: 6px;
|
|
115
|
+
background: var(--bg-hover);
|
|
116
|
+
color: var(--text-dim);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* Content */
|
|
120
|
+
.content {
|
|
121
|
+
flex: 1;
|
|
122
|
+
overflow-y: auto;
|
|
123
|
+
padding: 24px 32px;
|
|
124
|
+
}
|
|
125
|
+
.content-empty {
|
|
126
|
+
display: flex;
|
|
127
|
+
align-items: center;
|
|
128
|
+
justify-content: center;
|
|
129
|
+
height: 100%;
|
|
130
|
+
color: var(--text-muted);
|
|
131
|
+
font-size: 14px;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* App header */
|
|
135
|
+
.app-header { margin-bottom: 24px; }
|
|
136
|
+
.app-header h2 {
|
|
137
|
+
font-size: 20px;
|
|
138
|
+
font-weight: 600;
|
|
139
|
+
margin-bottom: 4px;
|
|
140
|
+
}
|
|
141
|
+
.app-type-badge {
|
|
142
|
+
display: inline-block;
|
|
143
|
+
font-family: var(--mono);
|
|
144
|
+
font-size: 11px;
|
|
145
|
+
color: var(--purple);
|
|
146
|
+
background: rgba(188, 140, 255, 0.1);
|
|
147
|
+
padding: 2px 8px;
|
|
148
|
+
border-radius: 4px;
|
|
149
|
+
margin-right: 8px;
|
|
150
|
+
}
|
|
151
|
+
.app-desc {
|
|
152
|
+
color: var(--text-dim);
|
|
153
|
+
font-size: 13px;
|
|
154
|
+
margin-top: 6px;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* Pipeline view */
|
|
158
|
+
.pipeline {
|
|
159
|
+
display: flex;
|
|
160
|
+
align-items: center;
|
|
161
|
+
gap: 0;
|
|
162
|
+
margin-bottom: 28px;
|
|
163
|
+
padding: 16px 0;
|
|
164
|
+
}
|
|
165
|
+
.pipeline-node {
|
|
166
|
+
display: flex;
|
|
167
|
+
flex-direction: column;
|
|
168
|
+
align-items: center;
|
|
169
|
+
gap: 6px;
|
|
170
|
+
cursor: pointer;
|
|
171
|
+
padding: 12px 20px;
|
|
172
|
+
border-radius: var(--radius);
|
|
173
|
+
border: 1px solid var(--border);
|
|
174
|
+
background: var(--bg-surface);
|
|
175
|
+
transition: border-color 0.15s, background 0.15s;
|
|
176
|
+
min-width: 120px;
|
|
177
|
+
}
|
|
178
|
+
.pipeline-node:hover { border-color: var(--text-dim); }
|
|
179
|
+
.pipeline-node.selected { border-color: var(--accent); background: rgba(88, 166, 255, 0.05); }
|
|
180
|
+
.pipeline-node .node-name { font-size: 12px; font-weight: 600; }
|
|
181
|
+
.pipeline-node .node-status {
|
|
182
|
+
font-family: var(--mono);
|
|
183
|
+
font-size: 10px;
|
|
184
|
+
padding: 2px 8px;
|
|
185
|
+
border-radius: 10px;
|
|
186
|
+
}
|
|
187
|
+
.pipeline-node .node-status.complete { background: rgba(63,185,80,0.15); color: var(--green); }
|
|
188
|
+
.pipeline-node .node-status.running { background: rgba(88,166,255,0.15); color: var(--blue); }
|
|
189
|
+
.pipeline-node .node-status.pending { background: rgba(139,148,158,0.1); color: var(--text-muted); }
|
|
190
|
+
.pipeline-connector {
|
|
191
|
+
width: 32px;
|
|
192
|
+
height: 2px;
|
|
193
|
+
background: var(--border);
|
|
194
|
+
flex-shrink: 0;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* Section */
|
|
198
|
+
.section {
|
|
199
|
+
margin-bottom: 24px;
|
|
200
|
+
}
|
|
201
|
+
.section-title {
|
|
202
|
+
font-size: 11px;
|
|
203
|
+
font-weight: 600;
|
|
204
|
+
text-transform: uppercase;
|
|
205
|
+
letter-spacing: 0.5px;
|
|
206
|
+
color: var(--text-dim);
|
|
207
|
+
margin-bottom: 12px;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/* Loop detail */
|
|
211
|
+
.loop-meta {
|
|
212
|
+
display: grid;
|
|
213
|
+
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
|
214
|
+
gap: 12px;
|
|
215
|
+
margin-bottom: 16px;
|
|
216
|
+
}
|
|
217
|
+
.meta-card {
|
|
218
|
+
padding: 12px;
|
|
219
|
+
background: var(--bg-surface);
|
|
220
|
+
border: 1px solid var(--border);
|
|
221
|
+
border-radius: var(--radius);
|
|
222
|
+
}
|
|
223
|
+
.meta-label {
|
|
224
|
+
font-size: 11px;
|
|
225
|
+
color: var(--text-dim);
|
|
226
|
+
margin-bottom: 4px;
|
|
227
|
+
}
|
|
228
|
+
.meta-value {
|
|
229
|
+
font-family: var(--mono);
|
|
230
|
+
font-size: 13px;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/* Progress bar */
|
|
234
|
+
.progress-bar {
|
|
235
|
+
height: 6px;
|
|
236
|
+
background: var(--bg-active);
|
|
237
|
+
border-radius: 3px;
|
|
238
|
+
overflow: hidden;
|
|
239
|
+
margin-top: 8px;
|
|
240
|
+
}
|
|
241
|
+
.progress-fill {
|
|
242
|
+
height: 100%;
|
|
243
|
+
background: var(--green);
|
|
244
|
+
border-radius: 3px;
|
|
245
|
+
transition: width 0.3s ease;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/* Agent table */
|
|
249
|
+
.agent-table {
|
|
250
|
+
width: 100%;
|
|
251
|
+
border-collapse: collapse;
|
|
252
|
+
font-size: 12px;
|
|
253
|
+
font-family: var(--mono);
|
|
254
|
+
}
|
|
255
|
+
.agent-table th {
|
|
256
|
+
text-align: left;
|
|
257
|
+
padding: 8px 12px;
|
|
258
|
+
border-bottom: 1px solid var(--border);
|
|
259
|
+
color: var(--text-dim);
|
|
260
|
+
font-weight: 500;
|
|
261
|
+
font-size: 11px;
|
|
262
|
+
}
|
|
263
|
+
.agent-table td {
|
|
264
|
+
padding: 8px 12px;
|
|
265
|
+
border-bottom: 1px solid var(--border);
|
|
266
|
+
color: var(--text);
|
|
267
|
+
}
|
|
268
|
+
.agent-table tr:hover td { background: var(--bg-hover); }
|
|
269
|
+
|
|
270
|
+
/* Editor */
|
|
271
|
+
.editor-wrap {
|
|
272
|
+
position: relative;
|
|
273
|
+
}
|
|
274
|
+
.editor {
|
|
275
|
+
width: 100%;
|
|
276
|
+
min-height: 300px;
|
|
277
|
+
background: var(--bg-surface);
|
|
278
|
+
border: 1px solid var(--border);
|
|
279
|
+
border-radius: var(--radius);
|
|
280
|
+
color: var(--text);
|
|
281
|
+
font-family: var(--mono);
|
|
282
|
+
font-size: 13px;
|
|
283
|
+
line-height: 1.6;
|
|
284
|
+
padding: 16px;
|
|
285
|
+
resize: vertical;
|
|
286
|
+
outline: none;
|
|
287
|
+
tab-size: 2;
|
|
288
|
+
}
|
|
289
|
+
.editor:focus { border-color: var(--accent); }
|
|
290
|
+
.editor-actions {
|
|
291
|
+
display: flex;
|
|
292
|
+
gap: 8px;
|
|
293
|
+
margin-top: 8px;
|
|
294
|
+
align-items: center;
|
|
295
|
+
}
|
|
296
|
+
.btn {
|
|
297
|
+
font-family: var(--sans);
|
|
298
|
+
font-size: 12px;
|
|
299
|
+
font-weight: 500;
|
|
300
|
+
padding: 6px 14px;
|
|
301
|
+
border-radius: var(--radius);
|
|
302
|
+
border: 1px solid var(--border);
|
|
303
|
+
cursor: pointer;
|
|
304
|
+
transition: background 0.1s, border-color 0.1s;
|
|
305
|
+
background: var(--bg-surface);
|
|
306
|
+
color: var(--text);
|
|
307
|
+
}
|
|
308
|
+
.btn:hover { background: var(--bg-hover); border-color: var(--text-dim); }
|
|
309
|
+
.btn-primary {
|
|
310
|
+
background: var(--accent);
|
|
311
|
+
color: #000;
|
|
312
|
+
border-color: var(--accent);
|
|
313
|
+
}
|
|
314
|
+
.btn-primary:hover { background: #79c0ff; }
|
|
315
|
+
.btn:disabled { opacity: 0.5; cursor: default; }
|
|
316
|
+
.dirty-indicator {
|
|
317
|
+
font-size: 11px;
|
|
318
|
+
color: var(--yellow);
|
|
319
|
+
}
|
|
320
|
+
.save-ok {
|
|
321
|
+
font-size: 11px;
|
|
322
|
+
color: var(--green);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/* Tracker viewer */
|
|
326
|
+
.tracker-viewer {
|
|
327
|
+
background: var(--bg-surface);
|
|
328
|
+
border: 1px solid var(--border);
|
|
329
|
+
border-radius: var(--radius);
|
|
330
|
+
padding: 16px;
|
|
331
|
+
font-family: var(--mono);
|
|
332
|
+
font-size: 13px;
|
|
333
|
+
line-height: 1.7;
|
|
334
|
+
max-height: 400px;
|
|
335
|
+
overflow-y: auto;
|
|
336
|
+
white-space: pre-wrap;
|
|
337
|
+
word-wrap: break-word;
|
|
338
|
+
}
|
|
339
|
+
.tracker-viewer h1, .tracker-viewer h2, .tracker-viewer h3 {
|
|
340
|
+
font-family: var(--sans);
|
|
341
|
+
margin: 12px 0 6px;
|
|
342
|
+
}
|
|
343
|
+
.tracker-viewer h1 { font-size: 16px; }
|
|
344
|
+
.tracker-viewer h2 { font-size: 14px; }
|
|
345
|
+
.tracker-viewer h3 { font-size: 13px; }
|
|
346
|
+
.tracker-viewer .cb-done { color: var(--green); }
|
|
347
|
+
.tracker-viewer .cb-todo { color: var(--text-muted); }
|
|
348
|
+
.tracker-viewer table {
|
|
349
|
+
border-collapse: collapse;
|
|
350
|
+
margin: 8px 0;
|
|
351
|
+
width: 100%;
|
|
352
|
+
}
|
|
353
|
+
.tracker-viewer th, .tracker-viewer td {
|
|
354
|
+
border: 1px solid var(--border);
|
|
355
|
+
padding: 4px 10px;
|
|
356
|
+
text-align: left;
|
|
357
|
+
font-size: 12px;
|
|
358
|
+
}
|
|
359
|
+
.tracker-viewer th { background: var(--bg-active); color: var(--text-dim); }
|
|
360
|
+
|
|
361
|
+
/* Status bar */
|
|
362
|
+
.statusbar {
|
|
363
|
+
display: flex;
|
|
364
|
+
align-items: center;
|
|
365
|
+
gap: 16px;
|
|
366
|
+
padding: 6px 20px;
|
|
367
|
+
border-top: 1px solid var(--border);
|
|
368
|
+
background: var(--bg-surface);
|
|
369
|
+
font-size: 11px;
|
|
370
|
+
color: var(--text-dim);
|
|
371
|
+
flex-shrink: 0;
|
|
372
|
+
}
|
|
373
|
+
.status-dot {
|
|
374
|
+
display: inline-block;
|
|
375
|
+
width: 7px;
|
|
376
|
+
height: 7px;
|
|
377
|
+
border-radius: 50%;
|
|
378
|
+
margin-right: 4px;
|
|
379
|
+
}
|
|
380
|
+
.status-dot.connected { background: var(--green); }
|
|
381
|
+
.status-dot.disconnected { background: var(--red); }
|
|
382
|
+
.status-dot.connecting { background: var(--yellow); }
|
|
383
|
+
</style>
|
|
384
|
+
</head>
|
|
385
|
+
<body>
|
|
386
|
+
|
|
387
|
+
<div class="header">
|
|
388
|
+
<h1>RalphFlow Dashboard</h1>
|
|
389
|
+
<span class="host" id="hostDisplay"></span>
|
|
390
|
+
</div>
|
|
391
|
+
|
|
392
|
+
<div class="main">
|
|
393
|
+
<div class="sidebar" id="sidebar">
|
|
394
|
+
<div class="sidebar-section">
|
|
395
|
+
<div class="sidebar-label">Apps</div>
|
|
396
|
+
<div id="sidebarApps"></div>
|
|
397
|
+
</div>
|
|
398
|
+
</div>
|
|
399
|
+
<div class="content" id="content">
|
|
400
|
+
<div class="content-empty">Select an app to view details</div>
|
|
401
|
+
</div>
|
|
402
|
+
</div>
|
|
403
|
+
|
|
404
|
+
<div class="statusbar">
|
|
405
|
+
<span><span class="status-dot disconnected" id="statusDot"></span> <span id="statusText">Connecting...</span></span>
|
|
406
|
+
<span>Last update: <span id="lastUpdate">--</span></span>
|
|
407
|
+
<span>Events: <span id="eventCount">0</span></span>
|
|
408
|
+
</div>
|
|
409
|
+
|
|
410
|
+
<script>
|
|
411
|
+
(function() {
|
|
412
|
+
// State
|
|
413
|
+
let apps = [];
|
|
414
|
+
let selectedApp = null;
|
|
415
|
+
let selectedLoop = null;
|
|
416
|
+
let eventCounter = 0;
|
|
417
|
+
let promptDirty = false;
|
|
418
|
+
let promptOriginal = '';
|
|
419
|
+
let ws = null;
|
|
420
|
+
let reconnectDelay = 1000;
|
|
421
|
+
|
|
422
|
+
// DOM refs
|
|
423
|
+
const $ = (sel) => document.querySelector(sel);
|
|
424
|
+
const hostDisplay = $('#hostDisplay');
|
|
425
|
+
const sidebarApps = $('#sidebarApps');
|
|
426
|
+
const content = $('#content');
|
|
427
|
+
const statusDot = $('#statusDot');
|
|
428
|
+
const statusText = $('#statusText');
|
|
429
|
+
const lastUpdate = $('#lastUpdate');
|
|
430
|
+
const eventCountEl = $('#eventCount');
|
|
431
|
+
|
|
432
|
+
hostDisplay.textContent = location.host;
|
|
433
|
+
|
|
434
|
+
// WebSocket
|
|
435
|
+
function connectWs() {
|
|
436
|
+
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
437
|
+
ws = new WebSocket(`${proto}//${location.host}/ws`);
|
|
438
|
+
|
|
439
|
+
ws.onopen = () => {
|
|
440
|
+
statusDot.className = 'status-dot connected';
|
|
441
|
+
statusText.textContent = 'Connected';
|
|
442
|
+
reconnectDelay = 1000;
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
ws.onclose = () => {
|
|
446
|
+
statusDot.className = 'status-dot disconnected';
|
|
447
|
+
statusText.textContent = 'Disconnected';
|
|
448
|
+
setTimeout(connectWs, reconnectDelay);
|
|
449
|
+
reconnectDelay = Math.min(reconnectDelay * 2, 30000);
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
ws.onerror = () => {
|
|
453
|
+
ws.close();
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
ws.onmessage = (e) => {
|
|
457
|
+
const event = JSON.parse(e.data);
|
|
458
|
+
eventCounter++;
|
|
459
|
+
eventCountEl.textContent = eventCounter;
|
|
460
|
+
lastUpdate.textContent = new Date().toLocaleTimeString();
|
|
461
|
+
handleWsEvent(event);
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function handleWsEvent(event) {
|
|
466
|
+
if (event.type === 'status:full') {
|
|
467
|
+
apps = event.apps;
|
|
468
|
+
renderSidebar();
|
|
469
|
+
if (selectedApp) {
|
|
470
|
+
const updated = apps.find(a => a.appName === selectedApp.appName);
|
|
471
|
+
if (updated) {
|
|
472
|
+
selectedApp = updated;
|
|
473
|
+
renderContent();
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
} else if (event.type === 'tracker:updated') {
|
|
477
|
+
if (selectedApp && selectedApp.appName === event.app) {
|
|
478
|
+
// Update the loop status in our local state
|
|
479
|
+
const loopEntry = selectedApp.loops.find(l => l.key === event.loop);
|
|
480
|
+
if (loopEntry) {
|
|
481
|
+
loopEntry.status = event.status;
|
|
482
|
+
}
|
|
483
|
+
renderContent();
|
|
484
|
+
// Refresh tracker viewer if this loop is selected
|
|
485
|
+
if (selectedLoop === event.loop) {
|
|
486
|
+
loadTracker(event.app, event.loop);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
} else if (event.type === 'file:changed') {
|
|
490
|
+
if (selectedApp && selectedApp.appName === event.app) {
|
|
491
|
+
// Refresh status
|
|
492
|
+
fetchAppStatus(event.app);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// API helpers
|
|
498
|
+
async function fetchJson(url) {
|
|
499
|
+
const res = await fetch(url);
|
|
500
|
+
return res.json();
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async function fetchApps() {
|
|
504
|
+
apps = await fetchJson('/api/apps');
|
|
505
|
+
renderSidebar();
|
|
506
|
+
if (apps.length > 0 && !selectedApp) {
|
|
507
|
+
selectApp(apps[0]);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async function fetchAppStatus(appName) {
|
|
512
|
+
const statuses = await fetchJson(`/api/apps/${encodeURIComponent(appName)}/status`);
|
|
513
|
+
if (selectedApp && selectedApp.appName === appName) {
|
|
514
|
+
statuses.forEach(s => {
|
|
515
|
+
const loop = selectedApp.loops.find(l => l.key === s.key);
|
|
516
|
+
if (loop) loop.status = s;
|
|
517
|
+
});
|
|
518
|
+
renderContent();
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Sidebar
|
|
523
|
+
function renderSidebar() {
|
|
524
|
+
let html = '';
|
|
525
|
+
for (const app of apps) {
|
|
526
|
+
const appActive = selectedApp && selectedApp.appName === app.appName;
|
|
527
|
+
html += `<div class="sidebar-item app-item${appActive ? ' active' : ''}" data-app="${esc(app.appName)}">
|
|
528
|
+
${esc(app.appName)}
|
|
529
|
+
<span class="badge">${esc(app.appType)}</span>
|
|
530
|
+
</div>`;
|
|
531
|
+
if (app.loops) {
|
|
532
|
+
for (const loop of app.loops) {
|
|
533
|
+
const loopActive = appActive && selectedLoop === loop.key;
|
|
534
|
+
html += `<div class="sidebar-item loop-item${loopActive ? ' active' : ''}" data-app="${esc(app.appName)}" data-loop="${esc(loop.key)}">
|
|
535
|
+
${esc(loop.name)}
|
|
536
|
+
</div>`;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
sidebarApps.innerHTML = html;
|
|
541
|
+
|
|
542
|
+
// Event delegation
|
|
543
|
+
sidebarApps.querySelectorAll('.app-item').forEach(el => {
|
|
544
|
+
el.addEventListener('click', () => {
|
|
545
|
+
const app = apps.find(a => a.appName === el.dataset.app);
|
|
546
|
+
if (app) selectApp(app);
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
sidebarApps.querySelectorAll('.loop-item').forEach(el => {
|
|
550
|
+
el.addEventListener('click', () => {
|
|
551
|
+
const app = apps.find(a => a.appName === el.dataset.app);
|
|
552
|
+
if (app) {
|
|
553
|
+
selectApp(app);
|
|
554
|
+
selectLoop(el.dataset.loop);
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function selectApp(app) {
|
|
561
|
+
selectedApp = app;
|
|
562
|
+
selectedLoop = app.loops.length > 0 ? app.loops[0].key : null;
|
|
563
|
+
promptDirty = false;
|
|
564
|
+
renderSidebar();
|
|
565
|
+
renderContent();
|
|
566
|
+
fetchAppStatus(app.appName);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function selectLoop(loopKey) {
|
|
570
|
+
selectedLoop = loopKey;
|
|
571
|
+
promptDirty = false;
|
|
572
|
+
renderSidebar();
|
|
573
|
+
renderContent();
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Main content
|
|
577
|
+
function renderContent() {
|
|
578
|
+
if (!selectedApp) {
|
|
579
|
+
content.innerHTML = '<div class="content-empty">Select an app to view details</div>';
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const app = selectedApp;
|
|
584
|
+
const currentLoop = app.loops.find(l => l.key === selectedLoop);
|
|
585
|
+
|
|
586
|
+
let html = '';
|
|
587
|
+
|
|
588
|
+
// App header
|
|
589
|
+
html += `<div class="app-header">
|
|
590
|
+
<h2>${esc(app.appName)}</h2>
|
|
591
|
+
<span class="app-type-badge">${esc(app.appType)}</span>
|
|
592
|
+
${app.description ? `<div class="app-desc">${esc(app.description)}</div>` : ''}
|
|
593
|
+
</div>`;
|
|
594
|
+
|
|
595
|
+
// Pipeline
|
|
596
|
+
html += '<div class="section"><div class="section-title">Pipeline</div><div class="pipeline">';
|
|
597
|
+
app.loops.forEach((loop, i) => {
|
|
598
|
+
if (i > 0) html += '<div class="pipeline-connector"></div>';
|
|
599
|
+
const statusClass = getLoopStatusClass(loop);
|
|
600
|
+
const isSelected = loop.key === selectedLoop;
|
|
601
|
+
html += `<div class="pipeline-node${isSelected ? ' selected' : ''}" data-loop="${esc(loop.key)}">
|
|
602
|
+
<span class="node-name">${esc(loop.name)}</span>
|
|
603
|
+
<span class="node-status ${statusClass}">${statusClass}</span>
|
|
604
|
+
</div>`;
|
|
605
|
+
});
|
|
606
|
+
html += '</div></div>';
|
|
607
|
+
|
|
608
|
+
// Loop detail
|
|
609
|
+
if (currentLoop) {
|
|
610
|
+
const st = currentLoop.status || {};
|
|
611
|
+
html += `<div class="section">
|
|
612
|
+
<div class="section-title">Loop Detail: ${esc(currentLoop.name)}</div>
|
|
613
|
+
<div class="loop-meta">
|
|
614
|
+
<div class="meta-card"><div class="meta-label">Stage</div><div class="meta-value">${esc(st.stage || '—')}</div></div>
|
|
615
|
+
<div class="meta-card"><div class="meta-label">Active</div><div class="meta-value">${esc(st.active || 'none')}</div></div>
|
|
616
|
+
<div class="meta-card">
|
|
617
|
+
<div class="meta-label">Progress</div>
|
|
618
|
+
<div class="meta-value">${st.completed || 0}/${st.total || 0}</div>
|
|
619
|
+
<div class="progress-bar"><div class="progress-fill" style="width:${st.total ? (st.completed / st.total * 100) : 0}%"></div></div>
|
|
620
|
+
</div>
|
|
621
|
+
<div class="meta-card"><div class="meta-label">Stages</div><div class="meta-value" style="font-size:11px">${(currentLoop.stages || []).join(' → ')}</div></div>
|
|
622
|
+
</div>`;
|
|
623
|
+
|
|
624
|
+
// Agent table
|
|
625
|
+
if (st.agents && st.agents.length > 0) {
|
|
626
|
+
html += `<div style="margin-bottom:16px">
|
|
627
|
+
<table class="agent-table">
|
|
628
|
+
<thead><tr><th>Agent</th><th>Active Task</th><th>Stage</th><th>Heartbeat</th></tr></thead>
|
|
629
|
+
<tbody>`;
|
|
630
|
+
for (const ag of st.agents) {
|
|
631
|
+
html += `<tr><td>${esc(ag.name)}</td><td>${esc(ag.activeTask)}</td><td>${esc(ag.stage)}</td><td>${esc(ag.lastHeartbeat)}</td></tr>`;
|
|
632
|
+
}
|
|
633
|
+
html += '</tbody></table></div>';
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
html += '</div>';
|
|
637
|
+
|
|
638
|
+
// Prompt editor
|
|
639
|
+
html += `<div class="section">
|
|
640
|
+
<div class="section-title">Prompt Editor</div>
|
|
641
|
+
<div class="editor-wrap">
|
|
642
|
+
<textarea class="editor" id="promptEditor" placeholder="Loading..."></textarea>
|
|
643
|
+
<div class="editor-actions">
|
|
644
|
+
<button class="btn btn-primary" id="savePromptBtn" disabled>Save</button>
|
|
645
|
+
<button class="btn" id="resetPromptBtn" disabled>Reset</button>
|
|
646
|
+
<span class="dirty-indicator" id="dirtyIndicator" style="display:none">Unsaved changes</span>
|
|
647
|
+
<span class="save-ok" id="saveOk" style="display:none">Saved</span>
|
|
648
|
+
</div>
|
|
649
|
+
</div>
|
|
650
|
+
</div>`;
|
|
651
|
+
|
|
652
|
+
// Tracker viewer
|
|
653
|
+
html += `<div class="section">
|
|
654
|
+
<div class="section-title">Tracker (live, read-only)</div>
|
|
655
|
+
<div class="tracker-viewer" id="trackerViewer">Loading...</div>
|
|
656
|
+
</div>`;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
content.innerHTML = html;
|
|
660
|
+
|
|
661
|
+
// Bind pipeline node clicks
|
|
662
|
+
content.querySelectorAll('.pipeline-node').forEach(el => {
|
|
663
|
+
el.addEventListener('click', () => selectLoop(el.dataset.loop));
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
// Load prompt + tracker
|
|
667
|
+
if (currentLoop) {
|
|
668
|
+
loadPrompt(app.appName, currentLoop.key);
|
|
669
|
+
loadTracker(app.appName, currentLoop.key);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
async function loadPrompt(appName, loopKey) {
|
|
674
|
+
const editor = $('#promptEditor');
|
|
675
|
+
if (!editor) return;
|
|
676
|
+
|
|
677
|
+
try {
|
|
678
|
+
const data = await fetchJson(`/api/apps/${encodeURIComponent(appName)}/loops/${encodeURIComponent(loopKey)}/prompt`);
|
|
679
|
+
editor.value = data.content || '';
|
|
680
|
+
promptOriginal = editor.value;
|
|
681
|
+
promptDirty = false;
|
|
682
|
+
updateDirtyState();
|
|
683
|
+
|
|
684
|
+
editor.addEventListener('input', () => {
|
|
685
|
+
promptDirty = editor.value !== promptOriginal;
|
|
686
|
+
updateDirtyState();
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
// Cmd/Ctrl+S to save
|
|
690
|
+
editor.addEventListener('keydown', (e) => {
|
|
691
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 's') {
|
|
692
|
+
e.preventDefault();
|
|
693
|
+
savePrompt(appName, loopKey);
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
const saveBtn = $('#savePromptBtn');
|
|
698
|
+
const resetBtn = $('#resetPromptBtn');
|
|
699
|
+
if (saveBtn) saveBtn.addEventListener('click', () => savePrompt(appName, loopKey));
|
|
700
|
+
if (resetBtn) resetBtn.addEventListener('click', () => {
|
|
701
|
+
editor.value = promptOriginal;
|
|
702
|
+
promptDirty = false;
|
|
703
|
+
updateDirtyState();
|
|
704
|
+
});
|
|
705
|
+
} catch {
|
|
706
|
+
editor.value = '(Error loading prompt)';
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
async function savePrompt(appName, loopKey) {
|
|
711
|
+
const editor = $('#promptEditor');
|
|
712
|
+
if (!editor || !promptDirty) return;
|
|
713
|
+
|
|
714
|
+
try {
|
|
715
|
+
await fetch(`/api/apps/${encodeURIComponent(appName)}/loops/${encodeURIComponent(loopKey)}/prompt`, {
|
|
716
|
+
method: 'PUT',
|
|
717
|
+
headers: { 'Content-Type': 'application/json' },
|
|
718
|
+
body: JSON.stringify({ content: editor.value }),
|
|
719
|
+
});
|
|
720
|
+
promptOriginal = editor.value;
|
|
721
|
+
promptDirty = false;
|
|
722
|
+
updateDirtyState();
|
|
723
|
+
const saveOk = $('#saveOk');
|
|
724
|
+
if (saveOk) {
|
|
725
|
+
saveOk.style.display = 'inline';
|
|
726
|
+
setTimeout(() => { saveOk.style.display = 'none'; }, 2000);
|
|
727
|
+
}
|
|
728
|
+
} catch {
|
|
729
|
+
alert('Failed to save prompt');
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function updateDirtyState() {
|
|
734
|
+
const saveBtn = $('#savePromptBtn');
|
|
735
|
+
const resetBtn = $('#resetPromptBtn');
|
|
736
|
+
const indicator = $('#dirtyIndicator');
|
|
737
|
+
if (saveBtn) saveBtn.disabled = !promptDirty;
|
|
738
|
+
if (resetBtn) resetBtn.disabled = !promptDirty;
|
|
739
|
+
if (indicator) indicator.style.display = promptDirty ? 'inline' : 'none';
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
async function loadTracker(appName, loopKey) {
|
|
743
|
+
const viewer = $('#trackerViewer');
|
|
744
|
+
if (!viewer) return;
|
|
745
|
+
|
|
746
|
+
try {
|
|
747
|
+
const data = await fetchJson(`/api/apps/${encodeURIComponent(appName)}/loops/${encodeURIComponent(loopKey)}/tracker`);
|
|
748
|
+
viewer.innerHTML = renderMarkdown(data.content || '(empty)');
|
|
749
|
+
} catch {
|
|
750
|
+
viewer.innerHTML = '(No tracker file found)';
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Minimal markdown renderer
|
|
755
|
+
function renderMarkdown(md) {
|
|
756
|
+
let html = '';
|
|
757
|
+
const lines = md.split('\n');
|
|
758
|
+
let inTable = false;
|
|
759
|
+
let tableHtml = '';
|
|
760
|
+
|
|
761
|
+
for (let i = 0; i < lines.length; i++) {
|
|
762
|
+
const line = lines[i];
|
|
763
|
+
|
|
764
|
+
// Table detection
|
|
765
|
+
if (line.match(/^\|.+\|$/)) {
|
|
766
|
+
if (!inTable) {
|
|
767
|
+
inTable = true;
|
|
768
|
+
tableHtml = '<table>';
|
|
769
|
+
// Header row
|
|
770
|
+
const cells = line.split('|').filter(Boolean).map(c => c.trim());
|
|
771
|
+
tableHtml += '<thead><tr>' + cells.map(c => `<th>${esc(c)}</th>`).join('') + '</tr></thead><tbody>';
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
// Separator row
|
|
775
|
+
if (line.match(/^\|[\s\-|]+\|$/)) continue;
|
|
776
|
+
// Data row
|
|
777
|
+
const cells = line.split('|').filter(Boolean).map(c => c.trim());
|
|
778
|
+
tableHtml += '<tr>' + cells.map(c => `<td>${esc(c)}</td>`).join('') + '</tr>';
|
|
779
|
+
continue;
|
|
780
|
+
} else if (inTable) {
|
|
781
|
+
inTable = false;
|
|
782
|
+
tableHtml += '</tbody></table>';
|
|
783
|
+
html += tableHtml;
|
|
784
|
+
tableHtml = '';
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// Headers
|
|
788
|
+
if (line.startsWith('### ')) { html += `<h3>${esc(line.slice(4))}</h3>`; continue; }
|
|
789
|
+
if (line.startsWith('## ')) { html += `<h2>${esc(line.slice(3))}</h2>`; continue; }
|
|
790
|
+
if (line.startsWith('# ')) { html += `<h1>${esc(line.slice(2))}</h1>`; continue; }
|
|
791
|
+
|
|
792
|
+
// Checkboxes
|
|
793
|
+
if (line.match(/^- \[x\]/i)) {
|
|
794
|
+
html += `<div class="cb-done">${esc(line)}</div>`;
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
797
|
+
if (line.match(/^- \[ \]/)) {
|
|
798
|
+
html += `<div class="cb-todo">${esc(line)}</div>`;
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// Regular lines
|
|
803
|
+
html += line.trim() === '' ? '<br>' : `<div>${esc(line)}</div>`;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (inTable) {
|
|
807
|
+
tableHtml += '</tbody></table>';
|
|
808
|
+
html += tableHtml;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
return html;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function getLoopStatusClass(loop) {
|
|
815
|
+
if (!loop.status) return 'pending';
|
|
816
|
+
const st = loop.status;
|
|
817
|
+
if (st.total > 0 && st.completed === st.total) return 'complete';
|
|
818
|
+
// Running if: has active agents, or has partial progress, or stage indicates activity (not idle/—)
|
|
819
|
+
if (st.agents && st.agents.length > 0) return 'running';
|
|
820
|
+
if (st.total > 0 && st.completed > 0 && st.completed < st.total) return 'running';
|
|
821
|
+
if (st.stage && st.stage !== '—' && st.stage !== 'idle') return 'running';
|
|
822
|
+
return 'pending';
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function esc(s) {
|
|
826
|
+
if (s == null) return '';
|
|
827
|
+
const d = document.createElement('div');
|
|
828
|
+
d.textContent = String(s);
|
|
829
|
+
return d.innerHTML;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// Init
|
|
833
|
+
fetchApps();
|
|
834
|
+
connectWs();
|
|
835
|
+
})();
|
|
836
|
+
</script>
|
|
837
|
+
</body>
|
|
838
|
+
</html>
|