iterate-plugin 3.13.0 → 3.14.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 +5 -5
- package/README.zh-CN.md +2 -1
- package/lib/client.js +230 -1
- package/lib/parse.js +28 -0
- package/package.json +21 -20
- package/src/client/context-menu.ts +289 -0
- package/src/client/index.ts +117 -0
package/README.md
CHANGED
|
@@ -114,6 +114,7 @@ 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).
|
|
117
118
|
|
|
118
119
|
The UI layer is **defensive by design**: it degrades gracefully if any of `slots` / `theme` / `React` is unavailable — it never crashes the client.
|
|
119
120
|
|
|
@@ -346,14 +347,13 @@ All runtime state lives under `.iterate/` at the project root (can be excluded v
|
|
|
346
347
|
`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
348
|
`0.2.0-rc.2`, `0.2.1-alpha.1`, `0.2.1-alpha.2`, `0.2.1-alpha.3`,
|
|
348
349
|
`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`).
|
|
350
|
+
`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`).
|
|
352
352
|
The plugin is built against `@deepseek-ai/dsh-tools` /
|
|
353
|
-
`@deepseek-ai/dsh-util-values` `0.2.2-alpha.
|
|
353
|
+
`@deepseek-ai/dsh-util-values` `0.2.2-alpha.3` and uses only public contracts
|
|
354
354
|
(tool registration, client slots + theme, bundle patch, and — since v3.9 —
|
|
355
355
|
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.
|
|
356
|
+
It also declares `engines.dsh` (`>=0.1.1-rc.1`) so the dsh ≥ 0.2.2-alpha.3
|
|
357
357
|
runtime evaluates compatibility from that single range (earlier runtimes fall
|
|
358
358
|
back to the `@deepseek-ai/dsh-*` peer versions listed in `package.json`).
|
|
359
359
|
- **Disposable-Profile evidence (real, `dsh` CLI `0.1.1-rc.1`)**: on a temp
|
package/README.zh-CN.md
CHANGED
|
@@ -113,6 +113,7 @@ 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` 时降级为无菜单项(停靠面板仍是入口)。
|
|
116
117
|
|
|
117
118
|
UI 层为**防御式设计**:`slots` / `theme` / `React` 任一不可用时自动降级,不会崩溃客户端。
|
|
118
119
|
|
|
@@ -336,7 +337,7 @@ validation:
|
|
|
336
337
|
### Node.js / DSH 兼容范围
|
|
337
338
|
|
|
338
339
|
- **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`(共
|
|
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 版本)。
|
|
340
341
|
- **一次性 Profile 证据(真实,`dsh` CLI `0.1.1-rc.1`)**:在临时 `$DSH_HOME` 下
|
|
341
342
|
`dsh plugin --profile <p> add <本仓库>` 约 449ms 完成安装;`dsh --profile <p>
|
|
342
343
|
--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: 5b22267e6de6fb0564d60874b3de1211755cdeb398f6c7ffa87dfa1007e6796f */
|
|
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,135 @@ function subscribeSession(listener) {
|
|
|
2858
2865
|
};
|
|
2859
2866
|
}
|
|
2860
2867
|
|
|
2868
|
+
// src/client/context-menu.ts
|
|
2869
|
+
var OBS_OPEN_EVENT = "iterate-plugin:open-observatory";
|
|
2870
|
+
function openObservatory(detail) {
|
|
2871
|
+
if (typeof window === "undefined" || typeof window.dispatchEvent !== "function") return false;
|
|
2872
|
+
try {
|
|
2873
|
+
window.dispatchEvent(new CustomEvent(OBS_OPEN_EVENT, { detail: detail ?? {} }));
|
|
2874
|
+
return true;
|
|
2875
|
+
} catch {
|
|
2876
|
+
return false;
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
var LABELS = {
|
|
2880
|
+
"iterate.open-panel": { zh: "\u6253\u5F00 iterate \u8D28\u91CF\u6307\u6325\u4E2D\u5FC3", en: "Open iterate command center" },
|
|
2881
|
+
"iterate.preflight": { zh: "\u590D\u5236\u5F00\u8DD1\u9884\u68C0\u6307\u4EE4", en: "Copy preflight instruction" },
|
|
2882
|
+
"iterate.dry-run": { zh: "\u590D\u5236 dry-run \u5BA1\u67E5\u6307\u4EE4", en: "Copy dry-run review instruction" },
|
|
2883
|
+
"iterate.resume": { zh: "\u590D\u5236\u65AD\u70B9\u6062\u590D\u6307\u4EE4", en: "Copy checkpoint-resume instruction" },
|
|
2884
|
+
"iterate.pull": { zh: "\u590D\u5236\u62C9\u53D6\u78C1\u76D8\u5FEB\u7167\u6307\u4EE4", en: "Copy disk-snapshot pull instruction" },
|
|
2885
|
+
"iterate.review-file": { zh: "\u590D\u5236\u300C\u4EC5\u5BA1\u67E5\u6B64\u6587\u4EF6\u300D\u6307\u4EE4", en: 'Copy "review this file only" instruction' },
|
|
2886
|
+
"iterate.review-dir": { zh: "\u590D\u5236\u300C\u4EC5\u5BA1\u67E5\u6B64\u76EE\u5F55\u300D\u6307\u4EE4", en: 'Copy "review this directory only" instruction' }
|
|
2887
|
+
};
|
|
2888
|
+
function pick(id, lang) {
|
|
2889
|
+
const entry = LABELS[id];
|
|
2890
|
+
if (!entry) return id;
|
|
2891
|
+
return lang === "zh" ? entry.zh : entry.en;
|
|
2892
|
+
}
|
|
2893
|
+
var COPY_FAILURE = {
|
|
2894
|
+
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",
|
|
2895
|
+
en: 'Copy failed: clipboard denied. Use the matching "Copy" button in the observatory panel, or select the instruction text manually.'
|
|
2896
|
+
};
|
|
2897
|
+
function buildSessionMenuEntries(lang, opts) {
|
|
2898
|
+
return [
|
|
2899
|
+
{ id: "iterate.open-panel", label: pick("iterate.open-panel", lang), openTab: "live" },
|
|
2900
|
+
{ id: "iterate.preflight", label: pick("iterate.preflight", lang), separatorBefore: true },
|
|
2901
|
+
{ id: "iterate.dry-run", label: pick("iterate.dry-run", lang) },
|
|
2902
|
+
{ id: "iterate.resume", label: pick("iterate.resume", lang), disabled: opts?.hasCheckpoint === false },
|
|
2903
|
+
{ id: "iterate.pull", label: pick("iterate.pull", lang), separatorBefore: true }
|
|
2904
|
+
];
|
|
2905
|
+
}
|
|
2906
|
+
function buildFileMenuEntries(lang) {
|
|
2907
|
+
return [
|
|
2908
|
+
{ id: "iterate.review-file", label: pick("iterate.review-file", lang) },
|
|
2909
|
+
{ id: "iterate.open-panel", label: pick("iterate.open-panel", lang) }
|
|
2910
|
+
];
|
|
2911
|
+
}
|
|
2912
|
+
function buildDirectoryMenuEntries(lang) {
|
|
2913
|
+
return [
|
|
2914
|
+
{ id: "iterate.review-dir", label: pick("iterate.review-dir", lang) },
|
|
2915
|
+
{ id: "iterate.preflight", label: pick("iterate.preflight", lang), separatorBefore: true },
|
|
2916
|
+
{ id: "iterate.open-panel", label: pick("iterate.open-panel", lang) }
|
|
2917
|
+
];
|
|
2918
|
+
}
|
|
2919
|
+
function toAction(entry, deps, lang) {
|
|
2920
|
+
const action = {
|
|
2921
|
+
id: entry.id,
|
|
2922
|
+
label: entry.label,
|
|
2923
|
+
danger: entry.danger,
|
|
2924
|
+
separatorBefore: entry.separatorBefore
|
|
2925
|
+
};
|
|
2926
|
+
if (entry.disabled) action.disabled = true;
|
|
2927
|
+
if (entry.openTab) {
|
|
2928
|
+
action.run = () => {
|
|
2929
|
+
deps.openPanel({ tab: entry.openTab });
|
|
2930
|
+
deps.notify(lang === "zh" ? "\u5DF2\u6253\u5F00 iterate \u89C2\u6D4B\u53F0\u3002" : "Iterate observatory opened.", true);
|
|
2931
|
+
};
|
|
2932
|
+
return action;
|
|
2933
|
+
}
|
|
2934
|
+
if (entry.instruction) {
|
|
2935
|
+
const text = entry.instruction;
|
|
2936
|
+
const failureLabel = COPY_FAILURE[lang];
|
|
2937
|
+
action.failureLabel = failureLabel;
|
|
2938
|
+
action.run = async () => {
|
|
2939
|
+
const ok = await deps.copy(text);
|
|
2940
|
+
if (!ok) throw new Error(failureLabel);
|
|
2941
|
+
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);
|
|
2942
|
+
};
|
|
2943
|
+
}
|
|
2944
|
+
return action;
|
|
2945
|
+
}
|
|
2946
|
+
function registerIterateContextMenus(service, deps, instructionByEntry) {
|
|
2947
|
+
const unregister = [];
|
|
2948
|
+
const register = (target, order, build) => {
|
|
2949
|
+
try {
|
|
2950
|
+
const dispose = service.register({
|
|
2951
|
+
id: `iterate-plugin.${target}`,
|
|
2952
|
+
target,
|
|
2953
|
+
order,
|
|
2954
|
+
resolve: (t2, _signal) => {
|
|
2955
|
+
const lang = getLang();
|
|
2956
|
+
return build(lang, t2).map((entry) => {
|
|
2957
|
+
const full = { ...entry, instruction: entry.instruction ?? instructionByEntry(entry.id) };
|
|
2958
|
+
return toAction(full, deps, lang);
|
|
2959
|
+
});
|
|
2960
|
+
}
|
|
2961
|
+
});
|
|
2962
|
+
unregister.push(dispose);
|
|
2963
|
+
} catch (err) {
|
|
2964
|
+
if (typeof console !== "undefined" && typeof console.error === "function") {
|
|
2965
|
+
console.error("[iterate-ui] context-menu registration failed for target", target, err);
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
};
|
|
2969
|
+
register("session", 300, (_lang, t2) => {
|
|
2970
|
+
const target = t2;
|
|
2971
|
+
return buildSessionMenuEntries(getLang(), { hasCheckpoint: target?.value?.sessionId ? true : void 0 });
|
|
2972
|
+
});
|
|
2973
|
+
register("file", 300, (_lang, t2) => {
|
|
2974
|
+
const target = t2;
|
|
2975
|
+
return buildFileMenuEntries(getLang()).map((e) => ({
|
|
2976
|
+
...e,
|
|
2977
|
+
instruction: e.id === "iterate.review-file" ? instructionByEntry(`file:${target?.value?.path ?? ""}${typeof target?.value?.line === "number" ? `:${target.value.line}` : ""}`) : e.instruction
|
|
2978
|
+
}));
|
|
2979
|
+
});
|
|
2980
|
+
register("directory", 300, (_lang, t2) => {
|
|
2981
|
+
const target = t2;
|
|
2982
|
+
return buildDirectoryMenuEntries(getLang()).map((e) => ({
|
|
2983
|
+
...e,
|
|
2984
|
+
instruction: e.id === "iterate.review-dir" ? instructionByEntry(`dir:${target?.value?.path ?? ""}`) : e.instruction
|
|
2985
|
+
}));
|
|
2986
|
+
});
|
|
2987
|
+
return () => {
|
|
2988
|
+
for (const dispose of unregister.splice(0)) {
|
|
2989
|
+
try {
|
|
2990
|
+
dispose();
|
|
2991
|
+
} catch {
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
};
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2861
2997
|
// src/client/index.ts
|
|
2862
2998
|
var React = require("react");
|
|
2863
2999
|
var name = "iterate-plugin";
|
|
@@ -3294,12 +3430,42 @@ function readTheme(ctx) {
|
|
|
3294
3430
|
}
|
|
3295
3431
|
return void 0;
|
|
3296
3432
|
}
|
|
3433
|
+
function readContextMenus(ctx) {
|
|
3434
|
+
if (!ctx || typeof ctx.get !== "function") return void 0;
|
|
3435
|
+
const raw = ctx.get("contextMenus", false);
|
|
3436
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
3437
|
+
const svc = raw;
|
|
3438
|
+
if (typeof svc.register !== "function") return void 0;
|
|
3439
|
+
if (typeof svc.notify !== "function") return void 0;
|
|
3440
|
+
return svc;
|
|
3441
|
+
}
|
|
3442
|
+
function sessionIdOfSession(session) {
|
|
3443
|
+
if (!session || typeof session !== "object") return null;
|
|
3444
|
+
const s = session;
|
|
3445
|
+
const candidates = [
|
|
3446
|
+
s.sessionId,
|
|
3447
|
+
s.id,
|
|
3448
|
+
s.header,
|
|
3449
|
+
s.session
|
|
3450
|
+
];
|
|
3451
|
+
for (const c of candidates) {
|
|
3452
|
+
if (typeof c === "string" && c) return c;
|
|
3453
|
+
if (c && typeof c === "object") {
|
|
3454
|
+
const o = c;
|
|
3455
|
+
for (const k of ["sessionId", "id"]) {
|
|
3456
|
+
if (typeof o[k] === "string" && o[k]) return o[k];
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
return null;
|
|
3461
|
+
}
|
|
3297
3462
|
var slotsSvc = void 0;
|
|
3298
3463
|
var themeSvc = void 0;
|
|
3299
3464
|
var storage2 = null;
|
|
3300
3465
|
var themeDisposer = null;
|
|
3301
3466
|
var themeListener = null;
|
|
3302
3467
|
var themeEnabled = true;
|
|
3468
|
+
var menuDisposer = null;
|
|
3303
3469
|
var roundPulseListeners = [];
|
|
3304
3470
|
function emitRoundPulse(round, converged) {
|
|
3305
3471
|
const payload = { round, converged: converged === true };
|
|
@@ -4607,6 +4773,21 @@ function ObservatoryPanel(props) {
|
|
|
4607
4773
|
doc.addEventListener("keydown", onKeyDown);
|
|
4608
4774
|
return () => doc.removeEventListener("keydown", onKeyDown);
|
|
4609
4775
|
}, [open]);
|
|
4776
|
+
const panelSessionId = sessionIdOfSession(session);
|
|
4777
|
+
React.useEffect(() => {
|
|
4778
|
+
if (typeof window === "undefined" || typeof window.addEventListener !== "function") return;
|
|
4779
|
+
const onOpen = (ev) => {
|
|
4780
|
+
const detail = ev.detail;
|
|
4781
|
+
const detailSession = detail && typeof detail.sessionId === "string" && detail.sessionId ? detail.sessionId : null;
|
|
4782
|
+
if (detailSession && panelSessionId && detailSession !== panelSessionId) return;
|
|
4783
|
+
setOpen(true);
|
|
4784
|
+
const target = detail && typeof detail.tab === "string" ? detail.tab : "live";
|
|
4785
|
+
const valid = OBS_TABS.some((x) => x.key === target);
|
|
4786
|
+
setTab(valid ? target : "live");
|
|
4787
|
+
};
|
|
4788
|
+
window.addEventListener(OBS_OPEN_EVENT, onOpen);
|
|
4789
|
+
return () => window.removeEventListener(OBS_OPEN_EVENT, onOpen);
|
|
4790
|
+
}, [panelSessionId]);
|
|
4610
4791
|
const copyInstruction = (key, text) => {
|
|
4611
4792
|
if (!text) return;
|
|
4612
4793
|
copyText(text).then((ok) => {
|
|
@@ -6217,6 +6398,54 @@ function apply(ctx) {
|
|
|
6217
6398
|
)
|
|
6218
6399
|
);
|
|
6219
6400
|
}
|
|
6401
|
+
{
|
|
6402
|
+
const menuSvc = readContextMenus(ctx);
|
|
6403
|
+
if (menuSvc) {
|
|
6404
|
+
const instructionByEntry = (id) => {
|
|
6405
|
+
if (id === "iterate.preflight") return buildPreflightInstruction();
|
|
6406
|
+
if (id === "iterate.dry-run") return START_INSTRUCTIONS.reviewOnly;
|
|
6407
|
+
if (id === "iterate.resume") return buildCheckpointResumeInstruction();
|
|
6408
|
+
if (id === "iterate.pull") return buildDiskSnapshotInstruction();
|
|
6409
|
+
if (id.startsWith("file:")) {
|
|
6410
|
+
const rest = id.slice("file:".length);
|
|
6411
|
+
const colon = rest.lastIndexOf(":");
|
|
6412
|
+
const path = colon > 0 ? rest.slice(0, colon) : rest;
|
|
6413
|
+
const line = colon > 0 ? Number(rest.slice(colon + 1)) : void 0;
|
|
6414
|
+
return buildScopedReviewInstruction({ file: path, ...Number.isInteger(line) && line > 0 ? { line } : {} });
|
|
6415
|
+
}
|
|
6416
|
+
if (id.startsWith("dir:")) return buildScopedReviewInstruction({ dir: id.slice("dir:".length) });
|
|
6417
|
+
return "";
|
|
6418
|
+
};
|
|
6419
|
+
if (menuDisposer) {
|
|
6420
|
+
try {
|
|
6421
|
+
menuDisposer();
|
|
6422
|
+
} catch {
|
|
6423
|
+
}
|
|
6424
|
+
menuDisposer = null;
|
|
6425
|
+
}
|
|
6426
|
+
menuDisposer = registerIterateContextMenus(
|
|
6427
|
+
menuSvc,
|
|
6428
|
+
{
|
|
6429
|
+
copy: (text) => copyText(text),
|
|
6430
|
+
openPanel: (detail) => {
|
|
6431
|
+
openObservatory(detail);
|
|
6432
|
+
},
|
|
6433
|
+
notify: (text, success, action) => {
|
|
6434
|
+
menuSvc.notify(text, success, action);
|
|
6435
|
+
}
|
|
6436
|
+
},
|
|
6437
|
+
instructionByEntry
|
|
6438
|
+
);
|
|
6439
|
+
if (typeof ctx.effect === "function") {
|
|
6440
|
+
ctx.effect(() => {
|
|
6441
|
+
if (menuDisposer) {
|
|
6442
|
+
menuDisposer();
|
|
6443
|
+
menuDisposer = null;
|
|
6444
|
+
}
|
|
6445
|
+
});
|
|
6446
|
+
}
|
|
6447
|
+
}
|
|
6448
|
+
}
|
|
6220
6449
|
registerThemeListener(ctx);
|
|
6221
6450
|
}
|
|
6222
6451
|
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.14.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,8 @@
|
|
|
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"
|
|
59
60
|
}
|
|
60
61
|
},
|
|
61
62
|
"client": {
|
|
@@ -96,10 +97,10 @@
|
|
|
96
97
|
"js-yaml": "4.3.2"
|
|
97
98
|
},
|
|
98
99
|
"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"
|
|
100
|
+
"@deepseek-ai/cordis": "^4.0.5-alpha.2 || ~4.0.6-alpha.1 || ~4.0.6-alpha.2",
|
|
101
|
+
"@deepseek-ai/dsh-session": "0.1.1-rc.1 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 0.2.1-alpha.5 || 0.2.1-rc.1 || 0.2.1-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.4 || 0.2.1-rc.5 || 0.2.2-alpha.2 || 0.2.2-alpha.3",
|
|
102
|
+
"@deepseek-ai/dsh-tools": "0.1.1-rc.1 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 0.2.1-alpha.5 || 0.2.1-rc.1 || 0.2.1-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.4 || 0.2.1-rc.5 || 0.2.2-alpha.2 || 0.2.2-alpha.3",
|
|
103
|
+
"@deepseek-ai/dsh-util-values": "0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 0.2.1-alpha.5 || 0.2.1-rc.1 || 0.2.1-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.4 || 0.2.1-rc.5 || 0.2.2-alpha.2 || 0.2.2-alpha.3"
|
|
103
104
|
},
|
|
104
105
|
"peerDependenciesMeta": {
|
|
105
106
|
"@deepseek-ai/cordis": {
|
|
@@ -116,20 +117,20 @@
|
|
|
116
117
|
}
|
|
117
118
|
},
|
|
118
119
|
"devDependencies": {
|
|
119
|
-
"@deepseek-ai/cordis": "4.0.6-alpha.
|
|
120
|
-
"@deepseek-ai/dsh-agent": "0.2.2-alpha.
|
|
121
|
-
"@deepseek-ai/dsh-jobs": "0.2.2-alpha.
|
|
122
|
-
"@deepseek-ai/dsh-llm": "0.2.2-alpha.
|
|
123
|
-
"@deepseek-ai/dsh-ptc-runtime": "0.2.2-alpha.
|
|
124
|
-
"@deepseek-ai/dsh-sandbox": "0.2.2-alpha.
|
|
125
|
-
"@deepseek-ai/dsh-sandbox-policy": "0.2.2-alpha.
|
|
126
|
-
"@deepseek-ai/dsh-scope": "0.2.2-alpha.
|
|
127
|
-
"@deepseek-ai/dsh-session": "0.2.2-alpha.
|
|
128
|
-
"@deepseek-ai/dsh-system-prompt": "0.2.2-alpha.
|
|
129
|
-
"@deepseek-ai/dsh-tools": "0.2.2-alpha.
|
|
130
|
-
"@deepseek-ai/dsh-user-approval": "0.2.2-alpha.
|
|
131
|
-
"@deepseek-ai/dsh-util-values": "0.2.2-alpha.
|
|
132
|
-
"@deepseek-ai/dsh-working-directory": "0.2.2-alpha.
|
|
120
|
+
"@deepseek-ai/cordis": "4.0.6-alpha.2",
|
|
121
|
+
"@deepseek-ai/dsh-agent": "0.2.2-alpha.3",
|
|
122
|
+
"@deepseek-ai/dsh-jobs": "0.2.2-alpha.3",
|
|
123
|
+
"@deepseek-ai/dsh-llm": "0.2.2-alpha.3",
|
|
124
|
+
"@deepseek-ai/dsh-ptc-runtime": "0.2.2-alpha.3",
|
|
125
|
+
"@deepseek-ai/dsh-sandbox": "0.2.2-alpha.3",
|
|
126
|
+
"@deepseek-ai/dsh-sandbox-policy": "0.2.2-alpha.3",
|
|
127
|
+
"@deepseek-ai/dsh-scope": "0.2.2-alpha.3",
|
|
128
|
+
"@deepseek-ai/dsh-session": "0.2.2-alpha.3",
|
|
129
|
+
"@deepseek-ai/dsh-system-prompt": "0.2.2-alpha.3",
|
|
130
|
+
"@deepseek-ai/dsh-tools": "0.2.2-alpha.3",
|
|
131
|
+
"@deepseek-ai/dsh-user-approval": "0.2.2-alpha.3",
|
|
132
|
+
"@deepseek-ai/dsh-util-values": "0.2.2-alpha.3",
|
|
133
|
+
"@deepseek-ai/dsh-working-directory": "0.2.2-alpha.3",
|
|
133
134
|
"@types/js-yaml": "4.0.9",
|
|
134
135
|
"@types/node": "22.15.0",
|
|
135
136
|
"@types/react": "19.2.2",
|
|
@@ -0,0 +1,289 @@
|
|
|
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
|
+
|
|
44
|
+
// ─── Structural service types (defensive; mirrors SlotsService) ──────────────
|
|
45
|
+
//
|
|
46
|
+
// The dsh client runtime is not type-imported (no compile-time coupling to a
|
|
47
|
+
// package a pre-0.2.2-alpha.3 host may not have). The surface below is the
|
|
48
|
+
// subset this plugin consumes; `register` is only ever called when a service
|
|
49
|
+
// with the right shape is present.
|
|
50
|
+
|
|
51
|
+
/** A right-click target this plugin contributes actions to. */
|
|
52
|
+
export type IterateMenuTarget =
|
|
53
|
+
| { kind: 'session'; value: { sessionId: string; displayTitle?: string } }
|
|
54
|
+
| { kind: 'file'; value: { sessionId: string; path: string; line?: number } }
|
|
55
|
+
| { kind: 'directory'; value: { sessionId: string; path: string } }
|
|
56
|
+
|
|
57
|
+
/** One menu action (presentation + optional execution). */
|
|
58
|
+
export interface IterateMenuAction {
|
|
59
|
+
id: string
|
|
60
|
+
label: string
|
|
61
|
+
disabled?: boolean
|
|
62
|
+
danger?: boolean
|
|
63
|
+
separatorBefore?: boolean
|
|
64
|
+
/** Localized operation + recovery guidance when `run` rejects. */
|
|
65
|
+
failureLabel?: string
|
|
66
|
+
run?: () => void | Promise<void>
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A namespaced contribution, ordered within its target family. */
|
|
70
|
+
export interface IterateMenuContribution<K extends string> {
|
|
71
|
+
id: string
|
|
72
|
+
target: K
|
|
73
|
+
order: number
|
|
74
|
+
resolve(target: unknown, signal: AbortSignal): readonly IterateMenuAction[]
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Structural view of the dsh `contextMenus` service this plugin consumes. */
|
|
78
|
+
export interface ContextMenusService {
|
|
79
|
+
register<K extends string>(contribution: IterateMenuContribution<K>): () => void
|
|
80
|
+
notify(
|
|
81
|
+
text: string,
|
|
82
|
+
success?: boolean,
|
|
83
|
+
action?: { label: string; run: () => void | Promise<void>; failureLabel: string },
|
|
84
|
+
): void
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ─── Observatory open event bridge ───────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
/** Window event that asks the (matching) ObservatoryPanel to expand. */
|
|
90
|
+
export const OBS_OPEN_EVENT = 'iterate-plugin:open-observatory'
|
|
91
|
+
|
|
92
|
+
/** Payload carried by {@link OBS_OPEN_EVENT}. */
|
|
93
|
+
export interface ObsOpenDetail {
|
|
94
|
+
/** Optional target session; omitted = open whatever panel is mounted. */
|
|
95
|
+
sessionId?: string
|
|
96
|
+
/** Tab key to switch to (`live` by default). */
|
|
97
|
+
tab?: string
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Dispatch the "open observatory" event. No-op outside a browser (SSR / test
|
|
102
|
+
* environments without `window`). Returns whether the event was dispatched.
|
|
103
|
+
*/
|
|
104
|
+
export function openObservatory(detail?: ObsOpenDetail): boolean {
|
|
105
|
+
if (typeof window === 'undefined' || typeof window.dispatchEvent !== 'function') return false
|
|
106
|
+
try {
|
|
107
|
+
window.dispatchEvent(new CustomEvent<ObsOpenDetail>(OBS_OPEN_EVENT, { detail: detail ?? {} }))
|
|
108
|
+
return true
|
|
109
|
+
} catch {
|
|
110
|
+
return false
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ─── Localized descriptors (pure, testable) ──────────────────────────────────
|
|
115
|
+
|
|
116
|
+
/** The action ids this plugin registers, for tests + duplicate guards. */
|
|
117
|
+
export const SESSION_MENU_IDS = ['iterate.open-panel', 'iterate.preflight', 'iterate.dry-run', 'iterate.resume', 'iterate.pull'] as const
|
|
118
|
+
export const FILE_MENU_IDS = ['iterate.review-file', 'iterate.open-panel'] as const
|
|
119
|
+
export const DIRECTORY_MENU_IDS = ['iterate.review-dir', 'iterate.preflight', 'iterate.open-panel'] as const
|
|
120
|
+
|
|
121
|
+
interface MenuEntry {
|
|
122
|
+
id: string
|
|
123
|
+
label: string
|
|
124
|
+
instruction?: string
|
|
125
|
+
/** Tab key to open when this entry is the "open panel" action. */
|
|
126
|
+
openTab?: string
|
|
127
|
+
danger?: boolean
|
|
128
|
+
disabled?: boolean
|
|
129
|
+
separatorBefore?: boolean
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Bilingual labels; resolved by `pick` at build time. */
|
|
133
|
+
const LABELS: Record<string, { zh: string; en: string }> = {
|
|
134
|
+
'iterate.open-panel': { zh: '打开 iterate 质量指挥中心', en: 'Open iterate command center' },
|
|
135
|
+
'iterate.preflight': { zh: '复制开跑预检指令', en: 'Copy preflight instruction' },
|
|
136
|
+
'iterate.dry-run': { zh: '复制 dry-run 审查指令', en: 'Copy dry-run review instruction' },
|
|
137
|
+
'iterate.resume': { zh: '复制断点恢复指令', en: 'Copy checkpoint-resume instruction' },
|
|
138
|
+
'iterate.pull': { zh: '复制拉取磁盘快照指令', en: 'Copy disk-snapshot pull instruction' },
|
|
139
|
+
'iterate.review-file': { zh: '复制「仅审查此文件」指令', en: 'Copy "review this file only" instruction' },
|
|
140
|
+
'iterate.review-dir': { zh: '复制「仅审查此目录」指令', en: 'Copy "review this directory only" instruction' },
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Pick a localized label; unknown ids degrade to the raw id (never throw). */
|
|
144
|
+
function pick(id: string, lang: Lang): string {
|
|
145
|
+
const entry = LABELS[id]
|
|
146
|
+
if (!entry) return id
|
|
147
|
+
return lang === 'zh' ? entry.zh : entry.en
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Copy-failure guidance (a menu action's `failureLabel`). */
|
|
151
|
+
const COPY_FAILURE: Record<'zh' | 'en', string> = {
|
|
152
|
+
zh: '复制失败:剪贴板被拒绝。请改为在观测台面板里用同一指令的「复制」按钮,或手动选中指令文本复制。',
|
|
153
|
+
en: 'Copy failed: clipboard denied. Use the matching "Copy" button in the observatory panel, or select the instruction text manually.',
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Build the Session target menu entries (pure). */
|
|
157
|
+
export function buildSessionMenuEntries(lang: Lang, opts?: { hasCheckpoint?: boolean }): MenuEntry[] {
|
|
158
|
+
return [
|
|
159
|
+
{ id: 'iterate.open-panel', label: pick('iterate.open-panel', lang), openTab: 'live' },
|
|
160
|
+
{ id: 'iterate.preflight', label: pick('iterate.preflight', lang), separatorBefore: true },
|
|
161
|
+
{ id: 'iterate.dry-run', label: pick('iterate.dry-run', lang) },
|
|
162
|
+
{ id: 'iterate.resume', label: pick('iterate.resume', lang), disabled: opts?.hasCheckpoint === false },
|
|
163
|
+
{ id: 'iterate.pull', label: pick('iterate.pull', lang), separatorBefore: true },
|
|
164
|
+
]
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Build the File target menu entries (pure). */
|
|
168
|
+
export function buildFileMenuEntries(lang: Lang): MenuEntry[] {
|
|
169
|
+
return [
|
|
170
|
+
{ id: 'iterate.review-file', label: pick('iterate.review-file', lang) },
|
|
171
|
+
{ id: 'iterate.open-panel', label: pick('iterate.open-panel', lang) },
|
|
172
|
+
]
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Build the Directory target menu entries (pure). */
|
|
176
|
+
export function buildDirectoryMenuEntries(lang: Lang): MenuEntry[] {
|
|
177
|
+
return [
|
|
178
|
+
{ id: 'iterate.review-dir', label: pick('iterate.review-dir', lang) },
|
|
179
|
+
{ id: 'iterate.preflight', label: pick('iterate.preflight', lang), separatorBefore: true },
|
|
180
|
+
{ id: 'iterate.open-panel', label: pick('iterate.open-panel', lang) },
|
|
181
|
+
]
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ─── Imperative wiring (register + run callbacks) ────────────────────────────
|
|
185
|
+
|
|
186
|
+
/** Injection points the menu wiring needs (kept dependency-light for tests). */
|
|
187
|
+
export interface IterateMenuDeps {
|
|
188
|
+
/** Copy an instruction; resolves to whether the clipboard write succeeded. */
|
|
189
|
+
copy: (text: string) => Promise<boolean>
|
|
190
|
+
/** Open the observatory panel (optionally for a session, on a tab). */
|
|
191
|
+
openPanel: (detail?: ObsOpenDetail) => void
|
|
192
|
+
/** Show the shared toast. */
|
|
193
|
+
notify: (text: string, success?: boolean, action?: { label: string; run: () => void | Promise<void>; failureLabel: string }) => void
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Map a pure entry to a runnable menu action, wiring the injected deps. */
|
|
197
|
+
function toAction(entry: MenuEntry, deps: IterateMenuDeps, lang: Lang): IterateMenuAction {
|
|
198
|
+
const action: IterateMenuAction = {
|
|
199
|
+
id: entry.id,
|
|
200
|
+
label: entry.label,
|
|
201
|
+
danger: entry.danger,
|
|
202
|
+
separatorBefore: entry.separatorBefore,
|
|
203
|
+
}
|
|
204
|
+
if (entry.disabled) action.disabled = true
|
|
205
|
+
if (entry.openTab) {
|
|
206
|
+
action.run = () => {
|
|
207
|
+
deps.openPanel({ tab: entry.openTab })
|
|
208
|
+
deps.notify(lang === 'zh' ? '已打开 iterate 观测台。' : 'Iterate observatory opened.', true)
|
|
209
|
+
}
|
|
210
|
+
return action
|
|
211
|
+
}
|
|
212
|
+
if (entry.instruction) {
|
|
213
|
+
const text = entry.instruction
|
|
214
|
+
const failureLabel = COPY_FAILURE[lang]
|
|
215
|
+
action.failureLabel = failureLabel
|
|
216
|
+
action.run = async () => {
|
|
217
|
+
const ok = await deps.copy(text)
|
|
218
|
+
if (!ok) throw new Error(failureLabel)
|
|
219
|
+
deps.notify(lang === 'zh' ? '指令已复制,粘贴给模型即可。' : 'Instruction copied — paste it to the model.', true)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return action
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Register this plugin's context-menu contributions on the (already-validated)
|
|
227
|
+
* `contextMenus` service. Returns an unregister function; a host without the
|
|
228
|
+
* service is a no-op that returns `() => {}` (callers guard).
|
|
229
|
+
*/
|
|
230
|
+
export function registerIterateContextMenus(
|
|
231
|
+
service: ContextMenusService,
|
|
232
|
+
deps: IterateMenuDeps,
|
|
233
|
+
instructionByEntry: (id: string) => string,
|
|
234
|
+
): () => void {
|
|
235
|
+
const unregister: Array<() => void> = []
|
|
236
|
+
|
|
237
|
+
const register = <K extends string>(target: K, order: number, build: (lang: Lang, target: unknown) => MenuEntry[]) => {
|
|
238
|
+
try {
|
|
239
|
+
const dispose = service.register({
|
|
240
|
+
id: `iterate-plugin.${target}`,
|
|
241
|
+
target,
|
|
242
|
+
order,
|
|
243
|
+
resolve: (t, _signal) => {
|
|
244
|
+
const lang = getLang()
|
|
245
|
+
return build(lang, t).map((entry) => {
|
|
246
|
+
const full: MenuEntry = { ...entry, instruction: entry.instruction ?? instructionByEntry(entry.id) }
|
|
247
|
+
return toAction(full, deps, lang)
|
|
248
|
+
})
|
|
249
|
+
},
|
|
250
|
+
})
|
|
251
|
+
unregister.push(dispose)
|
|
252
|
+
} catch (err) {
|
|
253
|
+
// A duplicate contribution id or an unsupported target family must not
|
|
254
|
+
// take the plugin down — log and continue with the remaining targets.
|
|
255
|
+
if (typeof console !== 'undefined' && typeof console.error === 'function') {
|
|
256
|
+
console.error('[iterate-ui] context-menu registration failed for target', target, err)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
register('session', 300, (_lang, t) => {
|
|
262
|
+
const target = t as { value?: { sessionId?: string } } | undefined
|
|
263
|
+
return buildSessionMenuEntries(getLang(), { hasCheckpoint: target?.value?.sessionId ? true : undefined })
|
|
264
|
+
})
|
|
265
|
+
register('file', 300, (_lang, t) => {
|
|
266
|
+
const target = t as { value?: { path?: string; line?: number } } | undefined
|
|
267
|
+
return buildFileMenuEntries(getLang()).map((e) => ({
|
|
268
|
+
...e,
|
|
269
|
+
instruction: e.id === 'iterate.review-file'
|
|
270
|
+
? instructionByEntry(`file:${target?.value?.path ?? ''}${typeof target?.value?.line === 'number' ? `:${target.value.line}` : ''}`)
|
|
271
|
+
: e.instruction,
|
|
272
|
+
}))
|
|
273
|
+
})
|
|
274
|
+
register('directory', 300, (_lang, t) => {
|
|
275
|
+
const target = t as { value?: { path?: string } } | undefined
|
|
276
|
+
return buildDirectoryMenuEntries(getLang()).map((e) => ({
|
|
277
|
+
...e,
|
|
278
|
+
instruction: e.id === 'iterate.review-dir'
|
|
279
|
+
? instructionByEntry(`dir:${target?.value?.path ?? ''}`)
|
|
280
|
+
: e.instruction,
|
|
281
|
+
}))
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
return () => {
|
|
285
|
+
for (const dispose of unregister.splice(0)) {
|
|
286
|
+
try { dispose() } catch { /* noop */ }
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -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,14 @@ 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'
|
|
135
144
|
|
|
136
145
|
// ─── Module contract ─────────────────────────────────────────────────────────
|
|
137
146
|
|
|
@@ -907,6 +916,47 @@ function readTheme(ctx: ClientContext): ThemeService | undefined {
|
|
|
907
916
|
return undefined
|
|
908
917
|
}
|
|
909
918
|
|
|
919
|
+
/**
|
|
920
|
+
* Resolve the dsh 0.2.2-alpha.3 `contextMenus` service. Unlike slots/theme it
|
|
921
|
+
* is NOT in the typed `ClientContext`, so it is probed through `ctx.get` (the
|
|
922
|
+
* cordis service locator) and structurally validated. A host that predates the
|
|
923
|
+
* capability returns `undefined` and the plugin simply has no menu entries.
|
|
924
|
+
*/
|
|
925
|
+
function readContextMenus(ctx: ClientContext): ContextMenusService | undefined {
|
|
926
|
+
if (!ctx || typeof ctx.get !== 'function') return undefined
|
|
927
|
+
const raw = ctx.get('contextMenus', false)
|
|
928
|
+
if (!raw || typeof raw !== 'object') return undefined
|
|
929
|
+
const svc = raw as { register?: unknown; notify?: unknown }
|
|
930
|
+
if (typeof svc.register !== 'function') return undefined
|
|
931
|
+
if (typeof svc.notify !== 'function') return undefined
|
|
932
|
+
return svc as unknown as ContextMenusService
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* Extract a session identifier from an opaque host-shaped session object.
|
|
937
|
+
* The slot `props.session` shape is host-defined; this probes the common
|
|
938
|
+
* keys so the observatory open-event can target the right session and be
|
|
939
|
+
* ignored by every other mounted panel. Returns `null` when unknown (the
|
|
940
|
+
* panel then responds to any open event — the single-panel fallback).
|
|
941
|
+
*/
|
|
942
|
+
function sessionIdOfSession(session: unknown): string | null {
|
|
943
|
+
if (!session || typeof session !== 'object') return null
|
|
944
|
+
const s = session as Record<string, unknown>
|
|
945
|
+
const candidates = [
|
|
946
|
+
s.sessionId, s.id, s.header, s.session,
|
|
947
|
+
] as unknown[]
|
|
948
|
+
for (const c of candidates) {
|
|
949
|
+
if (typeof c === 'string' && c) return c
|
|
950
|
+
if (c && typeof c === 'object') {
|
|
951
|
+
const o = c as Record<string, unknown>
|
|
952
|
+
for (const k of ['sessionId', 'id']) {
|
|
953
|
+
if (typeof o[k] === 'string' && o[k]) return o[k] as string
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
return null
|
|
958
|
+
}
|
|
959
|
+
|
|
910
960
|
// ─── Module-level runtime state (shared across slots) ────────────────────────
|
|
911
961
|
|
|
912
962
|
let slotsSvc: SlotsService | undefined = undefined
|
|
@@ -915,6 +965,8 @@ let storage: ReturnType<typeof createStorage> | null = null
|
|
|
915
965
|
let themeDisposer: (() => void) | null = null
|
|
916
966
|
let themeListener: (() => void) | null = null
|
|
917
967
|
let themeEnabled = true
|
|
968
|
+
/** v3.14: right-click menu contribution disposer (re-apply() never stacks). */
|
|
969
|
+
let menuDisposer: (() => void) | null = null
|
|
918
970
|
const roundPulseListeners: Array<(payload: { round: number; converged: boolean }) => void> = []
|
|
919
971
|
|
|
920
972
|
/**
|
|
@@ -2392,6 +2444,28 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2392
2444
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2393
2445
|
}, [open])
|
|
2394
2446
|
|
|
2447
|
+
// v3.14: the right-click "open command center" menu action dispatches a
|
|
2448
|
+
// window event; this panel expands and switches tab when the event targets
|
|
2449
|
+
// it (no sessionId = single-panel fallback, a different sessionId = ignore).
|
|
2450
|
+
const panelSessionId = sessionIdOfSession(session)
|
|
2451
|
+
React.useEffect(() => {
|
|
2452
|
+
if (typeof window === 'undefined' || typeof window.addEventListener !== 'function') return
|
|
2453
|
+
const onOpen = (ev: Event) => {
|
|
2454
|
+
const detail = (ev as CustomEvent<ObsOpenDetail>).detail
|
|
2455
|
+
const detailSession = detail && typeof detail.sessionId === 'string' && detail.sessionId
|
|
2456
|
+
? detail.sessionId
|
|
2457
|
+
: null
|
|
2458
|
+
if (detailSession && panelSessionId && detailSession !== panelSessionId) return
|
|
2459
|
+
setOpen(true)
|
|
2460
|
+
const target = detail && typeof detail.tab === 'string' ? detail.tab : 'live'
|
|
2461
|
+
const valid = OBS_TABS.some((x) => x.key === target)
|
|
2462
|
+
setTab(valid ? target : 'live')
|
|
2463
|
+
}
|
|
2464
|
+
window.addEventListener(OBS_OPEN_EVENT, onOpen)
|
|
2465
|
+
return () => window.removeEventListener(OBS_OPEN_EVENT, onOpen)
|
|
2466
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2467
|
+
}, [panelSessionId])
|
|
2468
|
+
|
|
2395
2469
|
/** Copy an instruction, flashing "已复制" on the originating button only when the copy actually succeeded. */
|
|
2396
2470
|
const copyInstruction = (key: string, text: string) => {
|
|
2397
2471
|
if (!text) return
|
|
@@ -3867,6 +3941,49 @@ export function apply(ctx: ClientContext): void {
|
|
|
3867
3941
|
)
|
|
3868
3942
|
}
|
|
3869
3943
|
|
|
3944
|
+
// 5b. v3.14: right-click menu contributions (dsh 0.2.2-alpha.3 unified
|
|
3945
|
+
// context menus). Optional service: a host that predates the capability has
|
|
3946
|
+
// no menu entries, and the docked observatory stays the entry point.
|
|
3947
|
+
{
|
|
3948
|
+
const menuSvc = readContextMenus(ctx)
|
|
3949
|
+
if (menuSvc) {
|
|
3950
|
+
// Resolve a paste-able instruction for a menu action id. Session-level
|
|
3951
|
+
// ids map to the shared builders (the SAME single source of truth the
|
|
3952
|
+
// observatory copy buttons use); file:/dir: ids are the scoped review.
|
|
3953
|
+
const instructionByEntry = (id: string): string => {
|
|
3954
|
+
if (id === 'iterate.preflight') return buildPreflightInstruction()
|
|
3955
|
+
if (id === 'iterate.dry-run') return START_INSTRUCTIONS.reviewOnly
|
|
3956
|
+
if (id === 'iterate.resume') return buildCheckpointResumeInstruction()
|
|
3957
|
+
if (id === 'iterate.pull') return buildDiskSnapshotInstruction()
|
|
3958
|
+
if (id.startsWith('file:')) {
|
|
3959
|
+
const rest = id.slice('file:'.length)
|
|
3960
|
+
const colon = rest.lastIndexOf(':')
|
|
3961
|
+
const path = colon > 0 ? rest.slice(0, colon) : rest
|
|
3962
|
+
const line = colon > 0 ? Number(rest.slice(colon + 1)) : undefined
|
|
3963
|
+
return buildScopedReviewInstruction({ file: path, ...(Number.isInteger(line) && line! > 0 ? { line: line! } : {}) })
|
|
3964
|
+
}
|
|
3965
|
+
if (id.startsWith('dir:')) return buildScopedReviewInstruction({ dir: id.slice('dir:'.length) })
|
|
3966
|
+
return ''
|
|
3967
|
+
}
|
|
3968
|
+
if (menuDisposer) {
|
|
3969
|
+
try { menuDisposer() } catch { /* noop */ }
|
|
3970
|
+
menuDisposer = null
|
|
3971
|
+
}
|
|
3972
|
+
menuDisposer = registerIterateContextMenus(
|
|
3973
|
+
menuSvc,
|
|
3974
|
+
{
|
|
3975
|
+
copy: (text) => copyText(text),
|
|
3976
|
+
openPanel: (detail) => { openObservatory(detail) },
|
|
3977
|
+
notify: (text, success, action) => { menuSvc.notify(text, success, action) },
|
|
3978
|
+
},
|
|
3979
|
+
instructionByEntry,
|
|
3980
|
+
)
|
|
3981
|
+
if (typeof ctx.effect === 'function') {
|
|
3982
|
+
ctx.effect(() => { if (menuDisposer) { menuDisposer(); menuDisposer = null } })
|
|
3983
|
+
}
|
|
3984
|
+
}
|
|
3985
|
+
}
|
|
3986
|
+
|
|
3870
3987
|
// 6: keep theme toggle state consistent across theme/change events.
|
|
3871
3988
|
// Registered through registerThemeListener so repeated apply() and client
|
|
3872
3989
|
// unmounts never leak a second listener.
|