iterate-plugin 2.11.0 → 2.12.1
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 +41 -1
- package/README.zh-CN.md +39 -1
- package/dist/approval-gate.js +92 -0
- package/dist/config-loader.js +4 -0
- package/dist/index.js +15 -5
- package/dist/live.js +155 -0
- package/dist/paths.js +4 -0
- package/dist/session-hooks.js +89 -0
- package/dist/skill-prompt.js +56 -4
- package/dist/tools/transcript.js +324 -0
- package/dist/transcript.js +421 -0
- package/lib/client.js +865 -0
- package/lib/parse.js +276 -0
- package/package.json +1 -1
- package/src/approval-gate.ts +119 -0
- package/src/client/index.ts +710 -6
- package/src/config-loader.ts +4 -0
- package/src/index.ts +17 -6
- package/src/live.ts +185 -0
- package/src/paths.ts +5 -0
- package/src/session-hooks.ts +90 -0
- package/src/skill-prompt.ts +56 -4
- package/src/tools/transcript.ts +334 -0
- package/src/transcript.ts +475 -0
- package/src/types.ts +117 -0
package/src/client/index.ts
CHANGED
|
@@ -17,13 +17,14 @@
|
|
|
17
17
|
* - `inject`: services the shell must have up before `apply` runs.
|
|
18
18
|
* - `apply(ctx)`: called by the client runtime with the ClientContext.
|
|
19
19
|
*
|
|
20
|
-
* Implements the §14.4 / §16
|
|
20
|
+
* Implements the §14.4 / §16 UI layer:
|
|
21
21
|
* 1. Convergence dashboard -> conversation.input.dock
|
|
22
|
-
* 2. Findings triage panel
|
|
23
|
-
* 3. Iteration stats card
|
|
24
|
-
* 4. iterate theme skin
|
|
25
|
-
* 5. Progress event linkage
|
|
26
|
-
* 6. Settings page section
|
|
22
|
+
* 2. Findings triage panel -> conversation.chat.turnTail
|
|
23
|
+
* 3. Iteration stats card -> conversation.chat.turnTail (same chain, empty-findings case)
|
|
24
|
+
* 4. iterate theme skin -> theme.overrideTokens
|
|
25
|
+
* 5. Progress event linkage -> ctx.on('slots/changed') + round-pulse + shell.overlay capsule
|
|
26
|
+
* 6. Settings page section -> settings.section
|
|
27
|
+
* 7. Runtime observatory -> conversation.input.dock (order 91, F1–F7 board)
|
|
27
28
|
*
|
|
28
29
|
* Design notes:
|
|
29
30
|
* - All styles are injected via one <style> tag; colors use dsh token
|
|
@@ -70,6 +71,8 @@ import {
|
|
|
70
71
|
buildRuntimeStatusGuide,
|
|
71
72
|
scanSessionForResume,
|
|
72
73
|
countSessionImages,
|
|
74
|
+
scanSessionForTranscript,
|
|
75
|
+
normalizeTranscript,
|
|
73
76
|
SEVERITY_LABEL,
|
|
74
77
|
SEVERITY_COLOR,
|
|
75
78
|
} from '../../lib/parse.js'
|
|
@@ -142,6 +145,100 @@ interface ThemeService {
|
|
|
142
145
|
overrideTokens(source: string, tokens: IterateTokenMap): () => void
|
|
143
146
|
}
|
|
144
147
|
|
|
148
|
+
// ─── Runtime observatory (transcript) types ─────────────────────────────────
|
|
149
|
+
// Mirrors src/types.ts Transcript* series, but every field is defensive/optional
|
|
150
|
+
// so the panel never crashes on a malformed or partial manifest.
|
|
151
|
+
|
|
152
|
+
interface ObsCheckpoint {
|
|
153
|
+
mode?: string
|
|
154
|
+
round?: number
|
|
155
|
+
maxRounds?: number
|
|
156
|
+
fixedCount?: number
|
|
157
|
+
resumeCount?: number
|
|
158
|
+
updatedAt?: string
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
interface ObsNudge {
|
|
162
|
+
timestamp?: string
|
|
163
|
+
text?: string
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
interface ObsApproval {
|
|
167
|
+
active?: boolean
|
|
168
|
+
policy?: string
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
interface ObsThread {
|
|
172
|
+
dimension?: string
|
|
173
|
+
attempt?: number
|
|
174
|
+
messages?: string[]
|
|
175
|
+
readFiles?: string[]
|
|
176
|
+
findings?: ObsFinding[]
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
interface ObsRound {
|
|
180
|
+
round?: number
|
|
181
|
+
threads?: ObsThread[]
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
interface ObsFinding {
|
|
185
|
+
dimension?: string
|
|
186
|
+
file?: string
|
|
187
|
+
line?: number
|
|
188
|
+
severity?: string
|
|
189
|
+
summary?: string
|
|
190
|
+
failure_scenario?: string
|
|
191
|
+
suggested_fix?: string
|
|
192
|
+
is_atomic?: boolean
|
|
193
|
+
acknowledged?: boolean
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
interface ObsFix {
|
|
197
|
+
id?: string
|
|
198
|
+
timestamp?: string
|
|
199
|
+
round?: number
|
|
200
|
+
file?: string
|
|
201
|
+
summary?: string
|
|
202
|
+
linesAdded?: number
|
|
203
|
+
linesRemoved?: number
|
|
204
|
+
success?: boolean
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
interface ObsEntry {
|
|
208
|
+
timestamp?: string
|
|
209
|
+
round?: number
|
|
210
|
+
type?: string
|
|
211
|
+
data?: Record<string, unknown>
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
interface ObsLive {
|
|
215
|
+
ts?: string
|
|
216
|
+
type?: string
|
|
217
|
+
tool?: string
|
|
218
|
+
target?: string
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
interface ObsManifest {
|
|
222
|
+
version?: number
|
|
223
|
+
project?: string
|
|
224
|
+
updatedAt?: string
|
|
225
|
+
active?: boolean
|
|
226
|
+
mode?: string
|
|
227
|
+
goal?: string
|
|
228
|
+
phases?: string[]
|
|
229
|
+
round?: number
|
|
230
|
+
maxRounds?: number
|
|
231
|
+
rounds?: ObsRound[]
|
|
232
|
+
convergence?: number[]
|
|
233
|
+
findings?: ObsFinding[]
|
|
234
|
+
fixes?: ObsFix[]
|
|
235
|
+
live?: ObsLive[]
|
|
236
|
+
checkpoint?: ObsCheckpoint | null
|
|
237
|
+
timeline?: ObsEntry[]
|
|
238
|
+
nudge?: ObsNudge | null
|
|
239
|
+
approval?: ObsApproval
|
|
240
|
+
}
|
|
241
|
+
|
|
145
242
|
/** Opaque slot props: heterogeneous per-slot payloads. */
|
|
146
243
|
type SlotProps = Record<string, unknown>
|
|
147
244
|
|
|
@@ -364,6 +461,38 @@ const ITERATE_CSS = `
|
|
|
364
461
|
.iterate-guide { margin-top: 4px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; overflow: hidden; background: var(--dsw-alias-bg-layer-2); }
|
|
365
462
|
.iterate-guide-bar { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 8px 12px; background: var(--dsw-alias-bg-layer-2); border-bottom: 1px solid var(--dsw-alias-border-l1); font-size: 12px; color: var(--dsw-alias-label-secondary); }
|
|
366
463
|
.iterate-guide-body { padding: 12px 14px; font-family: var(--dsw-font-mono, ui-monospace, monospace); font-size: 11.5px; line-height: 1.75; white-space: pre-wrap; color: var(--dsw-alias-label-primary); max-height: 260px; overflow: auto; }
|
|
464
|
+
|
|
465
|
+
/* ── Runtime observatory panel ──────────────────────────────────────────── */
|
|
466
|
+
.iterate-obs { margin: 6px 0; border: 1px solid var(--dsw-alias-border-l1); border-radius: 12px; background: var(--dsw-alias-bg-layer-1); overflow: hidden; font-size: 12px; color: var(--dsw-alias-label-primary); }
|
|
467
|
+
.iterate-obs-head { display: flex; align-items: center; gap: 10px; padding: 8px 14px; cursor: pointer; border-bottom: 1px solid var(--dsw-alias-border-l1); flex-wrap: wrap; }
|
|
468
|
+
.iterate-obs-head[data-closed] { border-bottom: none; }
|
|
469
|
+
.iterate-obs-title { font-weight: 650; }
|
|
470
|
+
.iterate-obs-badge { display: inline-flex; align-items: center; gap: 6px; padding: 2px 10px; border-radius: 999px; background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent); color: var(--dsw-alias-brand-primary); font-size: 11px; font-weight: 600; white-space: nowrap; }
|
|
471
|
+
.iterate-obs-badge[data-live] { color: var(--dsw-alias-state-success-primary); background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 12%, transparent); }
|
|
472
|
+
.iterate-obs-head-meta { margin-left: auto; font-size: 11px; color: var(--dsw-alias-label-secondary); white-space: nowrap; }
|
|
473
|
+
.iterate-obs-tabs { display: flex; gap: 4px; padding: 8px 14px 0; flex-wrap: wrap; border-bottom: 1px solid var(--dsw-alias-border-l1); }
|
|
474
|
+
.iterate-obs-tab { padding: 4px 10px; border-radius: 7px; border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-secondary); font-size: 11px; cursor: pointer; }
|
|
475
|
+
.iterate-obs-tab[data-active] { border-color: var(--dsw-alias-brand-primary); color: var(--dsw-alias-brand-primary); }
|
|
476
|
+
.iterate-obs-body { padding: 12px 14px; max-height: 460px; overflow: auto; }
|
|
477
|
+
.iterate-obs-block { border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; overflow: hidden; margin-bottom: 8px; }
|
|
478
|
+
.iterate-obs-block:last-child { margin-bottom: 0; }
|
|
479
|
+
.iterate-obs-block-head { display: flex; align-items: center; gap: 8px; padding: 7px 10px; background: var(--dsw-alias-bg-layer-2); border-bottom: 1px solid var(--dsw-alias-border-l1); font-size: 11px; color: var(--dsw-alias-label-primary); flex-wrap: wrap; }
|
|
480
|
+
.iterate-obs-block-head[data-click] { cursor: pointer; }
|
|
481
|
+
.iterate-obs-block-body { padding: 8px 10px; }
|
|
482
|
+
.iterate-obs-chip { display: inline-flex; align-items: center; gap: 6px; padding: 2px 8px; border-radius: 6px; background: var(--dsw-alias-bg-layer-1); border: 1px solid var(--dsw-alias-border-l1); font-size: 11px; color: var(--dsw-alias-label-secondary); white-space: nowrap; }
|
|
483
|
+
.iterate-obs-msg { padding: 2px 0; color: var(--dsw-alias-label-secondary); line-height: 1.5; font-size: 11px; word-break: break-word; }
|
|
484
|
+
.iterate-obs-file { font-family: var(--dsw-font-mono, ui-monospace, monospace); font-size: 10.5px; color: var(--dsw-alias-label-secondary); word-break: break-all; }
|
|
485
|
+
.iterate-obs-bar { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--dsw-alias-label-secondary); flex-wrap: wrap; }
|
|
486
|
+
.iterate-obs-bar b { color: var(--dsw-alias-label-primary); font-weight: 600; }
|
|
487
|
+
.iterate-obs-mono { font-family: var(--dsw-font-mono, ui-monospace, monospace); }
|
|
488
|
+
.iterate-obs-code { padding: 6px 8px; margin-top: 4px; border-radius: 6px; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); font-family: var(--dsw-font-mono, ui-monospace, monospace); font-size: 10.5px; color: var(--dsw-alias-label-secondary); white-space: pre-wrap; word-break: break-all; }
|
|
489
|
+
.iterate-obs-input { width: 100%; padding: 8px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); font-size: 11px; resize: vertical; }
|
|
490
|
+
.iterate-obs-input:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 2px; }
|
|
491
|
+
.iterate-obs-input::placeholder { color: var(--dsw-alias-label-secondary); }
|
|
492
|
+
.iterate-obs-empty { font-size: 11px; color: var(--dsw-alias-label-secondary); padding: 8px 0; }
|
|
493
|
+
.iterate-obs-row { display: flex; align-items: center; gap: 8px; padding: 5px 0; border-bottom: 1px solid var(--dsw-alias-border-l1); font-size: 11px; color: var(--dsw-alias-label-secondary); flex-wrap: wrap; }
|
|
494
|
+
.iterate-obs-row:last-child { border-bottom: none; }
|
|
495
|
+
.iterate-obs-livebadge { display: inline-flex; align-items: center; gap: 4px; padding: 1px 7px; border-radius: 6px; border: 1px solid currentColor; background: color-mix(in srgb, currentColor 10%, transparent); font-size: 10.5px; font-weight: 600; white-space: nowrap; }
|
|
367
496
|
`
|
|
368
497
|
|
|
369
498
|
// ─── Small helpers ───────────────────────────────────────────────────────────
|
|
@@ -838,6 +967,14 @@ function TriagePanel(props: SlotProps) {
|
|
|
838
967
|
const t = ev.target as HTMLElement | null
|
|
839
968
|
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.tagName === 'SELECT')) return
|
|
840
969
|
if (t && typeof t.isContentEditable === 'boolean' && t.isContentEditable) return
|
|
970
|
+
// Only act when focus is INSIDE the triage panel. Everything else keeps
|
|
971
|
+
// standard behavior (↑/↓ page scroll, typing in the composer), so we do
|
|
972
|
+
// not hijack document-level keys just because a report is mounted.
|
|
973
|
+
const rootEl = doc.querySelector('[data-iterate="triage"]')
|
|
974
|
+
if (!rootEl) return
|
|
975
|
+
const activeEl = doc.activeElement as HTMLElement | null
|
|
976
|
+
if (!activeEl) return
|
|
977
|
+
if (activeEl !== rootEl && !(typeof rootEl.contains === 'function' && rootEl.contains(activeEl))) return
|
|
841
978
|
const verdict = keyToVerdict(ev.key)
|
|
842
979
|
if (verdict && selected !== null && indices.includes(selected)) {
|
|
843
980
|
ev.preventDefault()
|
|
@@ -1248,6 +1385,564 @@ function SettingsPanel(_props: SlotProps) {
|
|
|
1248
1385
|
)
|
|
1249
1386
|
}
|
|
1250
1387
|
|
|
1388
|
+
// ─── Runtime observatory panel (F1–F7) ──────────────────────────────────────
|
|
1389
|
+
|
|
1390
|
+
/** Tab id → label for the observatory's eight panels. Live (real-time activity
|
|
1391
|
+
* stream) is first so it is the default view; F-panels follow. */
|
|
1392
|
+
const OBS_TABS = [
|
|
1393
|
+
{ key: 'live', label: '实时活动流' },
|
|
1394
|
+
{ key: 'f1', label: '审查线程' },
|
|
1395
|
+
{ key: 'f2', label: '收敛趋势' },
|
|
1396
|
+
{ key: 'f3', label: '发现定位' },
|
|
1397
|
+
{ key: 'f4', label: '修复与回滚' },
|
|
1398
|
+
{ key: 'f5', label: '断点恢复' },
|
|
1399
|
+
{ key: 'f6', label: '运行控制台' },
|
|
1400
|
+
{ key: 'f7', label: '决策时间线' },
|
|
1401
|
+
] as const
|
|
1402
|
+
|
|
1403
|
+
/** Live subagent activity badges: type → Chinese label + dsw token color accent. */
|
|
1404
|
+
const OBS_LIVE_META: Record<string, { label: string; color: string }> = {
|
|
1405
|
+
read: { label: '阅读', color: 'var(--dsw-alias-brand-primary)' },
|
|
1406
|
+
fix: { label: '修复', color: 'var(--dsw-alias-state-success-primary)' },
|
|
1407
|
+
rollback: { label: '回滚', color: 'var(--dsw-alias-state-error-primary)' },
|
|
1408
|
+
diff: { label: '差异', color: 'var(--dsw-alias-state-warn-primary)' },
|
|
1409
|
+
review: { label: '审查', color: 'var(--dsw-alias-brand-primary)' },
|
|
1410
|
+
triage: { label: '分流', color: 'var(--dsw-alias-state-warn-primary)' },
|
|
1411
|
+
checkpoint: { label: '断点', color: 'var(--dsw-alias-brand-primary)' },
|
|
1412
|
+
validate: { label: '校验', color: 'var(--dsw-alias-state-success-primary)' },
|
|
1413
|
+
log: { label: '记录', color: 'var(--dsw-alias-label-secondary)' },
|
|
1414
|
+
prune: { label: '清理', color: 'var(--dsw-alias-state-warn-primary)' },
|
|
1415
|
+
info: { label: '信息', color: 'var(--dsw-alias-label-secondary)' },
|
|
1416
|
+
}
|
|
1417
|
+
const OBS_LIVE_FALLBACK_COLOR = 'var(--dsw-alias-label-secondary)'
|
|
1418
|
+
|
|
1419
|
+
/**
|
|
1420
|
+
* Format a live-event timestamp as DD/MM HH:mm:ss (0-padded). Accepts an epoch
|
|
1421
|
+
* ms number or an ISO-ish string; any unparseable value degrades to ''.
|
|
1422
|
+
*/
|
|
1423
|
+
function formatObsLiveTime(ts?: string): string {
|
|
1424
|
+
if (!ts) return ''
|
|
1425
|
+
let date: Date | null = null
|
|
1426
|
+
const num = Number(ts)
|
|
1427
|
+
if (typeof ts === 'string' && ts !== '' && !Number.isNaN(num) && /^\d+$/.test(ts)) {
|
|
1428
|
+
date = new Date(num)
|
|
1429
|
+
} else {
|
|
1430
|
+
const d = new Date(ts)
|
|
1431
|
+
date = Number.isNaN(d.getTime()) ? null : d
|
|
1432
|
+
}
|
|
1433
|
+
if (!date) return String(ts)
|
|
1434
|
+
const p = (n: number) => String(n).padStart(2, '0')
|
|
1435
|
+
const h = p(date.getHours())
|
|
1436
|
+
const mi = p(date.getMinutes())
|
|
1437
|
+
const s = p(date.getSeconds())
|
|
1438
|
+
return `${p(date.getDate())}/${p(date.getMonth() + 1)} ${h}:${mi}:${s}`
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
/** Find the latest runtime-observatory manifest inside a session snapshot. */
|
|
1442
|
+
function latestTranscript(session: unknown): ObsManifest | null {
|
|
1443
|
+
if (!session) return null
|
|
1444
|
+
const raw = scanSessionForTranscript(session)
|
|
1445
|
+
return raw ? normalizeTranscript(raw) as ObsManifest : null
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
/** Build a human-pasteable `iterate_fix` instruction for one finding. */
|
|
1449
|
+
function buildObsFixInstruction(f: ObsFinding): string {
|
|
1450
|
+
const payload = JSON.stringify({
|
|
1451
|
+
file: String(f.file || ''),
|
|
1452
|
+
...(typeof f.line === 'number' && f.line > 0 ? { line: f.line } : {}),
|
|
1453
|
+
dimension: String(f.dimension || ''),
|
|
1454
|
+
summary: String(f.summary || ''),
|
|
1455
|
+
...(f.suggested_fix ? { suggested_fix: String(f.suggested_fix) } : {}),
|
|
1456
|
+
}, null, 2)
|
|
1457
|
+
return `请调用 \`iterate_fix\` 修复以下 finding:\n\n\`\`\`json\n${payload}\n\`\`\``
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
/**
|
|
1461
|
+
* ObservatoryPanel: an 8-tab runtime observatory that renders the latest
|
|
1462
|
+
* iterate_transcript manifest found in the dsh session stream. The panel is
|
|
1463
|
+
* collapsed by default to a single entry row so it never re-occludes the main
|
|
1464
|
+
* convergence dashboard; clicking expands it into tabs. Every field read is
|
|
1465
|
+
* defensively guarded (optional chaining + fallbacks) so a partial/malformed
|
|
1466
|
+
* manifest never crashes the slot. The client cannot call harness tools
|
|
1467
|
+
* directly, so every action copies a paste-able instruction text instead.
|
|
1468
|
+
*/
|
|
1469
|
+
function ObservatoryPanel(props: SlotProps) {
|
|
1470
|
+
const session = props && props.session ? props.session : null
|
|
1471
|
+
const manifest = latestTranscript(session)
|
|
1472
|
+
|
|
1473
|
+
const [open, setOpen] = React.useState(false)
|
|
1474
|
+
const [tab, setTab] = React.useState('live')
|
|
1475
|
+
const [expandedThreads, setExpandedThreads] = React.useState<Set<string>>(new Set())
|
|
1476
|
+
const [copiedKey, setCopiedKey] = React.useState<string | null>(null)
|
|
1477
|
+
// When a clipboard write is blocked (permissions/unsupported), reveal the
|
|
1478
|
+
// raw instruction text so the user can copy it manually instead of silently
|
|
1479
|
+
// dropping the action (mirrors the triage/settings fallback).
|
|
1480
|
+
const [copyFailText, setCopyFailText] = React.useState<string | null>(null)
|
|
1481
|
+
const [nudgeText, setNudgeText] = React.useState('')
|
|
1482
|
+
const [timelineType, setTimelineType] = React.useState('')
|
|
1483
|
+
const [timelineSearch, setTimelineSearch] = React.useState('')
|
|
1484
|
+
|
|
1485
|
+
// Single shared "copied" flash timer (reused across all copy buttons).
|
|
1486
|
+
const copyTimer = React.useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
1487
|
+
React.useEffect(() => () => { if (copyTimer.current) clearTimeout(copyTimer.current) }, [])
|
|
1488
|
+
|
|
1489
|
+
/** Copy an instruction, flashing "已复制" on the originating button only when the copy actually succeeded. */
|
|
1490
|
+
const copyInstruction = (key: string, text: string) => {
|
|
1491
|
+
if (!text) return
|
|
1492
|
+
copyText(text).then((ok) => {
|
|
1493
|
+
if (!ok) { setCopyFailText(text); return }
|
|
1494
|
+
setCopiedKey(key)
|
|
1495
|
+
if (copyTimer.current) clearTimeout(copyTimer.current)
|
|
1496
|
+
copyTimer.current = setTimeout(() => setCopiedKey((cur) => (cur === key ? null : cur)), 1600)
|
|
1497
|
+
})
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
const toggleThread = (key: string) => {
|
|
1501
|
+
setExpandedThreads((prev) => {
|
|
1502
|
+
const next = new Set(prev)
|
|
1503
|
+
if (next.has(key)) next.delete(key); else next.add(key)
|
|
1504
|
+
return next
|
|
1505
|
+
})
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
// No observable transcript yet: show a compact, collapsed header row.
|
|
1509
|
+
if (!manifest) {
|
|
1510
|
+
return React.createElement('div', { 'data-iterate-root': '', 'data-iterate': 'obs', className: 'iterate-obs' },
|
|
1511
|
+
React.createElement('div', { className: 'iterate-obs-head', 'data-closed': '' },
|
|
1512
|
+
React.createElement('span', { className: 'iterate-obs-title' }, 'iterate 观测台'),
|
|
1513
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' }, '暂无运行时观测数据'),
|
|
1514
|
+
),
|
|
1515
|
+
)
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
const live = manifest.active === true
|
|
1519
|
+
const roundMeta = typeof manifest.round === 'number'
|
|
1520
|
+
? (typeof manifest.maxRounds === 'number' ? `Round ${manifest.round}/${manifest.maxRounds}` : `Round ${manifest.round}`)
|
|
1521
|
+
: ''
|
|
1522
|
+
const headMeta = [manifest.mode || '', roundMeta, manifest.updatedAt || ''].filter(Boolean).join(' · ')
|
|
1523
|
+
|
|
1524
|
+
// ── F1: review threads ─────────────────────────────────────────────────
|
|
1525
|
+
const threadFindingPill = (f: ObsFinding, keyBase: string) => {
|
|
1526
|
+
const loc = `${String(f.file || '?')}${typeof f.line === 'number' && f.line > 0 ? `:${f.line}` : ''}`
|
|
1527
|
+
return React.createElement('div', { key: keyBase, className: 'iterate-obs-row' },
|
|
1528
|
+
React.createElement('span', { className: 'iterate-sev-dot', style: { background: severityColor(f.severity) } }),
|
|
1529
|
+
React.createElement('span', {}, severityLabel(f.severity)),
|
|
1530
|
+
React.createElement('span', { className: 'iterate-obs-file' }, loc),
|
|
1531
|
+
React.createElement('span', {}, String(f.dimension || '')),
|
|
1532
|
+
React.createElement('span', { className: 'iterate-obs-msg' }, String(f.summary || '')),
|
|
1533
|
+
)
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
const renderThreads = () => {
|
|
1537
|
+
const rounds = manifest.rounds || []
|
|
1538
|
+
if (rounds.length === 0) {
|
|
1539
|
+
return React.createElement('div', { className: 'iterate-obs-empty' }, '暂无审查线程')
|
|
1540
|
+
}
|
|
1541
|
+
return React.createElement('div', {}, ...rounds.map((r, ri) => {
|
|
1542
|
+
const threads = r.threads || []
|
|
1543
|
+
const fCount = threads.reduce((sum, t) => sum + (t.findings ? t.findings.length : 0), 0)
|
|
1544
|
+
const threadBlocks = threads.length === 0
|
|
1545
|
+
? [React.createElement('div', { key: 'none', className: 'iterate-obs-empty' }, '本轮无线程')]
|
|
1546
|
+
: threads.map((t, ti) => {
|
|
1547
|
+
const key = `${ri}-${ti}`
|
|
1548
|
+
const expanded = expandedThreads.has(key)
|
|
1549
|
+
const dim = t.dimension || '未命名维度'
|
|
1550
|
+
const tFindings = t.findings || []
|
|
1551
|
+
const files = t.readFiles || []
|
|
1552
|
+
const msgs = t.messages || []
|
|
1553
|
+
return React.createElement('div', { key, className: 'iterate-obs-block' },
|
|
1554
|
+
React.createElement('div', { className: 'iterate-obs-block-head', 'data-click': '', onClick: () => toggleThread(key), title: expanded ? '收起线程' : '展开线程' },
|
|
1555
|
+
React.createElement('span', {}, expanded ? '−' : '+'),
|
|
1556
|
+
React.createElement('b', {}, dim),
|
|
1557
|
+
React.createElement('span', { className: 'iterate-obs-chip' }, `attempt ${typeof t.attempt === 'number' ? t.attempt : '?'}`),
|
|
1558
|
+
React.createElement('span', { className: 'iterate-obs-chip' }, `${tFindings.length} findings`),
|
|
1559
|
+
),
|
|
1560
|
+
expanded
|
|
1561
|
+
? React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
1562
|
+
React.createElement('div', { className: 'iterate-obs-bar' },
|
|
1563
|
+
React.createElement('b', {}, '叙述'),
|
|
1564
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' }, `${msgs.length} 条`),
|
|
1565
|
+
),
|
|
1566
|
+
...(msgs.length === 0
|
|
1567
|
+
? [React.createElement('div', { key: 'msg-none', className: 'iterate-obs-empty' }, '无线索叙述')]
|
|
1568
|
+
: msgs.map((m, mi) => React.createElement('div', { key: mi, className: 'iterate-obs-msg' }, String(m)))),
|
|
1569
|
+
...(files.length > 0
|
|
1570
|
+
? [
|
|
1571
|
+
React.createElement('div', { key: 'files-h', className: 'iterate-obs-bar', style: { marginTop: 6 } },
|
|
1572
|
+
React.createElement('b', {}, '读取文件'),
|
|
1573
|
+
),
|
|
1574
|
+
...files.map((f, fi) =>
|
|
1575
|
+
React.createElement('div', { key: `f${fi}`, className: 'iterate-obs-file' }, String(f)),
|
|
1576
|
+
),
|
|
1577
|
+
]
|
|
1578
|
+
: []),
|
|
1579
|
+
React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 6 } },
|
|
1580
|
+
React.createElement('b', {}, '发现'),
|
|
1581
|
+
),
|
|
1582
|
+
...(tFindings.length === 0
|
|
1583
|
+
? [React.createElement('div', { key: 'finding-none', className: 'iterate-obs-empty' }, '本线程无发现')]
|
|
1584
|
+
: tFindings.map((f, fi) => threadFindingPill(f, `tf${fi}`))),
|
|
1585
|
+
)
|
|
1586
|
+
: null,
|
|
1587
|
+
)
|
|
1588
|
+
})
|
|
1589
|
+
return React.createElement('div', { key: ri, className: 'iterate-obs-block' },
|
|
1590
|
+
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
1591
|
+
React.createElement('b', {}, `Round ${typeof r.round === 'number' ? r.round : ri + 1}`),
|
|
1592
|
+
React.createElement('span', { className: 'iterate-obs-chip' }, `${threads.length} 线程`),
|
|
1593
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' }, `${fCount} findings`),
|
|
1594
|
+
),
|
|
1595
|
+
React.createElement('div', { className: 'iterate-obs-block-body' }, ...threadBlocks),
|
|
1596
|
+
)
|
|
1597
|
+
}))
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
// ── F2: convergence trend (inline bars, reusing TrendChart) ─────────────
|
|
1601
|
+
const renderTrend = () => {
|
|
1602
|
+
const conv = manifest.convergence || []
|
|
1603
|
+
if (conv.length === 0) {
|
|
1604
|
+
return React.createElement('div', { className: 'iterate-obs-empty' }, '暂无收敛数据')
|
|
1605
|
+
}
|
|
1606
|
+
const points = conv.map((n, i) => ({ round: i + 1, count: n }))
|
|
1607
|
+
return React.createElement('div', {},
|
|
1608
|
+
React.createElement(TrendChart, { points }),
|
|
1609
|
+
React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 6 } },
|
|
1610
|
+
`各轮发现数量:${conv.join(' → ')}${typeof manifest.round === 'number' ? ` · 当前 Round ${manifest.round}` : ''}`,
|
|
1611
|
+
),
|
|
1612
|
+
)
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
// ── F3: finding location + action copy buttons ─────────────────────────
|
|
1616
|
+
const renderFindings = () => {
|
|
1617
|
+
const findings = manifest.findings || []
|
|
1618
|
+
if (findings.length === 0) {
|
|
1619
|
+
return React.createElement('div', { className: 'iterate-obs-empty' }, '暂无发现')
|
|
1620
|
+
}
|
|
1621
|
+
return React.createElement('div', {}, ...findings.map((f, i) => {
|
|
1622
|
+
const k = `f3-${i}`
|
|
1623
|
+
const loc = `${String(f.file || '?')}${typeof f.line === 'number' && f.line > 0 ? `:${f.line}` : ''}`
|
|
1624
|
+
const entry = {
|
|
1625
|
+
file: String(f.file || ''),
|
|
1626
|
+
...(typeof f.line === 'number' && f.line > 0 ? { line: f.line } : {}),
|
|
1627
|
+
dimension: String(f.dimension || ''),
|
|
1628
|
+
reason: String(f.summary || ''),
|
|
1629
|
+
}
|
|
1630
|
+
const knownInstruction = buildApplyInstruction([entry])
|
|
1631
|
+
const fixInstruction = buildObsFixInstruction(f)
|
|
1632
|
+
return React.createElement('div', { key: k, className: 'iterate-obs-block' },
|
|
1633
|
+
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
1634
|
+
React.createElement('span', { className: 'iterate-sev-dot', style: { background: severityColor(f.severity) } }),
|
|
1635
|
+
React.createElement('span', {}, severityLabel(f.severity)),
|
|
1636
|
+
React.createElement('span', { className: 'iterate-obs-file' }, loc),
|
|
1637
|
+
React.createElement('span', {}, String(f.dimension || '')),
|
|
1638
|
+
f.acknowledged === true
|
|
1639
|
+
? React.createElement('span', { className: 'iterate-obs-chip' }, '已确认')
|
|
1640
|
+
: null,
|
|
1641
|
+
),
|
|
1642
|
+
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
1643
|
+
React.createElement('div', { className: 'iterate-obs-msg' }, String(f.summary || '')),
|
|
1644
|
+
React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 8 } },
|
|
1645
|
+
React.createElement('button', {
|
|
1646
|
+
className: 'iterate-btn', 'data-copied': copiedKey === `${k}-fix` ? '' : undefined,
|
|
1647
|
+
onClick: () => copyInstruction(`${k}-fix`, String(f.suggested_fix || '')),
|
|
1648
|
+
title: '复制该 finding 的建议修复文本',
|
|
1649
|
+
}, copiedKey === `${k}-fix` ? '已复制' : '复制 suggested_fix'),
|
|
1650
|
+
React.createElement('button', {
|
|
1651
|
+
className: 'iterate-btn', 'data-copied': copiedKey === `${k}-known` ? '' : undefined,
|
|
1652
|
+
onClick: () => copyInstruction(`${k}-known`, knownInstruction),
|
|
1653
|
+
title: '复制 iterate_triage 指令文本(标记为 known_intentional)',
|
|
1654
|
+
}, copiedKey === `${k}-known` ? '已复制' : 'known_intentional'),
|
|
1655
|
+
React.createElement('button', {
|
|
1656
|
+
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === `${k}-repair` ? '' : undefined,
|
|
1657
|
+
onClick: () => copyInstruction(`${k}-repair`, fixInstruction),
|
|
1658
|
+
title: '复制 iterate_fix 指令文本',
|
|
1659
|
+
}, copiedKey === `${k}-repair` ? '已复制' : '复制修复指令'),
|
|
1660
|
+
),
|
|
1661
|
+
),
|
|
1662
|
+
)
|
|
1663
|
+
}))
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
// ── F4: fixes + rollback ───────────────────────────────────────────────
|
|
1667
|
+
const renderFixes = () => {
|
|
1668
|
+
const fixes = manifest.fixes || []
|
|
1669
|
+
if (fixes.length === 0) {
|
|
1670
|
+
return React.createElement('div', { className: 'iterate-obs-empty' }, '暂无修复记录')
|
|
1671
|
+
}
|
|
1672
|
+
return React.createElement('div', {}, ...fixes.map((f, i) => {
|
|
1673
|
+
const k = `f4-${i}`
|
|
1674
|
+
const id = String(f.id || `fix#${i + 1}`)
|
|
1675
|
+
const rollbackText = `请调用 \`iterate_rollback\` 回滚以下修复:\n\n\`\`\`json\n${JSON.stringify({ id }, null, 2)}\n\`\`\``
|
|
1676
|
+
const added = typeof f.linesAdded === 'number' ? f.linesAdded : 0
|
|
1677
|
+
const removed = typeof f.linesRemoved === 'number' ? f.linesRemoved : 0
|
|
1678
|
+
return React.createElement('div', { key: k, className: 'iterate-obs-block' },
|
|
1679
|
+
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
1680
|
+
React.createElement('b', {}, id),
|
|
1681
|
+
React.createElement('span', { className: 'iterate-obs-file' }, String(f.file || '?')),
|
|
1682
|
+
React.createElement('span', { className: 'iterate-obs-chip' }, f.success === true ? '成功' : '失败'),
|
|
1683
|
+
typeof f.round === 'number'
|
|
1684
|
+
? React.createElement('span', { className: 'iterate-obs-head-meta' }, `Round ${f.round}`)
|
|
1685
|
+
: null,
|
|
1686
|
+
),
|
|
1687
|
+
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
1688
|
+
React.createElement('div', { className: 'iterate-obs-msg' }, String(f.summary || '')),
|
|
1689
|
+
React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 8 } },
|
|
1690
|
+
React.createElement('span', { className: 'iterate-obs-chip' }, `+${added}`),
|
|
1691
|
+
React.createElement('span', { className: 'iterate-obs-chip' }, `−${removed}`),
|
|
1692
|
+
React.createElement('button', {
|
|
1693
|
+
className: 'iterate-btn', 'data-danger': '', 'data-copied': copiedKey === `${k}-rb` ? '' : undefined,
|
|
1694
|
+
onClick: () => copyInstruction(`${k}-rb`, rollbackText),
|
|
1695
|
+
title: '复制 iterate_rollback 指令文本',
|
|
1696
|
+
}, copiedKey === `${k}-rb` ? '已复制' : '回滚'),
|
|
1697
|
+
),
|
|
1698
|
+
),
|
|
1699
|
+
)
|
|
1700
|
+
}))
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
// ── F5: checkpoint resume ──────────────────────────────────────────────
|
|
1704
|
+
const renderCheckpoint = () => {
|
|
1705
|
+
const cp = manifest.checkpoint
|
|
1706
|
+
if (!cp) {
|
|
1707
|
+
return React.createElement('div', { className: 'iterate-obs-empty' }, '暂无断点')
|
|
1708
|
+
}
|
|
1709
|
+
const resumeText = `请调用 \`iterate_checkpoint\` 从断点恢复迭代:\n\n\`\`\`json\n${JSON.stringify({
|
|
1710
|
+
operation: 'resume',
|
|
1711
|
+
mode: String(cp.mode || 'normal'),
|
|
1712
|
+
maxRounds: typeof cp.maxRounds === 'number' ? cp.maxRounds : null,
|
|
1713
|
+
}, null, 2)}\n\`\`\``
|
|
1714
|
+
const item = (label: string, value: unknown) =>
|
|
1715
|
+
React.createElement('span', { className: 'iterate-obs-chip' }, `${label} ${String(value ?? '?')}`)
|
|
1716
|
+
return React.createElement('div', { className: 'iterate-obs-block' },
|
|
1717
|
+
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
1718
|
+
React.createElement('b', {}, '断点'),
|
|
1719
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' }, String(cp.updatedAt || '')),
|
|
1720
|
+
),
|
|
1721
|
+
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
1722
|
+
React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
1723
|
+
item('mode', cp.mode || 'normal'),
|
|
1724
|
+
item('round', cp.round),
|
|
1725
|
+
item('maxRounds', cp.maxRounds),
|
|
1726
|
+
item('fixed', cp.fixedCount),
|
|
1727
|
+
item('resume', cp.resumeCount),
|
|
1728
|
+
),
|
|
1729
|
+
React.createElement('button', {
|
|
1730
|
+
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'cp-resume' ? '' : undefined,
|
|
1731
|
+
onClick: () => copyInstruction('cp-resume', resumeText),
|
|
1732
|
+
title: '复制 iterate_checkpoint 综/恢复指令文本',
|
|
1733
|
+
}, copiedKey === 'cp-resume' ? '已复制' : '复制恢复指令'),
|
|
1734
|
+
),
|
|
1735
|
+
)
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// ── F6: run console (status / approval / nudge draft) ──────────────────
|
|
1739
|
+
const renderConsole = () => {
|
|
1740
|
+
const approval = manifest.approval || { policy: 'ask' }
|
|
1741
|
+
const policyLabelMap: Record<string, string> = { ask: '询问用户', deny: '拒绝', allow: '直接执行' }
|
|
1742
|
+
const policyLabel = policyLabelMap[String(approval.policy || 'ask')] || String(approval.policy || 'ask')
|
|
1743
|
+
const nudge = manifest.nudge || null
|
|
1744
|
+
const present = Boolean(nudge && nudge.text)
|
|
1745
|
+
const activeNudgeText = nudge && nudge.text ? nudge.text : ''
|
|
1746
|
+
const nudgeInstruction = `请调用 \`iterate_transcript\` 写入 nudge 指令:\n\n\`\`\`json\n${JSON.stringify({ operation: 'nudge', text: nudgeText }, null, 2)}\n\`\`\``
|
|
1747
|
+
// Clearing the PERSISTED nudge (as opposed to the draft) requires
|
|
1748
|
+
// text:null. Copied as a paste-able instruction, mirroring the panel's
|
|
1749
|
+
// copy-to-command pattern — it cannot be cleared from the client directly.
|
|
1750
|
+
const activeClearInstruction = `请调用 \`iterate_transcript\` 清除当前 nudge:\n\n\`\`\`json\n${JSON.stringify({ operation: 'nudge', text: null }, null, 2)}\n\`\`\``
|
|
1751
|
+
return React.createElement('div', { className: 'iterate-obs-block' },
|
|
1752
|
+
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
1753
|
+
React.createElement('span', {}, '运行控制台'),
|
|
1754
|
+
React.createElement('span', { className: 'iterate-obs-badge', 'data-live': live ? '' : undefined }, live ? '运行中' : '已结束'),
|
|
1755
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' }, `批准策略:${policyLabel}`),
|
|
1756
|
+
),
|
|
1757
|
+
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
1758
|
+
present
|
|
1759
|
+
? React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 6 } },
|
|
1760
|
+
React.createElement('b', {}, '当前 nudge'),
|
|
1761
|
+
React.createElement('span', { className: 'iterate-obs-msg' }, activeNudgeText),
|
|
1762
|
+
React.createElement('button', {
|
|
1763
|
+
className: 'iterate-btn', 'data-copied': copiedKey === 'nudge-clear' ? '' : undefined,
|
|
1764
|
+
onClick: () => copyInstruction('nudge-clear', activeClearInstruction),
|
|
1765
|
+
title: '复制清除 nudge 指令(写 text:null),在运行控制台提示清除已持久化的 nudge',
|
|
1766
|
+
}, copiedKey === 'nudge-clear' ? '已复制清除指令' : '复制清除指令'),
|
|
1767
|
+
)
|
|
1768
|
+
: null,
|
|
1769
|
+
React.createElement('textarea', {
|
|
1770
|
+
className: 'iterate-obs-input iterate-obs-mono',
|
|
1771
|
+
rows: 3,
|
|
1772
|
+
placeholder: '起草一条 nudge 方向指令…',
|
|
1773
|
+
value: nudgeText,
|
|
1774
|
+
'aria-label': 'nudge 草稿',
|
|
1775
|
+
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => setNudgeText(e.target.value),
|
|
1776
|
+
}),
|
|
1777
|
+
React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 6 } },
|
|
1778
|
+
React.createElement('button', {
|
|
1779
|
+
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'nudge' ? '' : undefined,
|
|
1780
|
+
disabled: !nudgeText,
|
|
1781
|
+
onClick: () => copyInstruction('nudge', nudgeInstruction),
|
|
1782
|
+
title: '复制 iterate_transcript nudge 指令文本',
|
|
1783
|
+
}, copiedKey === 'nudge' ? '已复制' : '复制 nudge 指令'),
|
|
1784
|
+
),
|
|
1785
|
+
),
|
|
1786
|
+
)
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
// ── F7: decision timeline (type filter + search, newest first) ──────────
|
|
1790
|
+
const renderTimeline = () => {
|
|
1791
|
+
const entries = manifest.timeline || []
|
|
1792
|
+
if (entries.length === 0) {
|
|
1793
|
+
return React.createElement('div', { className: 'iterate-obs-empty' }, '暂无时间线条目')
|
|
1794
|
+
}
|
|
1795
|
+
const types = Array.from(new Set(entries.map((t) => String(t.type || 'unknown')).filter(Boolean))).sort()
|
|
1796
|
+
const q = timelineSearch.trim().toLowerCase()
|
|
1797
|
+
const filtered = entries.filter((t) => {
|
|
1798
|
+
if (timelineType && String(t.type || '') !== timelineType) return false
|
|
1799
|
+
if (!q) return true
|
|
1800
|
+
const hay = [String(t.type || ''), String(t.round ?? ''), JSON.stringify(t.data || {})].join(' ').toLowerCase()
|
|
1801
|
+
return hay.indexOf(q) >= 0
|
|
1802
|
+
})
|
|
1803
|
+
// Newest first: entries are not guaranteed to be reverse ordered in the
|
|
1804
|
+
// manifest, so sort by timestamp string descending for a stable timeline.
|
|
1805
|
+
const sorted = filtered.slice().sort((a, b) => String(b.timestamp || '').localeCompare(String(a.timestamp || '')))
|
|
1806
|
+
const rows = sorted.map((t, i) => {
|
|
1807
|
+
const k = `f7-${i}`
|
|
1808
|
+
const dataText = t.data && typeof t.data === 'object' ? JSON.stringify(t.data) : ''
|
|
1809
|
+
return React.createElement('div', { key: k, className: 'iterate-obs-row', style: { flexDirection: 'column', alignItems: 'flex-start' } },
|
|
1810
|
+
React.createElement('div', { className: 'iterate-obs-bar', style: { width: '100%' } },
|
|
1811
|
+
React.createElement('span', { className: 'iterate-obs-chip' }, String(t.type || '?')),
|
|
1812
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' },
|
|
1813
|
+
`${typeof t.round === 'number' ? `Round ${t.round} · ` : ''}${String(t.timestamp || '')}`,
|
|
1814
|
+
),
|
|
1815
|
+
),
|
|
1816
|
+
dataText ? React.createElement('div', { className: 'iterate-obs-code' }, dataText) : null,
|
|
1817
|
+
)
|
|
1818
|
+
})
|
|
1819
|
+
if (rows.length === 0) {
|
|
1820
|
+
return React.createElement('div', { className: 'iterate-obs-empty' }, '无匹配的时间线条目')
|
|
1821
|
+
}
|
|
1822
|
+
return React.createElement('div', {},
|
|
1823
|
+
React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
1824
|
+
React.createElement('select', {
|
|
1825
|
+
className: 'iterate-filter-select',
|
|
1826
|
+
value: timelineType,
|
|
1827
|
+
'aria-label': '按时间线类型筛选',
|
|
1828
|
+
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => setTimelineType(e.target.value),
|
|
1829
|
+
},
|
|
1830
|
+
React.createElement('option', { value: '' }, '全部类型'),
|
|
1831
|
+
...types.map((t) => React.createElement('option', { key: t, value: t }, t)),
|
|
1832
|
+
),
|
|
1833
|
+
React.createElement('input', {
|
|
1834
|
+
className: 'iterate-filter-search',
|
|
1835
|
+
type: 'search',
|
|
1836
|
+
placeholder: '搜索时间线…',
|
|
1837
|
+
'aria-label': '搜索时间线',
|
|
1838
|
+
value: timelineSearch,
|
|
1839
|
+
onChange: (e: React.ChangeEvent<HTMLInputElement>) => setTimelineSearch(e.target.value),
|
|
1840
|
+
}),
|
|
1841
|
+
React.createElement('span', { className: 'iterate-filter-count' }, `${filtered.length}/${entries.length}`),
|
|
1842
|
+
),
|
|
1843
|
+
...rows,
|
|
1844
|
+
)
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
// ── Live: real-time secondary-subagent activity stream (newest first) ─────
|
|
1848
|
+
const renderLive = () => {
|
|
1849
|
+
const liveEntries = manifest.live || []
|
|
1850
|
+
if (liveEntries.length === 0) {
|
|
1851
|
+
// Live entries only accumulate during an ACTIVE run. For a completed run
|
|
1852
|
+
// (or before any capture) this tab is empty — guide the user to the
|
|
1853
|
+
// populated summary tabs instead of leaving a dead-end "暂无实时活动" state.
|
|
1854
|
+
const hasThreads = (manifest.rounds || []).length > 0
|
|
1855
|
+
const hasFindings = (manifest.findings || []).length > 0
|
|
1856
|
+
if (hasThreads || hasFindings) {
|
|
1857
|
+
return React.createElement('div', { className: 'iterate-obs-empty' },
|
|
1858
|
+
React.createElement('div', {}, '实时活动流仅在运行期间记录,当前已完成。'),
|
|
1859
|
+
React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 8 } },
|
|
1860
|
+
React.createElement('button', { className: 'iterate-btn', onClick: () => setTab('f1') }, '查看审查线程'),
|
|
1861
|
+
React.createElement('button', { className: 'iterate-btn', onClick: () => setTab('f3') }, '查看发现'),
|
|
1862
|
+
),
|
|
1863
|
+
)
|
|
1864
|
+
}
|
|
1865
|
+
return React.createElement('div', { className: 'iterate-obs-empty' }, '暂无实时活动')
|
|
1866
|
+
}
|
|
1867
|
+
const rows = liveEntries.map((e, i) => {
|
|
1868
|
+
const type = String(e.type || 'unknown')
|
|
1869
|
+
const meta = OBS_LIVE_META[type] || { label: String(e.type || '?'), color: OBS_LIVE_FALLBACK_COLOR }
|
|
1870
|
+
const tool = String(e.tool || '')
|
|
1871
|
+
const target = String(e.target || '')
|
|
1872
|
+
const time = formatObsLiveTime(e.ts)
|
|
1873
|
+
return React.createElement('div', { key: `live-${i}`, className: 'iterate-obs-row' },
|
|
1874
|
+
React.createElement('span', { className: 'iterate-obs-livebadge', style: { color: meta.color } }, meta.label),
|
|
1875
|
+
tool
|
|
1876
|
+
? React.createElement('span', { className: 'iterate-obs-chip' }, tool)
|
|
1877
|
+
: null,
|
|
1878
|
+
React.createElement('span', { className: 'iterate-obs-msg' }, target || '—'),
|
|
1879
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' }, time),
|
|
1880
|
+
)
|
|
1881
|
+
})
|
|
1882
|
+
return React.createElement('div', {},
|
|
1883
|
+
React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
1884
|
+
React.createElement('b', {}, '实时活动'),
|
|
1885
|
+
React.createElement('span', { className: 'iterate-obs-chip' }, `${liveEntries.length} 项`),
|
|
1886
|
+
),
|
|
1887
|
+
...rows,
|
|
1888
|
+
)
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
const renderBody = () => {
|
|
1892
|
+
switch (tab) {
|
|
1893
|
+
case 'live': return renderLive()
|
|
1894
|
+
case 'f1': return renderThreads()
|
|
1895
|
+
case 'f2': return renderTrend()
|
|
1896
|
+
case 'f3': return renderFindings()
|
|
1897
|
+
case 'f4': return renderFixes()
|
|
1898
|
+
case 'f5': return renderCheckpoint()
|
|
1899
|
+
case 'f6': return renderConsole()
|
|
1900
|
+
case 'f7': return renderTimeline()
|
|
1901
|
+
default: return null
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
return React.createElement('div', { 'data-iterate-root': '', 'data-iterate': 'obs', className: 'iterate-obs' },
|
|
1906
|
+
React.createElement('div', { className: 'iterate-obs-head', 'data-closed': open ? undefined : '', onClick: () => setOpen((v) => !v) },
|
|
1907
|
+
React.createElement('span', { className: 'iterate-obs-title' }, 'iterate 观测台'),
|
|
1908
|
+
React.createElement('span', { className: 'iterate-obs-badge', 'data-live': live ? '' : undefined }, live ? '运行中' : '已结束'),
|
|
1909
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' }, headMeta || 'runtime'),
|
|
1910
|
+
React.createElement('button', {
|
|
1911
|
+
className: 'iterate-btn', 'data-ghost': '',
|
|
1912
|
+
onClick: (e: React.MouseEvent<HTMLButtonElement>) => { e.stopPropagation(); setOpen((v) => !v) },
|
|
1913
|
+
}, open ? '收起' : '展开'),
|
|
1914
|
+
),
|
|
1915
|
+
open
|
|
1916
|
+
? React.createElement('div', {},
|
|
1917
|
+
copyFailText
|
|
1918
|
+
? React.createElement('div', { className: 'iterate-obs-block' },
|
|
1919
|
+
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
1920
|
+
React.createElement('b', {}, '复制未成功'),
|
|
1921
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' }, '剪贴板写入被拒绝,请手动选中下方指令复制'),
|
|
1922
|
+
React.createElement('button', {
|
|
1923
|
+
className: 'iterate-btn', 'data-ghost': '', 'aria-label': '关闭复制失败提示',
|
|
1924
|
+
onClick: () => setCopyFailText(null),
|
|
1925
|
+
}, '关闭'),
|
|
1926
|
+
),
|
|
1927
|
+
React.createElement('div', { className: 'iterate-obs-code' }, copyFailText),
|
|
1928
|
+
)
|
|
1929
|
+
: null,
|
|
1930
|
+
React.createElement('div', { className: 'iterate-obs-tabs' },
|
|
1931
|
+
...OBS_TABS.map((t) =>
|
|
1932
|
+
React.createElement('button', {
|
|
1933
|
+
key: t.key,
|
|
1934
|
+
className: 'iterate-obs-tab',
|
|
1935
|
+
'data-active': tab === t.key ? '' : undefined,
|
|
1936
|
+
onClick: () => setTab(t.key),
|
|
1937
|
+
}, t.label),
|
|
1938
|
+
),
|
|
1939
|
+
),
|
|
1940
|
+
React.createElement('div', { className: 'iterate-obs-body' }, renderBody()),
|
|
1941
|
+
)
|
|
1942
|
+
: null,
|
|
1943
|
+
)
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1251
1946
|
// ─── Registration ────────────────────────────────────────────────────────────
|
|
1252
1947
|
|
|
1253
1948
|
/**
|
|
@@ -1303,6 +1998,15 @@ export function apply(ctx: ClientContext): void {
|
|
|
1303
1998
|
),
|
|
1304
1999
|
)
|
|
1305
2000
|
|
|
2001
|
+
// F1–F7: runtime observatory board (order 91, below the dashboard's 90 so
|
|
2002
|
+
// its collapsed header never occludes the main convergence dashboard).
|
|
2003
|
+
slotsSvc.inject('conversation.input.dock', () =>
|
|
2004
|
+
slotsSvc?.register(
|
|
2005
|
+
{ name: 'conversation.input.dock', id: 'iterate-observatory', order: 91 },
|
|
2006
|
+
(props) => React.createElement(ObservatoryPanel, props),
|
|
2007
|
+
),
|
|
2008
|
+
)
|
|
2009
|
+
|
|
1306
2010
|
// 2 + 3: triage panel / stats card (turn-tail chain, session scope).
|
|
1307
2011
|
slotsSvc.inject('conversation.chat.turnTail', () =>
|
|
1308
2012
|
slotsSvc?.register(
|