iterate-plugin 3.13.0 → 3.15.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 +6 -5
- package/README.zh-CN.md +3 -1
- package/lib/client.js +313 -1
- package/lib/parse.js +28 -0
- package/package.json +26 -21
- package/src/client/context-menu.ts +298 -0
- package/src/client/index.ts +163 -4
- package/src/client/shortcuts.ts +152 -0
package/README.md
CHANGED
|
@@ -114,6 +114,8 @@ Both modes share:
|
|
|
114
114
|
- **ProgressCapsule** (`shell.overlay`) — popup notification on each round completion / convergence (incl. convergence confirm).
|
|
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
|
+
- **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.
|
|
117
119
|
|
|
118
120
|
The UI layer is **defensive by design**: it degrades gracefully if any of `slots` / `theme` / `React` is unavailable — it never crashes the client.
|
|
119
121
|
|
|
@@ -346,14 +348,13 @@ All runtime state lives under `.iterate/` at the project root (can be excluded v
|
|
|
346
348
|
`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`,
|
|
347
349
|
`0.2.0-rc.2`, `0.2.1-alpha.1`, `0.2.1-alpha.2`, `0.2.1-alpha.3`,
|
|
348
350
|
`0.2.1-alpha.4`, `0.2.1-alpha.5`, `0.2.1-rc.1`, `0.2.1-rc.2`,
|
|
349
|
-
`0.2.1-rc.3`, `0.2.1-rc.4`, `0.2.1-rc.5`, `0.2.2-alpha.2
|
|
350
|
-
`0.1.1-rc.1` …
|
|
351
|
-
`0.2.2-alpha.2`).
|
|
351
|
+
`0.2.1-rc.3`, `0.2.1-rc.4`, `0.2.1-rc.5`, `0.2.2-alpha.2`,
|
|
352
|
+
`0.2.2-alpha.3`, `0.2.2-rc.1` (30 releases, `0.1.1-rc.1` … `0.2.2-rc.1`).
|
|
352
353
|
The plugin is built against `@deepseek-ai/dsh-tools` /
|
|
353
|
-
`@deepseek-ai/dsh-util-values` `0.2.2-
|
|
354
|
+
`@deepseek-ai/dsh-util-values` `0.2.2-rc.1` and uses only public contracts
|
|
354
355
|
(tool registration, client slots + theme, bundle patch, and — since v3.9 —
|
|
355
356
|
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-
|
|
357
|
+
It also declares `engines.dsh` (`>=0.1.1-rc.1`) so the dsh ≥ 0.2.2-rc.1
|
|
357
358
|
runtime evaluates compatibility from that single range (earlier runtimes fall
|
|
358
359
|
back to the `@deepseek-ai/dsh-*` peer versions listed in `package.json`).
|
|
359
360
|
- **Disposable-Profile evidence (real, `dsh` CLI `0.1.1-rc.1`)**: on a temp
|
package/README.zh-CN.md
CHANGED
|
@@ -113,6 +113,8 @@ dsh plugin --profile web add iterate-plugin
|
|
|
113
113
|
- **进度胶囊 `ProgressCapsule`**(`shell.overlay`)— 每轮完成 / 收敛时右下角弹出通知(含收敛确认)。
|
|
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
|
+
- **右键菜单(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` 服务时降级为无键盘命令(停靠面板与右键菜单仍是入口);默认绑定冲突时仅记录并跳过,绝不拖垮插件。
|
|
116
118
|
|
|
117
119
|
UI 层为**防御式设计**:`slots` / `theme` / `React` 任一不可用时自动降级,不会崩溃客户端。
|
|
118
120
|
|
|
@@ -336,7 +338,7 @@ validation:
|
|
|
336
338
|
### Node.js / DSH 兼容范围
|
|
337
339
|
|
|
338
340
|
- **Node.js**:`>=20`(`package.json` 的 `engines.node`)。
|
|
339
|
-
- **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`(共
|
|
341
|
+
- **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 版本)。
|
|
340
342
|
- **一次性 Profile 证据(真实,`dsh` CLI `0.1.1-rc.1`)**:在临时 `$DSH_HOME` 下
|
|
341
343
|
`dsh plugin --profile <p> add <本仓库>` 约 449ms 完成安装;`dsh --profile <p>
|
|
342
344
|
--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:
|
|
1
|
+
/* iterate-plugin client source-digest: 4f3bcac77cef83b80ea8fa2387f254121b9f94f7a2a2ca9422dd6342581c3463 */
|
|
2
2
|
window.__ModuleLoader__.load({ id: "iterate-plugin", factory: (require) => {
|
|
3
3
|
var module = { exports: {} };
|
|
4
4
|
var exports = module.exports;
|
|
@@ -1250,6 +1250,13 @@ var START_INSTRUCTIONS = {
|
|
|
1250
1250
|
full: START_INSTRUCTION_FULL,
|
|
1251
1251
|
reviewOnly: START_INSTRUCTION_REVIEW_ONLY
|
|
1252
1252
|
};
|
|
1253
|
+
function buildScopedReviewInstruction(scope) {
|
|
1254
|
+
const hasFile = scope && typeof scope.file === "string" && scope.file.length > 0;
|
|
1255
|
+
const hasDir = !hasFile && scope && typeof scope.dir === "string" && scope.dir.length > 0;
|
|
1256
|
+
if (!hasFile && !hasDir) return "";
|
|
1257
|
+
const subject = hasFile ? `\u6587\u4EF6 \`${scope.file}\`${typeof scope.line === "number" && scope.line > 0 ? `\uFF08\u7B2C ${scope.line} \u884C\uFF09` : ""}` : `\u76EE\u5F55 \`${scope.dir}\``;
|
|
1258
|
+
return `\u5F00\u59CB\u4E00\u6B21\u4EC5\u8BC4\u5BA1\uFF08dry-run\uFF0C\u4E0D\u4FEE\u6539\u4EFB\u4F55\u6587\u4EF6\uFF09\uFF0C\u5BA1\u67E5\u8303\u56F4\u9650\u5B9A\u5728${subject}\uFF1A\u8BF7\u8C03\u7528 \`workflow\` \u5DE5\u5177\u8FD0\u884C iterate \u5DE5\u4F5C\u6D41\uFF0Cmode: "dry-run"\uFF0C\u6309\u7CFB\u7EDF\u63D0\u793A\u7684 Iterate Workflow \u6267\u884C plan \u2192 \u8BC4\u5BA1 \u2192 \u5206\u8BCA \u2192 \`iterate_review aggregate\` / \`meta-review\` \u7EC8\u62A5\u5E76 \`iterate_transcript\` capture\uFF1B\u8BC4\u5BA1\u65F6\u628A\u8303\u56F4\u9650\u5B9A\u5728${subject}\uFF0C\u5176\u4F59\u6587\u4EF6\u4EC5\u4F5C\u4E0A\u4E0B\u6587\u3001\u4E0D\u5F97\u7ED9\u51FA\u4FEE\u590D\u3002\u5168\u7A0B\u4E0D\u8981\u8C03\u7528 \`iterate_fix\` / \`iterate_rollback\`\u3002`;
|
|
1259
|
+
}
|
|
1253
1260
|
function fixFindingPayload(finding) {
|
|
1254
1261
|
const f = finding && typeof finding === "object" ? finding : {};
|
|
1255
1262
|
const file = String(f.file ?? "");
|
|
@@ -2858,6 +2865,183 @@ function subscribeSession(listener) {
|
|
|
2858
2865
|
};
|
|
2859
2866
|
}
|
|
2860
2867
|
|
|
2868
|
+
// src/client/shortcuts.ts
|
|
2869
|
+
var OBSERVATORY_SHORTCUT_ID = "iterate.open-observatory";
|
|
2870
|
+
var DEFAULT_BINDING = { code: "KeyO", modifiers: ["primary", "shift"] };
|
|
2871
|
+
var DEFAULT_PROFILES = [
|
|
2872
|
+
"desktop:macos",
|
|
2873
|
+
"desktop:windows",
|
|
2874
|
+
"desktop:linux",
|
|
2875
|
+
"web:macos",
|
|
2876
|
+
"web:windows",
|
|
2877
|
+
"web:linux"
|
|
2878
|
+
];
|
|
2879
|
+
var OBSERVATORY_DEFAULT_BINDINGS = Object.fromEntries(
|
|
2880
|
+
DEFAULT_PROFILES.map((profile) => [profile, DEFAULT_BINDING])
|
|
2881
|
+
);
|
|
2882
|
+
function buildObservatoryShortcutCommand(deps) {
|
|
2883
|
+
const label = () => deps.lang() === "zh" ? "\u6253\u5F00 iterate \u89C2\u6D4B\u53F0" : "Open iterate observatory";
|
|
2884
|
+
return {
|
|
2885
|
+
id: OBSERVATORY_SHORTCUT_ID,
|
|
2886
|
+
label,
|
|
2887
|
+
aliases: [],
|
|
2888
|
+
defaults: OBSERVATORY_DEFAULT_BINDINGS,
|
|
2889
|
+
regions: ["page", "editable", "terminal"],
|
|
2890
|
+
modals: [],
|
|
2891
|
+
resolve: () => ({ status: "handled", run: () => deps.openObservatory({}) })
|
|
2892
|
+
};
|
|
2893
|
+
}
|
|
2894
|
+
function registerIterateShortcuts(service, deps) {
|
|
2895
|
+
try {
|
|
2896
|
+
const dispose = service.register(buildObservatoryShortcutCommand(deps));
|
|
2897
|
+
let done = false;
|
|
2898
|
+
return () => {
|
|
2899
|
+
if (done) return;
|
|
2900
|
+
done = true;
|
|
2901
|
+
try {
|
|
2902
|
+
dispose();
|
|
2903
|
+
} catch {
|
|
2904
|
+
}
|
|
2905
|
+
};
|
|
2906
|
+
} catch (err) {
|
|
2907
|
+
if (typeof console !== "undefined" && typeof console.error === "function") {
|
|
2908
|
+
console.error("[iterate-ui] keyboard-command registration failed; continuing without it.", err);
|
|
2909
|
+
}
|
|
2910
|
+
return () => {
|
|
2911
|
+
};
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
// src/client/context-menu.ts
|
|
2916
|
+
var OBS_OPEN_EVENT = "iterate-plugin:open-observatory";
|
|
2917
|
+
function openObservatory(detail) {
|
|
2918
|
+
if (typeof window === "undefined" || typeof window.dispatchEvent !== "function") return false;
|
|
2919
|
+
try {
|
|
2920
|
+
window.dispatchEvent(new CustomEvent(OBS_OPEN_EVENT, { detail: detail ?? {} }));
|
|
2921
|
+
return true;
|
|
2922
|
+
} catch {
|
|
2923
|
+
return false;
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
var LABELS = {
|
|
2927
|
+
"iterate.open-panel": { zh: "\u6253\u5F00 iterate \u8D28\u91CF\u6307\u6325\u4E2D\u5FC3", en: "Open iterate command center" },
|
|
2928
|
+
"iterate.preflight": { zh: "\u590D\u5236\u5F00\u8DD1\u9884\u68C0\u6307\u4EE4", en: "Copy preflight instruction" },
|
|
2929
|
+
"iterate.dry-run": { zh: "\u590D\u5236 dry-run \u5BA1\u67E5\u6307\u4EE4", en: "Copy dry-run review instruction" },
|
|
2930
|
+
"iterate.resume": { zh: "\u590D\u5236\u65AD\u70B9\u6062\u590D\u6307\u4EE4", en: "Copy checkpoint-resume instruction" },
|
|
2931
|
+
"iterate.pull": { zh: "\u590D\u5236\u62C9\u53D6\u78C1\u76D8\u5FEB\u7167\u6307\u4EE4", en: "Copy disk-snapshot pull instruction" },
|
|
2932
|
+
"iterate.review-file": { zh: "\u590D\u5236\u300C\u4EC5\u5BA1\u67E5\u6B64\u6587\u4EF6\u300D\u6307\u4EE4", en: 'Copy "review this file only" instruction' },
|
|
2933
|
+
"iterate.review-dir": { zh: "\u590D\u5236\u300C\u4EC5\u5BA1\u67E5\u6B64\u76EE\u5F55\u300D\u6307\u4EE4", en: 'Copy "review this directory only" instruction' }
|
|
2934
|
+
};
|
|
2935
|
+
function pick(id, lang) {
|
|
2936
|
+
const entry = LABELS[id];
|
|
2937
|
+
if (!entry) return id;
|
|
2938
|
+
return lang === "zh" ? entry.zh : entry.en;
|
|
2939
|
+
}
|
|
2940
|
+
var COPY_FAILURE = {
|
|
2941
|
+
zh: "\u590D\u5236\u5931\u8D25\uFF1A\u526A\u8D34\u677F\u88AB\u62D2\u7EDD\u3002\u8BF7\u6539\u4E3A\u5728\u89C2\u6D4B\u53F0\u9762\u677F\u91CC\u7528\u540C\u4E00\u6307\u4EE4\u7684\u300C\u590D\u5236\u300D\u6309\u94AE\uFF0C\u6216\u624B\u52A8\u9009\u4E2D\u6307\u4EE4\u6587\u672C\u590D\u5236\u3002",
|
|
2942
|
+
en: 'Copy failed: clipboard denied. Use the matching "Copy" button in the observatory panel, or select the instruction text manually.'
|
|
2943
|
+
};
|
|
2944
|
+
function buildSessionMenuEntries(lang, opts) {
|
|
2945
|
+
return [
|
|
2946
|
+
{ id: "iterate.open-panel", label: pick("iterate.open-panel", lang), openTab: "live" },
|
|
2947
|
+
{ id: "iterate.preflight", label: pick("iterate.preflight", lang), separatorBefore: true },
|
|
2948
|
+
{ id: "iterate.dry-run", label: pick("iterate.dry-run", lang) },
|
|
2949
|
+
{ id: "iterate.resume", label: pick("iterate.resume", lang), disabled: opts?.hasCheckpoint === false },
|
|
2950
|
+
{ id: "iterate.pull", label: pick("iterate.pull", lang), separatorBefore: true }
|
|
2951
|
+
];
|
|
2952
|
+
}
|
|
2953
|
+
function buildFileMenuEntries(lang) {
|
|
2954
|
+
return [
|
|
2955
|
+
{ id: "iterate.review-file", label: pick("iterate.review-file", lang) },
|
|
2956
|
+
{ id: "iterate.open-panel", label: pick("iterate.open-panel", lang), openTab: "live" }
|
|
2957
|
+
];
|
|
2958
|
+
}
|
|
2959
|
+
function buildDirectoryMenuEntries(lang) {
|
|
2960
|
+
return [
|
|
2961
|
+
{ id: "iterate.review-dir", label: pick("iterate.review-dir", lang) },
|
|
2962
|
+
{ id: "iterate.preflight", label: pick("iterate.preflight", lang), separatorBefore: true },
|
|
2963
|
+
{ id: "iterate.open-panel", label: pick("iterate.open-panel", lang), openTab: "live" }
|
|
2964
|
+
];
|
|
2965
|
+
}
|
|
2966
|
+
function toAction(entry, deps, lang) {
|
|
2967
|
+
const action = {
|
|
2968
|
+
id: entry.id,
|
|
2969
|
+
label: entry.label,
|
|
2970
|
+
danger: entry.danger,
|
|
2971
|
+
separatorBefore: entry.separatorBefore
|
|
2972
|
+
};
|
|
2973
|
+
if (entry.disabled) action.disabled = true;
|
|
2974
|
+
if (entry.openTab) {
|
|
2975
|
+
action.commandId = OBSERVATORY_SHORTCUT_ID;
|
|
2976
|
+
action.run = () => {
|
|
2977
|
+
deps.openPanel({ tab: entry.openTab });
|
|
2978
|
+
deps.notify(lang === "zh" ? "\u5DF2\u6253\u5F00 iterate \u89C2\u6D4B\u53F0\u3002" : "Iterate observatory opened.", true);
|
|
2979
|
+
};
|
|
2980
|
+
return action;
|
|
2981
|
+
}
|
|
2982
|
+
if (entry.instruction) {
|
|
2983
|
+
const text = entry.instruction;
|
|
2984
|
+
const failureLabel = COPY_FAILURE[lang];
|
|
2985
|
+
action.failureLabel = failureLabel;
|
|
2986
|
+
action.run = async () => {
|
|
2987
|
+
const ok = await deps.copy(text);
|
|
2988
|
+
if (!ok) throw new Error(failureLabel);
|
|
2989
|
+
deps.notify(lang === "zh" ? "\u6307\u4EE4\u5DF2\u590D\u5236\uFF0C\u7C98\u8D34\u7ED9\u6A21\u578B\u5373\u53EF\u3002" : "Instruction copied \u2014 paste it to the model.", true);
|
|
2990
|
+
};
|
|
2991
|
+
}
|
|
2992
|
+
return action;
|
|
2993
|
+
}
|
|
2994
|
+
function registerIterateContextMenus(service, deps, instructionByEntry) {
|
|
2995
|
+
const unregister = [];
|
|
2996
|
+
const register = (target, order, build) => {
|
|
2997
|
+
try {
|
|
2998
|
+
const dispose = service.register({
|
|
2999
|
+
id: `iterate-plugin.${target}`,
|
|
3000
|
+
target,
|
|
3001
|
+
order,
|
|
3002
|
+
resolve: (t2, _signal) => {
|
|
3003
|
+
const lang = getLang();
|
|
3004
|
+
return build(lang, t2).map((entry) => {
|
|
3005
|
+
const full = { ...entry, instruction: entry.instruction ?? instructionByEntry(entry.id) };
|
|
3006
|
+
return toAction(full, deps, lang);
|
|
3007
|
+
});
|
|
3008
|
+
}
|
|
3009
|
+
});
|
|
3010
|
+
unregister.push(dispose);
|
|
3011
|
+
} catch (err) {
|
|
3012
|
+
if (typeof console !== "undefined" && typeof console.error === "function") {
|
|
3013
|
+
console.error("[iterate-ui] context-menu registration failed for target", target, err);
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
};
|
|
3017
|
+
register("session", 300, (_lang, t2) => {
|
|
3018
|
+
const target = t2;
|
|
3019
|
+
return buildSessionMenuEntries(getLang(), { hasCheckpoint: target?.value?.sessionId ? true : void 0 });
|
|
3020
|
+
});
|
|
3021
|
+
register("file", 300, (_lang, t2) => {
|
|
3022
|
+
const target = t2;
|
|
3023
|
+
return buildFileMenuEntries(getLang()).map((e) => ({
|
|
3024
|
+
...e,
|
|
3025
|
+
instruction: e.id === "iterate.review-file" ? instructionByEntry(`file:${target?.value?.path ?? ""}${typeof target?.value?.line === "number" ? `:${target.value.line}` : ""}`) : e.instruction
|
|
3026
|
+
}));
|
|
3027
|
+
});
|
|
3028
|
+
register("directory", 300, (_lang, t2) => {
|
|
3029
|
+
const target = t2;
|
|
3030
|
+
return buildDirectoryMenuEntries(getLang()).map((e) => ({
|
|
3031
|
+
...e,
|
|
3032
|
+
instruction: e.id === "iterate.review-dir" ? instructionByEntry(`dir:${target?.value?.path ?? ""}`) : e.instruction
|
|
3033
|
+
}));
|
|
3034
|
+
});
|
|
3035
|
+
return () => {
|
|
3036
|
+
for (const dispose of unregister.splice(0)) {
|
|
3037
|
+
try {
|
|
3038
|
+
dispose();
|
|
3039
|
+
} catch {
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
};
|
|
3043
|
+
}
|
|
3044
|
+
|
|
2861
3045
|
// src/client/index.ts
|
|
2862
3046
|
var React = require("react");
|
|
2863
3047
|
var name = "iterate-plugin";
|
|
@@ -3294,12 +3478,51 @@ function readTheme(ctx) {
|
|
|
3294
3478
|
}
|
|
3295
3479
|
return void 0;
|
|
3296
3480
|
}
|
|
3481
|
+
function readContextMenus(ctx) {
|
|
3482
|
+
if (!ctx || typeof ctx.get !== "function") return void 0;
|
|
3483
|
+
const raw = ctx.get("contextMenus", false);
|
|
3484
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
3485
|
+
const svc = raw;
|
|
3486
|
+
if (typeof svc.register !== "function") return void 0;
|
|
3487
|
+
if (typeof svc.notify !== "function") return void 0;
|
|
3488
|
+
return svc;
|
|
3489
|
+
}
|
|
3490
|
+
function readShortcuts(ctx) {
|
|
3491
|
+
if (!ctx || typeof ctx.get !== "function") return void 0;
|
|
3492
|
+
const raw = ctx.get("shortcuts", false);
|
|
3493
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
3494
|
+
const svc = raw;
|
|
3495
|
+
if (typeof svc.register !== "function") return void 0;
|
|
3496
|
+
return svc;
|
|
3497
|
+
}
|
|
3498
|
+
function sessionIdOfSession(session) {
|
|
3499
|
+
if (!session || typeof session !== "object") return null;
|
|
3500
|
+
const s = session;
|
|
3501
|
+
const candidates = [
|
|
3502
|
+
s.sessionId,
|
|
3503
|
+
s.id,
|
|
3504
|
+
s.header,
|
|
3505
|
+
s.session
|
|
3506
|
+
];
|
|
3507
|
+
for (const c of candidates) {
|
|
3508
|
+
if (typeof c === "string" && c) return c;
|
|
3509
|
+
if (c && typeof c === "object") {
|
|
3510
|
+
const o = c;
|
|
3511
|
+
for (const k of ["sessionId", "id"]) {
|
|
3512
|
+
if (typeof o[k] === "string" && o[k]) return o[k];
|
|
3513
|
+
}
|
|
3514
|
+
}
|
|
3515
|
+
}
|
|
3516
|
+
return null;
|
|
3517
|
+
}
|
|
3297
3518
|
var slotsSvc = void 0;
|
|
3298
3519
|
var themeSvc = void 0;
|
|
3299
3520
|
var storage2 = null;
|
|
3300
3521
|
var themeDisposer = null;
|
|
3301
3522
|
var themeListener = null;
|
|
3302
3523
|
var themeEnabled = true;
|
|
3524
|
+
var menuDisposer = null;
|
|
3525
|
+
var shortcutDisposer = null;
|
|
3303
3526
|
var roundPulseListeners = [];
|
|
3304
3527
|
function emitRoundPulse(round, converged) {
|
|
3305
3528
|
const payload = { round, converged: converged === true };
|
|
@@ -4607,6 +4830,21 @@ function ObservatoryPanel(props) {
|
|
|
4607
4830
|
doc.addEventListener("keydown", onKeyDown);
|
|
4608
4831
|
return () => doc.removeEventListener("keydown", onKeyDown);
|
|
4609
4832
|
}, [open]);
|
|
4833
|
+
const panelSessionId = sessionIdOfSession(session);
|
|
4834
|
+
React.useEffect(() => {
|
|
4835
|
+
if (typeof window === "undefined" || typeof window.addEventListener !== "function") return;
|
|
4836
|
+
const onOpen = (ev) => {
|
|
4837
|
+
const detail = ev.detail;
|
|
4838
|
+
const detailSession = detail && typeof detail.sessionId === "string" && detail.sessionId ? detail.sessionId : null;
|
|
4839
|
+
if (detailSession && panelSessionId && detailSession !== panelSessionId) return;
|
|
4840
|
+
setOpen(true);
|
|
4841
|
+
const target = detail && typeof detail.tab === "string" ? detail.tab : "live";
|
|
4842
|
+
const valid = OBS_TABS.some((x) => x.key === target);
|
|
4843
|
+
setTab(valid ? target : "live");
|
|
4844
|
+
};
|
|
4845
|
+
window.addEventListener(OBS_OPEN_EVENT, onOpen);
|
|
4846
|
+
return () => window.removeEventListener(OBS_OPEN_EVENT, onOpen);
|
|
4847
|
+
}, [panelSessionId]);
|
|
4610
4848
|
const copyInstruction = (key, text) => {
|
|
4611
4849
|
if (!text) return;
|
|
4612
4850
|
copyText(text).then((ok) => {
|
|
@@ -6217,6 +6455,80 @@ function apply(ctx) {
|
|
|
6217
6455
|
)
|
|
6218
6456
|
);
|
|
6219
6457
|
}
|
|
6458
|
+
{
|
|
6459
|
+
const menuSvc = readContextMenus(ctx);
|
|
6460
|
+
if (menuSvc) {
|
|
6461
|
+
const instructionByEntry = (id) => {
|
|
6462
|
+
if (id === "iterate.preflight") return buildPreflightInstruction();
|
|
6463
|
+
if (id === "iterate.dry-run") return START_INSTRUCTIONS.reviewOnly;
|
|
6464
|
+
if (id === "iterate.resume") return buildCheckpointResumeInstruction();
|
|
6465
|
+
if (id === "iterate.pull") return buildDiskSnapshotInstruction();
|
|
6466
|
+
if (id.startsWith("file:")) {
|
|
6467
|
+
const rest = id.slice("file:".length);
|
|
6468
|
+
const colon = rest.lastIndexOf(":");
|
|
6469
|
+
const path = colon > 0 ? rest.slice(0, colon) : rest;
|
|
6470
|
+
const line = colon > 0 ? Number(rest.slice(colon + 1)) : void 0;
|
|
6471
|
+
return buildScopedReviewInstruction({ file: path, ...Number.isInteger(line) && line > 0 ? { line } : {} });
|
|
6472
|
+
}
|
|
6473
|
+
if (id.startsWith("dir:")) return buildScopedReviewInstruction({ dir: id.slice("dir:".length) });
|
|
6474
|
+
return "";
|
|
6475
|
+
};
|
|
6476
|
+
if (menuDisposer) {
|
|
6477
|
+
try {
|
|
6478
|
+
menuDisposer();
|
|
6479
|
+
} catch {
|
|
6480
|
+
}
|
|
6481
|
+
menuDisposer = null;
|
|
6482
|
+
}
|
|
6483
|
+
menuDisposer = registerIterateContextMenus(
|
|
6484
|
+
menuSvc,
|
|
6485
|
+
{
|
|
6486
|
+
copy: (text) => copyText(text),
|
|
6487
|
+
openPanel: (detail) => {
|
|
6488
|
+
openObservatory(detail);
|
|
6489
|
+
},
|
|
6490
|
+
notify: (text, success, action) => {
|
|
6491
|
+
menuSvc.notify(text, success, action);
|
|
6492
|
+
}
|
|
6493
|
+
},
|
|
6494
|
+
instructionByEntry
|
|
6495
|
+
);
|
|
6496
|
+
if (typeof ctx.effect === "function") {
|
|
6497
|
+
ctx.effect(() => {
|
|
6498
|
+
if (menuDisposer) {
|
|
6499
|
+
menuDisposer();
|
|
6500
|
+
menuDisposer = null;
|
|
6501
|
+
}
|
|
6502
|
+
});
|
|
6503
|
+
}
|
|
6504
|
+
}
|
|
6505
|
+
}
|
|
6506
|
+
{
|
|
6507
|
+
const shortcutSvc = readShortcuts(ctx);
|
|
6508
|
+
if (shortcutSvc) {
|
|
6509
|
+
if (shortcutDisposer) {
|
|
6510
|
+
try {
|
|
6511
|
+
shortcutDisposer();
|
|
6512
|
+
} catch {
|
|
6513
|
+
}
|
|
6514
|
+
shortcutDisposer = null;
|
|
6515
|
+
}
|
|
6516
|
+
shortcutDisposer = registerIterateShortcuts(shortcutSvc, {
|
|
6517
|
+
openObservatory: (detail) => {
|
|
6518
|
+
openObservatory(detail);
|
|
6519
|
+
},
|
|
6520
|
+
lang: () => getLang()
|
|
6521
|
+
});
|
|
6522
|
+
if (typeof ctx.effect === "function") {
|
|
6523
|
+
ctx.effect(() => {
|
|
6524
|
+
if (shortcutDisposer) {
|
|
6525
|
+
shortcutDisposer();
|
|
6526
|
+
shortcutDisposer = null;
|
|
6527
|
+
}
|
|
6528
|
+
});
|
|
6529
|
+
}
|
|
6530
|
+
}
|
|
6531
|
+
}
|
|
6220
6532
|
registerThemeListener(ctx);
|
|
6221
6533
|
}
|
|
6222
6534
|
return module.exports; } });
|
package/lib/parse.js
CHANGED
|
@@ -1788,6 +1788,34 @@ export const START_INSTRUCTIONS = {
|
|
|
1788
1788
|
reviewOnly: START_INSTRUCTION_REVIEW_ONLY,
|
|
1789
1789
|
}
|
|
1790
1790
|
|
|
1791
|
+
/**
|
|
1792
|
+
* Scoped dry-run review instruction (right-click menu: file / directory).
|
|
1793
|
+
*
|
|
1794
|
+
* `iterate_review plan` has no per-call `scope` parameter, so a single-file or
|
|
1795
|
+
* single-directory review is expressed as a directed instruction the model
|
|
1796
|
+
* resolves through the `workflow` tool exactly like the full dry-run flow, but
|
|
1797
|
+
* with the review scope explicitly narrowed to the clicked subject. The user
|
|
1798
|
+
* never has to type the path themselves.
|
|
1799
|
+
*
|
|
1800
|
+
* @param {{ file?: string, line?: number, dir?: string }} scope
|
|
1801
|
+
* @returns {string} empty when no scope is given (defensive; never throw)
|
|
1802
|
+
*/
|
|
1803
|
+
export function buildScopedReviewInstruction(scope) {
|
|
1804
|
+
const hasFile = scope && typeof scope.file === 'string' && scope.file.length > 0
|
|
1805
|
+
const hasDir = !hasFile && scope && typeof scope.dir === 'string' && scope.dir.length > 0
|
|
1806
|
+
if (!hasFile && !hasDir) return ''
|
|
1807
|
+
const subject = hasFile
|
|
1808
|
+
? `文件 \`${scope.file}\`${typeof scope.line === 'number' && scope.line > 0 ? `(第 ${scope.line} 行)` : ''}`
|
|
1809
|
+
: `目录 \`${scope.dir}\``
|
|
1810
|
+
return (
|
|
1811
|
+
`开始一次仅评审(dry-run,不修改任何文件),审查范围限定在${subject}:` +
|
|
1812
|
+
'请调用 `workflow` 工具运行 iterate 工作流,mode: "dry-run",' +
|
|
1813
|
+
`按系统提示的 Iterate Workflow 执行 plan → 评审 → 分诊 → \`iterate_review aggregate\` / \`meta-review\` 终报并 \`iterate_transcript\` capture;` +
|
|
1814
|
+
`评审时把范围限定在${subject},其余文件仅作上下文、不得给出修复。` +
|
|
1815
|
+
'全程不要调用 `iterate_fix` / `iterate_rollback`。'
|
|
1816
|
+
)
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1791
1819
|
/** Normalize an arbitrary finding-ish node into the payload field set. */
|
|
1792
1820
|
function fixFindingPayload(finding) {
|
|
1793
1821
|
const f = finding && typeof finding === 'object' ? finding : {}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iterate-plugin",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.15.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",
|
|
@@ -55,7 +55,9 @@
|
|
|
55
55
|
"0.2.1-rc.3": "compatible",
|
|
56
56
|
"0.2.1-rc.4": "compatible",
|
|
57
57
|
"0.2.1-rc.5": "compatible",
|
|
58
|
-
"0.2.2-alpha.2": "compatible"
|
|
58
|
+
"0.2.2-alpha.2": "compatible",
|
|
59
|
+
"0.2.2-alpha.3": "compatible",
|
|
60
|
+
"0.2.2-rc.1": "compatible"
|
|
59
61
|
}
|
|
60
62
|
},
|
|
61
63
|
"client": {
|
|
@@ -92,14 +94,14 @@
|
|
|
92
94
|
"test:validate": "tsx --test test/validate.test.ts"
|
|
93
95
|
},
|
|
94
96
|
"dependencies": {
|
|
95
|
-
"iterate-kernel": "0.1.
|
|
97
|
+
"iterate-kernel": "0.1.10",
|
|
96
98
|
"js-yaml": "4.3.2"
|
|
97
99
|
},
|
|
98
100
|
"peerDependencies": {
|
|
99
|
-
"@deepseek-ai/cordis": "^4.0.5-alpha.2 || ~4.0.6-alpha.1",
|
|
100
|
-
"@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",
|
|
101
|
-
"@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",
|
|
102
|
-
"@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"
|
|
101
|
+
"@deepseek-ai/cordis": "^4.0.5-alpha.2 || ~4.0.6-alpha.1 || ~4.0.6-alpha.2",
|
|
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"
|
|
103
105
|
},
|
|
104
106
|
"peerDependenciesMeta": {
|
|
105
107
|
"@deepseek-ai/cordis": {
|
|
@@ -116,20 +118,20 @@
|
|
|
116
118
|
}
|
|
117
119
|
},
|
|
118
120
|
"devDependencies": {
|
|
119
|
-
"@deepseek-ai/cordis": "4.0
|
|
120
|
-
"@deepseek-ai/dsh-agent": "0.2.2-
|
|
121
|
-
"@deepseek-ai/dsh-jobs": "0.2.2-
|
|
122
|
-
"@deepseek-ai/dsh-llm": "0.2.2-
|
|
123
|
-
"@deepseek-ai/dsh-ptc-runtime": "0.2.2-
|
|
124
|
-
"@deepseek-ai/dsh-sandbox": "0.2.2-
|
|
125
|
-
"@deepseek-ai/dsh-sandbox-policy": "0.2.2-
|
|
126
|
-
"@deepseek-ai/dsh-scope": "0.2.2-
|
|
127
|
-
"@deepseek-ai/dsh-session": "0.2.2-
|
|
128
|
-
"@deepseek-ai/dsh-system-prompt": "0.2.2-
|
|
129
|
-
"@deepseek-ai/dsh-tools": "0.2.2-
|
|
130
|
-
"@deepseek-ai/dsh-user-approval": "0.2.2-
|
|
131
|
-
"@deepseek-ai/dsh-util-values": "0.2.2-
|
|
132
|
-
"@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",
|
|
133
135
|
"@types/js-yaml": "4.0.9",
|
|
134
136
|
"@types/node": "22.15.0",
|
|
135
137
|
"@types/react": "19.2.2",
|
|
@@ -143,5 +145,8 @@
|
|
|
143
145
|
"engines": {
|
|
144
146
|
"node": ">=20",
|
|
145
147
|
"dsh": ">=0.1.1-rc.1"
|
|
148
|
+
},
|
|
149
|
+
"overrides": {
|
|
150
|
+
"zod": "4.6.7"
|
|
146
151
|
}
|
|
147
152
|
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/client/context-menu.ts — iterate-plugin right-click menu contributions.
|
|
3
|
+
*
|
|
4
|
+
* dsh 0.2.2-alpha.3 added the *unified context-menu* capability
|
|
5
|
+
* (`@deepseek-ai/dsh-client-ui-context-menu`): any client plugin may register
|
|
6
|
+
* actions that appear on right-click / Shift+F10 / ContextMenu key over a
|
|
7
|
+
* Session, file, directory, URL, image, tab, or workspace, without importing
|
|
8
|
+
* a React component. The captured target stays fixed even when selection
|
|
9
|
+
* changes, and contributions are withdrawn automatically when the plugin
|
|
10
|
+
* unloads.
|
|
11
|
+
*
|
|
12
|
+
* This module turns that new upstream capability into a *user-perceivable*
|
|
13
|
+
* iterate surface: instead of only being reachable through the docked
|
|
14
|
+
* observatory and the empty-dashboard CTA, the iterate entry points now also
|
|
15
|
+
* hang off the native right-click menus — on a Session (open the quality
|
|
16
|
+
* command center, copy a preflight / dry-run / resume / disk-snapshot
|
|
17
|
+
* instruction), on a file (dry-run review scoped to that file), and on a
|
|
18
|
+
* directory (scoped review + preflight).
|
|
19
|
+
*
|
|
20
|
+
* Design (mirrors the rest of the client half):
|
|
21
|
+
* - **Defensive**: the `contextMenus` service is optional and probed at
|
|
22
|
+
* apply-time; a host that predates 0.2.2-alpha.3 simply has no menu
|
|
23
|
+
* entries (the docked panel remains the entry point). Nothing throws.
|
|
24
|
+
* - **Pure builders**: `buildSessionMenuEntries` / `buildFileMenuEntries` /
|
|
25
|
+
* `buildDirectoryMenuEntries` return *localized descriptors* with no DOM
|
|
26
|
+
* or clipboard side effects, so they are unit-testable on their own
|
|
27
|
+
* (normal + empty + boundary). The imperative wiring (`copy` / `openPanel`
|
|
28
|
+
* / `notify`) is injected by `registerIterateContextMenus`.
|
|
29
|
+
* - **Bilingual**: every label carries zh + en and resolves against the
|
|
30
|
+
* language passed in (the caller reads `getLang()` at menu-build time, so
|
|
31
|
+
* a language switch is picked up on the next open).
|
|
32
|
+
*
|
|
33
|
+
* Event bridge: the observatory is a React panel with its own open/tab state.
|
|
34
|
+
* The browser client cannot call harness tools, so the "open command center"
|
|
35
|
+
* menu action dispatches a window CustomEvent (`OBS_OPEN_EVENT`) that the
|
|
36
|
+
* ObservatoryPanel listens for — the panel expands and switches tab without
|
|
37
|
+
* any cross-plugin import. The event carries an optional `sessionId` so a
|
|
38
|
+
* right-click in the session sidebar targets the right session's panel and is
|
|
39
|
+
* ignored by every other mounted panel.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import { getLang, type Lang } from './i18n.ts'
|
|
43
|
+
import { OBSERVATORY_SHORTCUT_ID } from './shortcuts.ts'
|
|
44
|
+
|
|
45
|
+
// ─── Structural service types (defensive; mirrors SlotsService) ──────────────
|
|
46
|
+
//
|
|
47
|
+
// The dsh client runtime is not type-imported (no compile-time coupling to a
|
|
48
|
+
// package a pre-0.2.2-alpha.3 host may not have). The surface below is the
|
|
49
|
+
// subset this plugin consumes; `register` is only ever called when a service
|
|
50
|
+
// with the right shape is present.
|
|
51
|
+
|
|
52
|
+
/** A right-click target this plugin contributes actions to. */
|
|
53
|
+
export type IterateMenuTarget =
|
|
54
|
+
| { kind: 'session'; value: { sessionId: string; displayTitle?: string } }
|
|
55
|
+
| { kind: 'file'; value: { sessionId: string; path: string; line?: number } }
|
|
56
|
+
| { kind: 'directory'; value: { sessionId: string; path: string } }
|
|
57
|
+
|
|
58
|
+
/** One menu action (presentation + optional execution). */
|
|
59
|
+
export interface IterateMenuAction {
|
|
60
|
+
id: string
|
|
61
|
+
label: string
|
|
62
|
+
disabled?: boolean
|
|
63
|
+
danger?: boolean
|
|
64
|
+
separatorBefore?: boolean
|
|
65
|
+
/** Localized operation + recovery guidance when `run` rejects. */
|
|
66
|
+
failureLabel?: string
|
|
67
|
+
/** Live-shortcut display for an action backed by a registered command. */
|
|
68
|
+
commandId?: string
|
|
69
|
+
run?: () => void | Promise<void>
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** A namespaced contribution, ordered within its target family. */
|
|
73
|
+
export interface IterateMenuContribution<K extends string> {
|
|
74
|
+
id: string
|
|
75
|
+
target: K
|
|
76
|
+
order: number
|
|
77
|
+
resolve(target: unknown, signal: AbortSignal): readonly IterateMenuAction[]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Structural view of the dsh `contextMenus` service this plugin consumes. */
|
|
81
|
+
export interface ContextMenusService {
|
|
82
|
+
register<K extends string>(contribution: IterateMenuContribution<K>): () => void
|
|
83
|
+
notify(
|
|
84
|
+
text: string,
|
|
85
|
+
success?: boolean,
|
|
86
|
+
action?: { label: string; run: () => void | Promise<void>; failureLabel: string },
|
|
87
|
+
): void
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ─── Observatory open event bridge ───────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
/** Window event that asks the (matching) ObservatoryPanel to expand. */
|
|
93
|
+
export const OBS_OPEN_EVENT = 'iterate-plugin:open-observatory'
|
|
94
|
+
|
|
95
|
+
/** Payload carried by {@link OBS_OPEN_EVENT}. */
|
|
96
|
+
export interface ObsOpenDetail {
|
|
97
|
+
/** Optional target session; omitted = open whatever panel is mounted. */
|
|
98
|
+
sessionId?: string
|
|
99
|
+
/** Tab key to switch to (`live` by default). */
|
|
100
|
+
tab?: string
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Dispatch the "open observatory" event. No-op outside a browser (SSR / test
|
|
105
|
+
* environments without `window`). Returns whether the event was dispatched.
|
|
106
|
+
*/
|
|
107
|
+
export function openObservatory(detail?: ObsOpenDetail): boolean {
|
|
108
|
+
if (typeof window === 'undefined' || typeof window.dispatchEvent !== 'function') return false
|
|
109
|
+
try {
|
|
110
|
+
window.dispatchEvent(new CustomEvent<ObsOpenDetail>(OBS_OPEN_EVENT, { detail: detail ?? {} }))
|
|
111
|
+
return true
|
|
112
|
+
} catch {
|
|
113
|
+
return false
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ─── Localized descriptors (pure, testable) ──────────────────────────────────
|
|
118
|
+
|
|
119
|
+
/** The action ids this plugin registers, for tests + duplicate guards. */
|
|
120
|
+
export const SESSION_MENU_IDS = ['iterate.open-panel', 'iterate.preflight', 'iterate.dry-run', 'iterate.resume', 'iterate.pull'] as const
|
|
121
|
+
export const FILE_MENU_IDS = ['iterate.review-file', 'iterate.open-panel'] as const
|
|
122
|
+
export const DIRECTORY_MENU_IDS = ['iterate.review-dir', 'iterate.preflight', 'iterate.open-panel'] as const
|
|
123
|
+
|
|
124
|
+
interface MenuEntry {
|
|
125
|
+
id: string
|
|
126
|
+
label: string
|
|
127
|
+
instruction?: string
|
|
128
|
+
/** Tab key to open when this entry is the "open panel" action. */
|
|
129
|
+
openTab?: string
|
|
130
|
+
danger?: boolean
|
|
131
|
+
disabled?: boolean
|
|
132
|
+
separatorBefore?: boolean
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Bilingual labels; resolved by `pick` at build time. */
|
|
136
|
+
const LABELS: Record<string, { zh: string; en: string }> = {
|
|
137
|
+
'iterate.open-panel': { zh: '打开 iterate 质量指挥中心', en: 'Open iterate command center' },
|
|
138
|
+
'iterate.preflight': { zh: '复制开跑预检指令', en: 'Copy preflight instruction' },
|
|
139
|
+
'iterate.dry-run': { zh: '复制 dry-run 审查指令', en: 'Copy dry-run review instruction' },
|
|
140
|
+
'iterate.resume': { zh: '复制断点恢复指令', en: 'Copy checkpoint-resume instruction' },
|
|
141
|
+
'iterate.pull': { zh: '复制拉取磁盘快照指令', en: 'Copy disk-snapshot pull instruction' },
|
|
142
|
+
'iterate.review-file': { zh: '复制「仅审查此文件」指令', en: 'Copy "review this file only" instruction' },
|
|
143
|
+
'iterate.review-dir': { zh: '复制「仅审查此目录」指令', en: 'Copy "review this directory only" instruction' },
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Pick a localized label; unknown ids degrade to the raw id (never throw). */
|
|
147
|
+
function pick(id: string, lang: Lang): string {
|
|
148
|
+
const entry = LABELS[id]
|
|
149
|
+
if (!entry) return id
|
|
150
|
+
return lang === 'zh' ? entry.zh : entry.en
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Copy-failure guidance (a menu action's `failureLabel`). */
|
|
154
|
+
const COPY_FAILURE: Record<'zh' | 'en', string> = {
|
|
155
|
+
zh: '复制失败:剪贴板被拒绝。请改为在观测台面板里用同一指令的「复制」按钮,或手动选中指令文本复制。',
|
|
156
|
+
en: 'Copy failed: clipboard denied. Use the matching "Copy" button in the observatory panel, or select the instruction text manually.',
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Build the Session target menu entries (pure). */
|
|
160
|
+
export function buildSessionMenuEntries(lang: Lang, opts?: { hasCheckpoint?: boolean }): MenuEntry[] {
|
|
161
|
+
return [
|
|
162
|
+
{ id: 'iterate.open-panel', label: pick('iterate.open-panel', lang), openTab: 'live' },
|
|
163
|
+
{ id: 'iterate.preflight', label: pick('iterate.preflight', lang), separatorBefore: true },
|
|
164
|
+
{ id: 'iterate.dry-run', label: pick('iterate.dry-run', lang) },
|
|
165
|
+
{ id: 'iterate.resume', label: pick('iterate.resume', lang), disabled: opts?.hasCheckpoint === false },
|
|
166
|
+
{ id: 'iterate.pull', label: pick('iterate.pull', lang), separatorBefore: true },
|
|
167
|
+
]
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Build the File target menu entries (pure). */
|
|
171
|
+
export function buildFileMenuEntries(lang: Lang): MenuEntry[] {
|
|
172
|
+
return [
|
|
173
|
+
{ id: 'iterate.review-file', label: pick('iterate.review-file', lang) },
|
|
174
|
+
{ id: 'iterate.open-panel', label: pick('iterate.open-panel', lang), openTab: 'live' },
|
|
175
|
+
]
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Build the Directory target menu entries (pure). */
|
|
179
|
+
export function buildDirectoryMenuEntries(lang: Lang): MenuEntry[] {
|
|
180
|
+
return [
|
|
181
|
+
{ id: 'iterate.review-dir', label: pick('iterate.review-dir', lang) },
|
|
182
|
+
{ id: 'iterate.preflight', label: pick('iterate.preflight', lang), separatorBefore: true },
|
|
183
|
+
{ id: 'iterate.open-panel', label: pick('iterate.open-panel', lang), openTab: 'live' },
|
|
184
|
+
]
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ─── Imperative wiring (register + run callbacks) ────────────────────────────
|
|
188
|
+
|
|
189
|
+
/** Injection points the menu wiring needs (kept dependency-light for tests). */
|
|
190
|
+
export interface IterateMenuDeps {
|
|
191
|
+
/** Copy an instruction; resolves to whether the clipboard write succeeded. */
|
|
192
|
+
copy: (text: string) => Promise<boolean>
|
|
193
|
+
/** Open the observatory panel (optionally for a session, on a tab). */
|
|
194
|
+
openPanel: (detail?: ObsOpenDetail) => void
|
|
195
|
+
/** Show the shared toast. */
|
|
196
|
+
notify: (text: string, success?: boolean, action?: { label: string; run: () => void | Promise<void>; failureLabel: string }) => void
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Map a pure entry to a runnable menu action, wiring the injected deps. */
|
|
200
|
+
function toAction(entry: MenuEntry, deps: IterateMenuDeps, lang: Lang): IterateMenuAction {
|
|
201
|
+
const action: IterateMenuAction = {
|
|
202
|
+
id: entry.id,
|
|
203
|
+
label: entry.label,
|
|
204
|
+
danger: entry.danger,
|
|
205
|
+
separatorBefore: entry.separatorBefore,
|
|
206
|
+
}
|
|
207
|
+
if (entry.disabled) action.disabled = true
|
|
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
|
|
215
|
+
action.run = () => {
|
|
216
|
+
deps.openPanel({ tab: entry.openTab })
|
|
217
|
+
deps.notify(lang === 'zh' ? '已打开 iterate 观测台。' : 'Iterate observatory opened.', true)
|
|
218
|
+
}
|
|
219
|
+
return action
|
|
220
|
+
}
|
|
221
|
+
if (entry.instruction) {
|
|
222
|
+
const text = entry.instruction
|
|
223
|
+
const failureLabel = COPY_FAILURE[lang]
|
|
224
|
+
action.failureLabel = failureLabel
|
|
225
|
+
action.run = async () => {
|
|
226
|
+
const ok = await deps.copy(text)
|
|
227
|
+
if (!ok) throw new Error(failureLabel)
|
|
228
|
+
deps.notify(lang === 'zh' ? '指令已复制,粘贴给模型即可。' : 'Instruction copied — paste it to the model.', true)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return action
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Register this plugin's context-menu contributions on the (already-validated)
|
|
236
|
+
* `contextMenus` service. Returns an unregister function; a host without the
|
|
237
|
+
* service is a no-op that returns `() => {}` (callers guard).
|
|
238
|
+
*/
|
|
239
|
+
export function registerIterateContextMenus(
|
|
240
|
+
service: ContextMenusService,
|
|
241
|
+
deps: IterateMenuDeps,
|
|
242
|
+
instructionByEntry: (id: string) => string,
|
|
243
|
+
): () => void {
|
|
244
|
+
const unregister: Array<() => void> = []
|
|
245
|
+
|
|
246
|
+
const register = <K extends string>(target: K, order: number, build: (lang: Lang, target: unknown) => MenuEntry[]) => {
|
|
247
|
+
try {
|
|
248
|
+
const dispose = service.register({
|
|
249
|
+
id: `iterate-plugin.${target}`,
|
|
250
|
+
target,
|
|
251
|
+
order,
|
|
252
|
+
resolve: (t, _signal) => {
|
|
253
|
+
const lang = getLang()
|
|
254
|
+
return build(lang, t).map((entry) => {
|
|
255
|
+
const full: MenuEntry = { ...entry, instruction: entry.instruction ?? instructionByEntry(entry.id) }
|
|
256
|
+
return toAction(full, deps, lang)
|
|
257
|
+
})
|
|
258
|
+
},
|
|
259
|
+
})
|
|
260
|
+
unregister.push(dispose)
|
|
261
|
+
} catch (err) {
|
|
262
|
+
// A duplicate contribution id or an unsupported target family must not
|
|
263
|
+
// take the plugin down — log and continue with the remaining targets.
|
|
264
|
+
if (typeof console !== 'undefined' && typeof console.error === 'function') {
|
|
265
|
+
console.error('[iterate-ui] context-menu registration failed for target', target, err)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
register('session', 300, (_lang, t) => {
|
|
271
|
+
const target = t as { value?: { sessionId?: string } } | undefined
|
|
272
|
+
return buildSessionMenuEntries(getLang(), { hasCheckpoint: target?.value?.sessionId ? true : undefined })
|
|
273
|
+
})
|
|
274
|
+
register('file', 300, (_lang, t) => {
|
|
275
|
+
const target = t as { value?: { path?: string; line?: number } } | undefined
|
|
276
|
+
return buildFileMenuEntries(getLang()).map((e) => ({
|
|
277
|
+
...e,
|
|
278
|
+
instruction: e.id === 'iterate.review-file'
|
|
279
|
+
? instructionByEntry(`file:${target?.value?.path ?? ''}${typeof target?.value?.line === 'number' ? `:${target.value.line}` : ''}`)
|
|
280
|
+
: e.instruction,
|
|
281
|
+
}))
|
|
282
|
+
})
|
|
283
|
+
register('directory', 300, (_lang, t) => {
|
|
284
|
+
const target = t as { value?: { path?: string } } | undefined
|
|
285
|
+
return buildDirectoryMenuEntries(getLang()).map((e) => ({
|
|
286
|
+
...e,
|
|
287
|
+
instruction: e.id === 'iterate.review-dir'
|
|
288
|
+
? instructionByEntry(`dir:${target?.value?.path ?? ''}`)
|
|
289
|
+
: e.instruction,
|
|
290
|
+
}))
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
return () => {
|
|
294
|
+
for (const dispose of unregister.splice(0)) {
|
|
295
|
+
try { dispose() } catch { /* noop */ }
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
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
|
|
@@ -78,6 +78,7 @@ import {
|
|
|
78
78
|
scanSessionForPreflight,
|
|
79
79
|
buildPreflightInstruction,
|
|
80
80
|
buildValidationApplyInstruction,
|
|
81
|
+
buildScopedReviewInstruction,
|
|
81
82
|
filterLiveEntries,
|
|
82
83
|
filterTimelineEntries,
|
|
83
84
|
serializeObservatoryExport,
|
|
@@ -132,6 +133,19 @@ import {
|
|
|
132
133
|
type Lang,
|
|
133
134
|
} from './i18n.ts'
|
|
134
135
|
import { noteSession, readSession, subscribeSession } from './session-cache.ts'
|
|
136
|
+
// v3.14: right-click menu contributions (dsh 0.2.2-alpha.3 unified context menus).
|
|
137
|
+
import {
|
|
138
|
+
openObservatory,
|
|
139
|
+
OBS_OPEN_EVENT,
|
|
140
|
+
registerIterateContextMenus,
|
|
141
|
+
type ContextMenusService,
|
|
142
|
+
type ObsOpenDetail,
|
|
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'
|
|
135
149
|
|
|
136
150
|
// ─── Module contract ─────────────────────────────────────────────────────────
|
|
137
151
|
|
|
@@ -907,6 +921,62 @@ function readTheme(ctx: ClientContext): ThemeService | undefined {
|
|
|
907
921
|
return undefined
|
|
908
922
|
}
|
|
909
923
|
|
|
924
|
+
/**
|
|
925
|
+
* Resolve the dsh 0.2.2-alpha.3 `contextMenus` service. Unlike slots/theme it
|
|
926
|
+
* is NOT in the typed `ClientContext`, so it is probed through `ctx.get` (the
|
|
927
|
+
* cordis service locator) and structurally validated. A host that predates the
|
|
928
|
+
* capability returns `undefined` and the plugin simply has no menu entries.
|
|
929
|
+
*/
|
|
930
|
+
function readContextMenus(ctx: ClientContext): ContextMenusService | undefined {
|
|
931
|
+
if (!ctx || typeof ctx.get !== 'function') return undefined
|
|
932
|
+
const raw = ctx.get('contextMenus', false)
|
|
933
|
+
if (!raw || typeof raw !== 'object') return undefined
|
|
934
|
+
const svc = raw as { register?: unknown; notify?: unknown }
|
|
935
|
+
if (typeof svc.register !== 'function') return undefined
|
|
936
|
+
if (typeof svc.notify !== 'function') return undefined
|
|
937
|
+
return svc as unknown as ContextMenusService
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* Resolve the dsh 0.2.2-rc.1 `shortcuts` service. Like contextMenus it is NOT
|
|
942
|
+
* in the typed `ClientContext`, so it is probed through `ctx.get` and
|
|
943
|
+
* structurally validated. A host that predates the capability returns
|
|
944
|
+
* `undefined` and the plugin simply has no keyboard command.
|
|
945
|
+
*/
|
|
946
|
+
function readShortcuts(ctx: ClientContext): ShortcutsService | undefined {
|
|
947
|
+
if (!ctx || typeof ctx.get !== 'function') return undefined
|
|
948
|
+
const raw = ctx.get('shortcuts', false)
|
|
949
|
+
if (!raw || typeof raw !== 'object') return undefined
|
|
950
|
+
const svc = raw as { register?: unknown }
|
|
951
|
+
if (typeof svc.register !== 'function') return undefined
|
|
952
|
+
return svc as unknown as ShortcutsService
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/**
|
|
956
|
+
* Extract a session identifier from an opaque host-shaped session object.
|
|
957
|
+
* The slot `props.session` shape is host-defined; this probes the common
|
|
958
|
+
* keys so the observatory open-event can target the right session and be
|
|
959
|
+
* ignored by every other mounted panel. Returns `null` when unknown (the
|
|
960
|
+
* panel then responds to any open event — the single-panel fallback).
|
|
961
|
+
*/
|
|
962
|
+
function sessionIdOfSession(session: unknown): string | null {
|
|
963
|
+
if (!session || typeof session !== 'object') return null
|
|
964
|
+
const s = session as Record<string, unknown>
|
|
965
|
+
const candidates = [
|
|
966
|
+
s.sessionId, s.id, s.header, s.session,
|
|
967
|
+
] as unknown[]
|
|
968
|
+
for (const c of candidates) {
|
|
969
|
+
if (typeof c === 'string' && c) return c
|
|
970
|
+
if (c && typeof c === 'object') {
|
|
971
|
+
const o = c as Record<string, unknown>
|
|
972
|
+
for (const k of ['sessionId', 'id']) {
|
|
973
|
+
if (typeof o[k] === 'string' && o[k]) return o[k] as string
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
return null
|
|
978
|
+
}
|
|
979
|
+
|
|
910
980
|
// ─── Module-level runtime state (shared across slots) ────────────────────────
|
|
911
981
|
|
|
912
982
|
let slotsSvc: SlotsService | undefined = undefined
|
|
@@ -915,6 +985,10 @@ let storage: ReturnType<typeof createStorage> | null = null
|
|
|
915
985
|
let themeDisposer: (() => void) | null = null
|
|
916
986
|
let themeListener: (() => void) | null = null
|
|
917
987
|
let themeEnabled = true
|
|
988
|
+
/** v3.14: right-click menu contribution disposer (re-apply() never stacks). */
|
|
989
|
+
let menuDisposer: (() => void) | null = null
|
|
990
|
+
/** v3.15: keyboard-command contribution disposer (re-apply() never stacks). */
|
|
991
|
+
let shortcutDisposer: (() => void) | null = null
|
|
918
992
|
const roundPulseListeners: Array<(payload: { round: number; converged: boolean }) => void> = []
|
|
919
993
|
|
|
920
994
|
/**
|
|
@@ -2180,7 +2254,7 @@ function SettingsPanel(props: SlotProps) {
|
|
|
2180
2254
|
)
|
|
2181
2255
|
}
|
|
2182
2256
|
|
|
2183
|
-
// ─── Runtime observatory panel (F1–
|
|
2257
|
+
// ─── Runtime observatory panel (F1–F11) ─────────────────────────────────────
|
|
2184
2258
|
|
|
2185
2259
|
/** Tab id → label for the observatory's eleven panels. Live (real-time
|
|
2186
2260
|
* activity stream) is first so it is the default view; F-panels follow.
|
|
@@ -2307,7 +2381,7 @@ function buildObsFixInstruction(f: ObsFinding): string {
|
|
|
2307
2381
|
}
|
|
2308
2382
|
|
|
2309
2383
|
/**
|
|
2310
|
-
* ObservatoryPanel:
|
|
2384
|
+
* ObservatoryPanel: a 12-tab runtime observatory that renders the latest
|
|
2311
2385
|
* iterate_transcript manifest found in the dsh session stream. The panel is
|
|
2312
2386
|
* collapsed by default to a single entry row so it never re-occludes the main
|
|
2313
2387
|
* convergence dashboard; clicking expands it into tabs. Every field read is
|
|
@@ -2392,6 +2466,28 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2392
2466
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2393
2467
|
}, [open])
|
|
2394
2468
|
|
|
2469
|
+
// v3.14: the right-click "open command center" menu action dispatches a
|
|
2470
|
+
// window event; this panel expands and switches tab when the event targets
|
|
2471
|
+
// it (no sessionId = single-panel fallback, a different sessionId = ignore).
|
|
2472
|
+
const panelSessionId = sessionIdOfSession(session)
|
|
2473
|
+
React.useEffect(() => {
|
|
2474
|
+
if (typeof window === 'undefined' || typeof window.addEventListener !== 'function') return
|
|
2475
|
+
const onOpen = (ev: Event) => {
|
|
2476
|
+
const detail = (ev as CustomEvent<ObsOpenDetail>).detail
|
|
2477
|
+
const detailSession = detail && typeof detail.sessionId === 'string' && detail.sessionId
|
|
2478
|
+
? detail.sessionId
|
|
2479
|
+
: null
|
|
2480
|
+
if (detailSession && panelSessionId && detailSession !== panelSessionId) return
|
|
2481
|
+
setOpen(true)
|
|
2482
|
+
const target = detail && typeof detail.tab === 'string' ? detail.tab : 'live'
|
|
2483
|
+
const valid = OBS_TABS.some((x) => x.key === target)
|
|
2484
|
+
setTab(valid ? target : 'live')
|
|
2485
|
+
}
|
|
2486
|
+
window.addEventListener(OBS_OPEN_EVENT, onOpen)
|
|
2487
|
+
return () => window.removeEventListener(OBS_OPEN_EVENT, onOpen)
|
|
2488
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2489
|
+
}, [panelSessionId])
|
|
2490
|
+
|
|
2395
2491
|
/** Copy an instruction, flashing "已复制" on the originating button only when the copy actually succeeded. */
|
|
2396
2492
|
const copyInstruction = (key: string, text: string) => {
|
|
2397
2493
|
if (!text) return
|
|
@@ -3833,7 +3929,7 @@ export function apply(ctx: ClientContext): void {
|
|
|
3833
3929
|
),
|
|
3834
3930
|
)
|
|
3835
3931
|
|
|
3836
|
-
// F1–
|
|
3932
|
+
// F1–F11: runtime observatory board (order 91, below the dashboard's 90 so
|
|
3837
3933
|
// its collapsed header never occludes the main convergence dashboard).
|
|
3838
3934
|
slotsSvc.inject('conversation.input.dock', () =>
|
|
3839
3935
|
slotsSvc?.register(
|
|
@@ -3867,6 +3963,69 @@ export function apply(ctx: ClientContext): void {
|
|
|
3867
3963
|
)
|
|
3868
3964
|
}
|
|
3869
3965
|
|
|
3966
|
+
// 5b. v3.14: right-click menu contributions (dsh 0.2.2-alpha.3 unified
|
|
3967
|
+
// context menus). Optional service: a host that predates the capability has
|
|
3968
|
+
// no menu entries, and the docked observatory stays the entry point.
|
|
3969
|
+
{
|
|
3970
|
+
const menuSvc = readContextMenus(ctx)
|
|
3971
|
+
if (menuSvc) {
|
|
3972
|
+
// Resolve a paste-able instruction for a menu action id. Session-level
|
|
3973
|
+
// ids map to the shared builders (the SAME single source of truth the
|
|
3974
|
+
// observatory copy buttons use); file:/dir: ids are the scoped review.
|
|
3975
|
+
const instructionByEntry = (id: string): string => {
|
|
3976
|
+
if (id === 'iterate.preflight') return buildPreflightInstruction()
|
|
3977
|
+
if (id === 'iterate.dry-run') return START_INSTRUCTIONS.reviewOnly
|
|
3978
|
+
if (id === 'iterate.resume') return buildCheckpointResumeInstruction()
|
|
3979
|
+
if (id === 'iterate.pull') return buildDiskSnapshotInstruction()
|
|
3980
|
+
if (id.startsWith('file:')) {
|
|
3981
|
+
const rest = id.slice('file:'.length)
|
|
3982
|
+
const colon = rest.lastIndexOf(':')
|
|
3983
|
+
const path = colon > 0 ? rest.slice(0, colon) : rest
|
|
3984
|
+
const line = colon > 0 ? Number(rest.slice(colon + 1)) : undefined
|
|
3985
|
+
return buildScopedReviewInstruction({ file: path, ...(Number.isInteger(line) && line! > 0 ? { line: line! } : {}) })
|
|
3986
|
+
}
|
|
3987
|
+
if (id.startsWith('dir:')) return buildScopedReviewInstruction({ dir: id.slice('dir:'.length) })
|
|
3988
|
+
return ''
|
|
3989
|
+
}
|
|
3990
|
+
if (menuDisposer) {
|
|
3991
|
+
try { menuDisposer() } catch { /* noop */ }
|
|
3992
|
+
menuDisposer = null
|
|
3993
|
+
}
|
|
3994
|
+
menuDisposer = registerIterateContextMenus(
|
|
3995
|
+
menuSvc,
|
|
3996
|
+
{
|
|
3997
|
+
copy: (text) => copyText(text),
|
|
3998
|
+
openPanel: (detail) => { openObservatory(detail) },
|
|
3999
|
+
notify: (text, success, action) => { menuSvc.notify(text, success, action) },
|
|
4000
|
+
},
|
|
4001
|
+
instructionByEntry,
|
|
4002
|
+
)
|
|
4003
|
+
if (typeof ctx.effect === 'function') {
|
|
4004
|
+
ctx.effect(() => { if (menuDisposer) { menuDisposer(); menuDisposer = null } })
|
|
4005
|
+
}
|
|
4006
|
+
}
|
|
4007
|
+
}
|
|
4008
|
+
|
|
4009
|
+
// 5c. v3.15: keyboard-command contribution (dsh 0.2.2-rc.1 unified
|
|
4010
|
+
// shortcuts). Optional service: a host that predates the capability has no
|
|
4011
|
+
// keyboard command, and the docked observatory stays the entry point.
|
|
4012
|
+
{
|
|
4013
|
+
const shortcutSvc = readShortcuts(ctx)
|
|
4014
|
+
if (shortcutSvc) {
|
|
4015
|
+
if (shortcutDisposer) {
|
|
4016
|
+
try { shortcutDisposer() } catch { /* noop */ }
|
|
4017
|
+
shortcutDisposer = null
|
|
4018
|
+
}
|
|
4019
|
+
shortcutDisposer = registerIterateShortcuts(shortcutSvc, {
|
|
4020
|
+
openObservatory: (detail) => { openObservatory(detail) },
|
|
4021
|
+
lang: () => getLang(),
|
|
4022
|
+
})
|
|
4023
|
+
if (typeof ctx.effect === 'function') {
|
|
4024
|
+
ctx.effect(() => { if (shortcutDisposer) { shortcutDisposer(); shortcutDisposer = null } })
|
|
4025
|
+
}
|
|
4026
|
+
}
|
|
4027
|
+
}
|
|
4028
|
+
|
|
3870
4029
|
// 6: keep theme toggle state consistent across theme/change events.
|
|
3871
4030
|
// Registered through registerThemeListener so repeated apply() and client
|
|
3872
4031
|
// unmounts never leak a second listener.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/client/shortcuts.ts — iterate-plugin keyboard-command contributions.
|
|
3
|
+
*
|
|
4
|
+
* dsh 0.2.2-rc.1 added the *unified keyboard-command* capability
|
|
5
|
+
* (`@deepseek-ai/dsh-client-shortcuts`): any client plugin may register a
|
|
6
|
+
* command with a default key binding and a target resolver, and dsh's own
|
|
7
|
+
* keyboard adapter dispatches the gesture to it. The same capability also lets
|
|
8
|
+
* a context-menu action carry a `commandId` so the menu shows the live
|
|
9
|
+
* keycap next to the action (selection still invokes the action's `run`).
|
|
10
|
+
*
|
|
11
|
+
* This module turns that new upstream capability into a *user-perceivable*
|
|
12
|
+
* iterate surface: instead of hunting for the docked observatory (or
|
|
13
|
+
* right-clicking a session), the user can now press **Cmd/Ctrl+Shift+O** to
|
|
14
|
+
* open the iterate quality command center from anywhere in the session. The
|
|
15
|
+
* matching context-menu action carries the same `commandId`, so the shortcut
|
|
16
|
+
* is discoverable in the right-click menu ("open command center" shows the
|
|
17
|
+
* keycap), and dsh's own shortcut settings page lists it as an editable
|
|
18
|
+
* binding.
|
|
19
|
+
*
|
|
20
|
+
* Design (mirrors `context-menu.ts` and the rest of the client half):
|
|
21
|
+
* - **Defensive**: the `shortcuts` service is optional and probed at
|
|
22
|
+
* apply-time (see `readShortcuts` in `index.ts`); a host that predates
|
|
23
|
+
* 0.2.2-rc.1 simply has no keyboard command (the docked panel stays the
|
|
24
|
+
* entry point). Nothing throws.
|
|
25
|
+
* - **Pure builder**: `buildObservatoryShortcutCommand` returns the command
|
|
26
|
+
* shape with no `ctx`/DOM side effects, so it is unit-testable on its own
|
|
27
|
+
* (label resolution, binding defaults, resolve→run). The imperative
|
|
28
|
+
* wiring (`openObservatory` / `lang`) is injected by
|
|
29
|
+
* `registerIterateShortcuts`.
|
|
30
|
+
* - **Bilingual**: the label resolves against the language passed in at
|
|
31
|
+
* registration (a language switch is picked up by dsh's own command
|
|
32
|
+
* catalog on the next render).
|
|
33
|
+
* - **No external import**: like the rest of the client half, the dsh
|
|
34
|
+
* `@deepseek-ai/dsh-client-shortcuts` types are NOT imported (no
|
|
35
|
+
* compile-time coupling to a package a pre-0.2.2-rc.1 host may not have);
|
|
36
|
+
* the surface below is the structural subset this plugin consumes.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import type { Lang } from './i18n.ts'
|
|
40
|
+
|
|
41
|
+
// ─── Structural service types (defensive; mirrors ContextMenusService) ───────
|
|
42
|
+
|
|
43
|
+
/** Stable command identity owned by this plugin (a branded string upstream). */
|
|
44
|
+
export const OBSERVATORY_SHORTCUT_ID = 'iterate.open-observatory'
|
|
45
|
+
|
|
46
|
+
/** One physical key + modifiers; `primary` expands to Meta on macOS. */
|
|
47
|
+
export interface ShortcutBinding {
|
|
48
|
+
code: string
|
|
49
|
+
secondCode?: string
|
|
50
|
+
modifiers: readonly string[]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The dsh command catalog shape this plugin registers. */
|
|
54
|
+
export interface ShortcutCommand {
|
|
55
|
+
id: string
|
|
56
|
+
label: () => string
|
|
57
|
+
aliases: readonly string[]
|
|
58
|
+
defaults: Readonly<Record<string, ShortcutBinding>>
|
|
59
|
+
regions: readonly string[]
|
|
60
|
+
modals: readonly string[]
|
|
61
|
+
resolve(context: { region?: string; modal?: string | null; target?: Element | null }): ShortcutResolution
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A resolved command either runs, is blocked with a reason, or passes. */
|
|
65
|
+
export type ShortcutResolution =
|
|
66
|
+
| { status: 'handled'; run: () => void }
|
|
67
|
+
| { status: 'blocked'; reason: string }
|
|
68
|
+
| { status: 'pass' }
|
|
69
|
+
|
|
70
|
+
/** Structural view of the dsh `shortcuts` service this plugin consumes. */
|
|
71
|
+
export interface ShortcutsService {
|
|
72
|
+
register(command: ShortcutCommand): () => void
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Injection points the shortcut wiring needs (kept dependency-light for tests). */
|
|
76
|
+
export interface ShortcutDeps {
|
|
77
|
+
/** Open the observatory panel (optionally for a session, on a tab). */
|
|
78
|
+
openObservatory: (detail?: { sessionId?: string; tab?: string }) => void
|
|
79
|
+
/** Resolve the current UI language at label-build time. */
|
|
80
|
+
lang: () => Lang
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Default key binding for the observatory command: Cmd/Ctrl+Shift+O
|
|
85
|
+
* ("O" = Open), applied uniformly across every runtime/platform profile so the
|
|
86
|
+
* feature is on by default on desktop and web. If an existing dsh command
|
|
87
|
+
* already owns this combination, `register` throws and the plugin degrades
|
|
88
|
+
* gracefully (no keyboard command; the observatory stays mouse-reachable).
|
|
89
|
+
*/
|
|
90
|
+
const DEFAULT_BINDING: ShortcutBinding = { code: 'KeyO', modifiers: ['primary', 'shift'] }
|
|
91
|
+
|
|
92
|
+
const DEFAULT_PROFILES = [
|
|
93
|
+
'desktop:macos',
|
|
94
|
+
'desktop:windows',
|
|
95
|
+
'desktop:linux',
|
|
96
|
+
'web:macos',
|
|
97
|
+
'web:windows',
|
|
98
|
+
'web:linux',
|
|
99
|
+
] as const
|
|
100
|
+
|
|
101
|
+
/** Every runtime/platform profile maps to the same default binding. */
|
|
102
|
+
export const OBSERVATORY_DEFAULT_BINDINGS: Readonly<Record<string, ShortcutBinding>> = Object.fromEntries(
|
|
103
|
+
DEFAULT_PROFILES.map((profile) => [profile, DEFAULT_BINDING]),
|
|
104
|
+
) as Readonly<Record<string, ShortcutBinding>>
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Build the pure command descriptor for "open iterate observatory". The
|
|
108
|
+
* resolver always handles the gesture and runs the injected opener; it never
|
|
109
|
+
* blocks (opening the observatory is safe from any region and no modal).
|
|
110
|
+
*/
|
|
111
|
+
export function buildObservatoryShortcutCommand(deps: ShortcutDeps): ShortcutCommand {
|
|
112
|
+
const label = (): string =>
|
|
113
|
+
deps.lang() === 'zh' ? '打开 iterate 观测台' : 'Open iterate observatory'
|
|
114
|
+
return {
|
|
115
|
+
id: OBSERVATORY_SHORTCUT_ID,
|
|
116
|
+
label,
|
|
117
|
+
aliases: [],
|
|
118
|
+
defaults: OBSERVATORY_DEFAULT_BINDINGS,
|
|
119
|
+
regions: ['page', 'editable', 'terminal'],
|
|
120
|
+
modals: [],
|
|
121
|
+
resolve: () => ({ status: 'handled', run: () => deps.openObservatory({}) }),
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Register the observatory keyboard command on the (already-validated)
|
|
127
|
+
* `shortcuts` service. Returns an unregister function; a host without the
|
|
128
|
+
* service is a no-op that returns `() => {}` (callers guard).
|
|
129
|
+
*/
|
|
130
|
+
export function registerIterateShortcuts(service: ShortcutsService, deps: ShortcutDeps): () => void {
|
|
131
|
+
try {
|
|
132
|
+
const dispose = service.register(buildObservatoryShortcutCommand(deps))
|
|
133
|
+
let done = false
|
|
134
|
+
return () => {
|
|
135
|
+
if (done) return
|
|
136
|
+
done = true
|
|
137
|
+
try {
|
|
138
|
+
dispose()
|
|
139
|
+
} catch {
|
|
140
|
+
/* noop */
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} catch (err) {
|
|
144
|
+
// A duplicate id or an overlapping default binding must not take the
|
|
145
|
+
// plugin down — log and continue with no keyboard command (the docked
|
|
146
|
+
// panel and the context-menu "open" action remain the entry points).
|
|
147
|
+
if (typeof console !== 'undefined' && typeof console.error === 'function') {
|
|
148
|
+
console.error('[iterate-ui] keyboard-command registration failed; continuing without it.', err)
|
|
149
|
+
}
|
|
150
|
+
return () => {}
|
|
151
|
+
}
|
|
152
|
+
}
|