@sebastianandreasson/pi-autonomous-agents 0.8.0 → 0.9.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 +4 -2
- package/docs/PI_SUPERVISOR.md +1 -1
- package/package.json +1 -1
- package/src/pi-visualizer-server.mjs +280 -42
package/README.md
CHANGED
|
@@ -272,9 +272,11 @@ Visualizer uses SSE for live updates instead of browser polling.
|
|
|
272
272
|
`pi-harness visualize` still exists as standalone viewer if you want to inspect run history without starting a new run.
|
|
273
273
|
|
|
274
274
|
Visualizer now includes:
|
|
275
|
+
- TODO-centric main view with current task open by default
|
|
275
276
|
- run history selector from `.pi-runtime/runs/`
|
|
276
|
-
- orchestration flow
|
|
277
|
-
-
|
|
277
|
+
- orchestration flow for selected todo
|
|
278
|
+
- 50/50 split between live worker feed and current repo edits
|
|
279
|
+
- per-iteration stage graph with retries/rechecks in diagnostics
|
|
278
280
|
- clickable graph nodes and timeline rows that show full event JSON
|
|
279
281
|
- historical run summaries and per-run last output snapshots
|
|
280
282
|
- live worker feed with thinking text, assistant text, tool calls, and tool output
|
package/docs/PI_SUPERVISOR.md
CHANGED
|
@@ -57,7 +57,7 @@ The package reads `PI_CONFIG_FILE` if provided. Otherwise it falls back to the b
|
|
|
57
57
|
|
|
58
58
|
`pi-harness visualize` remains available as standalone viewer.
|
|
59
59
|
|
|
60
|
-
Visualizer reads active-run lock, per-run state, per-run iteration summary, per-run last output snapshot, live feed JSONL, and telemetry to show current stage plus historical runs.
|
|
60
|
+
Visualizer reads active-run lock, TODO file, per-run state, per-run iteration summary, per-run last output snapshot, live feed JSONL, and telemetry to show current stage plus historical runs.
|
|
61
61
|
|
|
62
62
|
## Config Contract
|
|
63
63
|
|
package/package.json
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'node:fs/promises'
|
|
|
2
2
|
import http from 'node:http'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import process from 'node:process'
|
|
5
|
+
import { execFileSync } from 'node:child_process'
|
|
5
6
|
import { readTelemetry } from './pi-telemetry.mjs'
|
|
6
7
|
import { readJsonFile } from './pi-repo.mjs'
|
|
7
8
|
import { deriveFlowSnapshot, deriveStageGraph, formatActiveLabel } from './pi-visualizer-shared.mjs'
|
|
@@ -42,6 +43,147 @@ async function readJsonlTail(filePath, maxItems = 200) {
|
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
const MAX_DIFF_FILES = 10
|
|
47
|
+
const MAX_DIFF_CHARS_PER_FILE = 12000
|
|
48
|
+
const MAX_DIFF_TOTAL_CHARS = 40000
|
|
49
|
+
const REPO_DIFF_CACHE_MS = 2000
|
|
50
|
+
|
|
51
|
+
let repoDiffCache = {
|
|
52
|
+
cwd: '',
|
|
53
|
+
updatedAt: 0,
|
|
54
|
+
result: [],
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function clampText(text, maxChars) {
|
|
58
|
+
const value = String(text ?? '')
|
|
59
|
+
if (value.length <= maxChars) {
|
|
60
|
+
return value
|
|
61
|
+
}
|
|
62
|
+
return `${value.slice(0, maxChars - 16)}\n... [truncated]`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function parseTodos(taskFile, activeTaskText = '') {
|
|
66
|
+
try {
|
|
67
|
+
const raw = await fs.readFile(taskFile, 'utf8')
|
|
68
|
+
const lines = raw.split('\n')
|
|
69
|
+
const items = []
|
|
70
|
+
let currentPhase = ''
|
|
71
|
+
|
|
72
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
73
|
+
const line = lines[index]
|
|
74
|
+
const headingMatch = /^(#+)\s+(.+)$/.exec(line)
|
|
75
|
+
if (headingMatch) {
|
|
76
|
+
const level = headingMatch[1].length
|
|
77
|
+
const text = headingMatch[2].trim()
|
|
78
|
+
if (level === 2) {
|
|
79
|
+
currentPhase = text
|
|
80
|
+
}
|
|
81
|
+
items.push({
|
|
82
|
+
id: `line-${index + 1}`,
|
|
83
|
+
kind: 'heading',
|
|
84
|
+
lineNumber: index + 1,
|
|
85
|
+
level,
|
|
86
|
+
text,
|
|
87
|
+
phase: currentPhase || text,
|
|
88
|
+
raw: line,
|
|
89
|
+
checked: false,
|
|
90
|
+
active: false,
|
|
91
|
+
})
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const checkboxMatch = /^\s*[-*]\s+\[( |x)\]\s+(.+)$/.exec(line)
|
|
96
|
+
if (checkboxMatch) {
|
|
97
|
+
const checked = checkboxMatch[1].toLowerCase() === 'x'
|
|
98
|
+
const text = checkboxMatch[2].trim()
|
|
99
|
+
items.push({
|
|
100
|
+
id: `line-${index + 1}`,
|
|
101
|
+
kind: 'task',
|
|
102
|
+
lineNumber: index + 1,
|
|
103
|
+
level: 0,
|
|
104
|
+
text,
|
|
105
|
+
phase: currentPhase,
|
|
106
|
+
raw: line,
|
|
107
|
+
checked,
|
|
108
|
+
active: text === activeTaskText,
|
|
109
|
+
})
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (line.trim() !== '') {
|
|
114
|
+
items.push({
|
|
115
|
+
id: `line-${index + 1}`,
|
|
116
|
+
kind: 'line',
|
|
117
|
+
lineNumber: index + 1,
|
|
118
|
+
level: 0,
|
|
119
|
+
text: line.trim(),
|
|
120
|
+
phase: currentPhase,
|
|
121
|
+
raw: line,
|
|
122
|
+
checked: false,
|
|
123
|
+
active: false,
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return items
|
|
129
|
+
} catch {
|
|
130
|
+
return []
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function readRepoDiff(cwd) {
|
|
135
|
+
const now = Date.now()
|
|
136
|
+
if (repoDiffCache.cwd === cwd && (now - repoDiffCache.updatedAt) < REPO_DIFF_CACHE_MS) {
|
|
137
|
+
return repoDiffCache.result
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
const status = execFileSync('git', ['status', '--short'], {
|
|
142
|
+
cwd,
|
|
143
|
+
encoding: 'utf8',
|
|
144
|
+
maxBuffer: 1024 * 1024,
|
|
145
|
+
}).trim()
|
|
146
|
+
if (status === '') {
|
|
147
|
+
repoDiffCache = { cwd, updatedAt: now, result: [] }
|
|
148
|
+
return []
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const files = status
|
|
152
|
+
.split('\n')
|
|
153
|
+
.map((line) => line.slice(3).trim())
|
|
154
|
+
.filter(Boolean)
|
|
155
|
+
.slice(0, MAX_DIFF_FILES)
|
|
156
|
+
|
|
157
|
+
let remainingChars = MAX_DIFF_TOTAL_CHARS
|
|
158
|
+
const result = files.map((file) => {
|
|
159
|
+
let diff = ''
|
|
160
|
+
try {
|
|
161
|
+
diff = execFileSync('git', ['diff', '--no-ext-diff', '--unified=1', '--', file], {
|
|
162
|
+
cwd,
|
|
163
|
+
encoding: 'utf8',
|
|
164
|
+
maxBuffer: 1024 * 1024,
|
|
165
|
+
}).trim()
|
|
166
|
+
} catch {
|
|
167
|
+
diff = ''
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const allowedChars = Math.max(500, Math.min(MAX_DIFF_CHARS_PER_FILE, remainingChars))
|
|
171
|
+
const truncatedDiff = clampText(diff, allowedChars)
|
|
172
|
+
remainingChars = Math.max(0, remainingChars - truncatedDiff.length)
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
file,
|
|
176
|
+
diff: truncatedDiff,
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
repoDiffCache = { cwd, updatedAt: now, result }
|
|
181
|
+
return result
|
|
182
|
+
} catch {
|
|
183
|
+
return []
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
45
187
|
function getRunDir(config, runId) {
|
|
46
188
|
return path.join(config.piRuntimeDir, 'runs', runId)
|
|
47
189
|
}
|
|
@@ -139,6 +281,13 @@ export async function buildSnapshot(config, queryRunId = '') {
|
|
|
139
281
|
telemetry,
|
|
140
282
|
})
|
|
141
283
|
|
|
284
|
+
const selectedRunIsActive = selectedRunId !== '' && String(activeRun?.runId ?? '') === selectedRunId
|
|
285
|
+
const activeTaskText = String((selectedRunIsActive ? activeRun?.task : state?.inProgress?.task) ?? summary?.task ?? '').trim()
|
|
286
|
+
const [todos, currentEdits] = await Promise.all([
|
|
287
|
+
parseTodos(config.taskFile, activeTaskText),
|
|
288
|
+
Promise.resolve(selectedRunIsActive ? readRepoDiff(config.cwd) : []),
|
|
289
|
+
])
|
|
290
|
+
|
|
142
291
|
return {
|
|
143
292
|
now: new Date().toISOString(),
|
|
144
293
|
config: {
|
|
@@ -159,6 +308,8 @@ export async function buildSnapshot(config, queryRunId = '') {
|
|
|
159
308
|
activeLabel: formatActiveLabel(activeRun, flow),
|
|
160
309
|
},
|
|
161
310
|
graph,
|
|
311
|
+
todos,
|
|
312
|
+
currentEdits,
|
|
162
313
|
lastOutput: currentOutput,
|
|
163
314
|
liveFeed,
|
|
164
315
|
recentTelemetry,
|
|
@@ -205,8 +356,8 @@ export function renderHtml() {
|
|
|
205
356
|
.dot { width: 10px; height: 10px; border-radius: 50%; background: var(--pending); }
|
|
206
357
|
.dot.active { background: var(--active); box-shadow: 0 0 18px rgba(110,231,255,.6); }
|
|
207
358
|
.grid { display: grid; gap: 16px; }
|
|
208
|
-
.grid.
|
|
209
|
-
.
|
|
359
|
+
.grid.main { grid-template-columns: minmax(320px, 420px) 1fr; align-items: start; }
|
|
360
|
+
.detail-split { display:grid; grid-template-columns: 1fr 1fr; gap:16px; margin-top:16px; }
|
|
210
361
|
.card {
|
|
211
362
|
background: linear-gradient(180deg, rgba(255,255,255,.02), rgba(255,255,255,.01));
|
|
212
363
|
border: 1px solid var(--line); border-radius: 16px; padding: 16px;
|
|
@@ -215,6 +366,17 @@ export function renderHtml() {
|
|
|
215
366
|
.label { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: .08em; }
|
|
216
367
|
.value { margin-top: 8px; font-size: 22px; font-weight: 700; }
|
|
217
368
|
.value.small { font-size: 16px; }
|
|
369
|
+
.todo-list { max-height: calc(100vh - 140px); overflow: auto; padding-right: 4px; }
|
|
370
|
+
.todo-item { border:1px solid var(--line); border-radius:14px; background: var(--panel); margin-bottom:10px; overflow:hidden; }
|
|
371
|
+
.todo-item.active { border-color: var(--active); box-shadow: 0 0 0 1px rgba(110,231,255,.25) inset; }
|
|
372
|
+
.todo-summary { list-style:none; cursor:pointer; padding:12px 14px; display:flex; gap:10px; align-items:flex-start; }
|
|
373
|
+
.todo-summary::-webkit-details-marker { display:none; }
|
|
374
|
+
.todo-line { color: var(--muted); font-size: 11px; min-width: 52px; }
|
|
375
|
+
.todo-text { flex:1; }
|
|
376
|
+
.todo-heading { font-weight:700; }
|
|
377
|
+
.todo-task { font-weight:600; }
|
|
378
|
+
.todo-checked { color: var(--done); }
|
|
379
|
+
.todo-open-body { padding: 0 14px 14px 14px; color: var(--muted); font-size: 12px; }
|
|
218
380
|
.flow { display: grid; grid-template-columns: repeat(8, minmax(0, 1fr)); gap: 10px; margin-top: 14px; }
|
|
219
381
|
.step, .graph-node {
|
|
220
382
|
border: 1px solid var(--line); border-radius: 14px; padding: 12px; background: var(--panel);
|
|
@@ -233,6 +395,8 @@ export function renderHtml() {
|
|
|
233
395
|
.graph { display:grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap:12px; margin-top:12px; }
|
|
234
396
|
.graph-node { min-height: 120px; width: 100%; text-align: left; color: var(--text); font: inherit; cursor: pointer; }
|
|
235
397
|
.graph-arrow { color: var(--muted); text-align: center; align-self: center; }
|
|
398
|
+
.state-bar { display:flex; gap:10px; flex-wrap:wrap; margin-top:12px; }
|
|
399
|
+
.state-chip { border:1px solid var(--line); border-radius:999px; padding:6px 10px; color: var(--muted); background: rgba(255,255,255,.03); }
|
|
236
400
|
.kv { display: grid; grid-template-columns: 140px 1fr; gap: 6px 10px; margin-top: 12px; }
|
|
237
401
|
.feed-toolbar { display:flex; gap:12px; align-items:center; flex-wrap:wrap; margin-top:12px; margin-bottom:10px; }
|
|
238
402
|
.feed-toggle { display:flex; gap:6px; align-items:center; color: var(--muted); font-size: 12px; }
|
|
@@ -269,8 +433,13 @@ export function renderHtml() {
|
|
|
269
433
|
.status-pill.error { color: var(--error); }
|
|
270
434
|
.status-pill.skipped { color: var(--skip); }
|
|
271
435
|
.status-pill.active { color: var(--active); }
|
|
436
|
+
.edit-list { max-height: 360px; overflow:auto; }
|
|
437
|
+
.edit-item { border:1px solid var(--line); border-radius:12px; margin-bottom:10px; overflow:hidden; }
|
|
438
|
+
.edit-head { padding:10px 12px; background: rgba(255,255,255,.03); font-weight:600; }
|
|
272
439
|
.muted { color: var(--muted); }
|
|
273
|
-
|
|
440
|
+
details.bottom { margin-top: 16px; }
|
|
441
|
+
details.bottom summary { cursor:pointer; color: var(--muted); margin-bottom:10px; }
|
|
442
|
+
@media (max-width: 1100px) { .grid.main, .detail-split, .flow { grid-template-columns: 1fr; } .todo-list { max-height:none; } }
|
|
274
443
|
</style>
|
|
275
444
|
</head>
|
|
276
445
|
<body>
|
|
@@ -286,48 +455,48 @@ export function renderHtml() {
|
|
|
286
455
|
</div>
|
|
287
456
|
</div>
|
|
288
457
|
|
|
289
|
-
<div class="grid top">
|
|
290
|
-
<div class="card"><div class="label">Current activity</div><div class="value" id="active-label">—</div></div>
|
|
291
|
-
<div class="card"><div class="label">Iteration</div><div class="value" id="iteration">—</div></div>
|
|
292
|
-
<div class="card"><div class="label">Phase</div><div class="value small" id="phase">—</div></div>
|
|
293
|
-
<div class="card"><div class="label">Task</div><div class="value small" id="task">—</div></div>
|
|
294
|
-
</div>
|
|
295
|
-
|
|
296
|
-
<div class="card" style="margin-bottom: 16px;">
|
|
297
|
-
<div class="label">Orchestration flow</div>
|
|
298
|
-
<div class="flow" id="flow"></div>
|
|
299
|
-
</div>
|
|
300
|
-
|
|
301
|
-
<div class="card" style="margin-bottom: 16px;">
|
|
302
|
-
<div class="label">Iteration stage graph</div>
|
|
303
|
-
<div class="graph" id="graph"></div>
|
|
304
|
-
</div>
|
|
305
|
-
|
|
306
458
|
<div class="grid main">
|
|
307
459
|
<div class="card">
|
|
308
|
-
<div class="label">
|
|
309
|
-
<div
|
|
310
|
-
<table>
|
|
311
|
-
<thead><tr><th>Time</th><th>Iteration</th><th>Kind</th><th>Status</th><th>Notes</th></tr></thead>
|
|
312
|
-
<tbody id="timeline"></tbody>
|
|
313
|
-
</table>
|
|
314
|
-
</div>
|
|
460
|
+
<div class="label">TODOS</div>
|
|
461
|
+
<div class="todo-list" id="todo-list"></div>
|
|
315
462
|
</div>
|
|
316
463
|
|
|
317
|
-
<div
|
|
318
|
-
<div class="card"><div class="label">Run state</div><div class="kv" id="run-state"></div></div>
|
|
319
|
-
<div class="card"><div class="label">Latest tool output</div><div class="pinned-tool" id="pinned-tool">No tool activity yet.</div></div>
|
|
464
|
+
<div>
|
|
320
465
|
<div class="card">
|
|
321
|
-
<div class="label">
|
|
322
|
-
<div class="
|
|
323
|
-
|
|
324
|
-
|
|
466
|
+
<div class="label">Focused todo</div>
|
|
467
|
+
<div class="value small" id="todo-focus-title">—</div>
|
|
468
|
+
<div class="state-bar" id="todo-state-bar"></div>
|
|
469
|
+
<div class="flow" id="flow"></div>
|
|
470
|
+
<div class="detail-split">
|
|
471
|
+
<div class="card" style="margin:0;">
|
|
472
|
+
<div class="label">Live worker feed</div>
|
|
473
|
+
<div class="feed-toolbar">
|
|
474
|
+
<label class="feed-toggle"><input type="checkbox" id="feed-show-thinking" checked /> <span>Show thinking</span></label>
|
|
475
|
+
<label class="feed-toggle"><input type="checkbox" id="feed-collapse-deltas" checked /> <span>Collapse deltas</span></label>
|
|
476
|
+
</div>
|
|
477
|
+
<div class="feed" id="feed">No live feed yet.</div>
|
|
478
|
+
</div>
|
|
479
|
+
<div class="grid" style="gap:16px;">
|
|
480
|
+
<div class="card" style="margin:0;"><div class="label">Latest tool output</div><div class="pinned-tool" id="pinned-tool">No tool activity yet.</div></div>
|
|
481
|
+
<div class="card" style="margin:0;">
|
|
482
|
+
<div class="label">Current edits for focused todo</div>
|
|
483
|
+
<div class="edit-list" id="edit-list">No repo edits yet.</div>
|
|
484
|
+
</div>
|
|
485
|
+
</div>
|
|
325
486
|
</div>
|
|
326
|
-
<div class="feed" id="feed">No live feed yet.</div>
|
|
327
487
|
</div>
|
|
328
|
-
|
|
329
|
-
<
|
|
330
|
-
|
|
488
|
+
|
|
489
|
+
<details class="bottom card">
|
|
490
|
+
<summary>Diagnostics</summary>
|
|
491
|
+
<div class="grid" style="gap:16px;">
|
|
492
|
+
<div class="card" style="margin:0;"><div class="label">Run state</div><div class="kv" id="run-state"></div></div>
|
|
493
|
+
<div class="card" style="margin:0;"><div class="label">Iteration stage graph</div><div class="graph" id="graph"></div></div>
|
|
494
|
+
<div class="card" style="margin:0;"><div class="label">Recent telemetry timeline</div><div style="margin-top: 12px; overflow: auto; max-height: 360px;"><table><thead><tr><th>Time</th><th>Iteration</th><th>Kind</th><th>Status</th><th>Notes</th></tr></thead><tbody id="timeline"></tbody></table></div></div>
|
|
495
|
+
<div class="card" style="margin:0;"><div class="label">Selected event</div><pre id="selected-event">Click graph node or timeline row.</pre></div>
|
|
496
|
+
<div class="card" style="margin:0;"><div class="label">Last iteration summary</div><pre id="summary">—</pre></div>
|
|
497
|
+
<div class="card" style="margin:0;"><div class="label">Last agent output</div><pre id="output">—</pre></div>
|
|
498
|
+
</div>
|
|
499
|
+
</details>
|
|
331
500
|
</div>
|
|
332
501
|
</div>
|
|
333
502
|
</div>
|
|
@@ -361,6 +530,7 @@ export function renderHtml() {
|
|
|
361
530
|
|
|
362
531
|
let latestSnapshot = null
|
|
363
532
|
let selectedEventId = ''
|
|
533
|
+
let selectedTodoId = ''
|
|
364
534
|
let eventSource = null
|
|
365
535
|
|
|
366
536
|
function normalizeFeedEntry(entry) {
|
|
@@ -420,6 +590,16 @@ export function renderHtml() {
|
|
|
420
590
|
|| null
|
|
421
591
|
}
|
|
422
592
|
|
|
593
|
+
function findSelectedTodo(snapshot) {
|
|
594
|
+
if (!snapshot) return null
|
|
595
|
+
const todos = Array.isArray(snapshot.todos) ? snapshot.todos : []
|
|
596
|
+
if (selectedTodoId) {
|
|
597
|
+
const direct = todos.find((item) => item.id === selectedTodoId)
|
|
598
|
+
if (direct) return direct
|
|
599
|
+
}
|
|
600
|
+
return todos.find((item) => item.active) || todos.find((item) => item.kind === 'task') || todos[0] || null
|
|
601
|
+
}
|
|
602
|
+
|
|
423
603
|
function renderSelectedEvent() {
|
|
424
604
|
const event = findEventById(latestSnapshot, selectedEventId)
|
|
425
605
|
document.getElementById('selected-event').textContent = event
|
|
@@ -427,6 +607,60 @@ export function renderHtml() {
|
|
|
427
607
|
: 'Click graph node or timeline row.'
|
|
428
608
|
}
|
|
429
609
|
|
|
610
|
+
function renderTodos(snapshot) {
|
|
611
|
+
const todos = Array.isArray(snapshot?.todos) ? snapshot.todos : []
|
|
612
|
+
const selected = findSelectedTodo(snapshot)
|
|
613
|
+
if (selected && !selectedTodoId) {
|
|
614
|
+
selectedTodoId = selected.id
|
|
615
|
+
}
|
|
616
|
+
const list = document.getElementById('todo-list')
|
|
617
|
+
list.innerHTML = todos.length > 0
|
|
618
|
+
? todos.map((item) => {
|
|
619
|
+
const active = selected && item.id === selected.id
|
|
620
|
+
const textClass = item.kind === 'heading' ? 'todo-heading' : (item.kind === 'task' ? 'todo-task' : '')
|
|
621
|
+
const checkedMark = item.kind === 'task' ? (item.checked ? '✓' : '○') : (item.kind === 'heading' ? '#' : '·')
|
|
622
|
+
const checkedClass = item.checked ? 'todo-checked' : ''
|
|
623
|
+
return '<details class="todo-item ' + (active ? 'active' : '') + '" ' + (active ? 'open' : '') + ' data-todo-id="' + esc(item.id) + '">' +
|
|
624
|
+
'<summary class="todo-summary">' +
|
|
625
|
+
'<div class="todo-line">' + esc(String(item.lineNumber)) + '</div>' +
|
|
626
|
+
'<div class="' + textClass + ' ' + checkedClass + '">' + esc(checkedMark + ' ' + item.text) + '</div>' +
|
|
627
|
+
'</summary>' +
|
|
628
|
+
'<div class="todo-open-body">' + esc(item.phase || '') + '</div>' +
|
|
629
|
+
'</details>'
|
|
630
|
+
}).join('')
|
|
631
|
+
: '<div class="muted">No TODO items found.</div>'
|
|
632
|
+
|
|
633
|
+
list.querySelectorAll('[data-todo-id]').forEach((element) => {
|
|
634
|
+
element.addEventListener('toggle', () => {
|
|
635
|
+
if (element.open) {
|
|
636
|
+
selectedTodoId = element.getAttribute('data-todo-id') || ''
|
|
637
|
+
renderSnapshot(snapshot)
|
|
638
|
+
}
|
|
639
|
+
})
|
|
640
|
+
})
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function renderFocusedTodo(snapshot) {
|
|
644
|
+
const todo = findSelectedTodo(snapshot)
|
|
645
|
+
document.getElementById('todo-focus-title').textContent = todo ? todo.text : 'No todo selected.'
|
|
646
|
+
const stateBar = document.getElementById('todo-state-bar')
|
|
647
|
+
const chips = [
|
|
648
|
+
['Current activity', snapshot?.flow?.activeLabel || 'Idle'],
|
|
649
|
+
['Iteration', snapshot?.flow?.iteration || '—'],
|
|
650
|
+
['Phase', todo?.phase || snapshot?.summary?.phase || '—'],
|
|
651
|
+
['Task status', todo?.kind === 'task' ? (todo.checked ? 'Done' : (todo.active ? 'Active' : 'Pending')) : 'Info'],
|
|
652
|
+
]
|
|
653
|
+
stateBar.innerHTML = chips.map(([label, value]) => '<div class="state-chip">' + esc(label + ': ' + value) + '</div>').join('')
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function renderCurrentEdits(snapshot) {
|
|
657
|
+
const edits = Array.isArray(snapshot?.currentEdits) ? snapshot.currentEdits : []
|
|
658
|
+
const target = document.getElementById('edit-list')
|
|
659
|
+
target.innerHTML = edits.length > 0
|
|
660
|
+
? edits.map((entry) => '<details class="edit-item" open><summary class="edit-head">' + esc(entry.file) + '</summary><pre>' + esc(entry.diff || 'No diff available.') + '</pre></details>').join('')
|
|
661
|
+
: '<div class="muted">No repo edits yet.</div>'
|
|
662
|
+
}
|
|
663
|
+
|
|
430
664
|
function bindSelectableEvents() {
|
|
431
665
|
document.querySelectorAll('[data-event-id]').forEach((element) => {
|
|
432
666
|
element.addEventListener('click', () => {
|
|
@@ -451,10 +685,10 @@ export function renderHtml() {
|
|
|
451
685
|
latestSnapshot = data
|
|
452
686
|
document.getElementById('cwd').textContent = data.config.cwd
|
|
453
687
|
document.getElementById('last-refresh').textContent = 'Updated ' + new Date(data.now).toLocaleTimeString()
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
688
|
+
if (!selectedTodoId) {
|
|
689
|
+
const activeTodo = (Array.isArray(data.todos) ? data.todos.find((item) => item.active) : null) || null
|
|
690
|
+
selectedTodoId = activeTodo?.id || ''
|
|
691
|
+
}
|
|
458
692
|
|
|
459
693
|
const select = document.getElementById('run-select')
|
|
460
694
|
const selected = data.config.selectedRunId || ''
|
|
@@ -471,6 +705,10 @@ export function renderHtml() {
|
|
|
471
705
|
select.dataset.bound = '1'
|
|
472
706
|
}
|
|
473
707
|
|
|
708
|
+
renderTodos(data)
|
|
709
|
+
renderFocusedTodo(data)
|
|
710
|
+
renderCurrentEdits(data)
|
|
711
|
+
|
|
474
712
|
const flowEl = document.getElementById('flow')
|
|
475
713
|
flowEl.innerHTML = data.flow.steps.map((step) => {
|
|
476
714
|
const latest = step.latestEvent
|