dsh-patch-edit-plus 0.3.0 → 0.3.2

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.ru.md ADDED
@@ -0,0 +1,127 @@
1
+ # dsh-patch-edit-plus
2
+
3
+ Русский | [English](./README.md) | [简体中文](./README.zh.md) | [日本語](./README.ja.md) | [한국어](./README.ko.md) | [Français](./README.fr.md) | [Deutsch](./README.de.md) | [Italiano](./README.it.md) | [Español](./README.es.md)
4
+
5
+ Патчевое редактирование файлов для [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness): единственный доступный модели инструмент `apply_patch`, который принимает **git/unified diff** (по умолчанию) и **синтаксис `apply_patch` от Codex** (по явному включению), применяет каждое изменение по принципу **всё-или-ничего** и абсолютно не мешает работе DSH.
6
+
7
+ ## Совместимость с версиями DSH
8
+
9
+ | Версия DSH | Статус | Примечания |
10
+ |---|---|---|
11
+ | `0.2.0-rc.1+` | ✅ поддерживается (эта линия, v0.3.0+) | Декларативные настройки: `allowCodexPatch` помечен `.volatile()` — форма настроек формируется автоматически, а `loader/volatile-update` перерегистрирует инструмент без перемонтирования. Вызов регистрации не нужен. Delete/Move переведены на foreground-API `ShellExecutor.execute()` + `result()`. |
12
+ | `0.1.7-rc.1+` | ↗ линия сопровождения | Обслуживается веткой `compat/0.1.7` / dist-tag `dsh-0.1.7` (там v0.2.1). Shell-исполнитель 0.2.0 переименовал `run` → `execute` (изменение появилось между 0.1.2 и 0.1.7), поэтому одна кодовая база не может типобезопасно обслуживать обе линии. |
13
+ | `0.1.2-rc.1` ~ `0.1.5-rc.2` | ↗ линия сопровождения | Обслуживается веткой до 0.1.7 / dist-tag `dsh-0.1.5`. В 0.1.7 удалены императивные API настроек, на которых держался двух-API-фолбэк. |
14
+
15
+ На этой линии настройки декларативны: поля, помеченные `.volatile()` в `Config`, становятся формой настроек; плагин подписывается на `loader/volatile-update` вместо получения регистрационных хуков.
16
+
17
+ ## Зачем вообще патчи? (руководство по маршрутизации инструментов)
18
+
19
+ Описание инструмента явно направляет модель:
20
+
21
+ - **Одиночное небольшое изменение в уже прочитанном файле** → предпочитайте нативный инструмент `edit` (патч стоит больше токенов, чем буквальное редактирование).
22
+ - **Небольшое изменение внутри БОЛЬШОГО файла** → `apply_patch` (не нужно повторять весь файл).
23
+ - **Пакетные изменения во МНОГИХ файлах** → `apply_patch` (один вызов, атомарная проверка).
24
+ - **Новый файл с существенным содержимым / удаление / переименование** → `apply_patch`.
25
+
26
+ ## Установка
27
+
28
+ ```bash
29
+ dsh plugin --profile web add <path-to-dsh-patch-edit-plus>
30
+ dsh web --dump-config # verify the plugin row appears
31
+ # restart DSH
32
+ ```
33
+
34
+ ## Использование
35
+
36
+ ### Unified diff (по умолчанию, включён)
37
+
38
+ ```text
39
+ --- a/src/app.ts
40
+ +++ b/src/app.ts
41
+ @@ -10,4 +10,4 @@ export function main() {
42
+ init()
43
+ - start(oldPort)
44
+ + start(newPort)
45
+ await shutdown()
46
+ }
47
+ ```
48
+
49
+ ### Синтаксис apply_patch от Codex (по явному включению)
50
+
51
+ ```text
52
+ *** Begin Patch
53
+ *** Update File: src/app.ts
54
+ @@
55
+ init()
56
+ - start(oldPort)
57
+ + start(newPort)
58
+ *** Add File: docs/notes.md
59
+ +# Notes
60
+ *** Delete File: tmp/junk.txt
61
+ *** Move to: src/app.ts (via `*** Move to:` after *** Update File:)
62
+ *** End Patch
63
+ ```
64
+
65
+ Включается через `allowCodexPatch: true`. Если патч Codex приходит при выключенной опции, инструмент возвращает **действенную подсказку** (как её включить или как повторить попытку в unified diff) вместо общей ошибки разбора — синтаксис Codex является сильным априорным признаком для моделей семейства GPT, и молчаливый сбой затягивает их в циклы повторов.
66
+
67
+ ### Операции
68
+
69
+ | Операция | Unified diff | Codex | Путь выполнения |
70
+ |---|---|---|---|
71
+ | Добавление | `new file mode` + `/dev/null` | `*** Add File:` | `ctx.fs.writeText` (официальная intent-последовательность) |
72
+ | Обновление (multi-hunk) | `@@` hunks | `@@` hunks | `ctx.fs.writeText` |
73
+ | Удаление | `deleted file mode` + `/dev/null` | `*** Delete File:` | `ctx.shell` (с учётом sandbox, путь передаётся через env) |
74
+ | Перемещение / переименование | `rename from/to` | `*** Move to:` | `ctx.shell` |
75
+
76
+ ## Гарантии
77
+
78
+ - **Всё-или-ничего**: весь патч сначала проверяется на соответствие текущему содержимому файлов (локализация контекста, удержание в рабочей области, политика symlink, дубликаты путей); любой сбой прерывает операцию без записи хотя бы байта.
79
+ - **Соблюдение запрета на запись без чтения**: каждая запись воспроизводит intent-последовательность официального инструмента `write` — водопад `fs/write-intent` → защищённый `writeText` → эмиссия `fs/observed`. Голый `writeText` молча обходил бы этот барьер, поскольку провайдеры DSH никогда сами не диспетчеризуют события `fs/*`.
80
+ - **Точная диагностика сбоев**: при несовпадении хунка ошибка содержит файл + индекс хунка, исходную строку поиска, превью ожидаемой строки (с показанными пробелами), фактический фрагмент файла и адресную подсказку.
81
+ - **Точность переводов строк**: файлы CRLF записываются обратно как CRLF; окончания без завершающего перевода строки сохраняются, если только патч не меняет окончание.
82
+ - **Чистое дополнение**: плагин регистрирует ровно один инструмент, никогда не вызывает `tools.restrict()`, никогда не переопределяет нативный инструмент, не регистрирует глобальных служб и при выгрузке удаляет всё. Конфликты имён с другими провайдерами `apply_patch` (`bainianlaoyao/dsh-codex-mode`, `shuind/dsh-codex-harness`, …) разрешаются избеганием переименования (`apply_patch_1`, `…_2`, …), так что DSH никогда не падает при старте.
83
+
84
+ ## Конфигурация
85
+
86
+ | Опция | По умолчанию | Описание |
87
+ |---|---|---|
88
+ | `toolName` | `apply_patch` | Имя инструмента, доступного модели. |
89
+ | `conflictPolicy` | `rename` | `rename` / `skip` / `fail`, если имя инструмента уже занято. |
90
+ | `renameSuffix` | `_1` | Суффикс, используемый при избегании переименования. |
91
+ | `allowUnifiedDiff` | `true` | Принимать git/unified diff. |
92
+ | `allowCodexPatch` | `false` | Принимать синтаксис `apply_patch` от Codex. Также редактируется в панели настроек DSH; изменения вступают в силу немедленно, без перезапуска. |
93
+ | `deleteBackend` | `shell` | `shell` или `none` (Delete/Move возвращают структурированную ошибку). |
94
+ | `shellDialect` | `auto` | `auto` (pwsh на win32) / `posix` / `pwsh`. |
95
+ | `deleteCommand` / `moveCommand` | built-in | Пользовательские шаблоны команд. Пути по-прежнему передаются **только через переменные окружения** (`DSH_PATCH_TARGET` / `DSH_PATCH_SOURCE`); никогда не подставляйте их в строку команды интерполяцией. |
96
+ | `dryRunByDefault` | `false` | Считать вызовы пробными запусками (dry run), если не задано `dryRun: false`. |
97
+ | `followSymlinks` | `false` | Разрешить путям патча переходить по symlink. |
98
+ | `maxFiles` | `50` | Максимум файловых секций на один патч. |
99
+ | `maxPatchBytes` | `524288` | Максимальный размер текста патча в байтах. |
100
+ | `maxDiffBytes` | `16384` | Максимальный размер диффа на файл, сохраняемого в метаданных результата (большие диффы опустошаются и помечаются `truncated`). |
101
+
102
+ ## Ограничения (раскрытые)
103
+
104
+ - **Delete/Move выполняются через `ctx.shell`.** Сила sandbox такова, каков обеспечивающий её загруженный shell-исполнитель (`bash-sandbox` изолирует; `bash-local` — нет) — та же позиция риска, что и у нативного инструмента bash. Каждый запрос несёт `sandboxPolicy` и сообщает факты о sandbox, чтобы «отказ политики» можно было отличить от «сбоя команды».
105
+ - **Каждая запись и каждый Delete/Move несут политику в области действия сессии.** `apply_patch` разрешает `ctx.sandboxPolicy.resolve({ session })` при каждом вызове — переопределение режима сессии плюс её cwd как корень рабочей области — в точности как нативные инструменты `write`/`edit`, и разрешает пути плана относительно того же корня. Без этого файловая система принуждения откатывается к корню развёртывания, из-за чего записи внутри рабочей области завершаются отказами `workspace-write` даже в сессии `danger-full-access`. Отказ `workspace-write` проявляется как `PatchError`, сообщение которой несёт текст бэкенда (структурированный маркер `[sandbox: …]` и поля эскалации в том же ходе не реализованы; если нужна эскалация, пользуйтесь нативными `write`/`edit`).
106
+ - **Add не создаёт родительские каталоги.** Это соответствует нативному инструменту `write` (у `ctx.fs` нет mkdir); в ошибке называется недостающий каталог.
107
+ - Нет нечёткого/со смещением сопоставления: локализация хунка точная → `trimEnd` → `trim`, причём намеренно (нечёткое сопоставление небезопасно для разрушающих операций; оно есть в дорожной карте).
108
+ - Двоичные патчи отклоняются с понятной ошибкой.
109
+
110
+ ## Разработка
111
+
112
+ ```bash
113
+ npm install
114
+ npm run typecheck # against 0.2.0-rc.1 peers (devDependencies)
115
+ npm run typecheck:0.2.0 # against real 0.2.0-rc.1 peer packages installed from the registry (dual-baseline proof)
116
+ npm test # vitest, 92 tests
117
+ npm run lint
118
+ npm run build # lib/
119
+ npm run verify:source # static safety assertions (intent dance, no node:fs, …)
120
+ npm run smoke # load-level smoke against a stub host
121
+ ```
122
+
123
+ Ручная проверка на живом DSH (`dsh plugin --profile web add` → перезапуск → вызов инструмента в сессии) намеренно оставлена оператору.
124
+
125
+ ## Лицензия
126
+
127
+ MIT
package/README.zh.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # dsh-patch-edit-plus
2
2
 
