iterate-plugin 3.14.0 → 3.16.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 +7 -4
- package/README.zh-CN.md +5 -2
- package/lib/client.js +361 -9
- package/package.json +24 -20
- package/src/client/context-menu.ts +11 -2
- package/src/client/i18n.ts +21 -2
- package/src/client/index.ts +242 -25
- package/src/client/run-compare.ts +213 -0
- package/src/client/shortcuts.ts +152 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iterate-plugin",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.16.0",
|
|
4
4
|
"description": "dsh plugin that turns the iterate skill into an autonomous closed-loop harness with quality command center and experience bank (v3.3). Features: plan -> parallel review xN -> atomic fixes -> validate -> loop -> auto-stop, plus dry-run pure-review mode, quality gate compute/persist, writable experience bank, defense events stream (record + bilingual labels), and native command buttons — with a live quality command center (F8/F9/F10 render real session data), §8 assign-fix instruction, and task_mode indicator wired end to end.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -56,7 +56,8 @@
|
|
|
56
56
|
"0.2.1-rc.4": "compatible",
|
|
57
57
|
"0.2.1-rc.5": "compatible",
|
|
58
58
|
"0.2.2-alpha.2": "compatible",
|
|
59
|
-
"0.2.2-alpha.3": "compatible"
|
|
59
|
+
"0.2.2-alpha.3": "compatible",
|
|
60
|
+
"0.2.2-rc.1": "compatible"
|
|
60
61
|
}
|
|
61
62
|
},
|
|
62
63
|
"client": {
|
|
@@ -93,14 +94,14 @@
|
|
|
93
94
|
"test:validate": "tsx --test test/validate.test.ts"
|
|
94
95
|
},
|
|
95
96
|
"dependencies": {
|
|
96
|
-
"iterate-kernel": "0.1.
|
|
97
|
+
"iterate-kernel": "0.1.10",
|
|
97
98
|
"js-yaml": "4.3.2"
|
|
98
99
|
},
|
|
99
100
|
"peerDependencies": {
|
|
100
101
|
"@deepseek-ai/cordis": "^4.0.5-alpha.2 || ~4.0.6-alpha.1 || ~4.0.6-alpha.2",
|
|
101
|
-
"@deepseek-ai/dsh-session": "0.1.1-rc.1 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 0.2.1-alpha.5 || 0.2.1-rc.1 || 0.2.1-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.4 || 0.2.1-rc.5 || 0.2.2-alpha.2 || 0.2.2-alpha.3",
|
|
102
|
-
"@deepseek-ai/dsh-tools": "0.1.1-rc.1 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 0.2.1-alpha.5 || 0.2.1-rc.1 || 0.2.1-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.4 || 0.2.1-rc.5 || 0.2.2-alpha.2 || 0.2.2-alpha.3",
|
|
103
|
-
"@deepseek-ai/dsh-util-values": "0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 0.2.1-alpha.5 || 0.2.1-rc.1 || 0.2.1-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.4 || 0.2.1-rc.5 || 0.2.2-alpha.2 || 0.2.2-alpha.3"
|
|
102
|
+
"@deepseek-ai/dsh-session": "0.1.1-rc.1 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 0.2.1-alpha.5 || 0.2.1-rc.1 || 0.2.1-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.4 || 0.2.1-rc.5 || 0.2.2-alpha.2 || 0.2.2-alpha.3 || 0.2.2-rc.1",
|
|
103
|
+
"@deepseek-ai/dsh-tools": "0.1.1-rc.1 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 0.2.1-alpha.5 || 0.2.1-rc.1 || 0.2.1-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.4 || 0.2.1-rc.5 || 0.2.2-alpha.2 || 0.2.2-alpha.3 || 0.2.2-rc.1",
|
|
104
|
+
"@deepseek-ai/dsh-util-values": "0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 0.2.1-alpha.5 || 0.2.1-rc.1 || 0.2.1-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.4 || 0.2.1-rc.5 || 0.2.2-alpha.2 || 0.2.2-alpha.3 || 0.2.2-rc.1"
|
|
104
105
|
},
|
|
105
106
|
"peerDependenciesMeta": {
|
|
106
107
|
"@deepseek-ai/cordis": {
|
|
@@ -117,20 +118,20 @@
|
|
|
117
118
|
}
|
|
118
119
|
},
|
|
119
120
|
"devDependencies": {
|
|
120
|
-
"@deepseek-ai/cordis": "4.0
|
|
121
|
-
"@deepseek-ai/dsh-agent": "0.2.2-
|
|
122
|
-
"@deepseek-ai/dsh-jobs": "0.2.2-
|
|
123
|
-
"@deepseek-ai/dsh-llm": "0.2.2-
|
|
124
|
-
"@deepseek-ai/dsh-ptc-runtime": "0.2.2-
|
|
125
|
-
"@deepseek-ai/dsh-sandbox": "0.2.2-
|
|
126
|
-
"@deepseek-ai/dsh-sandbox-policy": "0.2.2-
|
|
127
|
-
"@deepseek-ai/dsh-scope": "0.2.2-
|
|
128
|
-
"@deepseek-ai/dsh-session": "0.2.2-
|
|
129
|
-
"@deepseek-ai/dsh-system-prompt": "0.2.2-
|
|
130
|
-
"@deepseek-ai/dsh-tools": "0.2.2-
|
|
131
|
-
"@deepseek-ai/dsh-user-approval": "0.2.2-
|
|
132
|
-
"@deepseek-ai/dsh-util-values": "0.2.2-
|
|
133
|
-
"@deepseek-ai/dsh-working-directory": "0.2.2-
|
|
121
|
+
"@deepseek-ai/cordis": "4.1.0",
|
|
122
|
+
"@deepseek-ai/dsh-agent": "0.2.2-rc.1",
|
|
123
|
+
"@deepseek-ai/dsh-jobs": "0.2.2-rc.1",
|
|
124
|
+
"@deepseek-ai/dsh-llm": "0.2.2-rc.1",
|
|
125
|
+
"@deepseek-ai/dsh-ptc-runtime": "0.2.2-rc.1",
|
|
126
|
+
"@deepseek-ai/dsh-sandbox": "0.2.2-rc.1",
|
|
127
|
+
"@deepseek-ai/dsh-sandbox-policy": "0.2.2-rc.1",
|
|
128
|
+
"@deepseek-ai/dsh-scope": "0.2.2-rc.1",
|
|
129
|
+
"@deepseek-ai/dsh-session": "0.2.2-rc.1",
|
|
130
|
+
"@deepseek-ai/dsh-system-prompt": "0.2.2-rc.1",
|
|
131
|
+
"@deepseek-ai/dsh-tools": "0.2.2-rc.1",
|
|
132
|
+
"@deepseek-ai/dsh-user-approval": "0.2.2-rc.1",
|
|
133
|
+
"@deepseek-ai/dsh-util-values": "0.2.2-rc.1",
|
|
134
|
+
"@deepseek-ai/dsh-working-directory": "0.2.2-rc.1",
|
|
134
135
|
"@types/js-yaml": "4.0.9",
|
|
135
136
|
"@types/node": "22.15.0",
|
|
136
137
|
"@types/react": "19.2.2",
|
|
@@ -144,5 +145,8 @@
|
|
|
144
145
|
"engines": {
|
|
145
146
|
"node": ">=20",
|
|
146
147
|
"dsh": ">=0.1.1-rc.1"
|
|
148
|
+
},
|
|
149
|
+
"overrides": {
|
|
150
|
+
"zod": "4.6.7"
|
|
147
151
|
}
|
|
148
152
|
}
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
42
|
import { getLang, type Lang } from './i18n.ts'
|
|
43
|
+
import { OBSERVATORY_SHORTCUT_ID } from './shortcuts.ts'
|
|
43
44
|
|
|
44
45
|
// ─── Structural service types (defensive; mirrors SlotsService) ──────────────
|
|
45
46
|
//
|
|
@@ -63,6 +64,8 @@ export interface IterateMenuAction {
|
|
|
63
64
|
separatorBefore?: boolean
|
|
64
65
|
/** Localized operation + recovery guidance when `run` rejects. */
|
|
65
66
|
failureLabel?: string
|
|
67
|
+
/** Live-shortcut display for an action backed by a registered command. */
|
|
68
|
+
commandId?: string
|
|
66
69
|
run?: () => void | Promise<void>
|
|
67
70
|
}
|
|
68
71
|
|
|
@@ -168,7 +171,7 @@ export function buildSessionMenuEntries(lang: Lang, opts?: { hasCheckpoint?: boo
|
|
|
168
171
|
export function buildFileMenuEntries(lang: Lang): MenuEntry[] {
|
|
169
172
|
return [
|
|
170
173
|
{ id: 'iterate.review-file', label: pick('iterate.review-file', lang) },
|
|
171
|
-
{ id: 'iterate.open-panel', label: pick('iterate.open-panel', lang) },
|
|
174
|
+
{ id: 'iterate.open-panel', label: pick('iterate.open-panel', lang), openTab: 'live' },
|
|
172
175
|
]
|
|
173
176
|
}
|
|
174
177
|
|
|
@@ -177,7 +180,7 @@ export function buildDirectoryMenuEntries(lang: Lang): MenuEntry[] {
|
|
|
177
180
|
return [
|
|
178
181
|
{ id: 'iterate.review-dir', label: pick('iterate.review-dir', lang) },
|
|
179
182
|
{ id: 'iterate.preflight', label: pick('iterate.preflight', lang), separatorBefore: true },
|
|
180
|
-
{ id: 'iterate.open-panel', label: pick('iterate.open-panel', lang) },
|
|
183
|
+
{ id: 'iterate.open-panel', label: pick('iterate.open-panel', lang), openTab: 'live' },
|
|
181
184
|
]
|
|
182
185
|
}
|
|
183
186
|
|
|
@@ -203,6 +206,12 @@ function toAction(entry: MenuEntry, deps: IterateMenuDeps, lang: Lang): IterateM
|
|
|
203
206
|
}
|
|
204
207
|
if (entry.disabled) action.disabled = true
|
|
205
208
|
if (entry.openTab) {
|
|
209
|
+
// The "open command center" action is backed by the registered
|
|
210
|
+
// `iterate.open-observatory` keyboard command, so the context menu shows
|
|
211
|
+
// its live keycap (Cmd/Ctrl+Shift+O) — the shortcut is discoverable in the
|
|
212
|
+
// same place the user would look for it. Display only: selection still
|
|
213
|
+
// runs this action's `run`.
|
|
214
|
+
action.commandId = OBSERVATORY_SHORTCUT_ID
|
|
206
215
|
action.run = () => {
|
|
207
216
|
deps.openPanel({ tab: entry.openTab })
|
|
208
217
|
deps.notify(lang === 'zh' ? '已打开 iterate 观测台。' : 'Iterate observatory opened.', true)
|
package/src/client/i18n.ts
CHANGED
|
@@ -296,8 +296,27 @@ export const EN: Record<string, string> = {
|
|
|
296
296
|
'各轮发现数量:{a0}{a1}': 'Findings per round: {a0}{a1}',
|
|
297
297
|
' · 当前 Round {a0}': ' · current round {a0}',
|
|
298
298
|
'跨轮对比(本会话)': 'Cross-round comparison (this session)',
|
|
299
|
-
'
|
|
300
|
-
'Cross-session comparison: this table only covers the current session —
|
|
299
|
+
'跨会话对比:本表只覆盖当前会话;要跨两次迭代对比,用下方「存为对比基准」保存本次运行,再与下一次迭代对比。':
|
|
300
|
+
'Cross-session comparison: this table only covers the current session — to compare two iterations, save this run with \"Save as comparison baseline\" below, then compare the next iteration against it.',
|
|
301
|
+
跨会话对比: 'Cross-session comparison',
|
|
302
|
+
'基准已保存:{a0}': 'Baseline saved: {a0}',
|
|
303
|
+
'先保存当前运行为基准,再跑下一次迭代后一键对比':
|
|
304
|
+
'Save the current run as the baseline, then one-click compare the next iteration against it',
|
|
305
|
+
'把当前运行摘要保存为对比基准(含轮次/发现/修复/验证/门禁)':
|
|
306
|
+
'Save the current run summary as the comparison baseline (rounds / findings / fixes / validation / gate)',
|
|
307
|
+
'当前没有可保存的运行数据': 'No run data to save yet',
|
|
308
|
+
已存为基准: 'Saved as baseline',
|
|
309
|
+
存为对比基准: 'Save as comparison baseline',
|
|
310
|
+
'显示或隐藏与已保存基准的对比表': 'Show or hide the comparison against the saved baseline',
|
|
311
|
+
收起对比: 'Hide comparison',
|
|
312
|
+
与基准对比: 'Compare with baseline',
|
|
313
|
+
'清除已保存的对比基准': 'Clear the saved comparison baseline',
|
|
314
|
+
清除基准: 'Clear baseline',
|
|
315
|
+
'当前会话没有运行数据可对比': 'This session has no run data to compare yet',
|
|
316
|
+
指标: 'Metric',
|
|
317
|
+
基准: 'Baseline',
|
|
318
|
+
当前: 'Current',
|
|
319
|
+
变化: 'Delta',
|
|
301
320
|
|
|
302
321
|
// ── F3 finding locations ─────────────────────────────────────────────────
|
|
303
322
|
暂无发现: 'No findings',
|
package/src/client/index.ts
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* 4. iterate theme skin -> theme.overrideTokens
|
|
25
25
|
* 5. Progress event linkage -> ctx.on('slots/changed') + round-pulse + shell.overlay capsule
|
|
26
26
|
* 6. Settings page section -> settings.section
|
|
27
|
-
* 7. Runtime observatory -> conversation.input.dock (order 91, F1–
|
|
27
|
+
* 7. Runtime observatory -> conversation.input.dock (order 91, F1–F11 board)
|
|
28
28
|
*
|
|
29
29
|
* Design notes:
|
|
30
30
|
* - All styles are injected via one <style> tag; colors use dsh token
|
|
@@ -141,6 +141,22 @@ import {
|
|
|
141
141
|
type ContextMenusService,
|
|
142
142
|
type ObsOpenDetail,
|
|
143
143
|
} from './context-menu.ts'
|
|
144
|
+
// v3.15: keyboard-command contribution (dsh 0.2.2-rc.1 unified shortcuts).
|
|
145
|
+
import {
|
|
146
|
+
registerIterateShortcuts,
|
|
147
|
+
type ShortcutsService,
|
|
148
|
+
} from './shortcuts.ts'
|
|
149
|
+
// v3.16: cross-session run comparison — the F2 tab compares two iterations of
|
|
150
|
+
// the same project (save the current run as a baseline, then compare any later
|
|
151
|
+
// run against it), instead of the old export-two-JSONs-and-diff-by-hand chore.
|
|
152
|
+
import {
|
|
153
|
+
buildRunBaseline,
|
|
154
|
+
parseRunBaseline,
|
|
155
|
+
compareRuns,
|
|
156
|
+
RUN_BASELINE_STORAGE_KEY,
|
|
157
|
+
type RunBaseline,
|
|
158
|
+
type RunCompareMetric,
|
|
159
|
+
} from './run-compare.ts'
|
|
144
160
|
|
|
145
161
|
// ─── Module contract ─────────────────────────────────────────────────────────
|
|
146
162
|
|
|
@@ -932,6 +948,21 @@ function readContextMenus(ctx: ClientContext): ContextMenusService | undefined {
|
|
|
932
948
|
return svc as unknown as ContextMenusService
|
|
933
949
|
}
|
|
934
950
|
|
|
951
|
+
/**
|
|
952
|
+
* Resolve the dsh 0.2.2-rc.1 `shortcuts` service. Like contextMenus it is NOT
|
|
953
|
+
* in the typed `ClientContext`, so it is probed through `ctx.get` and
|
|
954
|
+
* structurally validated. A host that predates the capability returns
|
|
955
|
+
* `undefined` and the plugin simply has no keyboard command.
|
|
956
|
+
*/
|
|
957
|
+
function readShortcuts(ctx: ClientContext): ShortcutsService | undefined {
|
|
958
|
+
if (!ctx || typeof ctx.get !== 'function') return undefined
|
|
959
|
+
const raw = ctx.get('shortcuts', false)
|
|
960
|
+
if (!raw || typeof raw !== 'object') return undefined
|
|
961
|
+
const svc = raw as { register?: unknown }
|
|
962
|
+
if (typeof svc.register !== 'function') return undefined
|
|
963
|
+
return svc as unknown as ShortcutsService
|
|
964
|
+
}
|
|
965
|
+
|
|
935
966
|
/**
|
|
936
967
|
* Extract a session identifier from an opaque host-shaped session object.
|
|
937
968
|
* The slot `props.session` shape is host-defined; this probes the common
|
|
@@ -967,6 +998,8 @@ let themeListener: (() => void) | null = null
|
|
|
967
998
|
let themeEnabled = true
|
|
968
999
|
/** v3.14: right-click menu contribution disposer (re-apply() never stacks). */
|
|
969
1000
|
let menuDisposer: (() => void) | null = null
|
|
1001
|
+
/** v3.15: keyboard-command contribution disposer (re-apply() never stacks). */
|
|
1002
|
+
let shortcutDisposer: (() => void) | null = null
|
|
970
1003
|
const roundPulseListeners: Array<(payload: { round: number; converged: boolean }) => void> = []
|
|
971
1004
|
|
|
972
1005
|
/**
|
|
@@ -2232,7 +2265,7 @@ function SettingsPanel(props: SlotProps) {
|
|
|
2232
2265
|
)
|
|
2233
2266
|
}
|
|
2234
2267
|
|
|
2235
|
-
// ─── Runtime observatory panel (F1–
|
|
2268
|
+
// ─── Runtime observatory panel (F1–F11) ─────────────────────────────────────
|
|
2236
2269
|
|
|
2237
2270
|
/** Tab id → label for the observatory's eleven panels. Live (real-time
|
|
2238
2271
|
* activity stream) is first so it is the default view; F-panels follow.
|
|
@@ -2296,6 +2329,17 @@ const OBS_LIVE_META: Record<string, { label: { zh: string; en: string }; color:
|
|
|
2296
2329
|
}
|
|
2297
2330
|
const OBS_LIVE_FALLBACK_COLOR = 'var(--dsw-alias-label-secondary)'
|
|
2298
2331
|
|
|
2332
|
+
/** v3.16: cross-session comparison metric labels (inline {zh,en} pair, like OBS_TABS). */
|
|
2333
|
+
const RUN_COMPARE_METRIC_LABELS: Record<string, { zh: string; en: string }> = {
|
|
2334
|
+
rounds: { zh: '轮次', en: 'Rounds' },
|
|
2335
|
+
findings: { zh: '发现', en: 'Findings' },
|
|
2336
|
+
criticalHigh: { zh: '严重(critical+high)', en: 'Critical + high' },
|
|
2337
|
+
fixed: { zh: '修复成功', en: 'Fixes applied' },
|
|
2338
|
+
failedFixes: { zh: '修复失败', en: 'Failed fixes' },
|
|
2339
|
+
validationPassRate: { zh: '验证通过率', en: 'Validation pass rate' },
|
|
2340
|
+
qualityGateScore: { zh: '门禁得分', en: 'Quality gate score' },
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2299
2343
|
/**
|
|
2300
2344
|
* Format a live-event timestamp as DD/MM HH:mm:ss (0-padded). Accepts an epoch
|
|
2301
2345
|
* ms number or an ISO-ish string; any unparseable value degrades to ''.
|
|
@@ -2359,7 +2403,7 @@ function buildObsFixInstruction(f: ObsFinding): string {
|
|
|
2359
2403
|
}
|
|
2360
2404
|
|
|
2361
2405
|
/**
|
|
2362
|
-
* ObservatoryPanel:
|
|
2406
|
+
* ObservatoryPanel: a 12-tab runtime observatory that renders the latest
|
|
2363
2407
|
* iterate_transcript manifest found in the dsh session stream. The panel is
|
|
2364
2408
|
* collapsed by default to a single entry row so it never re-occludes the main
|
|
2365
2409
|
* convergence dashboard; clicking expands it into tabs. Every field read is
|
|
@@ -2406,6 +2450,20 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2406
2450
|
// v3.0: Defense events filter
|
|
2407
2451
|
const [defenseFilter, setDefenseFilter] = React.useState('')
|
|
2408
2452
|
|
|
2453
|
+
// v3.16: cross-session comparison — the baseline is read from localStorage
|
|
2454
|
+
// once on mount (a corrupt/stale payload reads as "no baseline", never as a
|
|
2455
|
+
// phantom run); `showCompare` toggles the side-by-side table, `baselineFlash`
|
|
2456
|
+
// confirms the save action.
|
|
2457
|
+
const [baseline, setBaseline] = React.useState<RunBaseline | null>(() => {
|
|
2458
|
+
try {
|
|
2459
|
+
return parseRunBaseline(storage ? storage.get(RUN_BASELINE_STORAGE_KEY) : null)
|
|
2460
|
+
} catch {
|
|
2461
|
+
return null
|
|
2462
|
+
}
|
|
2463
|
+
})
|
|
2464
|
+
const [showCompare, setShowCompare] = React.useState(false)
|
|
2465
|
+
const [baselineFlash, setBaselineFlash] = React.useState(false)
|
|
2466
|
+
|
|
2409
2467
|
// Single shared "copied" flash timer (reused across all copy buttons).
|
|
2410
2468
|
const copyTimer = React.useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
2411
2469
|
React.useEffect(() => () => { if (copyTimer.current) clearTimeout(copyTimer.current) }, [])
|
|
@@ -2669,18 +2727,62 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2669
2727
|
}))
|
|
2670
2728
|
}
|
|
2671
2729
|
|
|
2730
|
+
// ── v3.16: cross-session run comparison handlers ────────────────────────
|
|
2731
|
+
// The summary is built from the SAME manifest the F2 tab renders, so a saved
|
|
2732
|
+
// baseline can never disagree with what the user is looking at.
|
|
2733
|
+
const currentRunSummary = (): RunBaseline | null => {
|
|
2734
|
+
const hasRun =
|
|
2735
|
+
(manifest.convergence || []).length > 0 ||
|
|
2736
|
+
(manifest.findings || []).length > 0 ||
|
|
2737
|
+
(manifest.fixes || []).length > 0 ||
|
|
2738
|
+
(typeof manifest.round === 'number' && manifest.round > 0)
|
|
2739
|
+
if (!hasRun) return null
|
|
2740
|
+
return buildRunBaseline(
|
|
2741
|
+
{
|
|
2742
|
+
project: manifest.project || undefined,
|
|
2743
|
+
mode: manifest.mode || undefined,
|
|
2744
|
+
convergence: manifest.convergence || [],
|
|
2745
|
+
findings: (manifest.findings || []) as Array<{ severity?: string }>,
|
|
2746
|
+
fixes: (manifest.fixes || []) as Array<{ success?: boolean }>,
|
|
2747
|
+
validations: (manifest.validations || []) as Array<{ allowed?: boolean }>,
|
|
2748
|
+
qualityGateStatus: (qualityGate && qualityGate.overallStatus) || null,
|
|
2749
|
+
qualityGateScore: qualityGate && typeof qualityGate.overallScore === 'number' ? qualityGate.overallScore : null,
|
|
2750
|
+
stoppedReason: manifest.stoppedReason || null,
|
|
2751
|
+
},
|
|
2752
|
+
new Date().toISOString(),
|
|
2753
|
+
)
|
|
2754
|
+
}
|
|
2755
|
+
const saveBaseline = () => {
|
|
2756
|
+
const summary = currentRunSummary()
|
|
2757
|
+
if (!summary || !storage) return
|
|
2758
|
+
try {
|
|
2759
|
+
storage.set(RUN_BASELINE_STORAGE_KEY, JSON.stringify(summary))
|
|
2760
|
+
setBaseline(summary)
|
|
2761
|
+
setShowCompare(true)
|
|
2762
|
+
setBaselineFlash(true)
|
|
2763
|
+
if (copyTimer.current) clearTimeout(copyTimer.current)
|
|
2764
|
+
copyTimer.current = setTimeout(() => setBaselineFlash(false), 1600)
|
|
2765
|
+
} catch (err) {
|
|
2766
|
+
log('failed to persist run baseline', err)
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
const clearBaseline = () => {
|
|
2770
|
+
if (storage) {
|
|
2771
|
+
try { storage.remove(RUN_BASELINE_STORAGE_KEY) } catch (err) { log('failed to clear run baseline', err) }
|
|
2772
|
+
}
|
|
2773
|
+
setBaseline(null)
|
|
2774
|
+
setShowCompare(false)
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2672
2777
|
// ── F2: convergence trend (inline bars, reusing TrendChart) ─────────────
|
|
2673
2778
|
const renderTrend = () => {
|
|
2674
2779
|
const conv = manifest.convergence || []
|
|
2675
|
-
if (conv.length === 0) {
|
|
2676
|
-
return React.createElement('div', { className: 'iterate-obs-empty' }, t('暂无收敛数据'))
|
|
2677
|
-
}
|
|
2678
2780
|
const points = conv.map((n, i) => ({ round: i + 1, count: n }))
|
|
2679
2781
|
// Gap #13 (step ①): single-session cross-round comparison. One row per
|
|
2680
2782
|
// round — findings raised / fixed / severity mix / validation outcome.
|
|
2681
|
-
//
|
|
2682
|
-
//
|
|
2683
|
-
//
|
|
2783
|
+
// v3.16: the cross-SESSION half is now the `renderCrossSessionCompare`
|
|
2784
|
+
// block below (save the current run as a baseline, then compare any later
|
|
2785
|
+
// run against it) instead of the old export-two-JSONs-and-diff-by-hand chore.
|
|
2684
2786
|
const report = latestReport(session)
|
|
2685
2787
|
const comparison = buildRoundComparison(report, manifest.validations || [], manifest.fixes || [])
|
|
2686
2788
|
const comparisonTable = comparison.length === 0
|
|
@@ -2720,22 +2822,117 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2720
2822
|
}),
|
|
2721
2823
|
),
|
|
2722
2824
|
)
|
|
2723
|
-
|
|
2724
|
-
React.createElement(
|
|
2725
|
-
React.createElement('div', {
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2825
|
+
const trendBody = conv.length === 0
|
|
2826
|
+
? React.createElement('div', { className: 'iterate-obs-empty' }, t('暂无收敛数据'))
|
|
2827
|
+
: React.createElement('div', {},
|
|
2828
|
+
React.createElement(TrendChart, { points }),
|
|
2829
|
+
React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 6 } },
|
|
2830
|
+
tf(`各轮发现数量:{a0}{a1}`, { a0: conv.join(' → '), a1: typeof manifest.round === 'number' ? tf(` · 当前 Round {a0}`, { a0: manifest.round }) : '' }),
|
|
2831
|
+
),
|
|
2832
|
+
comparisonTable
|
|
2833
|
+
? React.createElement('div', { className: 'iterate-obs-block', style: { marginTop: 8 } },
|
|
2834
|
+
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2835
|
+
React.createElement('span', {}, t('跨轮对比(本会话)')),
|
|
2836
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' },
|
|
2837
|
+
t('跨会话对比:本表只覆盖当前会话;要跨两次迭代对比,用下方「存为对比基准」保存本次运行,再与下一次迭代对比。'),
|
|
2838
|
+
),
|
|
2839
|
+
),
|
|
2840
|
+
React.createElement('div', { className: 'iterate-obs-block-body' }, comparisonTable),
|
|
2841
|
+
)
|
|
2842
|
+
: null,
|
|
2843
|
+
)
|
|
2844
|
+
return React.createElement('div', {}, trendBody, renderCrossSessionCompare())
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
// ── v3.16: cross-session run comparison (F2) ─────────────────────────────
|
|
2848
|
+
// Side-by-side comparison of the CURRENT run against a previously saved
|
|
2849
|
+
// baseline, persisted in localStorage. This replaces the manual
|
|
2850
|
+
// export-JSON-and-diff chore the old cross-session hint pointed at: save the
|
|
2851
|
+
// current run once, then compare any later run against it in one click.
|
|
2852
|
+
const renderCrossSessionCompare = () => {
|
|
2853
|
+
const current = currentRunSummary()
|
|
2854
|
+
const hasRun = current !== null
|
|
2855
|
+
const metricLabel = (key: string): string => {
|
|
2856
|
+
const m = RUN_COMPARE_METRIC_LABELS[key]
|
|
2857
|
+
return m ? m[getLang()] : key
|
|
2858
|
+
}
|
|
2859
|
+
const fmtCell = (v: number | null, key: string): string => {
|
|
2860
|
+
if (v === null) return '—'
|
|
2861
|
+
return key === 'validationPassRate' ? `${v}%` : String(v)
|
|
2862
|
+
}
|
|
2863
|
+
const deltaText = (row: RunCompareMetric): string => {
|
|
2864
|
+
if (row.baseline === null || row.current === null) return '—'
|
|
2865
|
+
const d = row.current - row.baseline
|
|
2866
|
+
return d === 0 ? '0' : `${d > 0 ? '+' : ''}${d}`
|
|
2867
|
+
}
|
|
2868
|
+
const deltaTint = (row: RunCompareMetric): { color: string; fontWeight: number } | undefined => {
|
|
2869
|
+
if (row.baseline === null || row.current === null || row.higherIsBetter === null) return undefined
|
|
2870
|
+
const d = row.current - row.baseline
|
|
2871
|
+
if (d === 0) return undefined
|
|
2872
|
+
const improved = row.higherIsBetter ? d > 0 : d < 0
|
|
2873
|
+
return improved
|
|
2874
|
+
? { color: 'var(--dsw-alias-state-success-primary)', fontWeight: 600 }
|
|
2875
|
+
: { color: 'var(--dsw-alias-state-error-primary)', fontWeight: 600 }
|
|
2876
|
+
}
|
|
2877
|
+
let compareBody: React.ReactNode = null
|
|
2878
|
+
if (baseline && current && showCompare) {
|
|
2879
|
+
const rows = compareRuns(baseline, current)
|
|
2880
|
+
compareBody = React.createElement('table', { className: 'iterate-obs-table' },
|
|
2881
|
+
React.createElement('thead', {},
|
|
2882
|
+
React.createElement('tr', {},
|
|
2883
|
+
React.createElement('th', {}, t('指标')),
|
|
2884
|
+
React.createElement('th', {}, t('基准')),
|
|
2885
|
+
React.createElement('th', {}, t('当前')),
|
|
2886
|
+
React.createElement('th', {}, t('变化')),
|
|
2887
|
+
),
|
|
2888
|
+
),
|
|
2889
|
+
React.createElement('tbody', {},
|
|
2890
|
+
...rows.map((row) =>
|
|
2891
|
+
React.createElement('tr', { key: `rc-${row.key}` },
|
|
2892
|
+
React.createElement('td', {}, metricLabel(row.key)),
|
|
2893
|
+
React.createElement('td', {}, fmtCell(row.baseline, row.key)),
|
|
2894
|
+
React.createElement('td', {}, fmtCell(row.current, row.key)),
|
|
2895
|
+
React.createElement('td', { style: deltaTint(row) }, deltaText(row)),
|
|
2735
2896
|
),
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2897
|
+
),
|
|
2898
|
+
),
|
|
2899
|
+
)
|
|
2900
|
+
}
|
|
2901
|
+
return React.createElement('div', { className: 'iterate-obs-block', style: { marginTop: 8 } },
|
|
2902
|
+
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2903
|
+
React.createElement('span', {}, t('跨会话对比')),
|
|
2904
|
+
baseline
|
|
2905
|
+
? React.createElement('span', { className: 'iterate-obs-head-meta' }, tf(`基准已保存:{a0}`, { a0: baseline.savedAt }))
|
|
2906
|
+
: React.createElement('span', { className: 'iterate-obs-head-meta' }, t('先保存当前运行为基准,再跑下一次迭代后一键对比')),
|
|
2907
|
+
),
|
|
2908
|
+
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
2909
|
+
React.createElement('div', { className: 'iterate-obs-bar', style: { flexWrap: 'wrap' } },
|
|
2910
|
+
React.createElement('button', {
|
|
2911
|
+
className: 'iterate-btn', 'data-primary': '', disabled: !hasRun,
|
|
2912
|
+
title: hasRun ? t('把当前运行摘要保存为对比基准(含轮次/发现/修复/验证/门禁)') : t('当前没有可保存的运行数据'),
|
|
2913
|
+
onClick: saveBaseline,
|
|
2914
|
+
}, baselineFlash ? t('已存为基准') : t('存为对比基准')),
|
|
2915
|
+
baseline
|
|
2916
|
+
? React.createElement('button', {
|
|
2917
|
+
className: 'iterate-btn', 'data-copied': showCompare ? '' : undefined,
|
|
2918
|
+
disabled: !hasRun,
|
|
2919
|
+
title: t('显示或隐藏与已保存基准的对比表'),
|
|
2920
|
+
onClick: () => setShowCompare((v) => !v),
|
|
2921
|
+
}, showCompare ? t('收起对比') : t('与基准对比'))
|
|
2922
|
+
: null,
|
|
2923
|
+
baseline
|
|
2924
|
+
? React.createElement('button', {
|
|
2925
|
+
className: 'iterate-btn', 'data-ghost': '',
|
|
2926
|
+
title: t('清除已保存的对比基准'),
|
|
2927
|
+
onClick: clearBaseline,
|
|
2928
|
+
}, t('清除基准'))
|
|
2929
|
+
: null,
|
|
2930
|
+
),
|
|
2931
|
+
showCompare && baseline && !hasRun
|
|
2932
|
+
? React.createElement('div', { className: 'iterate-obs-empty', style: { marginTop: 8 } }, t('当前会话没有运行数据可对比'))
|
|
2933
|
+
: null,
|
|
2934
|
+
compareBody,
|
|
2935
|
+
),
|
|
2739
2936
|
)
|
|
2740
2937
|
}
|
|
2741
2938
|
|
|
@@ -3907,7 +4104,7 @@ export function apply(ctx: ClientContext): void {
|
|
|
3907
4104
|
),
|
|
3908
4105
|
)
|
|
3909
4106
|
|
|
3910
|
-
// F1–
|
|
4107
|
+
// F1–F11: runtime observatory board (order 91, below the dashboard's 90 so
|
|
3911
4108
|
// its collapsed header never occludes the main convergence dashboard).
|
|
3912
4109
|
slotsSvc.inject('conversation.input.dock', () =>
|
|
3913
4110
|
slotsSvc?.register(
|
|
@@ -3984,6 +4181,26 @@ export function apply(ctx: ClientContext): void {
|
|
|
3984
4181
|
}
|
|
3985
4182
|
}
|
|
3986
4183
|
|
|
4184
|
+
// 5c. v3.15: keyboard-command contribution (dsh 0.2.2-rc.1 unified
|
|
4185
|
+
// shortcuts). Optional service: a host that predates the capability has no
|
|
4186
|
+
// keyboard command, and the docked observatory stays the entry point.
|
|
4187
|
+
{
|
|
4188
|
+
const shortcutSvc = readShortcuts(ctx)
|
|
4189
|
+
if (shortcutSvc) {
|
|
4190
|
+
if (shortcutDisposer) {
|
|
4191
|
+
try { shortcutDisposer() } catch { /* noop */ }
|
|
4192
|
+
shortcutDisposer = null
|
|
4193
|
+
}
|
|
4194
|
+
shortcutDisposer = registerIterateShortcuts(shortcutSvc, {
|
|
4195
|
+
openObservatory: (detail) => { openObservatory(detail) },
|
|
4196
|
+
lang: () => getLang(),
|
|
4197
|
+
})
|
|
4198
|
+
if (typeof ctx.effect === 'function') {
|
|
4199
|
+
ctx.effect(() => { if (shortcutDisposer) { shortcutDisposer(); shortcutDisposer = null } })
|
|
4200
|
+
}
|
|
4201
|
+
}
|
|
4202
|
+
}
|
|
4203
|
+
|
|
3987
4204
|
// 6: keep theme toggle state consistent across theme/change events.
|
|
3988
4205
|
// Registered through registerThemeListener so repeated apply() and client
|
|
3989
4206
|
// unmounts never leak a second listener.
|