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 CHANGED
@@ -107,7 +107,7 @@ Both modes share:
107
107
  ### UI layer (build-free client slots, v3.12: 12 tabs, bilingual)
108
108
 
109
109
  - **ConvergenceDashboard** (`conversation.input.dock`) — live round progress bar, severity stats, dimension badges, trend mini-chart above the input; normal mode also shows fix-count badges; plus a live workflow-phase chip (current phase + running/stopped) and a **task_mode indicator (code / iterate)**, now backed by the persisted transcript (`iterate_status` / `iterate_transcript` emit `taskMode`). While a run is active the dashboard switches to a running state (round + phase) instead of the static empty state, and the start buttons are hidden.
110
- - **ObservatoryPanel** (`conversation.input.dock`) — a **twelve-tab** runtime observatory below the input: **live activity stream** (default tab, type filter) + **F1** review threads (expand / collapse all), **F2** convergence trend (incl. the per-round comparison table: findings raised / fixed / severity mix / validation outcomes), **F3** finding locations (severity / dimension / search filter), **F4** fixes + rollback, **F5** checkpoint resume, **F6** run console, **F7** decision timeline (type / round filter + search), **F8** Quality Gate, **F9** Experience Bank, **F10** Defense Events, **F11** Preflight. Number-key shortcuts: `1`–`9` jump to F1–F9, `0` to F10, `-` back to the live stream (F11 is click-only). **F8/F9/F10 are live** — they scan the session stream for the latest `iterate_quality_gate` / `iterate_experience` / `iterate_defense_events` results and render real data (PASS/FAIL chip + per-dimension score bars, experience entries with adopt buttons + client-side search, defense type-count chips + filtered event stream); when the session stream has none they distinguish "no data on disk" from "not pulled yet" and offer a **拉取磁盘快照 (pull disk snapshot)** instruction that has the model call the read-side tools and echo the `.iterate/` artifacts back into the stream. One-click JSON export of all observatory data — manifest + live + `qualityGate` / `experienceBank` / `defenseEvents` / `report` (download, copy fallback). **v3.9: report-export buttons** (导出报告 / 导出 SARIF, **v3.11 + 导出 JSON 报告**) render the *same shared data model* as the `iterate_report` tool — a PR-pasteable Markdown run report, a SARIF 2.1.0 log for CI / GitHub Code Scanning, and (v3.11) the complete normalized run model as machine-readable JSON. **v3.5.3: reset actions surfaced** — F5 copies an `iterate_checkpoint clear` instruction, F8 an `iterate_quality_gate clear` instruction, F10 an `iterate_defense_events clear` instruction. **v3.12: pre-run readiness (Preflight)** — F11 renders the latest `iterate_preflight` checklist scanned from the session stream (validation commands configured / git repo / dirty tree / stale FAIL gate / interrupted checkpoint), each warn/fail row offering the exact fix instruction (configure `validation.commands`, clear the stale gate, resume the checkpoint) so the environment is repaired BEFORE a round is spent on it; the empty state offers a **运行预检** instruction that has the model call `iterate_preflight`. **v3.13: the blocking validation row auto-detects commands from the project manifest and offers a one-click 「应用建议」 (copies an `iterate_config write` payload).**
110
+ - **ObservatoryPanel** (`conversation.input.dock`) — a **twelve-tab** runtime observatory below the input: **live activity stream** (default tab, type filter) + **F1** review threads (expand / collapse all), **F2** convergence trend (incl. the per-round comparison table: findings raised / fixed / severity mix / validation outcomes, plus **v3.16 a cross-session comparison**: save the current run as a baseline (`存为对比基准`), then compare any later run against it side by side (`与基准对比`) with per-metric deltas tinted by direction), **F3** finding locations (severity / dimension / search filter), **F4** fixes + rollback, **F5** checkpoint resume, **F6** run console, **F7** decision timeline (type / round filter + search), **F8** Quality Gate, **F9** Experience Bank, **F10** Defense Events, **F11** Preflight. Number-key shortcuts: `1`–`9` jump to F1–F9, `0` to F10, `-` back to the live stream (F11 is click-only). **F8/F9/F10 are live** — they scan the session stream for the latest `iterate_quality_gate` / `iterate_experience` / `iterate_defense_events` results and render real data (PASS/FAIL chip + per-dimension score bars, experience entries with adopt buttons + client-side search, defense type-count chips + filtered event stream); when the session stream has none they distinguish "no data on disk" from "not pulled yet" and offer a **拉取磁盘快照 (pull disk snapshot)** instruction that has the model call the read-side tools and echo the `.iterate/` artifacts back into the stream. One-click JSON export of all observatory data — manifest + live + `qualityGate` / `experienceBank` / `defenseEvents` / `report` (download, copy fallback). **v3.9: report-export buttons** (导出报告 / 导出 SARIF, **v3.11 + 导出 JSON 报告**) render the *same shared data model* as the `iterate_report` tool — a PR-pasteable Markdown run report, a SARIF 2.1.0 log for CI / GitHub Code Scanning, and (v3.11) the complete normalized run model as machine-readable JSON. **v3.5.3: reset actions surfaced** — F5 copies an `iterate_checkpoint clear` instruction, F8 an `iterate_quality_gate clear` instruction, F10 an `iterate_defense_events clear` instruction. **v3.12: pre-run readiness (Preflight)** — F11 renders the latest `iterate_preflight` checklist scanned from the session stream (validation commands configured / git repo / dirty tree / stale FAIL gate / interrupted checkpoint), each warn/fail row offering the exact fix instruction (configure `validation.commands`, clear the stale gate, resume the checkpoint) so the environment is repaired BEFORE a round is spent on it; the empty state offers a **运行预检** instruction that has the model call `iterate_preflight`. **v3.13: the blocking validation row auto-detects commands from the project manifest and offers a one-click 「应用建议」 (copies an `iterate_config write` payload).**
111
111
  - **TriagePanel** (`conversation.chat.turnTail`) — per-finding triage with `y` (keep → fix), `n` (skip), `a` (known-intentional) keyboard shortcuts and `↑`/`↓` navigation (focus-guarded: only active while the panel has focus), filtering, batch ops (incl. select-all), localStorage persistence. **Write-back closed loop**: the primary button copies an `iterate_triage apply` instruction for the marked entries (the tool validates, dedupes, backs up, rolls back on failure); a secondary button copies the raw `known_intentional` YAML; **回读已写入条目** copies an `iterate_triage list` instruction to confirm what actually landed. Copy payloads are aligned to the target tool schemas. **v3.2: native command buttons** (approve architectural fix, trigger new round, rollback to checkpoint). **v3.3: §8 指派修复 (assign)** — one click copies an `iterate_fix` instruction carrying every in-scope finding (respecting the batch toggle).
112
112
  - **StatsCard** (`conversation.chat.turnTail`) — convergence stats, round history, trend chart and completion summary when no findings remain.
113
113
  - **iterate theme skin** (`theme.overrideTokens`) — a warm-amber 13-token `--dsw-*` override, light/dark modes, togglable in settings.
@@ -115,6 +115,7 @@ Both modes share:
115
115
  - **SettingsPanel** (`settings.section`) — theme toggle, triage-persistence notes, config-management guide (covers `validation.commands` / `language` / `personalization.known_intentional` with an explicit "empty validation.commands = unprotected" warning; a field picker generates the matching `iterate_config write` instruction), **v3.7: a 回读当前配置 (read back current config) button that closes the write → read-back loop** — it copies an `iterate_config read` instruction and, once the model echoes the result into the session, shows the current value of the selected field, runtime status overview (artifact layout + view/cleanup tool guide + disk-snapshot pull), one-click triage data reset.