3
+ [English](./README.md) | 简体中文 | [日本語](./README.ja.md) | [한국어](./README.ko.md) | [Français](./README.fr.md) | [Deutsch](./README.de.md) | [Italiano](./README.it.md) | [Русский](./README.ru.md) | [Español](./README.es.md)
4
+
3
5
  面向 [DeepSeek Harness(DSH)](https://github.com/deepseek-ai/deepseek-harness) 的补丁风格文件编辑插件:只提供 **1 个**模型可见工具 `apply_patch`,接受 **git/unified diff**(默认开启)与 **Codex `apply_patch` 语法**(可选开启),全量验证后**原子应用**,对 DSH 既有功能零负面影响。
4
6
 
5
7
  ## DSH 版本适配矩阵
package/dsh.plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "dsh-patch-edit-plus",
3
3
  "name": "dsh-patch-edit-plus",
4
- "version": "0.3.0",
4
+ "version": "0.3.1",
5
5
  "description": {
6
6
  "en": "Patch-style file editing: one apply_patch tool for git/unified diff (default) and Codex apply_patch syntax (opt-in), with all-or-nothing application and sandbox-aware delete/move.",
7
7
  "zh": "补丁风格文件编辑:单个 apply_patch 工具,接受 git/unified diff(默认)与 Codex apply_patch 语法(可选开启),全量验证后原子应用,删除/移动走沙箱感知 shell。"
@@ -0,0 +1,135 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * dsh-patch-edit-plus — 浏览器 half(0.2.0 新增)。
4
+ *
5
+ * 单一职责:在 Plugins 页 bundle 详情页的 `plugins.bundle.config` 席位
6
+ * (key = 包名)下注册一张配置卡,暴露两个补丁风格开关——
7
+ * - `allowUnifiedDiff`(Git/unified diff,默认开)
8
+ * - `allowCodexPatch`(Codex apply_patch,默认关)
9
+ *
10
+ * 字段全部是 host 侧 `Config` 的 `.volatile()` 字段:卡片经
11
+ * `configForms.get('dsh-patch-edit-plus')` 自绑本插件 entry(entry id 与
12
+ * 包名一致,无错位),写入走 `form.set(field, !cur)`,提交后宿主经
13
+ * `loader/volatile-update` 原地重注册工具(见 src/index.ts 的 rejudge)。
14
+ *
15
+ * 构建:tsdown → lib/client.js(__ModuleLoader__.load 闭包工厂,id = 包名)。
16
+ * react 是唯一 value-import(平台 external);slots/configForms 经 cordis
17
+ * 服务消费,configForms 缺席时(理论上仅剩极老宿主)卡片整卡隐藏,不阻塞
18
+ * 客户端半。文案内联双语字典(zh/en),不建 locale 文件。
19
+ */
20
+ import { createElement, useSyncExternalStore } from 'react';
21
+ /** 客户端所需服务:slots(席位注册)+ configForms(entry 配置读写)。同族插件
22
+ * (session-guard/perm-gate)同样显式声明——runner 只把声明了的服务提供给模块
23
+ * 上下文,不声明时 `ctx.get('configForms')` 恒为 undefined,卡会渲染成 null。 */
24
+ export const inject = ['slots', 'configForms'];
25
+ /** 内联双语字典(zh/en;按 document.documentElement.lang → navigator.language 兜底)。 */
26
+ const STRINGS = {
27
+ zh: {
28
+ cardName: '补丁风格',
29
+ cardDesc: 'apply_patch 接受的补丁语法',
30
+ unifiedTitle: 'Git 风格补丁',
31
+ unifiedDesc: '接受 Git/unified diff 补丁语法(默认开)',
32
+ codexTitle: 'Codex 风格补丁',
33
+ codexDesc: '接受 Codex apply_patch 语法(默认关)',
34
+ readonlyHint: '当前只读,无法修改。',
35
+ },
36
+ en: {
37
+ cardName: 'Patch style',
38
+ cardDesc: 'Patch syntax accepted by apply_patch',
39
+ unifiedTitle: 'Git-style patches',
40
+ unifiedDesc: 'Accept Git/unified diff patch syntax (default on)',
41
+ codexTitle: 'Codex-style patches',
42
+ codexDesc: 'Accept Codex apply_patch syntax (default off)',
43
+ readonlyHint: 'Read-only right now; changes are not allowed.',
44
+ },
45
+ };
46
+ /** 解析当前语言:document.documentElement.lang 优先,navigator.language 兜底,再退 zh。 */
47
+ function pickLang() {
48
+ try {
49
+ const docLang = typeof document !== 'undefined' ? document.documentElement.lang : '';
50
+ const navLang = typeof navigator !== 'undefined' ? navigator.language : '';
51
+ for (const tag of [docLang, navLang]) {
52
+ if (typeof tag === 'string' && /^zh/i.test(tag))
53
+ return 'zh';
54
+ if (typeof tag === 'string' && /^en/i.test(tag))
55
+ return 'en';
56
+ }
57
+ }
58
+ catch {
59
+ // 非 DOM 环境保底 zh
60
+ }
61
+ return 'zh';
62
+ }
63
+ /** 卡片样式,注入一次(保持 bundle CSS-free;变量与 session-guard 卡同源)。 */
64
+ const CARD_CSS = `
65
+ .ppeCard{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:4px 16px 8px}
66
+ .ppeCardName{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4;margin:10px 0 2px}
67
+ .ppeCardDesc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5;margin:0 0 4px}
68
+ .ppeRow{border-bottom:1px solid var(--dsw-alias-border-l2);align-items:center;gap:8px;padding:14px 0;display:flex}
69
+ .ppeRow:last-of-type{border-bottom:0}
70
+ .ppeRowText{flex-direction:column;flex:1;gap:4px;min-width:0;padding-right:48px;display:flex}
71
+ .ppeTitle{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}
72
+ .ppeDesc{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:400;line-height:18px}
73
+ .ppeSwitch{appearance:none;position:relative;width:40px;height:22px;flex:none;cursor:pointer;background:transparent;border:0;padding:0}
74
+ .ppeSwitch:disabled{cursor:not-allowed}
75
+ .ppeSwitchTrack{position:absolute;inset:0;border-radius:22px;background:var(--dsw-alias-interactive-bg-hover);transition:background .16s}
76
+ .ppeSwitch-on>.ppeSwitchTrack{background:var(--dsw-alias-button-primary-fill)}
77
+ .ppeSwitchThumb{position:absolute;top:2px;left:2px;width:18px;height:18px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);transition:transform .16s}
78
+ .ppeSwitch-on>.ppeSwitchThumb{transform:translateX(18px)}
79
+ .ppeSwitch:disabled>.ppeSwitchTrack{opacity:.5}
80
+ .ppeReadonly{color:var(--dsw-alias-label-tertiary);font-size:12px;margin:8px 0 4px}
81
+ `;
82
+ /** 注入一次卡片样式。 */
83
+ function injectCss() {
84
+ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css="patch-edit-plus-card"]') === null) {
85
+ const tag = document.createElement('style');
86
+ tag.dataset.plugin = 'dsh-patch-edit-plus';
87
+ tag.dataset.pluginCss = 'patch-edit-plus-card';
88
+ tag.textContent = CARD_CSS;
89
+ document.head.appendChild(tag);
90
+ }
91
+ }
92
+ const NOOP_SNAPSHOT = { status: 'unavailable', value: undefined, writable: false };
93
+ const unsubscribeNoop = () => { };
94
+ /** 一行 role="switch" 开关(aria-checked + data-* 便于测试选择器)。 */
95
+ function SwitchRow(props) {
96
+ const { field, title, description, checked, disabled, onToggle } = props;
97
+ return (_jsxs("div", { className: "ppeRow", children: [_jsxs("div", { className: "ppeRowText", children: [_jsx("div", { className: "ppeTitle", children: title }), _jsx("div", { className: "ppeDesc", children: description })] }), _jsxs("button", { type: "button", role: "switch", "aria-checked": checked, "data-plugin": "dsh-patch-edit-plus", "data-field": field, "data-checked": checked ? 'true' : 'false', className: 'ppeSwitch' + (checked ? ' ppeSwitch-on' : ''), disabled: disabled, onClick: () => onToggle(field, checked), children: [_jsx("span", { className: "ppeSwitchTrack" }), _jsx("span", { className: "ppeSwitchThumb" })] })] }));
98
+ }
99
+ /**
100
+ * 插件页配置卡主体:两行补丁风格开关。
101
+ * - status === 'unavailable'(无 configForms / entry 未装配)→ 整卡隐藏(null);
102
+ * - status === 'loading' → 开关禁用;
103
+ * - 仅 snapshot.writable 时可写(写路径 `form.set(field, !cur)`)。
104
+ */
105
+ export function PatchStyleCard(props) {
106
+ const form = props.form;
107
+ const snapshot = useSyncExternalStore(form
108
+ ? (listener) => form.subscribe(listener)
109
+ : () => unsubscribeNoop, () => form?.getSnapshot() ?? NOOP_SNAPSHOT);
110
+ injectCss();
111
+ if (snapshot.status === 'unavailable')
112
+ return null;
113
+ const t = STRINGS[pickLang()];
114
+ // loading / 只读都归约为「不可写」:开关禁用,仅 ready + writable 时可写。
115
+ const editable = snapshot.status === 'ready' && snapshot.writable;
116
+ const value = snapshot.value ?? {};
117
+ const toggle = (field, current) => {
118
+ if (form === undefined || !editable)
119
+ return;
120
+ void form.set(field, !current).catch(() => { });
121
+ };
122
+ return (_jsxs("section", { className: "ppeCard", "data-plugin": "dsh-patch-edit-plus", children: [_jsx("div", { className: "ppeCardName", children: t.cardName }), _jsx("p", { className: "ppeCardDesc", children: t.cardDesc }), _jsx(SwitchRow, { field: "allowUnifiedDiff", title: t.unifiedTitle, description: t.unifiedDesc, checked: value.allowUnifiedDiff ?? true, disabled: !editable, onToggle: toggle }), _jsx(SwitchRow, { field: "allowCodexPatch", title: t.codexTitle, description: t.codexDesc, checked: value.allowCodexPatch ?? false, disabled: !editable, onToggle: toggle }), snapshot.status === 'ready' && !snapshot.writable && _jsx("p", { className: "ppeReadonly", children: t.readonlyHint })] }));
123
+ }
124
+ /**
125
+ * 客户端插件体:在 Plugins 页 bundle 详情页的 keyed 席位(key = 包名)下
126
+ * 注册配置卡。卡片自绑 `configForms.get('dsh-patch-edit-plus')`——form 在
127
+ * 渲染期解析(而非 apply 期固化),宿主晚于客户端半开始服务该 entry 时
128
+ * 也能追上;页面注入的 owner props(`{ view: 'page', form }`)不消费,
129
+ * 以本插件 entry 的作用域为唯一事实来源。
130
+ */
131
+ export function apply(ctx) {
132
+ const configForms = (ctx.configForms
133
+ ?? (typeof ctx.get === 'function' ? ctx.get('configForms') : undefined));
134
+ ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({ name: 'plugins.bundle.config', key: 'dsh-patch-edit-plus' }, () => createElement(PatchStyleCard, { form: configForms?.get?.('dsh-patch-edit-plus') })));
135
+ }
package/lib/client.js ADDED
@@ -0,0 +1,203 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-patch-edit-plus",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ let react_jsx_runtime = require("react/jsx-runtime");
9
+ //#region src/client/index.tsx
10
+ /**
11
+ * dsh-patch-edit-plus — 浏览器 half(0.2.0 新增)。
12
+ *
13
+ * 单一职责:在 Plugins 页 bundle 详情页的 `plugins.bundle.config` 席位
14
+ * (key = 包名)下注册一张配置卡,暴露两个补丁风格开关——
15
+ * - `allowUnifiedDiff`(Git/unified diff,默认开)
16
+ * - `allowCodexPatch`(Codex apply_patch,默认关)
17
+ *
18
+ * 字段全部是 host 侧 `Config` 的 `.volatile()` 字段:卡片经
19
+ * `configForms.get('dsh-patch-edit-plus')` 自绑本插件 entry(entry id 与
20
+ * 包名一致,无错位),写入走 `form.set(field, !cur)`,提交后宿主经
21
+ * `loader/volatile-update` 原地重注册工具(见 src/index.ts 的 rejudge)。
22
+ *
23
+ * 构建:tsdown → lib/client.js(__ModuleLoader__.load 闭包工厂,id = 包名)。
24
+ * react 是唯一 value-import(平台 external);slots/configForms 经 cordis
25
+ * 服务消费,configForms 缺席时(理论上仅剩极老宿主)卡片整卡隐藏,不阻塞
26
+ * 客户端半。文案内联双语字典(zh/en),不建 locale 文件。
27
+ */
28
+ /** 客户端所需服务:slots(席位注册)+ configForms(entry 配置读写)。同族插件
29
+ * (session-guard/perm-gate)同样显式声明——runner 只把声明了的服务提供给模块
30
+ * 上下文,不声明时 `ctx.get('configForms')` 恒为 undefined,卡会渲染成 null。 */
31
+ const inject = ["slots", "configForms"];
32
+ /** 内联双语字典(zh/en;按 document.documentElement.lang → navigator.language 兜底)。 */
33
+ const STRINGS = {
34
+ zh: {
35
+ cardName: "补丁风格",
36
+ cardDesc: "apply_patch 接受的补丁语法",
37
+ unifiedTitle: "Git 风格补丁",
38
+ unifiedDesc: "接受 Git/unified diff 补丁语法(默认开)",
39
+ codexTitle: "Codex 风格补丁",
40
+ codexDesc: "接受 Codex apply_patch 语法(默认关)",
41
+ readonlyHint: "当前只读,无法修改。"
42
+ },
43
+ en: {
44
+ cardName: "Patch style",
45
+ cardDesc: "Patch syntax accepted by apply_patch",
46
+ unifiedTitle: "Git-style patches",
47
+ unifiedDesc: "Accept Git/unified diff patch syntax (default on)",
48
+ codexTitle: "Codex-style patches",
49
+ codexDesc: "Accept Codex apply_patch syntax (default off)",
50
+ readonlyHint: "Read-only right now; changes are not allowed."
51
+ }
52
+ };
53
+ /** 解析当前语言:document.documentElement.lang 优先,navigator.language 兜底,再退 zh。 */
54
+ function pickLang() {
55
+ try {
56
+ const docLang = typeof document !== "undefined" ? document.documentElement.lang : "";
57
+ const navLang = typeof navigator !== "undefined" ? navigator.language : "";
58
+ for (const tag of [docLang, navLang]) {
59
+ if (typeof tag === "string" && /^zh/i.test(tag)) return "zh";
60
+ if (typeof tag === "string" && /^en/i.test(tag)) return "en";
61
+ }
62
+ } catch {}
63
+ return "zh";
64
+ }
65
+ /** 卡片样式,注入一次(保持 bundle CSS-free;变量与 session-guard 卡同源)。 */
66
+ const CARD_CSS = `
67
+ .ppeCard{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:4px 16px 8px}
68
+ .ppeCardName{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4;margin:10px 0 2px}
69
+ .ppeCardDesc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5;margin:0 0 4px}
70
+ .ppeRow{border-bottom:1px solid var(--dsw-alias-border-l2);align-items:center;gap:8px;padding:14px 0;display:flex}
71
+ .ppeRow:last-of-type{border-bottom:0}
72
+ .ppeRowText{flex-direction:column;flex:1;gap:4px;min-width:0;padding-right:48px;display:flex}
73
+ .ppeTitle{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}
74
+ .ppeDesc{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:400;line-height:18px}
75
+ .ppeSwitch{appearance:none;position:relative;width:40px;height:22px;flex:none;cursor:pointer;background:transparent;border:0;padding:0}
76
+ .ppeSwitch:disabled{cursor:not-allowed}
77
+ .ppeSwitchTrack{position:absolute;inset:0;border-radius:22px;background:var(--dsw-alias-interactive-bg-hover);transition:background .16s}
78
+ .ppeSwitch-on>.ppeSwitchTrack{background:var(--dsw-alias-button-primary-fill)}
79
+ .ppeSwitchThumb{position:absolute;top:2px;left:2px;width:18px;height:18px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);transition:transform .16s}
80
+ .ppeSwitch-on>.ppeSwitchThumb{transform:translateX(18px)}
81
+ .ppeSwitch:disabled>.ppeSwitchTrack{opacity:.5}
82
+ .ppeReadonly{color:var(--dsw-alias-label-tertiary);font-size:12px;margin:8px 0 4px}
83
+ `;
84
+ /** 注入一次卡片样式。 */
85
+ function injectCss() {
86
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"patch-edit-plus-card\"]") === null) {
87
+ const tag = document.createElement("style");
88
+ tag.dataset.plugin = "dsh-patch-edit-plus";
89
+ tag.dataset.pluginCss = "patch-edit-plus-card";
90
+ tag.textContent = CARD_CSS;
91
+ document.head.appendChild(tag);
92
+ }
93
+ }
94
+ const NOOP_SNAPSHOT = {
95
+ status: "unavailable",
96
+ value: void 0,
97
+ writable: false
98
+ };
99
+ const unsubscribeNoop = () => {};
100
+ /** 一行 role="switch" 开关(aria-checked + data-* 便于测试选择器)。 */
101
+ function SwitchRow(props) {
102
+ const { field, title, description, checked, disabled, onToggle } = props;
103
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
104
+ className: "ppeRow",
105
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
106
+ className: "ppeRowText",
107
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
108
+ className: "ppeTitle",
109
+ children: title
110
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
111
+ className: "ppeDesc",
112
+ children: description
113
+ })]
114
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
115
+ type: "button",
116
+ role: "switch",
117
+ "aria-checked": checked,
118
+ "data-plugin": "dsh-patch-edit-plus",
119
+ "data-field": field,
120
+ "data-checked": checked ? "true" : "false",
121
+ className: "ppeSwitch" + (checked ? " ppeSwitch-on" : ""),
122
+ disabled,
123
+ onClick: () => onToggle(field, checked),
124
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "ppeSwitchTrack" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "ppeSwitchThumb" })]
125
+ })]
126
+ });
127
+ }
128
+ /**
129
+ * 插件页配置卡主体:两行补丁风格开关。
130
+ * - status === 'unavailable'(无 configForms / entry 未装配)→ 整卡隐藏(null);
131
+ * - status === 'loading' → 开关禁用;
132
+ * - 仅 snapshot.writable 时可写(写路径 `form.set(field, !cur)`)。
133
+ */
134
+ function PatchStyleCard(props) {
135
+ const form = props.form;
136
+ const snapshot = (0, react.useSyncExternalStore)(form ? (listener) => form.subscribe(listener) : () => unsubscribeNoop, () => form?.getSnapshot() ?? NOOP_SNAPSHOT);
137
+ injectCss();
138
+ if (snapshot.status === "unavailable") return null;
139
+ const t = STRINGS[pickLang()];
140
+ const editable = snapshot.status === "ready" && snapshot.writable;
141
+ const value = snapshot.value ?? {};
142
+ const toggle = (field, current) => {
143
+ if (form === void 0 || !editable) return;
144
+ form.set(field, !current).catch(() => {});
145
+ };
146
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
147
+ className: "ppeCard",
148
+ "data-plugin": "dsh-patch-edit-plus",
149
+ children: [
150
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
151
+ className: "ppeCardName",
152
+ children: t.cardName
153
+ }),
154
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
155
+ className: "ppeCardDesc",
156
+ children: t.cardDesc
157
+ }),
158
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwitchRow, {
159
+ field: "allowUnifiedDiff",
160
+ title: t.unifiedTitle,
161
+ description: t.unifiedDesc,
162
+ checked: value.allowUnifiedDiff ?? true,
163
+ disabled: !editable,
164
+ onToggle: toggle
165
+ }),
166
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwitchRow, {
167
+ field: "allowCodexPatch",
168
+ title: t.codexTitle,
169
+ description: t.codexDesc,
170
+ checked: value.allowCodexPatch ?? false,
171
+ disabled: !editable,
172
+ onToggle: toggle
173
+ }),
174
+ snapshot.status === "ready" && !snapshot.writable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
175
+ className: "ppeReadonly",
176
+ children: t.readonlyHint
177
+ })
178
+ ]
179
+ });
180
+ }
181
+ /**
182
+ * 客户端插件体:在 Plugins 页 bundle 详情页的 keyed 席位(key = 包名)下
183
+ * 注册配置卡。卡片自绑 `configForms.get('dsh-patch-edit-plus')`——form 在
184
+ * 渲染期解析(而非 apply 期固化),宿主晚于客户端半开始服务该 entry 时
185
+ * 也能追上;页面注入的 owner props(`{ view: 'page', form }`)不消费,
186
+ * 以本插件 entry 的作用域为唯一事实来源。
187
+ */
188
+ function apply(ctx) {
189
+ const configForms = ctx.configForms ?? (typeof ctx.get === "function" ? ctx.get("configForms") : void 0);
190
+ ctx.slots.inject("plugins.bundle.config", () => ctx.slots.register({
191
+ name: "plugins.bundle.config",
192
+ key: "dsh-patch-edit-plus"
193
+ }, () => (0, react.createElement)(PatchStyleCard, { form: configForms?.get?.("dsh-patch-edit-plus") })));
194
+ }
195
+ //#endregion
196
+ exports.PatchStyleCard = PatchStyleCard;
197
+ exports.apply = apply;
198
+ exports.inject = inject;
199
+ return module.exports;
200
+ }
201
+ });
202
+
203
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","names":["useSyncExternalStore","createElement"],"sources":["../src/client/index.tsx"],"sourcesContent":["/**\n * dsh-patch-edit-plus — 浏览器 half(0.2.0 新增)。\n *\n * 单一职责:在 Plugins 页 bundle 详情页的 `plugins.bundle.config` 席位\n * (key = 包名)下注册一张配置卡,暴露两个补丁风格开关——\n * - `allowUnifiedDiff`(Git/unified diff,默认开)\n * - `allowCodexPatch`(Codex apply_patch,默认关)\n *\n * 字段全部是 host 侧 `Config` 的 `.volatile()` 字段:卡片经\n * `configForms.get('dsh-patch-edit-plus')` 自绑本插件 entry(entry id 与\n * 包名一致,无错位),写入走 `form.set(field, !cur)`,提交后宿主经\n * `loader/volatile-update` 原地重注册工具(见 src/index.ts 的 rejudge)。\n *\n * 构建:tsdown → lib/client.js(__ModuleLoader__.load 闭包工厂,id = 包名)。\n * react 是唯一 value-import(平台 external);slots/configForms 经 cordis\n * 服务消费,configForms 缺席时(理论上仅剩极老宿主)卡片整卡隐藏,不阻塞\n * 客户端半。文案内联双语字典(zh/en),不建 locale 文件。\n */\nimport { createElement, useSyncExternalStore } from 'react'\nimport type { JSX } from 'react'\n\n/** 本插件消费的 configForms 值形状(与 host ResolvedConfig 的易变布尔对齐)。 */\nexport interface PatchEditPlusConfig {\n allowUnifiedDiff: boolean\n allowCodexPatch: boolean\n}\n\n/** configForms 作用域面(结构性子集;0.1.7+ 缝,避免硬 peer 类型)。 */\nexport interface PatchFormScope {\n getSnapshot(): {\n status: 'loading' | 'ready' | 'unavailable'\n value: Partial<PatchEditPlusConfig> | undefined\n writable: boolean\n }\n subscribe(listener: () => void): () => void\n set(field: string, value: unknown): Promise<boolean>\n}\n\n/** configForms 服务面(以 entry id 取句柄)。 */\ninterface ConfigFormsFace {\n get?(entryId: string): PatchFormScope | undefined\n}\n\n/** 客户端所需服务:slots(席位注册)+ configForms(entry 配置读写)。同族插件\n * (session-guard/perm-gate)同样显式声明——runner 只把声明了的服务提供给模块\n * 上下文,不声明时 `ctx.get('configForms')` 恒为 undefined,卡会渲染成 null。 */\nexport const inject = ['slots', 'configForms']\n\n/** 轻量 ctx 类型(仅本客户端用到的方法;构建时类型被剥离)。 */\ninterface SlotsFace {\n inject: (_name: string, _fn: () => unknown) => () => void\n register: (_options: Record<string, unknown>, _component: unknown) => () => void\n}\ninterface ClientCtx {\n slots: SlotsFace\n get?: (_name: string) => unknown\n}\n\n/** 内联双语字典(zh/en;按 document.documentElement.lang → navigator.language 兜底)。 */\nconst STRINGS = {\n zh: {\n cardName: '补丁风格',\n cardDesc: 'apply_patch 接受的补丁语法',\n unifiedTitle: 'Git 风格补丁',\n unifiedDesc: '接受 Git/unified diff 补丁语法(默认开)',\n codexTitle: 'Codex 风格补丁',\n codexDesc: '接受 Codex apply_patch 语法(默认关)',\n readonlyHint: '当前只读,无法修改。',\n },\n en: {\n cardName: 'Patch style',\n cardDesc: 'Patch syntax accepted by apply_patch',\n unifiedTitle: 'Git-style patches',\n unifiedDesc: 'Accept Git/unified diff patch syntax (default on)',\n codexTitle: 'Codex-style patches',\n codexDesc: 'Accept Codex apply_patch syntax (default off)',\n readonlyHint: 'Read-only right now; changes are not allowed.',\n },\n} as const\n\ntype Lang = keyof typeof STRINGS\n\n/** 解析当前语言:document.documentElement.lang 优先,navigator.language 兜底,再退 zh。 */\nfunction pickLang(): Lang {\n try {\n const docLang = typeof document !== 'undefined' ? document.documentElement.lang : ''\n const navLang = typeof navigator !== 'undefined' ? navigator.language : ''\n for (const tag of [docLang, navLang]) {\n if (typeof tag === 'string' && /^zh/i.test(tag)) return 'zh'\n if (typeof tag === 'string' && /^en/i.test(tag)) return 'en'\n }\n } catch {\n // 非 DOM 环境保底 zh\n }\n return 'zh'\n}\n\n/** 卡片样式,注入一次(保持 bundle CSS-free;变量与 session-guard 卡同源)。 */\nconst CARD_CSS = `\n.ppeCard{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:4px 16px 8px}\n.ppeCardName{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4;margin:10px 0 2px}\n.ppeCardDesc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5;margin:0 0 4px}\n.ppeRow{border-bottom:1px solid var(--dsw-alias-border-l2);align-items:center;gap:8px;padding:14px 0;display:flex}\n.ppeRow:last-of-type{border-bottom:0}\n.ppeRowText{flex-direction:column;flex:1;gap:4px;min-width:0;padding-right:48px;display:flex}\n.ppeTitle{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}\n.ppeDesc{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:400;line-height:18px}\n.ppeSwitch{appearance:none;position:relative;width:40px;height:22px;flex:none;cursor:pointer;background:transparent;border:0;padding:0}\n.ppeSwitch:disabled{cursor:not-allowed}\n.ppeSwitchTrack{position:absolute;inset:0;border-radius:22px;background:var(--dsw-alias-interactive-bg-hover);transition:background .16s}\n.ppeSwitch-on>.ppeSwitchTrack{background:var(--dsw-alias-button-primary-fill)}\n.ppeSwitchThumb{position:absolute;top:2px;left:2px;width:18px;height:18px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);transition:transform .16s}\n.ppeSwitch-on>.ppeSwitchThumb{transform:translateX(18px)}\n.ppeSwitch:disabled>.ppeSwitchTrack{opacity:.5}\n.ppeReadonly{color:var(--dsw-alias-label-tertiary);font-size:12px;margin:8px 0 4px}\n`\n\n/** 注入一次卡片样式。 */\nfunction injectCss(): void {\n if (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css=\"patch-edit-plus-card\"]') === null) {\n const tag = document.createElement('style')\n tag.dataset.plugin = 'dsh-patch-edit-plus'\n tag.dataset.pluginCss = 'patch-edit-plus-card'\n tag.textContent = CARD_CSS\n document.head.appendChild(tag)\n }\n}\n\nconst NOOP_SNAPSHOT = { status: 'unavailable' as const, value: undefined, writable: false }\nconst unsubscribeNoop = (): void => {}\n\n/** 一行 role=\"switch\" 开关(aria-checked + data-* 便于测试选择器)。 */\nfunction SwitchRow(props: {\n field: 'allowUnifiedDiff' | 'allowCodexPatch'\n title: string\n description: string\n checked: boolean\n disabled: boolean\n onToggle: (_field: 'allowUnifiedDiff' | 'allowCodexPatch', _current: boolean) => void\n}): JSX.Element {\n const { field, title, description, checked, disabled, onToggle } = props\n return (\n <div className=\"ppeRow\">\n <div className=\"ppeRowText\">\n <div className=\"ppeTitle\">{title}</div>\n <div className=\"ppeDesc\">{description}</div>\n </div>\n <button\n type=\"button\"\n role=\"switch\"\n aria-checked={checked}\n data-plugin=\"dsh-patch-edit-plus\"\n data-field={field}\n data-checked={checked ? 'true' : 'false'}\n className={'ppeSwitch' + (checked ? ' ppeSwitch-on' : '')}\n disabled={disabled}\n onClick={() => onToggle(field, checked)}\n >\n <span className=\"ppeSwitchTrack\" />\n <span className=\"ppeSwitchThumb\" />\n </button>\n </div>\n )\n}\n\n/**\n * 插件页配置卡主体:两行补丁风格开关。\n * - status === 'unavailable'(无 configForms / entry 未装配)→ 整卡隐藏(null);\n * - status === 'loading' → 开关禁用;\n * - 仅 snapshot.writable 时可写(写路径 `form.set(field, !cur)`)。\n */\nexport function PatchStyleCard(props: { form?: PatchFormScope | undefined }): JSX.Element | null {\n const form = props.form\n const snapshot = useSyncExternalStore(\n form\n ? (listener) => form.subscribe(listener)\n : () => unsubscribeNoop,\n () => form?.getSnapshot() ?? NOOP_SNAPSHOT,\n )\n\n injectCss()\n\n if (snapshot.status === 'unavailable') return null\n const t = STRINGS[pickLang()]\n // loading / 只读都归约为「不可写」:开关禁用,仅 ready + writable 时可写。\n const editable = snapshot.status === 'ready' && snapshot.writable\n const value = snapshot.value ?? {}\n const toggle = (field: 'allowUnifiedDiff' | 'allowCodexPatch', current: boolean): void => {\n if (form === undefined || !editable) return\n void form.set(field, !current).catch(() => {})\n }\n return (\n <section className=\"ppeCard\" data-plugin=\"dsh-patch-edit-plus\">\n <div className=\"ppeCardName\">{t.cardName}</div>\n <p className=\"ppeCardDesc\">{t.cardDesc}</p>\n <SwitchRow\n field=\"allowUnifiedDiff\"\n title={t.unifiedTitle}\n description={t.unifiedDesc}\n checked={value.allowUnifiedDiff ?? true}\n disabled={!editable}\n onToggle={toggle}\n />\n <SwitchRow\n field=\"allowCodexPatch\"\n title={t.codexTitle}\n description={t.codexDesc}\n checked={value.allowCodexPatch ?? false}\n disabled={!editable}\n onToggle={toggle}\n />\n {snapshot.status === 'ready' && !snapshot.writable && <p className=\"ppeReadonly\">{t.readonlyHint}</p>}\n </section>\n )\n}\n\n/**\n * 客户端插件体:在 Plugins 页 bundle 详情页的 keyed 席位(key = 包名)下\n * 注册配置卡。卡片自绑 `configForms.get('dsh-patch-edit-plus')`——form 在\n * 渲染期解析(而非 apply 期固化),宿主晚于客户端半开始服务该 entry 时\n * 也能追上;页面注入的 owner props(`{ view: 'page', form }`)不消费,\n * 以本插件 entry 的作用域为唯一事实来源。\n */\nexport function apply(ctx: ClientCtx): void {\n const configForms = ((ctx as unknown as { configForms?: ConfigFormsFace }).configForms\n ?? (typeof ctx.get === 'function' ? ctx.get('configForms') : undefined)) as ConfigFormsFace | undefined\n ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register(\n { name: 'plugins.bundle.config', key: 'dsh-patch-edit-plus' },\n () => createElement(PatchStyleCard, { form: configForms?.get?.('dsh-patch-edit-plus') }),\n ))\n}\n"],"mappings":"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"}
package/lib/config.js CHANGED
@@ -11,7 +11,7 @@ export const Config = z.object({
11
11
  toolName: z.string().default('apply_patch'),
12
12
  conflictPolicy: z.union(['rename', 'skip', 'fail']).default('rename'),
13
13
  renameSuffix: z.string().default('_1'),
14
- allowUnifiedDiff: z.boolean().default(true),
14
+ allowUnifiedDiff: z.boolean().default(true).volatile(),
15
15
  allowCodexPatch: z.boolean().default(false).volatile(),
16
16
  deleteBackend: z.union(['shell', 'none']).default('shell'),
17
17
  shellDialect: z.union(['auto', 'posix', 'pwsh']).default('auto'),
@@ -38,7 +38,7 @@ export function resolveConfig(config) {
38
38
  toolName: typeof c.toolName === 'string' && c.toolName.trim() !== '' ? c.toolName : 'apply_patch',
39
39
  conflictPolicy: c.conflictPolicy ?? 'rename',
40
40
  renameSuffix: typeof c.renameSuffix === 'string' ? c.renameSuffix : '_1',
41
- allowUnifiedDiff: c.allowUnifiedDiff ?? true,
41
+ allowUnifiedDiff: readVolatileBoolean(c.allowUnifiedDiff, true),
42
42
  allowCodexPatch: readVolatileBoolean(c.allowCodexPatch, false),
43
43
  deleteBackend: c.deleteBackend ?? 'shell',
44
44
  shellDialect: c.shellDialect ?? 'auto',
@@ -55,8 +55,12 @@ export function parseUnifiedDiff(patch) {
55
55
  continue;
56
56
  }
57
57
  if (line.startsWith('--- ')) {
58
- if (current === null) {
59
- // Bare unified diff without a git header: open an implicit section.
58
+ // A bare multi-file unified diff repeats `--- `/`+++ ` per file with no
59
+ // `diff --git` separator. Once the current section has been claimed by
60
+ // a path or a hunk, this header therefore opens the NEXT file section;
61
+ // reusing the section would drag the previous file's hunks into this
62
+ // one and fail them against the wrong content.
63
+ if (current === null || current.oldPath !== null || current.newPath !== null || current.hunks.length > 0) {
60
64
  current = { oldPath: null, newPath: null, newFile: false, deletedFile: false, renameFrom: null, renameTo: null, hunks: [], startLine: lineNo };
61
65
  sections.push(current);
62
66
  }
@@ -0,0 +1,47 @@
1
+ import type { JSX } from 'react';
2
+ /** 本插件消费的 configForms 值形状(与 host ResolvedConfig 的易变布尔对齐)。 */
3
+ export interface PatchEditPlusConfig {
4
+ allowUnifiedDiff: boolean;
5
+ allowCodexPatch: boolean;
6
+ }
7
+ /** configForms 作用域面(结构性子集;0.1.7+ 缝,避免硬 peer 类型)。 */
8
+ export interface PatchFormScope {
9
+ getSnapshot(): {
10
+ status: 'loading' | 'ready' | 'unavailable';
11
+ value: Partial<PatchEditPlusConfig> | undefined;
12
+ writable: boolean;
13
+ };
14
+ subscribe(listener: () => void): () => void;
15
+ set(field: string, value: unknown): Promise<boolean>;
16
+ }
17
+ /** 客户端所需服务:slots(席位注册)+ configForms(entry 配置读写)。同族插件
18
+ * (session-guard/perm-gate)同样显式声明——runner 只把声明了的服务提供给模块
19
+ * 上下文,不声明时 `ctx.get('configForms')` 恒为 undefined,卡会渲染成 null。 */
20
+ export declare const inject: string[];
21
+ /** 轻量 ctx 类型(仅本客户端用到的方法;构建时类型被剥离)。 */
22
+ interface SlotsFace {
23
+ inject: (_name: string, _fn: () => unknown) => () => void;
24
+ register: (_options: Record<string, unknown>, _component: unknown) => () => void;
25
+ }
26
+ interface ClientCtx {
27
+ slots: SlotsFace;
28
+ get?: (_name: string) => unknown;
29
+ }
30
+ /**
31
+ * 插件页配置卡主体:两行补丁风格开关。
32
+ * - status === 'unavailable'(无 configForms / entry 未装配)→ 整卡隐藏(null);
33
+ * - status === 'loading' → 开关禁用;
34
+ * - 仅 snapshot.writable 时可写(写路径 `form.set(field, !cur)`)。
35
+ */
36
+ export declare function PatchStyleCard(props: {
37
+ form?: PatchFormScope | undefined;
38
+ }): JSX.Element | null;
39
+ /**
40
+ * 客户端插件体:在 Plugins 页 bundle 详情页的 keyed 席位(key = 包名)下
41
+ * 注册配置卡。卡片自绑 `configForms.get('dsh-patch-edit-plus')`——form 在
42
+ * 渲染期解析(而非 apply 期固化),宿主晚于客户端半开始服务该 entry 时
43
+ * 也能追上;页面注入的 owner props(`{ view: 'page', form }`)不消费,
44
+ * 以本插件 entry 的作用域为唯一事实来源。
45
+ */
46
+ export declare function apply(ctx: ClientCtx): void;
47
+ export {};