116
116
  - **Bilingual UI (v3.7)** — every piece of UI chrome (panels, empty states, tabs, badges, settings, stop-reason and completion labels) ships in 简体中文 and English. Pick the language under **Settings → 界面语言 / Interface language**; the choice persists in `localStorage` (`iterate-ui-lang`) and otherwise auto-detects from `navigator.language` (a stored choice always wins, and auto-detection never writes a preference). **Model-facing copy deliberately stays Chinese** — the `build*Instruction` payloads and the copy-paste guides are contract text mirroring `SKILL.md`, so the two expanded guides carry a 「中文(粘贴给模型的契约文案)」 chip instead of looking like a missed translation.
117
117
  - **Right-click menu (v3.14, dsh ≥ 0.2.2-alpha.3)** — the iterate entry points now also hang off the **native unified context menus**: right-click a **Session** in the sidebar to `打开 iterate 质量指挥中心` (expands the docked observatory and jumps to the live stream), copy the preflight / dry-run / checkpoint-resume / disk-snapshot instructions, or right-click a **file** (with line) / **directory** to copy a *scoped* dry-run review instruction. Bilingual labels, copy-failure recovery guidance, and a shared success toast; a host without `contextMenus` degrades to no menu items (the docked panel remains the entry point).
118
+ - **Keyboard shortcut (v3.15, dsh ≥ 0.2.2-rc.1)** — the iterate command center is now one keystroke away: **Cmd/Ctrl+Shift+O** opens the docked observatory from anywhere in a session (dsh's new unified keyboard-command capability). The shortcut is registered on dsh's `shortcuts` service with the same command id the right-click "open command center" action carries, so the context menu shows the live keycap and dsh's own shortcut settings page lists it as an editable binding. A host that predates the `shortcuts` service degrades to no keyboard command (the docked panel and the context menu remain the entry points); a conflicting default binding is logged and skipped rather than breaking the plugin.
118
119
 
119
120
  The UI layer is **defensive by design**: it degrades gracefully if any of `slots` / `theme` / `React` is unavailable — it never crashes the client.
120
121
 
@@ -201,6 +202,8 @@ The observatory below the input carries **12 tabs**: the live activity stream (d
201
202
 
202
203
  `F1` review threads · `F2` convergence trend (incl. per-round comparison) · `F3` finding locations · `F4` fixes + rollback · `F5` checkpoint resume · `F6` run console · `F7` decision timeline · `F8` quality gate · `F9` experience bank · `F10` defense events · `F11` preflight
203
204
 
205
+ **Cross-session comparison (v3.16)** — the F2 tab also carries a **跨会话对比 (cross-session comparison)** block: **存为对比基准** saves the current run's summary (rounds / findings / critical+high / fixes applied / failed fixes / validation pass rate / quality-gate score, persisted to `localStorage`) and **与基准对比** renders the next run against it side by side with a directional delta (better / worse tinted). This replaces the old "export two JSONs and diff by hand" chore; a corrupt / stale saved baseline reads as "no baseline", never a phantom run.
206
+
204
207
  Press `1`–`9` to jump to F1–F9, `0` for F10, and `-` to return to the live stream.
205
208
 
206
209
  ### Disk snapshot pull & export
@@ -348,12 +351,12 @@ All runtime state lives under `.iterate/` at the project root (can be excluded v
348
351
  `0.2.0-rc.2`, `0.2.1-alpha.1`, `0.2.1-alpha.2`, `0.2.1-alpha.3`,
349
352
  `0.2.1-alpha.4`, `0.2.1-alpha.5`, `0.2.1-rc.1`, `0.2.1-rc.2`,
350
353
  `0.2.1-rc.3`, `0.2.1-rc.4`, `0.2.1-rc.5`, `0.2.2-alpha.2`,
351
- `0.2.2-alpha.3` (29 releases, `0.1.1-rc.1` … `0.2.2-alpha.3`).
354
+ `0.2.2-alpha.3`, `0.2.2-rc.1` (30 releases, `0.1.1-rc.1` … `0.2.2-rc.1`).
352
355
  The plugin is built against `@deepseek-ai/dsh-tools` /
353
- `@deepseek-ai/dsh-util-values` `0.2.2-alpha.3` and uses only public contracts
356
+ `@deepseek-ai/dsh-util-values` `0.2.2-rc.1` and uses only public contracts
354
357
  (tool registration, client slots + theme, bundle patch, and — since v3.9 —
355
358
  the session state-record API for session-linked run records).
356
- It also declares `engines.dsh` (`>=0.1.1-rc.1`) so the dsh ≥ 0.2.2-alpha.3
359
+ It also declares `engines.dsh` (`>=0.1.1-rc.1`) so the dsh ≥ 0.2.2-rc.1
357
360
  runtime evaluates compatibility from that single range (earlier runtimes fall
358
361
  back to the `@deepseek-ai/dsh-*` peer versions listed in `package.json`).
359
362
  - **Disposable-Profile evidence (real, `dsh` CLI `0.1.1-rc.1`)**: on a temp
package/README.zh-CN.md CHANGED
@@ -106,7 +106,7 @@ dsh plugin --profile web add iterate-plugin
106
106
  ### UI 层(客户端免构建槽位,v3.12:12 个页签,双语)
107
107
 
108
108
  - **收敛看板 `ConvergenceDashboard`**(`conversation.input.dock`)— 输入框上方实时显示轮次进度条、严重度统计、维度徽章、趋势迷你图;normal 模式另显示修复计数徽章;还有运行阶段芯片(当前工作流阶段 + 运行中/已结束)与 **task_mode 指示器(code / iterate)**。运行期间看板切换为"运行中"状态(显示轮次与阶段),并隐藏启动按钮。
109
- - **运行时观测台 `ObservatoryPanel`**(`conversation.input.dock`)— 输入框下方**十二个页签**运行时观测台:**实时活动流**(默认页,支持按活动类型筛选)+ **F1** 审查线程(全部展开/全部收起)、**F2** 收敛趋势(含跨轮对比表:每轮 findings / fixed / 严重度分布 / 验证结果)、**F3** 发现定位(按严重度/维度/关键词筛选)、**F4** 修复与回滚、**F5** 断点恢复、**F6** 运行控制台、**F7** 决策时间线(按类型/轮次筛选与关键词搜索)、**F8** 质量门禁、**F9** 经验银行、**F10** 防御事件、**F11** 开跑预检。数字键快捷键:`1`–`9` 跳到 F1–F9,`0` 到 F10,`-` 回实时活动流(F11 仅点击进入)。**F8/F9/F10 为实时数据**——扫描会话流中最近一次 `iterate_quality_gate` / `iterate_experience` / `iterate_defense_events` 结果并渲染真实数据(PASS/FAIL 徽章 + 分维度评分条、经验条目(含采纳按钮 + 客户端搜索)、防御类型计数芯片 + 筛选后的事件流);会话流中没有数据时区分"磁盘无数据"与"尚未拉取",并提供**拉取磁盘快照**指令(让模型调用只读工具、把 `.iterate/` 产物回显进会话流)。一键导出全部观测数据为 JSON——manifest + live + `qualityGate` / `experienceBank` / `defenseEvents` / `report`(优先下载,失败回退复制)。**v3.9 新增报告导出按钮**(导出报告 / 导出 SARIF,**v3.11 新增导出 JSON 报告**),与 `iterate_report` 工具共用同一份数据模型——分别产出可直接粘贴进 PR 的 Markdown 运行报告、供 CI / GitHub Code Scanning 消费的 SARIF 2.1.0 日志,以及(v3.11)完整的归一化运行模型 JSON(无损格式)。**v3.5.3 新增重置操作**:F5 复制 `iterate_checkpoint clear`、F8 复制 `iterate_quality_gate clear`、F10 复制 `iterate_defense_events clear` 指令。**v3.12 新增开跑预检(Preflight)**——F11 渲染会话流中最近一次 `iterate_preflight` 清单(验证命令是否配置 / 是否 Git 仓库 / 脏工作区 / 陈旧 FAIL 门禁 / 中断断点),每个 warn/fail 行都给出精确的修复指令(配置 `validation.commands`、清除陈旧门禁、恢复断点),让环境在**花掉一轮之前**就被修好;空态提供**运行预检**指令(让模型调用 `iterate_preflight`)。**v3.13:阻断的验证命令行自动检测项目 manifest,提供一键「应用建议」**(复制 `iterate_config` 的 `operation:"write"` 指令,携带完整 `validation.commands.default` 载荷)。
109
+ - **运行时观测台 `ObservatoryPanel`**(`conversation.input.dock`)— 输入框下方**十二个页签**运行时观测台:**实时活动流**(默认页,支持按活动类型筛选)+ **F1** 审查线程(全部展开/全部收起)、**F2** 收敛趋势(含跨轮对比表:每轮 findings / fixed / 严重度分布 / 验证结果;**v3.16 新增跨会话对比**:把当前运行**存为对比基准**,下次运行一键**与基准对比**,逐项并排 + 带方向的差值着色)、**F3** 发现定位(按严重度/维度/关键词筛选)、**F4** 修复与回滚、**F5** 断点恢复、**F6** 运行控制台、**F7** 决策时间线(按类型/轮次筛选与关键词搜索)、**F8** 质量门禁、**F9** 经验银行、**F10** 防御事件、**F11** 开跑预检。数字键快捷键:`1`–`9` 跳到 F1–F9,`0` 到 F10,`-` 回实时活动流(F11 仅点击进入)。**F8/F9/F10 为实时数据**——扫描会话流中最近一次 `iterate_quality_gate` / `iterate_experience` / `iterate_defense_events` 结果并渲染真实数据(PASS/FAIL 徽章 + 分维度评分条、经验条目(含采纳按钮 + 客户端搜索)、防御类型计数芯片 + 筛选后的事件流);会话流中没有数据时区分"磁盘无数据"与"尚未拉取",并提供**拉取磁盘快照**指令(让模型调用只读工具、把 `.iterate/` 产物回显进会话流)。一键导出全部观测数据为 JSON——manifest + live + `qualityGate` / `experienceBank` / `defenseEvents` / `report`(优先下载,失败回退复制)。**v3.9 新增报告导出按钮**(导出报告 / 导出 SARIF,**v3.11 新增导出 JSON 报告**),与 `iterate_report` 工具共用同一份数据模型——分别产出可直接粘贴进 PR 的 Markdown 运行报告、供 CI / GitHub Code Scanning 消费的 SARIF 2.1.0 日志,以及(v3.11)完整的归一化运行模型 JSON(无损格式)。**v3.5.3 新增重置操作**:F5 复制 `iterate_checkpoint clear`、F8 复制 `iterate_quality_gate clear`、F10 复制 `iterate_defense_events clear` 指令。**v3.12 新增开跑预检(Preflight)**——F11 渲染会话流中最近一次 `iterate_preflight` 清单(验证命令是否配置 / 是否 Git 仓库 / 脏工作区 / 陈旧 FAIL 门禁 / 中断断点),每个 warn/fail 行都给出精确的修复指令(配置 `validation.commands`、清除陈旧门禁、恢复断点),让环境在**花掉一轮之前**就被修好;空态提供**运行预检**指令(让模型调用 `iterate_preflight`)。**v3.13:阻断的验证命令行自动检测项目 manifest,提供一键「应用建议」**(复制 `iterate_config` 的 `operation:"write"` 指令,携带完整 `validation.commands.default` 载荷)。
110
110
  - **Findings 分诊面板 `TriagePanel`**(`conversation.chat.turnTail`)— 逐条分诊,键盘快捷键 `y`(保留→修复)、`n`(跳过)、`a`(已知有意)与 `↑`/`↓` 移动(焦点守卫:仅面板内焦点时生效),支持筛选、批量(含一键全选所有 findings)、localStorage 持久化。**写回闭环**:主按钮复制已标记条目的 `iterate_triage apply` 指令(工具负责校验、去重、备份、失败回滚);次按钮复制裸 `known_intentional` YAML;**回读已写入条目**复制 `iterate_triage list` 指令核对实际落盘内容。复制载荷与目标工具 schema 对齐。**v3.2:原生命令按钮**(批准架构修复、触发新一轮、回滚到断点)。
111
111
  - **收敛统计卡片 `StatsCard`**(`conversation.chat.turnTail`)— 无 findings 时显示收敛统计、历史轮次、趋势图、完成摘要。
112
112
  - **iterate 主题皮肤**(`theme.overrideTokens`)— 暖琥珀配色的 13 个 `--dsw-*` token 覆盖,明暗双模式,可在设置页开关。
@@ -114,6 +114,7 @@ dsh plugin --profile web add iterate-plugin
114
114
  - **iterate 设置区 `SettingsPanel`**(`settings.section`)— 主题开关、分诊持久化说明、配置管理指引(覆盖 `validation.commands` / `language` / `personalization.known_intentional`,并显式警告"validation.commands 为空 = 本轮不受任何测试保护";字段选择器可生成对应的 `iterate_config write` 指令),**v3.7:新增「回读当前配置」补齐写入 → 回读闭环**——复制 `iterate_config read` 指令,模型把结果回显进会话后,卡片就地展示所选字段的当前值;运行时状态概览(产物布局 + 查看/清理工具指引 + 磁盘快照拉取)、一键清空分诊数据。
115
115
  - **双语 UI(v3.7)**——所有界面文案(面板、空态、页签、徽标、设置、停止原因与完成汇总)均提供简体中文与 English。在**设置 → 界面语言 / Interface language**切换;选择持久化在 `localStorage`(`iterate-ui-lang`),否则按 `navigator.language` 自动探测(已保存的选择始终优先,且自动探测不会写入偏好)。**面向模型的文案有意保持中文**——`build*Instruction` 指令与可粘贴指引是与 `SKILL.md` 对齐的契约文本,因此两处展开的指引带「中文(粘贴给模型的契约文案)」标签,避免被误当成漏翻。
116
116
  - **右键菜单(v3.14,dsh ≥ 0.2.2-alpha.3)**——iterate 入口现在也挂在**原生统一右键菜单**上:右键侧边栏的**会话**可 `打开 iterate 质量指挥中心`(展开停靠观测台并跳到实时流)、复制预检 / dry-run / 断点恢复 / 磁盘快照指令;右键**文件**(带行号)/ **目录**可复制**定向** dry-run 审查指令(仅审查该文件/目录)。双语标签、复制失败的自救指引、共享成功 toast;宿主无 `contextMenus` 时降级为无菜单项(停靠面板仍是入口)。
117
+ - **键盘快捷键(v3.15,dsh ≥ 0.2.2-rc.1)**——iterate 指挥中心现在一键可达:任意会话内按 **Cmd/Ctrl+Shift+O** 即可打开停靠观测台(dsh 新增的统一键盘命令能力)。快捷键注册在 dsh 的 `shortcuts` 服务上,与右键「打开指挥中心」动作共用同一命令 id,因此右键菜单会显示实时键帽、dsh 自身的快捷键设置页也会将其列为可编辑绑定。宿主早于 `shortcuts` 服务时降级为无键盘命令(停靠面板与右键菜单仍是入口);默认绑定冲突时仅记录并跳过,绝不拖垮插件。
117
118
 
118
119
  UI 层为**防御式设计**:`slots` / `theme` / `React` 任一不可用时自动降级,不会崩溃客户端。
119
120
 
@@ -200,6 +201,8 @@ pnpm add /path/to/iterate-skill/harness/iterate-plugin
200
201
 
201
202
  `F1` 审查线程 · `F2` 收敛趋势(含跨轮对比)· `F3` 发现定位 · `F4` 修复与回滚 · `F5` 断点恢复 · `F6` 运行控制台 · `F7` 决策时间线 · `F8` 质量门禁 · `F9` 经验银行 · `F10` 防御事件 · `F11` 开跑预检
202
203
 
204
+ **跨会话对比(v3.16)**——F2 页签还带一个**跨会话对比**块:**存为对比基准**把当前运行摘要(轮次 / 发现 / critical+high / 修复成功 / 修复失败 / 验证通过率 / 门禁得分,持久化到 `localStorage`)保存下来,**与基准对比**把下一次运行与它并排对比,差值按「更好 / 更差」方向着色。这取代了以前「导出两份 JSON 手工并排对比」的繁琐做法;损坏 / 过期的已存基准读作「无基准」,绝不变成幽灵运行。
205
+
203
206
  按 `1`–`9` 跳到 F1–F9,`0` 到 F10,`-` 回实时活动流。
204
207
 
205
208
  ### 磁盘快照拉取与导出
@@ -337,7 +340,7 @@ validation:
337
340
  ### Node.js / DSH 兼容范围
338
341
 
339
342
  - **Node.js**:`>=20`(`package.json` 的 `engines.node`)。
340
- - **DSH**:`package.json` 的 `dsh.compatibility.dshReleases` 字段是发布兼容性的**唯一真源**(本 README 逐字镜像该字段;两者需同步更新)。以下官方版本声明为 `compatible`:`0.1.1-rc.1`、`0.1.2-alpha.4`、`0.1.2-alpha.5`、`0.1.2-rc.1`、`0.1.3-alpha.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`(共 29 个版本,`0.1.1-rc.1` … `0.2.2-alpha.3`)。插件基于 `@deepseek-ai/dsh-tools` / `@deepseek-ai/dsh-util-values` `0.2.2-alpha.3` 构建,只使用公开契约(工具注册、客户端 slots + theme、bundle patch,以及 v3.9 起的会话状态记录接口)。插件同时声明 `engines.dsh`(`>=0.1.1-rc.1`),使 dsh ≥ 0.2.2-alpha.3 运行时只依据该范围判断兼容性(更早的运行时仍回退校验 `package.json` 中列出的 `@deepseek-ai/dsh-*` peer 版本)。
343
+ - **DSH**:`package.json` 的 `dsh.compatibility.dshReleases` 字段是发布兼容性的**唯一真源**(本 README 逐字镜像该字段;两者需同步更新)。以下官方版本声明为 `compatible`:`0.1.1-rc.1`、`0.1.2-alpha.4`、`0.1.2-alpha.5`、`0.1.2-rc.1`、`0.1.3-alpha.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`(共 30 个版本,`0.1.1-rc.1` … `0.2.2-rc.1`)。插件基于 `@deepseek-ai/dsh-tools` / `@deepseek-ai/dsh-util-values` `0.2.2-rc.1` 构建,只使用公开契约(工具注册、客户端 slots + theme、bundle patch,以及 v3.9 起的会话状态记录接口)。插件同时声明 `engines.dsh`(`>=0.1.1-rc.1`),使 dsh ≥ 0.2.2-rc.1 运行时只依据该范围判断兼容性(更早的运行时仍回退校验 `package.json` 中列出的 `@deepseek-ai/dsh-*` peer 版本)。
341
344
  - **一次性 Profile 证据(真实,`dsh` CLI `0.1.1-rc.1`)**:在临时 `$DSH_HOME` 下
342
345
  `dsh plugin --profile <p> add <本仓库>` 约 449ms 完成安装;`dsh --profile <p>
343
346
  --dump-config` 正确合成 `iterate-plugin` bundle patch;`dsh plugin --profile <p>
package/lib/client.js CHANGED
@@ -1,4 +1,4 @@
1
- /* iterate-plugin client source-digest: 5b22267e6de6fb0564d60874b3de1211755cdeb398f6c7ffa87dfa1007e6796f */
1
+ /* iterate-plugin client source-digest: 55b24ad8a8a5d8abf400d42314a6bf75394e8e529fc587ac991bb4e50d868393 */
2
2
  window.__ModuleLoader__.load({ id: "iterate-plugin", factory: (require) => {
3
3
  var module = { exports: {} };
4
4
  var exports = module.exports;
@@ -2645,7 +2645,24 @@ var EN = {
2645
2645
  "\u5404\u8F6E\u53D1\u73B0\u6570\u91CF\uFF1A{a0}{a1}": "Findings per round: {a0}{a1}",
2646
2646
  " \xB7 \u5F53\u524D Round {a0}": " \xB7 current round {a0}",
2647
2647
  "\u8DE8\u8F6E\u5BF9\u6BD4\uFF08\u672C\u4F1A\u8BDD\uFF09": "Cross-round comparison (this session)",
2648
- "\u8DE8\u4F1A\u8BDD\u5BF9\u6BD4\uFF1A\u672C\u8868\u53EA\u8986\u76D6\u5F53\u524D\u4F1A\u8BDD\uFF1B\u70B9\u300C\u62C9\u53D6\u78C1\u76D8\u5FEB\u7167\u300D\u8BA9\u6A21\u578B\u56DE\u663E iterate_history\uFF0C\u518D\u7528\u300C\u5BFC\u51FA JSON\u300D\u5E76\u6392\u6BD4\u8F83\u4E24\u6B21\u8FD0\u884C": 'Cross-session comparison: this table only covers the current session \u2014 click "Pull disk snapshot" to have the model echo iterate_history, then use "Export JSON" to compare two runs side by side',
2648
+ "\u8DE8\u4F1A\u8BDD\u5BF9\u6BD4\uFF1A\u672C\u8868\u53EA\u8986\u76D6\u5F53\u524D\u4F1A\u8BDD\uFF1B\u8981\u8DE8\u4E24\u6B21\u8FED\u4EE3\u5BF9\u6BD4\uFF0C\u7528\u4E0B\u65B9\u300C\u5B58\u4E3A\u5BF9\u6BD4\u57FA\u51C6\u300D\u4FDD\u5B58\u672C\u6B21\u8FD0\u884C\uFF0C\u518D\u4E0E\u4E0B\u4E00\u6B21\u8FED\u4EE3\u5BF9\u6BD4\u3002": 'Cross-session comparison: this table only covers the current session \u2014 to compare two iterations, save this run with "Save as comparison baseline" below, then compare the next iteration against it.',
2649
+ \u8DE8\u4F1A\u8BDD\u5BF9\u6BD4: "Cross-session comparison",
2650
+ "\u57FA\u51C6\u5DF2\u4FDD\u5B58\uFF1A{a0}": "Baseline saved: {a0}",
2651
+ "\u5148\u4FDD\u5B58\u5F53\u524D\u8FD0\u884C\u4E3A\u57FA\u51C6\uFF0C\u518D\u8DD1\u4E0B\u4E00\u6B21\u8FED\u4EE3\u540E\u4E00\u952E\u5BF9\u6BD4": "Save the current run as the baseline, then one-click compare the next iteration against it",
2652
+ "\u628A\u5F53\u524D\u8FD0\u884C\u6458\u8981\u4FDD\u5B58\u4E3A\u5BF9\u6BD4\u57FA\u51C6\uFF08\u542B\u8F6E\u6B21/\u53D1\u73B0/\u4FEE\u590D/\u9A8C\u8BC1/\u95E8\u7981\uFF09": "Save the current run summary as the comparison baseline (rounds / findings / fixes / validation / gate)",
2653
+ "\u5F53\u524D\u6CA1\u6709\u53EF\u4FDD\u5B58\u7684\u8FD0\u884C\u6570\u636E": "No run data to save yet",
2654
+ \u5DF2\u5B58\u4E3A\u57FA\u51C6: "Saved as baseline",
2655
+ \u5B58\u4E3A\u5BF9\u6BD4\u57FA\u51C6: "Save as comparison baseline",
2656
+ "\u663E\u793A\u6216\u9690\u85CF\u4E0E\u5DF2\u4FDD\u5B58\u57FA\u51C6\u7684\u5BF9\u6BD4\u8868": "Show or hide the comparison against the saved baseline",
2657
+ \u6536\u8D77\u5BF9\u6BD4: "Hide comparison",
2658
+ \u4E0E\u57FA\u51C6\u5BF9\u6BD4: "Compare with baseline",
2659
+ "\u6E05\u9664\u5DF2\u4FDD\u5B58\u7684\u5BF9\u6BD4\u57FA\u51C6": "Clear the saved comparison baseline",
2660
+ \u6E05\u9664\u57FA\u51C6: "Clear baseline",
2661
+ "\u5F53\u524D\u4F1A\u8BDD\u6CA1\u6709\u8FD0\u884C\u6570\u636E\u53EF\u5BF9\u6BD4": "This session has no run data to compare yet",
2662
+ \u6307\u6807: "Metric",
2663
+ \u57FA\u51C6: "Baseline",
2664
+ \u5F53\u524D: "Current",
2665
+ \u53D8\u5316: "Delta",
2649
2666
  // ── F3 finding locations ─────────────────────────────────────────────────
2650
2667
  \u6682\u65E0\u53D1\u73B0: "No findings",
2651
2668
  "{a0}\uFF08{a1} \u9879\uFF09": "{a0} ({a1} findings)",
@@ -2865,6 +2882,53 @@ function subscribeSession(listener) {
2865
2882
  };
2866
2883
  }
2867
2884
 
2885
+ // src/client/shortcuts.ts
2886
+ var OBSERVATORY_SHORTCUT_ID = "iterate.open-observatory";
2887
+ var DEFAULT_BINDING = { code: "KeyO", modifiers: ["primary", "shift"] };
2888
+ var DEFAULT_PROFILES = [
2889
+ "desktop:macos",
2890
+ "desktop:windows",
2891
+ "desktop:linux",
2892
+ "web:macos",
2893
+ "web:windows",
2894
+ "web:linux"
2895
+ ];
2896
+ var OBSERVATORY_DEFAULT_BINDINGS = Object.fromEntries(
2897
+ DEFAULT_PROFILES.map((profile) => [profile, DEFAULT_BINDING])
2898
+ );
2899
+ function buildObservatoryShortcutCommand(deps) {
2900
+ const label = () => deps.lang() === "zh" ? "\u6253\u5F00 iterate \u89C2\u6D4B\u53F0" : "Open iterate observatory";
2901
+ return {
2902
+ id: OBSERVATORY_SHORTCUT_ID,
2903
+ label,
2904
+ aliases: [],
2905
+ defaults: OBSERVATORY_DEFAULT_BINDINGS,
2906
+ regions: ["page", "editable", "terminal"],
2907
+ modals: [],
2908
+ resolve: () => ({ status: "handled", run: () => deps.openObservatory({}) })
2909
+ };
2910
+ }
2911
+ function registerIterateShortcuts(service, deps) {
2912
+ try {
2913
+ const dispose = service.register(buildObservatoryShortcutCommand(deps));
2914
+ let done = false;
2915
+ return () => {
2916
+ if (done) return;
2917
+ done = true;
2918
+ try {
2919
+ dispose();
2920
+ } catch {
2921
+ }
2922
+ };
2923
+ } catch (err) {
2924
+ if (typeof console !== "undefined" && typeof console.error === "function") {
2925
+ console.error("[iterate-ui] keyboard-command registration failed; continuing without it.", err);
2926
+ }
2927
+ return () => {
2928
+ };
2929
+ }
2930
+ }
2931
+
2868
2932
  // src/client/context-menu.ts
2869
2933
  var OBS_OPEN_EVENT = "iterate-plugin:open-observatory";
2870
2934
  function openObservatory(detail) {
@@ -2906,14 +2970,14 @@ function buildSessionMenuEntries(lang, opts) {
2906
2970
  function buildFileMenuEntries(lang) {
2907
2971
  return [
2908
2972
  { id: "iterate.review-file", label: pick("iterate.review-file", lang) },
2909
- { id: "iterate.open-panel", label: pick("iterate.open-panel", lang) }
2973
+ { id: "iterate.open-panel", label: pick("iterate.open-panel", lang), openTab: "live" }
2910
2974
  ];
2911
2975
  }
2912
2976
  function buildDirectoryMenuEntries(lang) {
2913
2977
  return [
2914
2978
  { id: "iterate.review-dir", label: pick("iterate.review-dir", lang) },
2915
2979
  { id: "iterate.preflight", label: pick("iterate.preflight", lang), separatorBefore: true },
2916
- { id: "iterate.open-panel", label: pick("iterate.open-panel", lang) }
2980
+ { id: "iterate.open-panel", label: pick("iterate.open-panel", lang), openTab: "live" }
2917
2981
  ];
2918
2982
  }
2919
2983
  function toAction(entry, deps, lang) {
@@ -2925,6 +2989,7 @@ function toAction(entry, deps, lang) {
2925
2989
  };
2926
2990
  if (entry.disabled) action.disabled = true;
2927
2991
  if (entry.openTab) {
2992
+ action.commandId = OBSERVATORY_SHORTCUT_ID;
2928
2993
  action.run = () => {
2929
2994
  deps.openPanel({ tab: entry.openTab });
2930
2995
  deps.notify(lang === "zh" ? "\u5DF2\u6253\u5F00 iterate \u89C2\u6D4B\u53F0\u3002" : "Iterate observatory opened.", true);
@@ -2994,6 +3059,101 @@ function registerIterateContextMenus(service, deps, instructionByEntry) {
2994
3059
  };
2995
3060
  }
2996
3061
 
3062
+ // src/client/run-compare.ts
3063
+ var RUN_BASELINE_VERSION = 1;
3064
+ var RUN_BASELINE_STORAGE_KEY = "iterate.run-baseline";
3065
+ function buildRunBaseline(input, savedAt) {
3066
+ const convergence = Array.isArray(input.convergence) ? input.convergence : [];
3067
+ const findings = (Array.isArray(input.findings) ? input.findings : []).filter(
3068
+ (f) => typeof f === "object" && f !== null
3069
+ );
3070
+ const fixes = (Array.isArray(input.fixes) ? input.fixes : []).filter(
3071
+ (f) => typeof f === "object" && f !== null
3072
+ );
3073
+ const validations = (Array.isArray(input.validations) ? input.validations : []).filter(
3074
+ (v) => typeof v === "object" && v !== null
3075
+ );
3076
+ let criticalHigh = 0;
3077
+ for (const f of findings) {
3078
+ const sev = String(f && f.severity || "").toLowerCase();
3079
+ if (sev === "critical" || sev === "high") criticalHigh += 1;
3080
+ }
3081
+ let fixed = 0;
3082
+ let failedFixes = 0;
3083
+ for (const x of fixes) {
3084
+ if (x && x.success === true) fixed += 1;
3085
+ else if (x && x.success === false) failedFixes += 1;
3086
+ }
3087
+ let passed = 0;
3088
+ for (const v of validations) {
3089
+ if (v && v.allowed === true) passed += 1;
3090
+ }
3091
+ const validationTotal = validations.length;
3092
+ const validationPassRate = validationTotal > 0 ? Math.round(passed / validationTotal * 100) : null;
3093
+ return {
3094
+ version: RUN_BASELINE_VERSION,
3095
+ savedAt,
3096
+ ...input.project ? { project: input.project } : {},
3097
+ ...input.mode ? { mode: input.mode } : {},
3098
+ rounds: convergence.length,
3099
+ findings: findings.length,
3100
+ criticalHigh,
3101
+ fixed,
3102
+ failedFixes,
3103
+ validationPassRate,
3104
+ validationTotal,
3105
+ qualityGateStatus: input.qualityGateStatus || null,
3106
+ qualityGateScore: typeof input.qualityGateScore === "number" ? input.qualityGateScore : null,
3107
+ stoppedReason: input.stoppedReason || null,
3108
+ convergence
3109
+ };
3110
+ }
3111
+ function parseRunBaseline(raw) {
3112
+ if (!raw) return null;
3113
+ let parsed;
3114
+ try {
3115
+ parsed = JSON.parse(raw);
3116
+ } catch {
3117
+ return null;
3118
+ }
3119
+ if (typeof parsed !== "object" || parsed === null) return null;
3120
+ const b = parsed;
3121
+ if (b.version !== RUN_BASELINE_VERSION) return null;
3122
+ if (typeof b.savedAt !== "string" || b.savedAt.length === 0) return null;
3123
+ if (typeof b.findings !== "number" || !Number.isFinite(b.findings)) return null;
3124
+ const convergence = Array.isArray(b.convergence) ? b.convergence.filter((n) => typeof n === "number" && Number.isFinite(n)) : [];
3125
+ return {
3126
+ version: RUN_BASELINE_VERSION,
3127
+ savedAt: b.savedAt,
3128
+ ...typeof b.project === "string" && b.project ? { project: b.project } : {},
3129
+ ...typeof b.mode === "string" && b.mode ? { mode: b.mode } : {},
3130
+ rounds: typeof b.rounds === "number" && Number.isFinite(b.rounds) ? b.rounds : convergence.length,
3131
+ findings: b.findings,
3132
+ criticalHigh: typeof b.criticalHigh === "number" && Number.isFinite(b.criticalHigh) ? b.criticalHigh : 0,
3133
+ fixed: typeof b.fixed === "number" && Number.isFinite(b.fixed) ? b.fixed : 0,
3134
+ failedFixes: typeof b.failedFixes === "number" && Number.isFinite(b.failedFixes) ? b.failedFixes : 0,
3135
+ validationPassRate: typeof b.validationPassRate === "number" && Number.isFinite(b.validationPassRate) ? b.validationPassRate : null,
3136
+ validationTotal: typeof b.validationTotal === "number" && Number.isFinite(b.validationTotal) ? b.validationTotal : 0,
3137
+ qualityGateStatus: typeof b.qualityGateStatus === "string" && b.qualityGateStatus ? b.qualityGateStatus : null,
3138
+ qualityGateScore: typeof b.qualityGateScore === "number" && Number.isFinite(b.qualityGateScore) ? b.qualityGateScore : null,
3139
+ stoppedReason: typeof b.stoppedReason === "string" && b.stoppedReason ? b.stoppedReason : null,
3140
+ convergence
3141
+ };
3142
+ }
3143
+ function compareRuns(baseline, current3) {
3144
+ const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : null;
3145
+ const rows = [
3146
+ { key: "rounds", baseline: num(baseline.rounds), current: num(current3.rounds), higherIsBetter: null },
3147
+ { key: "findings", baseline: num(baseline.findings), current: num(current3.findings), higherIsBetter: false },
3148
+ { key: "criticalHigh", baseline: num(baseline.criticalHigh), current: num(current3.criticalHigh), higherIsBetter: false },
3149
+ { key: "fixed", baseline: num(baseline.fixed), current: num(current3.fixed), higherIsBetter: true },
3150
+ { key: "failedFixes", baseline: num(baseline.failedFixes), current: num(current3.failedFixes), higherIsBetter: false },
3151
+ { key: "validationPassRate", baseline: num(baseline.validationPassRate), current: num(current3.validationPassRate), higherIsBetter: true },
3152
+ { key: "qualityGateScore", baseline: num(baseline.qualityGateScore), current: num(current3.qualityGateScore), higherIsBetter: true }
3153
+ ];
3154
+ return rows;
3155
+ }
3156
+
2997
3157
  // src/client/index.ts
2998
3158
  var React = require("react");
2999
3159
  var name = "iterate-plugin";
@@ -3439,6 +3599,14 @@ function readContextMenus(ctx) {
3439
3599
  if (typeof svc.notify !== "function") return void 0;
3440
3600
  return svc;
3441
3601
  }
3602
+ function readShortcuts(ctx) {
3603
+ if (!ctx || typeof ctx.get !== "function") return void 0;
3604
+ const raw = ctx.get("shortcuts", false);
3605
+ if (!raw || typeof raw !== "object") return void 0;
3606
+ const svc = raw;
3607
+ if (typeof svc.register !== "function") return void 0;
3608
+ return svc;
3609
+ }
3442
3610
  function sessionIdOfSession(session) {
3443
3611
  if (!session || typeof session !== "object") return null;
3444
3612
  const s = session;
@@ -3466,6 +3634,7 @@ var themeDisposer = null;
3466
3634
  var themeListener = null;
3467
3635
  var themeEnabled = true;
3468
3636
  var menuDisposer = null;
3637
+ var shortcutDisposer = null;
3469
3638
  var roundPulseListeners = [];
3470
3639
  function emitRoundPulse(round, converged) {
3471
3640
  const payload = { round, converged: converged === true };
@@ -4680,6 +4849,15 @@ var OBS_LIVE_META = {
4680
4849
  info: { label: { zh: "\u4FE1\u606F", en: "Info" }, color: "var(--dsw-alias-label-secondary)" }
4681
4850
  };
4682
4851
  var OBS_LIVE_FALLBACK_COLOR = "var(--dsw-alias-label-secondary)";
4852
+ var RUN_COMPARE_METRIC_LABELS = {
4853
+ rounds: { zh: "\u8F6E\u6B21", en: "Rounds" },
4854
+ findings: { zh: "\u53D1\u73B0", en: "Findings" },
4855
+ criticalHigh: { zh: "\u4E25\u91CD\uFF08critical+high\uFF09", en: "Critical + high" },
4856
+ fixed: { zh: "\u4FEE\u590D\u6210\u529F", en: "Fixes applied" },
4857
+ failedFixes: { zh: "\u4FEE\u590D\u5931\u8D25", en: "Failed fixes" },
4858
+ validationPassRate: { zh: "\u9A8C\u8BC1\u901A\u8FC7\u7387", en: "Validation pass rate" },
4859
+ qualityGateScore: { zh: "\u95E8\u7981\u5F97\u5206", en: "Quality gate score" }
4860
+ };
4683
4861
  function formatObsLiveTime(ts) {
4684
4862
  if (!ts) return "";
4685
4863
  let date = null;
@@ -4747,6 +4925,15 @@ function ObservatoryPanel(props) {
4747
4925
  );
4748
4926
  const [expSearch, setExpSearch] = React.useState("");
4749
4927
  const [defenseFilter, setDefenseFilter] = React.useState("");
4928
+ const [baseline, setBaseline] = React.useState(() => {
4929
+ try {
4930
+ return parseRunBaseline(storage2 ? storage2.get(RUN_BASELINE_STORAGE_KEY) : null);
4931
+ } catch {
4932
+ return null;
4933
+ }
4934
+ });
4935
+ const [showCompare, setShowCompare] = React.useState(false);
4936
+ const [baselineFlash, setBaselineFlash] = React.useState(false);
4750
4937
  const copyTimer = React.useRef(null);
4751
4938
  React.useEffect(() => () => {
4752
4939
  if (copyTimer.current) clearTimeout(copyTimer.current);
@@ -4990,11 +5177,51 @@ function ObservatoryPanel(props) {
4990
5177
  })
4991
5178
  );
4992
5179
  };
5180
+ const currentRunSummary = () => {
5181
+ const hasRun = (manifest.convergence || []).length > 0 || (manifest.findings || []).length > 0 || (manifest.fixes || []).length > 0 || typeof manifest.round === "number" && manifest.round > 0;
5182
+ if (!hasRun) return null;
5183
+ return buildRunBaseline(
5184
+ {
5185
+ project: manifest.project || void 0,
5186
+ mode: manifest.mode || void 0,
5187
+ convergence: manifest.convergence || [],
5188
+ findings: manifest.findings || [],
5189
+ fixes: manifest.fixes || [],
5190
+ validations: manifest.validations || [],
5191
+ qualityGateStatus: qualityGate && qualityGate.overallStatus || null,
5192
+ qualityGateScore: qualityGate && typeof qualityGate.overallScore === "number" ? qualityGate.overallScore : null,
5193
+ stoppedReason: manifest.stoppedReason || null
5194
+ },
5195
+ (/* @__PURE__ */ new Date()).toISOString()
5196
+ );
5197
+ };
5198
+ const saveBaseline = () => {
5199
+ const summary = currentRunSummary();
5200
+ if (!summary || !storage2) return;
5201
+ try {
5202
+ storage2.set(RUN_BASELINE_STORAGE_KEY, JSON.stringify(summary));
5203
+ setBaseline(summary);
5204
+ setShowCompare(true);
5205
+ setBaselineFlash(true);
5206
+ if (copyTimer.current) clearTimeout(copyTimer.current);
5207
+ copyTimer.current = setTimeout(() => setBaselineFlash(false), 1600);
5208
+ } catch (err) {
5209
+ log("failed to persist run baseline", err);
5210
+ }
5211
+ };
5212
+ const clearBaseline = () => {
5213
+ if (storage2) {
5214
+ try {
5215
+ storage2.remove(RUN_BASELINE_STORAGE_KEY);
5216
+ } catch (err) {
5217
+ log("failed to clear run baseline", err);
5218
+ }
5219
+ }
5220
+ setBaseline(null);
5221
+ setShowCompare(false);
5222
+ };
4993
5223
  const renderTrend = () => {
4994
5224
  const conv = manifest.convergence || [];
4995
- if (conv.length === 0) {
4996
- return React.createElement("div", { className: "iterate-obs-empty" }, t("\u6682\u65E0\u6536\u655B\u6570\u636E"));
4997
- }
4998
5225
  const points = conv.map((n, i) => ({ round: i + 1, count: n }));
4999
5226
  const report = latestReport(session);
5000
5227
  const comparison = buildRoundComparison(report, manifest.validations || [], manifest.fixes || []);
@@ -5035,7 +5262,7 @@ function ObservatoryPanel(props) {
5035
5262
  })
5036
5263
  )
5037
5264
  );
5038
- return React.createElement(
5265
+ const trendBody = conv.length === 0 ? React.createElement("div", { className: "iterate-obs-empty" }, t("\u6682\u65E0\u6536\u655B\u6570\u636E")) : React.createElement(
5039
5266
  "div",
5040
5267
  {},
5041
5268
  React.createElement(TrendChart, { points }),
@@ -5054,12 +5281,111 @@ function ObservatoryPanel(props) {
5054
5281
  React.createElement(
5055
5282
  "span",
5056
5283
  { className: "iterate-obs-head-meta" },
5057
- t("\u8DE8\u4F1A\u8BDD\u5BF9\u6BD4\uFF1A\u672C\u8868\u53EA\u8986\u76D6\u5F53\u524D\u4F1A\u8BDD\uFF1B\u70B9\u300C\u62C9\u53D6\u78C1\u76D8\u5FEB\u7167\u300D\u8BA9\u6A21\u578B\u56DE\u663E iterate_history\uFF0C\u518D\u7528\u300C\u5BFC\u51FA JSON\u300D\u5E76\u6392\u6BD4\u8F83\u4E24\u6B21\u8FD0\u884C")
5284
+ t("\u8DE8\u4F1A\u8BDD\u5BF9\u6BD4\uFF1A\u672C\u8868\u53EA\u8986\u76D6\u5F53\u524D\u4F1A\u8BDD\uFF1B\u8981\u8DE8\u4E24\u6B21\u8FED\u4EE3\u5BF9\u6BD4\uFF0C\u7528\u4E0B\u65B9\u300C\u5B58\u4E3A\u5BF9\u6BD4\u57FA\u51C6\u300D\u4FDD\u5B58\u672C\u6B21\u8FD0\u884C\uFF0C\u518D\u4E0E\u4E0B\u4E00\u6B21\u8FED\u4EE3\u5BF9\u6BD4\u3002")
5058
5285
  )
5059
5286
  ),
5060
5287
  React.createElement("div", { className: "iterate-obs-block-body" }, comparisonTable)
5061
5288
  ) : null
5062
5289
  );
5290
+ return React.createElement("div", {}, trendBody, renderCrossSessionCompare());
5291
+ };
5292
+ const renderCrossSessionCompare = () => {
5293
+ const current3 = currentRunSummary();
5294
+ const hasRun = current3 !== null;
5295
+ const metricLabel = (key) => {
5296
+ const m = RUN_COMPARE_METRIC_LABELS[key];
5297
+ return m ? m[getLang()] : key;
5298
+ };
5299
+ const fmtCell = (v, key) => {
5300
+ if (v === null) return "\u2014";
5301
+ return key === "validationPassRate" ? `${v}%` : String(v);
5302
+ };
5303
+ const deltaText = (row) => {
5304
+ if (row.baseline === null || row.current === null) return "\u2014";
5305
+ const d = row.current - row.baseline;
5306
+ return d === 0 ? "0" : `${d > 0 ? "+" : ""}${d}`;
5307
+ };
5308
+ const deltaTint = (row) => {
5309
+ if (row.baseline === null || row.current === null || row.higherIsBetter === null) return void 0;
5310
+ const d = row.current - row.baseline;
5311
+ if (d === 0) return void 0;
5312
+ const improved = row.higherIsBetter ? d > 0 : d < 0;
5313
+ return improved ? { color: "var(--dsw-alias-state-success-primary)", fontWeight: 600 } : { color: "var(--dsw-alias-state-error-primary)", fontWeight: 600 };
5314
+ };
5315
+ let compareBody = null;
5316
+ if (baseline && current3 && showCompare) {
5317
+ const rows = compareRuns(baseline, current3);
5318
+ compareBody = React.createElement(
5319
+ "table",
5320
+ { className: "iterate-obs-table" },
5321
+ React.createElement(
5322
+ "thead",
5323
+ {},
5324
+ React.createElement(
5325
+ "tr",
5326
+ {},
5327
+ React.createElement("th", {}, t("\u6307\u6807")),
5328
+ React.createElement("th", {}, t("\u57FA\u51C6")),
5329
+ React.createElement("th", {}, t("\u5F53\u524D")),
5330
+ React.createElement("th", {}, t("\u53D8\u5316"))
5331
+ )
5332
+ ),
5333
+ React.createElement(
5334
+ "tbody",
5335
+ {},
5336
+ ...rows.map(
5337
+ (row) => React.createElement(
5338
+ "tr",
5339
+ { key: `rc-${row.key}` },
5340
+ React.createElement("td", {}, metricLabel(row.key)),
5341
+ React.createElement("td", {}, fmtCell(row.baseline, row.key)),
5342
+ React.createElement("td", {}, fmtCell(row.current, row.key)),
5343
+ React.createElement("td", { style: deltaTint(row) }, deltaText(row))
5344
+ )
5345
+ )
5346
+ )
5347
+ );
5348
+ }
5349
+ return React.createElement(
5350
+ "div",
5351
+ { className: "iterate-obs-block", style: { marginTop: 8 } },
5352
+ React.createElement(
5353
+ "div",
5354
+ { className: "iterate-obs-block-head" },
5355
+ React.createElement("span", {}, t("\u8DE8\u4F1A\u8BDD\u5BF9\u6BD4")),
5356
+ baseline ? React.createElement("span", { className: "iterate-obs-head-meta" }, tf(`\u57FA\u51C6\u5DF2\u4FDD\u5B58\uFF1A{a0}`, { a0: baseline.savedAt })) : React.createElement("span", { className: "iterate-obs-head-meta" }, t("\u5148\u4FDD\u5B58\u5F53\u524D\u8FD0\u884C\u4E3A\u57FA\u51C6\uFF0C\u518D\u8DD1\u4E0B\u4E00\u6B21\u8FED\u4EE3\u540E\u4E00\u952E\u5BF9\u6BD4"))
5357
+ ),
5358
+ React.createElement(
5359
+ "div",
5360
+ { className: "iterate-obs-block-body" },
5361
+ React.createElement(
5362
+ "div",
5363
+ { className: "iterate-obs-bar", style: { flexWrap: "wrap" } },
5364
+ React.createElement("button", {
5365
+ className: "iterate-btn",
5366
+ "data-primary": "",
5367
+ disabled: !hasRun,
5368
+ title: hasRun ? t("\u628A\u5F53\u524D\u8FD0\u884C\u6458\u8981\u4FDD\u5B58\u4E3A\u5BF9\u6BD4\u57FA\u51C6\uFF08\u542B\u8F6E\u6B21/\u53D1\u73B0/\u4FEE\u590D/\u9A8C\u8BC1/\u95E8\u7981\uFF09") : t("\u5F53\u524D\u6CA1\u6709\u53EF\u4FDD\u5B58\u7684\u8FD0\u884C\u6570\u636E"),
5369
+ onClick: saveBaseline
5370
+ }, baselineFlash ? t("\u5DF2\u5B58\u4E3A\u57FA\u51C6") : t("\u5B58\u4E3A\u5BF9\u6BD4\u57FA\u51C6")),
5371
+ baseline ? React.createElement("button", {
5372
+ className: "iterate-btn",
5373
+ "data-copied": showCompare ? "" : void 0,
5374
+ disabled: !hasRun,
5375
+ title: t("\u663E\u793A\u6216\u9690\u85CF\u4E0E\u5DF2\u4FDD\u5B58\u57FA\u51C6\u7684\u5BF9\u6BD4\u8868"),
5376
+ onClick: () => setShowCompare((v) => !v)
5377
+ }, showCompare ? t("\u6536\u8D77\u5BF9\u6BD4") : t("\u4E0E\u57FA\u51C6\u5BF9\u6BD4")) : null,
5378
+ baseline ? React.createElement("button", {
5379
+ className: "iterate-btn",
5380
+ "data-ghost": "",
5381
+ title: t("\u6E05\u9664\u5DF2\u4FDD\u5B58\u7684\u5BF9\u6BD4\u57FA\u51C6"),
5382
+ onClick: clearBaseline
5383
+ }, t("\u6E05\u9664\u57FA\u51C6")) : null
5384
+ ),
5385
+ showCompare && baseline && !hasRun ? React.createElement("div", { className: "iterate-obs-empty", style: { marginTop: 8 } }, t("\u5F53\u524D\u4F1A\u8BDD\u6CA1\u6709\u8FD0\u884C\u6570\u636E\u53EF\u5BF9\u6BD4")) : null,
5386
+ compareBody
5387
+ )
5388
+ );
5063
5389
  };
5064
5390
  const renderFindings = () => {
5065
5391
  const findings = manifest.findings || [];
@@ -6446,6 +6772,32 @@ function apply(ctx) {
6446
6772
  }
6447
6773
  }
6448
6774
  }
6775
+ {
6776
+ const shortcutSvc = readShortcuts(ctx);
6777
+ if (shortcutSvc) {
6778
+ if (shortcutDisposer) {
6779
+ try {
6780
+ shortcutDisposer();
6781
+ } catch {
6782
+ }
6783
+ shortcutDisposer = null;
6784
+ }
6785
+ shortcutDisposer = registerIterateShortcuts(shortcutSvc, {
6786
+ openObservatory: (detail) => {
6787
+ openObservatory(detail);
6788
+ },
6789
+ lang: () => getLang()
6790
+ });
6791
+ if (typeof ctx.effect === "function") {
6792
+ ctx.effect(() => {
6793
+ if (shortcutDisposer) {
6794
+ shortcutDisposer();
6795
+ shortcutDisposer = null;
6796
+ }
6797
+ });
6798
+ }
6799
+ }
6800
+ }
6449
6801
  registerThemeListener(ctx);
6450
6802
  }
6451
6803
  return module.exports; } });