@khorsheed/dsh-local-agent-dsh 0.2.0 → 0.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # 变更记录
2
2
 
3
+ ## 0.2.2(2026-10-27)
4
+
5
+ - 跟随本地 Agent 家族统一版本线发布,本包没有功能改动。
6
+
7
+ ## 0.2.1(2026-10-27)
8
+
9
+ - **不用再打开开关**:「DeepSeek 委派」的启用开关去掉了,装上就能用;委派工具 `subagent_dsh` 随模式授予,只在开发、游戏开发、剪辑模式的会话里出现,不再出现在每个会话里。
10
+ - **常驻模式(live)默认打开**:遇到问题可以在卡片上关掉,改回每次新起一个。
11
+ - **设置存在自己的目录里**:默认模型等设置改存在子 DSH 自己的目录里,几个模式套件一起装时也能保存;改不成时显示「保存失败」,不再假装「已保存」。
12
+
3
13
  ## 0.2.0(2026-10-26)
4
14
 
5
15
  - **版本号去掉 rc**:社区包不再发 prerelease 版本(docs/publishing.md「版本号规则」),家族各包同版 0.2.0;本节原记为 0.2.0-rc.4。
package/README.en.md CHANGED
@@ -4,19 +4,19 @@ English | [中文](README.md)
4
4
 
5
5
  Hand a subtask to a second, separately running DSH on your machine: it has its own sessions and settings, the result comes back to your session, and you can keep the conversation going.
6
6
 
7
- Some subtasks deserve a clean DSH: none of the current session's context, its own session history, and no way to disturb the instance you're using. With this plugin, turn on "DeepSeek delegation" in settings and the agent can hand such subtasks to a separate DSH started on your machine. Its output shows up live in a sub-session, the result comes back to you, and you can keep working in the same sub-session afterwards.
7
+ Some subtasks deserve a clean DSH: none of the current session's context, its own session history, and no way to disturb the instance you're using. With this plugin, in sessions of the Dev, Game Dev and Video modes the agent can hand such subtasks to a separate DSH started on your machine. Its output shows up live in a sub-session, the result comes back to you, and you can keep working in the same sub-session afterwards.
8
8
 
9
- <img src="https://raw.githubusercontent.com/Khorsheed/dsh-basic/main/docs/screenshots/local-agent-dsh.png" width="560" alt="The expanded DSH section: credentials follow the current DSH, the "DeepSeek delegation" switch on, the default-model picker and the resident-mode switch">
9
+ <img src="https://raw.githubusercontent.com/Khorsheed/dsh-basic/main/docs/screenshots/local-agent-dsh.png" width="560" alt="The “Local Agent Delegation” card with the “Local DSH Delegation” section open: signed in (0.2.1-rc.1), credentials taken from the host instance with no separate login, DeepSeek-V4.1-Flash as the default model, and resident mode (live) on">
10
10
 
11
11
  ## What it does
12
12
 
13
- - **Off by default, on when you want it**: with the switch off nothing changes; turn on "DeepSeek delegation" in the DSH section of the 「本机 Agent 委派」 (Local Agent Delegation) card and the agent can hand subtasks to it, no restart needed.
13
+ - **Works once installed**: no switch to turn on; the delegation tool follows the mode — sessions of the Dev, Game Dev and Video modes get it, every other session is unchanged.
14
14
  - **No separate sign-in**: it uses the model and key you picked in your current DSH, and the card shows whether that credential works.
15
15
  - **Kept apart**: the second DSH's sessions, settings, and state live in their own folder; they never show up in your session list or change your current instance.
16
16
  - **See the work as it happens**: its output appears in the sub-session as it runs.
17
17
  - **Keep talking**: have the agent follow up, or open the sub-session and message it yourself; it's the same session on the second DSH.
18
18
  - **Choose the model**: pick one under "Default model" on the card and every hand-off uses it; leave it blank to follow your current DSH's default.
19
- - **Resident mode is faster**: turn on "Resident mode (live)" and the second DSH stays up; stopping doesn't kill it.
19
+ - **Resident mode is faster**: on by default — the second DSH stays up and stopping doesn't kill it; if it misbehaves, turn off "Resident mode (live)" on the card to start a fresh one each time.
20
20
 
21
21
  ## Install
22
22
 
@@ -26,7 +26,7 @@ In DSH, open Plugins → Add plugin, enter this package name, and press Install:
26
26
  @khorsheed/dsh-bundle-local-agent
27
27
  ```
28
28
 
29
- This plugin comes with the 「多Agent协作」 (multi-agent) suite; all the local agents share one core, so on its own it won't work. The Dev Suite `@khorsheed/dsh-dev-suite` includes it too. Restart DSH, open the 「本机 Agent 委派」 card on the Plugins page, expand the DSH section, turn on "DeepSeek delegation", then tell the agent in a session "hand this subtask to the local DSH".
29
+ This plugin comes with the 「多Agent协作」 (multi-agent) suite; all the local agents share one core, so on its own it won't work. The Dev Suite `@khorsheed/dsh-dev-suite` includes it too. Restart DSH, then in a Dev (or Game Dev, Video) mode session tell the agent "hand this subtask to the local DSH".
30
30
 
31
31
  ## Known limitations
32
32
 
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # last confirmed-consistent state. Both languages carry equal authority; after
3
3
  # editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/local-agent-dsh/README.en.md
5
- packages/local-agent-dsh/README.en.md: 40e63c45195893d980e8b2d8ababbdc3a1d6745d
6
- packages/local-agent-dsh/README.md: 65d1e8a0762e1fbc33f58e594f15cbf1a5e64fb5
5
+ packages/local-agent-dsh/README.en.md: ae3dd795c6fe850b185d05dc2857f6c91d1aeaac
6
+ packages/local-agent-dsh/README.md: 91968cfebfd1e6a70454e5fdb09bb87a0881d38f
package/README.md CHANGED
@@ -4,19 +4,19 @@
4
4
 
5
5
  把子任务交给你电脑上另一个单独运行的 DSH:它有自己的会话和设置,跑完结果回到当前会话,还能接着聊。
6
6
 
7
- 有些子任务你想让一个干净的 DSH 来做:不带当前会话的上下文,有自己的会话记录,也不会动到你正在用的这个实例。装上之后,在设置里打开「DeepSeek 委派」,agent 就能把这类子任务交给本机另起的一个 DSH;它的输出实时出现在子会话里,跑完把结果带回来,之后还能在同一个子会话里接着干。
7
+ 有些子任务你想让一个干净的 DSH 来做:不带当前会话的上下文,有自己的会话记录,也不会动到你正在用的这个实例。装上之后,在开发、游戏开发、剪辑这几个模式的会话里,agent 就能把这类子任务交给本机另起的一个 DSH;它的输出实时出现在子会话里,跑完把结果带回来,之后还能在同一个子会话里接着干。
8
8
 
9
- <img src="https://raw.githubusercontent.com/Khorsheed/dsh-basic/main/docs/screenshots/local-agent-dsh.png" width="560" alt="展开的 DSH 一节:凭据沿用当前 DSH,「DeepSeek 委派」开关已打开,默认模型选择和常驻模式开关">
9
+ <img src="https://raw.githubusercontent.com/Khorsheed/dsh-basic/main/docs/screenshots/local-agent-dsh.png" width="560" alt="「本机 Agent 委派」卡片里展开的「本机 DSH 委派」一节:已登录(0.2.1-rc.1),凭据走宿主实例、无需单独登录,默认模型选的是 DeepSeek-V4.1-Flash,常驻模式(live)开着">
10
10
 
11
11
  ## 能做什么
12
12
 
13
- - **默认关着,想用再开**:开关关着时一切照旧;在「本机 Agent 委派」卡片的 DSH 一节打开「DeepSeek 委派」,agent 才能把子任务交给它,不用重启。
13
+ - **装上就能用**:不用再打开什么开关;委派工具跟着模式走,开发、游戏开发、剪辑模式的会话里才有,其他会话一切照旧。
14
14
  - **不用另外登录**:它沿用你在当前 DSH 里选的模型和 key,卡片上直接显示凭据能不能用。
15
15
  - **互不打扰**:子 DSH 的会话、设置和状态都放在单独的目录里,不会出现在你的会话列表里,也不会改动当前实例。
16
16
  - **过程看得见**:子 DSH 的输出边跑边出现在子会话里。
17
17
  - **接着聊**:让 agent 追问,或者打开子会话直接发消息,继续的是同一个子 DSH 会话。
18
18
  - **指定模型**:卡片的「默认模型」里选一个模型,之后每次委派都用它;留空就跟随当前 DSH 的默认模型。
19
- - **常驻模式更快**:打开「常驻模式(live)」,子 DSH 常驻,停下不杀进程。
19
+ - **常驻模式更快**:默认开着,子 DSH 常驻,停下不杀进程;遇到问题可以在卡片上关掉「常驻模式(live)」,改回每次新起一个。
20
20
 
21
21
  ## 安装
22
22
 
@@ -26,7 +26,7 @@
26
26
  @khorsheed/dsh-bundle-local-agent
27
27
  ```
28
28
 
29
- 本插件会跟着「多Agent协作」套件一起装好(各家本机 agent 共用同一个底座,单装用不起来);开发套件 `@khorsheed/dsh-dev-suite` 也包含它。装完重启 DSH,到「插件」页的「本机 Agent 委派」卡片里展开 DSH 一节,打开「DeepSeek 委派」,然后在会话里对 agent 说「把这个子任务交给本机的 DSH」。
29
+ 本插件会跟着「多Agent协作」套件一起装好(各家本机 agent 共用同一个底座,单装用不起来);开发套件 `@khorsheed/dsh-dev-suite` 也包含它。装完重启 DSH,在开发(或游戏开发、剪辑)模式的会话里对 agent 说「把这个子任务交给本机的 DSH」。
30
30
 
31
31
  ## 已知限制
32
32
 
package/lib/client.js CHANGED
@@ -93,6 +93,83 @@ window.__ModuleLoader__.load({
93
93
  });
94
94
  return channel;
95
95
  }
96
+ /**
97
+ * Bind a settings card to the harness's own card-settings store (card-settings.ts
98
+ * on the host) layered over the row's host settings form: reads are the
99
+ * form's value with the stored overrides on top; writes go to the store only,
100
+ * never the profile patch — so they cannot be refused when several suites
101
+ * insert the same row. A host without the store keeps the plain row scope.
102
+ * @param ctx - the provider's client context.
103
+ * @param harness - the harness name (`dsh`, `kimi`, …).
104
+ * @param entryId - the plugin row id the base values come from.
105
+ * @returns the bound scope.
106
+ */
107
+ function bindCardSettingsScope(ctx, harness, entryId) {
108
+ const row = bindSettingsScope(ctx, entryId);
109
+ const gateway = () => {
110
+ const remote = ctx.get("remote.localAgentGateway");
111
+ return typeof remote?.cardSettings === "function" && typeof remote.setCardSettings === "function" ? remote : void 0;
112
+ };
113
+ const listeners = /* @__PURE__ */ new Set();
114
+ let stored;
115
+ let cached;
116
+ const emit = () => {
117
+ for (const listener of [...listeners]) listener();
118
+ };
119
+ const load = () => {
120
+ gateway()?.cardSettings(harness).then((result) => {
121
+ if (!result.ok || result.value === void 0) return;
122
+ stored = result.value;
123
+ emit();
124
+ }).catch(() => {});
125
+ };
126
+ const write = async (patch) => {
127
+ const remote = gateway();
128
+ if (remote === void 0) return false;
129
+ try {
130
+ const result = await remote.setCardSettings(harness, patch);
131
+ if (!result.ok || result.value === void 0) return false;
132
+ stored = result.value;
133
+ emit();
134
+ return true;
135
+ } catch {
136
+ return false;
137
+ }
138
+ };
139
+ return {
140
+ getSnapshot: () => {
141
+ const rowSnapshot = row.getSnapshot();
142
+ if (cached !== void 0 && cached.rowSnapshot === rowSnapshot && cached.stored === stored) return cached.snapshot;
143
+ const overrides = stored ?? {};
144
+ const snapshot = gateway() === void 0 ? rowSnapshot : {
145
+ ...rowSnapshot,
146
+ value: rowSnapshot.value === void 0 && stored === void 0 ? void 0 : {
147
+ ...rowSnapshot.value,
148
+ ...overrides
149
+ },
150
+ writable: true,
151
+ status: rowSnapshot.status === "loading" && stored !== void 0 ? "ready" : rowSnapshot.status === "unavailable" && stored !== void 0 ? "ready" : rowSnapshot.status
152
+ };
153
+ cached = {
154
+ rowSnapshot,
155
+ stored,
156
+ snapshot
157
+ };
158
+ return snapshot;
159
+ },
160
+ subscribe: (listener) => {
161
+ if (listeners.size === 0) load();
162
+ listeners.add(listener);
163
+ const off = row.subscribe(listener);
164
+ return () => {
165
+ listeners.delete(listener);
166
+ off();
167
+ };
168
+ },
169
+ set: (field, value) => gateway() === void 0 ? row.set(field, value) : write({ [field]: value }),
170
+ unset: (field) => gateway() === void 0 ? row.unset(field) : write({ [field]: null })
171
+ };
172
+ }
96
173
  //#endregion
97
174
  //#region src/client/locales.ts
98
175
  /** `local-agent-dsh` namespace dictionaries. */
@@ -118,11 +195,6 @@ window.__ModuleLoader__.load({
118
195
  "model.applied": "已保存,下一轮委派生效;进行中的轮次不受影响",
119
196
  "model.error": "保存失败,请重试",
120
197
  "auth.host": "dsh 凭据走宿主实例的凭证设置,无需单独登录。",
121
- "enable.title": "DeepSeek 委派",
122
- "enable.switch": "启用",
123
- "enable.switch.aria": "DeepSeek 委派开关",
124
- "enable.on": "已开启:模型可使用 subagent_dsh 委派给本机 dsh",
125
- "enable.off": "已关闭:委派只走官方内置子代理工具",
126
198
  "live.title": "常驻模式(live)",
127
199
  "live.info.aria": "常驻模式说明",
128
200
  "live.info": "常驻模式 = 成员进程常驻:输出实时流入成员会话、取消不杀进程、崩溃后自动续上原会话;关闭则每轮独立进程,跑完一次性出结果。设置为全局偏好,对所有后续委派生效。",
@@ -150,11 +222,6 @@ window.__ModuleLoader__.load({
150
222
  "model.applied": "Saved — takes effect on the next delegation round; rounds in flight are unaffected",
151
223
  "model.error": "Save failed, please retry",
152
224
  "auth.host": "dsh authenticates through the host instance’s credential settings — no separate sign-in.",
153
- "enable.title": "DeepSeek delegation",
154
- "enable.switch": "Enabled",
155
- "enable.switch.aria": "DeepSeek delegation switch",
156
- "enable.on": "ON: the model can delegate to the local dsh via subagent_dsh",
157
- "enable.off": "OFF: delegation goes through the official in-process subagent tool only",
158
225
  "live.title": "Resident mode (live)",
159
226
  "live.info.aria": "About resident mode",
160
227
  "live.info": "Resident mode keeps the member process alive: output streams into the member session in real time, cancelling does not kill the process, and a crash resumes the same session. Off means every round runs as an independent one-shot process. The setting is global and applies to every later delegation.",
@@ -204,8 +271,13 @@ window.__ModuleLoader__.load({
204
271
  * re-probes so a mid-session credential loss cannot leave a stale green dot.
205
272
  * @module @khorsheed/dsh-local-agent/client — auth status bus
206
273
  */
207
- const statuses = /* @__PURE__ */ new Map();
208
- const listeners = /* @__PURE__ */ new Set();
274
+ const BUS_KEY = Symbol.for("dsh.local-agent.auth-status");
275
+ const bus = globalThis[BUS_KEY] ??= {
276
+ statuses: /* @__PURE__ */ new Map(),
277
+ listeners: /* @__PURE__ */ new Set()
278
+ };
279
+ const statuses = bus.statuses;
280
+ const listeners = bus.listeners;
209
281
  /** Record one harness's latest probe result and notify subscribers. */
210
282
  function publishAuthStatus(harnessId, status) {
211
283
  if (statuses.get(harnessId) === status) return;
@@ -263,30 +335,30 @@ window.__ModuleLoader__.load({
263
335
  document.head.appendChild(tag);
264
336
  }
265
337
  var ProviderAuthBlock_module_css_default = {
266
- "rowCard": "On5p7W_rowCard",
267
- "rowName": "On5p7W_rowName",
268
- "rowHead": "On5p7W_rowHead",
269
- "section": "On5p7W_section",
270
- "dotOff": "On5p7W_dotOff",
271
- "logoutButton": "On5p7W_logoutButton",
272
- "rosterError": "On5p7W_rosterError",
273
- "dotOn": "On5p7W_dotOn",
274
- "promptText": "On5p7W_promptText",
275
- "title": "On5p7W_title",
276
- "rowTag": "On5p7W_rowTag",
277
- "intro": "On5p7W_intro",
278
338
  "rows": "On5p7W_rows",
279
- "dotPending": "On5p7W_dotPending",
280
- "rowActions": "On5p7W_rowActions",
281
- "loginButton": "On5p7W_loginButton",
339
+ "logoutButton": "On5p7W_logoutButton",
282
340
  "rowPending": "On5p7W_rowPending",
283
- "retryButton": "On5p7W_retryButton",
284
- "openPage": "On5p7W_openPage",
285
- "status": "On5p7W_status",
341
+ "intro": "On5p7W_intro",
342
+ "title": "On5p7W_title",
286
343
  "credentialDot": "On5p7W_credentialDot",
287
344
  "loginPrompt": "On5p7W_loginPrompt",
345
+ "rowCard": "On5p7W_rowCard",
346
+ "dotOn": "On5p7W_dotOn",
347
+ "rosterError": "On5p7W_rosterError",
348
+ "dotPending": "On5p7W_dotPending",
349
+ "status": "On5p7W_status",
350
+ "loginButton": "On5p7W_loginButton",
351
+ "promptText": "On5p7W_promptText",
352
+ "dotOff": "On5p7W_dotOff",
288
353
  "rowIdentity": "On5p7W_rowIdentity",
289
- "codeInput": "On5p7W_codeInput"
354
+ "openPage": "On5p7W_openPage",
355
+ "rowTag": "On5p7W_rowTag",
356
+ "rowHead": "On5p7W_rowHead",
357
+ "retryButton": "On5p7W_retryButton",
358
+ "codeInput": "On5p7W_codeInput",
359
+ "section": "On5p7W_section",
360
+ "rowActions": "On5p7W_rowActions",
361
+ "rowName": "On5p7W_rowName"
290
362
  };
291
363
  //#endregion
292
364
  //#region ../local-agent/src/client/ProviderAuthBlock.tsx
@@ -439,8 +511,8 @@ window.__ModuleLoader__.load({
439
511
  const statusKind = usePublishedAuthStatus(harness.id) ?? view.status;
440
512
  const authenticated = statusKind === "authenticated";
441
513
  const loginPending = pendingSince !== void 0;
442
- const loginable = view.loginable ?? true;
443
- const logoutable = view.logoutable ?? true;
514
+ const loginable = view.loginable ?? statusKind !== "unavailable";
515
+ const logoutable = view.logoutable ?? statusKind !== "unavailable";
444
516
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
445
517
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
446
518
  className: ProviderAuthBlock_module_css_default.rowHead,
@@ -473,7 +545,7 @@ window.__ModuleLoader__.load({
473
545
  statusKind === "checking" && t("loading"),
474
546
  statusKind === "authenticated" && t("settings.authenticated"),
475
547
  statusKind === "anonymous" && t("settings.notAuthenticated"),
476
- statusKind === "unavailable" && t("error")
548
+ statusKind === "unavailable" && t("settings.statusFailed")
477
549
  ]
478
550
  }),
479
551
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
@@ -565,10 +637,10 @@ window.__ModuleLoader__.load({
565
637
  document.head.appendChild(tag);
566
638
  }
567
639
  var AuthStatusDot_module_css_default = {
568
- "credentialDot": "OFjNUG_credentialDot",
569
640
  "dotOff": "OFjNUG_dotOff",
570
- "dotOn": "OFjNUG_dotOn",
571
- "dotPending": "OFjNUG_dotPending"
641
+ "credentialDot": "OFjNUG_credentialDot",
642
+ "dotPending": "OFjNUG_dotPending",
643
+ "dotOn": "OFjNUG_dotOn"
572
644
  };
573
645
  //#endregion
574
646
  //#region ../local-agent/src/client/AuthStatusDot.tsx
@@ -594,40 +666,40 @@ window.__ModuleLoader__.load({
594
666
  document.head.appendChild(tag);
595
667
  }
596
668
  var SettingsCard_module_css_default = {
597
- "errorText": "_3LBPpW_errorText",
598
- "modelMenu": "_3LBPpW_modelMenu",
599
- "rowLabel": "_3LBPpW_rowLabel",
600
- "description": "_3LBPpW_description",
601
- "body": "_3LBPpW_body",
602
- "chevron": "_3LBPpW_chevron",
603
- "modelMenuButton": "_3LBPpW_modelMenuButton",
669
+ "blockTitle": "_3LBPpW_blockTitle",
670
+ "info": "_3LBPpW_info",
604
671
  "modelItemCheck": "_3LBPpW_modelItemCheck",
605
- "headText": "_3LBPpW_headText",
606
- "modelItemCurrent": "_3LBPpW_modelItemCurrent",
607
- "header": "_3LBPpW_header",
608
672
  "chevronOpen": "_3LBPpW_chevronOpen",
673
+ "description": "_3LBPpW_description",
674
+ "switchOn": "_3LBPpW_switchOn",
675
+ "modelMenu": "_3LBPpW_modelMenu",
676
+ "row": "_3LBPpW_row",
677
+ "card": "_3LBPpW_card",
678
+ "headText": "_3LBPpW_headText",
679
+ "rowLabel": "_3LBPpW_rowLabel",
680
+ "modelMenuChevron": "_3LBPpW_modelMenuChevron",
681
+ "modelMenuUp": "_3LBPpW_modelMenuUp",
682
+ "modelItemLabel": "_3LBPpW_modelItemLabel",
683
+ "block": "_3LBPpW_block",
609
684
  "knob": "_3LBPpW_knob",
685
+ "saved": "_3LBPpW_saved",
610
686
  "modelMenuChevronOpen": "_3LBPpW_modelMenuChevronOpen",
687
+ "cardOpen": "_3LBPpW_cardOpen",
611
688
  "modelItemDefault": "_3LBPpW_modelItemDefault",
612
- "modelItemLabel": "_3LBPpW_modelItemLabel",
613
689
  "switch": "_3LBPpW_switch",
614
- "blockTitle": "_3LBPpW_blockTitle",
615
- "modelItem": "_3LBPpW_modelItem",
616
690
  "modelInput": "_3LBPpW_modelInput",
617
- "card": "_3LBPpW_card",
618
- "cardOpen": "_3LBPpW_cardOpen",
619
- "row": "_3LBPpW_row",
691
+ "body": "_3LBPpW_body",
692
+ "chevron": "_3LBPpW_chevron",
693
+ "modelField": "_3LBPpW_modelField",
694
+ "header": "_3LBPpW_header",
695
+ "modelItem": "_3LBPpW_modelItem",
696
+ "errorText": "_3LBPpW_errorText",
697
+ "modelMenuButton": "_3LBPpW_modelMenuButton",
698
+ "modelItemCurrent": "_3LBPpW_modelItemCurrent",
699
+ "name": "_3LBPpW_name",
620
700
  "modelSave": "_3LBPpW_modelSave",
621
- "modelMenuUp": "_3LBPpW_modelMenuUp",
622
- "saved": "_3LBPpW_saved",
623
701
  "hint": "_3LBPpW_hint",
624
- "modelMenuChevron": "_3LBPpW_modelMenuChevron",
625
- "switchOn": "_3LBPpW_switchOn",
626
- "block": "_3LBPpW_block",
627
- "nameRow": "_3LBPpW_nameRow",
628
- "modelField": "_3LBPpW_modelField",
629
- "info": "_3LBPpW_info",
630
- "name": "_3LBPpW_name"
702
+ "nameRow": "_3LBPpW_nameRow"
631
703
  };
632
704
  //#endregion
633
705
  //#region src/client/SettingsCard.tsx
@@ -694,8 +766,9 @@ window.__ModuleLoader__.load({
694
766
  const write = (field, value) => {
695
767
  setSaved(false);
696
768
  setError(false);
697
- scope.set(field, value).then(() => {
698
- setSaved(true);
769
+ scope.set(field, value).then((accepted) => {
770
+ if (accepted === false) setError(true);
771
+ else setSaved(true);
699
772
  }, () => {
700
773
  setError(true);
701
774
  });
@@ -756,7 +829,7 @@ window.__ModuleLoader__.load({
756
829
  }
757
830
  /** The card body both settings surfaces share: the auth block, the delegation switch, the default-model block, and the resident-mode block. */
758
831
  function CardBody({ state, auth, authT, renderModelPicker, useSessions, t }) {
759
- const { snapshot, saved, error, ready, enabled, live, storedModel, modelValue, modelSaved, modelError, choices, modelMenuOpen, modelMenuUp, modelFieldRef, modelPlaceholder, modelDefaultItem, saveModel, write, toggleModelMenu, setModelDraft, setModelMenuOpen } = state;
832
+ const { snapshot, saved, error, ready, live, storedModel, modelValue, modelSaved, modelError, choices, modelMenuOpen, modelMenuUp, modelFieldRef, modelPlaceholder, modelDefaultItem, saveModel, write, toggleModelMenu, setModelDraft, setModelMenuOpen } = state;
760
833
  const modelInputElement = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
761
834
  type: "text",
762
835
  className: SettingsCard_module_css_default.modelInput,
@@ -794,37 +867,6 @@ window.__ModuleLoader__.load({
794
867
  })
795
868
  ]
796
869
  }),
797
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
798
- className: SettingsCard_module_css_default.block,
799
- children: [
800
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
801
- className: SettingsCard_module_css_default.blockTitle,
802
- children: t("enable.title")
803
- }),
804
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
805
- className: SettingsCard_module_css_default.row,
806
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
807
- className: SettingsCard_module_css_default.rowLabel,
808
- children: t("enable.switch")
809
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
810
- type: "button",
811
- role: "switch",
812
- "aria-checked": enabled,
813
- "aria-label": t("enable.switch.aria"),
814
- className: enabled ? `${SettingsCard_module_css_default.switch} ${SettingsCard_module_css_default.switchOn}` : SettingsCard_module_css_default.switch,
815
- disabled: !ready,
816
- onClick: () => {
817
- write("enabled", !enabled);
818
- },
819
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: SettingsCard_module_css_default.knob })
820
- })]
821
- }),
822
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
823
- className: SettingsCard_module_css_default.hint,
824
- children: t(enabled ? "enable.on" : "enable.off")
825
- })
826
- ]
827
- }),
828
870
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
829
871
  className: SettingsCard_module_css_default.block,
830
872
  children: [
@@ -1122,7 +1164,7 @@ window.__ModuleLoader__.load({
1122
1164
  en
1123
1165
  }), "local-agent-dsh: dictionaries");
1124
1166
  const t = ctx.locale.bind(NS);
1125
- const scope = bindSettingsScope(ctx, "local-agent-dsh");
1167
+ const scope = bindCardSettingsScope(ctx, "dsh", "local-agent-dsh");
1126
1168
  const authT = ctx.locale.bind("local-agent");
1127
1169
  const gateway = () => ctx.get("remote.localAgentGateway");
1128
1170
  const cardFace = () => ({
package/lib/index.js CHANGED
@@ -5,7 +5,6 @@ import { dirname } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import z from "@deepseek-ai/schemastery";
7
7
  import { ModelDirectoryCache, extendModelDirectory, settingsFace, vol } from "@khorsheed/dsh-local-agent";
8
- import { mountSubagentDelegationTool } from "@khorsheed/dsh-local-agent/tool-factory";
9
8
  //#region lib/types/live-switch.js
10
9
  /**
11
10
  * The settings-driven live-driver switch: owns the active driver generation
@@ -404,7 +403,7 @@ const inject = [
404
403
  */
405
404
  const SETTINGS_FIELDS = {
406
405
  enabled: z.boolean().default(false),
407
- live: z.boolean().default(false),
406
+ live: z.boolean().default(true),
408
407
  liveMirrorGranularity: z.union([z.const("event"), z.const("token")]).default("token"),
409
408
  model: z.string(),
410
409
  recentModels: z.array(z.string()).default([])
@@ -446,34 +445,6 @@ const DSH_SETTINGS_NAMESPACE = "local-agent-dsh";
446
445
  * the YAML config's deployment defaults through the composition base.
447
446
  */
448
447
  const DSH_SETTINGS_SCHEMA = z.object(SETTINGS_FIELDS);
449
- /** The delegation tool the toggle mounts while ON. */
450
- const DSH_TOOL_NAME = "subagent_dsh";
451
- /**
452
- * This package's own name, passed to the family tool factory as the
453
- * tool-origin owner (AGENTS.md § Tool origin tagging — a shared factory never
454
- * hardcodes an owner), the same pattern the worktrees/room `./tool` entries
455
- * use.
456
- */
457
- const PACKAGE_NAME = "@khorsheed/dsh-local-agent-dsh";
458
- /**
459
- * The `subagent_dsh` mount, as a plugin object over the family factory: the
460
- * toggle owns its lifecycle, so it mounts dynamically through `ctx.plugin`
461
- * instead of shipping a `./tool` composition row like the CLI providers.
462
- */
463
- const dshSubagentTool = {
464
- name: "local-agent-dsh-tool",
465
- inject: [
466
- "tools",
467
- "subagents",
468
- "localAgent"
469
- ],
470
- apply: (toolCtx) => {
471
- mountSubagentDelegationTool(toolCtx, {
472
- provider: "dsh-cli",
473
- toolName: DSH_TOOL_NAME
474
- }, PACKAGE_NAME);
475
- }
476
- };
477
448
  /**
478
449
  * Mount the toggle controller: register the settings namespace, then mirror
479
450
  * its value into harness/provider/tool registrations, live.
@@ -485,7 +456,7 @@ function apply(ctx, config) {
485
456
  ctx.effect(() => {
486
457
  const readSettings = () => ({
487
458
  enabled: vol(config.enabled) ?? false,
488
- live: vol(config.live) ?? false,
459
+ live: vol(config.live) ?? true,
489
460
  liveMirrorGranularity: vol(config.liveMirrorGranularity) ?? "token",
490
461
  model: vol(config.model),
491
462
  recentModels: vol(config.recentModels) ?? []
@@ -502,6 +473,7 @@ function apply(ctx, config) {
502
473
  }
503
474
  const face = settingsFace(ctx, {
504
475
  namespace: DSH_SETTINGS_NAMESPACE,
476
+ harness: "dsh",
505
477
  legacySchema: DSH_SETTINGS_SCHEMA,
506
478
  legacyBase,
507
479
  read: readSettings
@@ -620,7 +592,6 @@ function apply(ctx, config) {
620
592
  if (enabled === currentEnabled) return;
621
593
  currentEnabled = enabled;
622
594
  generation += 1;
623
- const gen = generation;
624
595
  for (const dispose of disposers.splice(0)) dispose();
625
596
  if (!enabled) return;
626
597
  provisionDshScope(homeDir, config, profileDirOption());
@@ -632,25 +603,10 @@ function apply(ctx, config) {
632
603
  if (currentLiveSwitch === liveSwitch) currentLiveSwitch = void 0;
633
604
  liveSwitch.dispose();
634
605
  });
635
- ctx.plugin(dshSubagentTool).then((fiber) => {
636
- if (gen !== generation) {
637
- fiber.dispose();
638
- return;
639
- }
640
- disposers.push(() => {
641
- fiber.dispose();
642
- });
643
- }, (error) => {
644
- ctx.logger.warn(`local-agent-dsh: delegation tool mount failed: ${error instanceof Error ? error.message : String(error)}`);
645
- });
646
606
  };
647
- sync(face.get().enabled);
648
- const unwatch = face.watch((next) => {
649
- sync(next.enabled);
650
- });
607
+ sync(true);
651
608
  return () => {
652
609
  modelCatalog.dispose();
653
- unwatch();
654
610
  generation += 1;
655
611
  for (const dispose of disposers.splice(0)) dispose();
656
612
  };
package/lib/tool.js ADDED
@@ -0,0 +1,40 @@
1
+ import { mountSubagentDelegationTool, subagentToolConfigSchema } from "@khorsheed/dsh-local-agent/tool-factory";
2
+ //#region lib/types/tool.js
3
+ const PACKAGE_NAME = "@khorsheed/dsh-local-agent-dsh";
4
+ /** This entry's own defaults: a preset row needs no config. */
5
+ const DEFAULT_PROVIDER = "dsh-cli";
6
+ const DEFAULT_TOOL_NAME = "subagent_dsh";
7
+ /** Cordis plugin name used by loader diagnostics. */
8
+ const name = "local-agent-dsh-tool";
9
+ /**
10
+ * The family core's `localAgent` service is a declared inject (the
11
+ * owning-family companion exception to the community-service probe rule): a
12
+ * preset's standing scope mounts at registry-activation time, BEFORE the
13
+ * profile's later bundle rows provide the core, so a one-shot ctx.get probe
14
+ * at apply saw ABSENT there and nothing re-ran the row (rc.1 boot order; the
15
+ * worktrees `./tool` entry documents the incident). The declared inject pends
16
+ * the row until the core provides, then the body applies.
17
+ */
18
+ const inject = [
19
+ "tools",
20
+ "subagents",
21
+ "localAgent"
22
+ ];
23
+ const Config = subagentToolConfigSchema(DEFAULT_PROVIDER, DEFAULT_TOOL_NAME);
24
+ /**
25
+ * Plugin body: mount the delegation tool through the family factory, stamped
26
+ * with THIS package as the tool-origin owner (AGENTS.md § Tool origin
27
+ * tagging — the shared factory never hardcodes an owner).
28
+ * @param ctx - Cordis context (the preset's agent-plane mount).
29
+ * @param config - row config (schema defaults already applied; the `??`
30
+ * fallbacks cover a direct ctx.plugin caller that bypassed validation).
31
+ */
32
+ function apply(ctx, config) {
33
+ const resolved = {
34
+ provider: config.provider ?? DEFAULT_PROVIDER,
35
+ toolName: config.toolName ?? DEFAULT_TOOL_NAME
36
+ };
37
+ mountSubagentDelegationTool(ctx, resolved, PACKAGE_NAME);
38
+ }
39
+ //#endregion
40
+ export { Config, apply, inject, name };
@@ -78,7 +78,11 @@ function useCardState(active, useSettings, scope, harnessModel, t) {
78
78
  const write = (field, value) => {
79
79
  setSaved(false);
80
80
  setError(false);
81
- void scope.set(field, value).then(() => { setSaved(true); }, () => { setError(true); });
81
+ // The host answers a refused write with `false` (ConfigForm), not a rejection.
82
+ void scope.set(field, value).then((accepted) => { if (accepted === false)
83
+ setError(true);
84
+ else
85
+ setSaved(true); }, () => { setError(true); });
82
86
  };
83
87
  // The suggestion vocabulary: the broker's deduped union (settings + host
84
88
  // default selection + recent) when the surface answered, the card's own
@@ -152,13 +156,13 @@ function useCardState(active, useSettings, scope, harnessModel, t) {
152
156
  }
153
157
  /** The card body both settings surfaces share: the auth block, the delegation switch, the default-model block, and the resident-mode block. */
154
158
  function CardBody({ state, auth, authT, renderModelPicker, useSessions, t }) {
155
- const { snapshot, saved, error, ready, enabled, live, storedModel, modelValue, modelSaved, modelError, choices, modelMenuOpen, modelMenuUp, modelFieldRef, modelPlaceholder, modelDefaultItem, saveModel, write, toggleModelMenu, setModelDraft, setModelMenuOpen, } = state;
159
+ const { snapshot, saved, error, ready, live, storedModel, modelValue, modelSaved, modelError, choices, modelMenuOpen, modelMenuUp, modelFieldRef, modelPlaceholder, modelDefaultItem, saveModel, write, toggleModelMenu, setModelDraft, setModelMenuOpen, } = state;
156
160
  // The free-text input, single-sourced for both presentations: bare when no
157
161
  // vocabulary exists (a fresh install degrades to exactly the pre-picker
158
162
  // field), or inside the select-like field next to its chevron. No datalist:
159
163
  // the chevron menu is the single choice list.
160
164
  const modelInputElement = (_jsx("input", { type: "text", className: css.modelInput, value: modelValue, placeholder: modelPlaceholder, "aria-label": t('model.title'), disabled: !ready, onChange: (event) => { setModelDraft(event.target.value); } }));
161
- return (_jsxs("div", { className: css.body, children: [_jsxs("section", { className: css.block, children: [_jsx("h3", { className: css.blockTitle, children: t('auth.title') }), _jsx(ProviderAuthBlock, { harness: { id: 'dsh', label: 'dsh' }, useSessions: useSessions, status: auth.status, runCommand: auth.runCommand, t: authT }), _jsx("span", { className: css.hint, children: t('auth.host') })] }), _jsxs("section", { className: css.block, children: [_jsx("h3", { className: css.blockTitle, children: t('enable.title') }), _jsxs("div", { className: css.row, children: [_jsx("span", { className: css.rowLabel, children: t('enable.switch') }), _jsx("button", { type: "button", role: "switch", "aria-checked": enabled, "aria-label": t('enable.switch.aria'), className: enabled ? `${css.switch} ${css.switchOn}` : css.switch, disabled: !ready, onClick: () => { write('enabled', !enabled); }, children: _jsx("span", { className: css.knob }) })] }), _jsx("span", { className: css.hint, children: t(enabled ? 'enable.on' : 'enable.off') })] }), _jsxs("section", { className: css.block, children: [_jsxs("h3", { className: css.blockTitle, children: [t('model.title'), _jsx(Tooltip, { label: t('model.info'), side: "bottom", maxWidth: 360, children: _jsx("button", { type: "button", className: css.info, "aria-label": t('model.info.aria'), children: "\u24D8" }) })] }), _jsxs("div", { className: css.row, children: [renderModelPicker?.({ value: modelValue, onChange: setModelDraft, disabled: !ready, defaultLabel: modelDefaultItem }) ?? (choices.length > 0 ? (_jsxs("div", { className: css.modelField, ref: modelFieldRef, children: [modelInputElement, _jsx("button", { type: "button", className: css.modelMenuButton, "aria-label": t('model.menu'), "aria-haspopup": "menu", "aria-expanded": modelMenuOpen, disabled: !ready, onClick: () => { toggleModelMenu(); }, children: _jsx(IconChevronDownOutlineMedium, { className: modelMenuOpen ? `${css.modelMenuChevron} ${css.modelMenuChevronOpen}` : css.modelMenuChevron }) }), modelMenuOpen && (_jsxs("div", { className: modelMenuUp ? `${css.modelMenu} ${css.modelMenuUp}` : css.modelMenu, role: "menu", "aria-label": t('model.menu'), children: [_jsxs("button", { type: "button", role: "menuitemradio", "aria-checked": modelValue.trim() === '', className: modelValue.trim() === '' ? `${css.modelItemDefault} ${css.modelItemCurrent}` : css.modelItemDefault, onClick: () => { setModelDraft(''); setModelMenuOpen(false); }, children: [_jsx("span", { className: css.modelItemLabel, children: modelDefaultItem }), _jsx("span", { className: css.modelItemCheck, "aria-hidden": true, children: modelValue.trim() === '' ? '✓' : '' })] }), choices.map(choice => (_jsxs("button", { type: "button", role: "menuitemradio", "aria-checked": choice === modelValue, className: choice === modelValue ? css.modelItemCurrent : css.modelItem, onClick: () => { setModelDraft(choice); setModelMenuOpen(false); }, children: [_jsx("span", { className: css.modelItemLabel, children: choice }), _jsx("span", { className: css.modelItemCheck, "aria-hidden": true, children: choice === modelValue ? '✓' : '' })] }, choice)))] }))] })) : modelInputElement), _jsx("button", { type: "button", className: css.modelSave, disabled: !ready || modelValue.trim() === storedModel, onClick: () => { saveModel(); }, children: t('model.save') })] }), modelSaved && _jsx("span", { className: css.saved, children: t('model.applied') }), modelError && _jsx("span", { className: css.errorText, children: t('model.error') })] }), _jsxs("section", { className: css.block, children: [_jsxs("div", { className: css.row, children: [_jsxs("span", { className: css.rowLabel, children: [t('live.title'), _jsx(Tooltip, { label: t('live.info'), side: "bottom", maxWidth: 360, children: _jsx("button", { type: "button", className: css.info, "aria-label": t('live.info.aria'), children: "\u24D8" }) })] }), _jsx("button", { type: "button", role: "switch", "aria-checked": live, "aria-label": t('live.title'), className: live ? `${css.switch} ${css.switchOn}` : css.switch, disabled: !ready, onClick: () => { write('live', !live); }, children: _jsx("span", { className: css.knob }) })] }), snapshot.status === 'unavailable' && (_jsx("span", { className: css.hint, children: t('live.unavailable') }))] }), saved && _jsx("span", { className: css.saved, children: t('live.applied') }), error && _jsx("span", { className: css.errorText, children: t('live.error') })] }));
165
+ return (_jsxs("div", { className: css.body, children: [_jsxs("section", { className: css.block, children: [_jsx("h3", { className: css.blockTitle, children: t('auth.title') }), _jsx(ProviderAuthBlock, { harness: { id: 'dsh', label: 'dsh' }, useSessions: useSessions, status: auth.status, runCommand: auth.runCommand, t: authT }), _jsx("span", { className: css.hint, children: t('auth.host') })] }), _jsxs("section", { className: css.block, children: [_jsxs("h3", { className: css.blockTitle, children: [t('model.title'), _jsx(Tooltip, { label: t('model.info'), side: "bottom", maxWidth: 360, children: _jsx("button", { type: "button", className: css.info, "aria-label": t('model.info.aria'), children: "\u24D8" }) })] }), _jsxs("div", { className: css.row, children: [renderModelPicker?.({ value: modelValue, onChange: setModelDraft, disabled: !ready, defaultLabel: modelDefaultItem }) ?? (choices.length > 0 ? (_jsxs("div", { className: css.modelField, ref: modelFieldRef, children: [modelInputElement, _jsx("button", { type: "button", className: css.modelMenuButton, "aria-label": t('model.menu'), "aria-haspopup": "menu", "aria-expanded": modelMenuOpen, disabled: !ready, onClick: () => { toggleModelMenu(); }, children: _jsx(IconChevronDownOutlineMedium, { className: modelMenuOpen ? `${css.modelMenuChevron} ${css.modelMenuChevronOpen}` : css.modelMenuChevron }) }), modelMenuOpen && (_jsxs("div", { className: modelMenuUp ? `${css.modelMenu} ${css.modelMenuUp}` : css.modelMenu, role: "menu", "aria-label": t('model.menu'), children: [_jsxs("button", { type: "button", role: "menuitemradio", "aria-checked": modelValue.trim() === '', className: modelValue.trim() === '' ? `${css.modelItemDefault} ${css.modelItemCurrent}` : css.modelItemDefault, onClick: () => { setModelDraft(''); setModelMenuOpen(false); }, children: [_jsx("span", { className: css.modelItemLabel, children: modelDefaultItem }), _jsx("span", { className: css.modelItemCheck, "aria-hidden": true, children: modelValue.trim() === '' ? '✓' : '' })] }), choices.map(choice => (_jsxs("button", { type: "button", role: "menuitemradio", "aria-checked": choice === modelValue, className: choice === modelValue ? css.modelItemCurrent : css.modelItem, onClick: () => { setModelDraft(choice); setModelMenuOpen(false); }, children: [_jsx("span", { className: css.modelItemLabel, children: choice }), _jsx("span", { className: css.modelItemCheck, "aria-hidden": true, children: choice === modelValue ? '✓' : '' })] }, choice)))] }))] })) : modelInputElement), _jsx("button", { type: "button", className: css.modelSave, disabled: !ready || modelValue.trim() === storedModel, onClick: () => { saveModel(); }, children: t('model.save') })] }), modelSaved && _jsx("span", { className: css.saved, children: t('model.applied') }), modelError && _jsx("span", { className: css.errorText, children: t('model.error') })] }), _jsxs("section", { className: css.block, children: [_jsxs("div", { className: css.row, children: [_jsxs("span", { className: css.rowLabel, children: [t('live.title'), _jsx(Tooltip, { label: t('live.info'), side: "bottom", maxWidth: 360, children: _jsx("button", { type: "button", className: css.info, "aria-label": t('live.info.aria'), children: "\u24D8" }) })] }), _jsx("button", { type: "button", role: "switch", "aria-checked": live, "aria-label": t('live.title'), className: live ? `${css.switch} ${css.switchOn}` : css.switch, disabled: !ready, onClick: () => { write('live', !live); }, children: _jsx("span", { className: css.knob }) })] }), snapshot.status === 'unavailable' && (_jsx("span", { className: css.hint, children: t('live.unavailable') }))] }), saved && _jsx("span", { className: css.saved, children: t('live.applied') }), error && _jsx("span", { className: css.errorText, children: t('live.error') })] }));
162
166
  }
163
167
  /**
164
168
  * The dsh harness settings card of the retired 0.1.5 plugin configuration
@@ -1,4 +1,4 @@
1
- import { bindSettingsScope } from '@khorsheed/dsh-local-agent/src/client/settings-scope.ts';
1
+ import { bindCardSettingsScope } from '@khorsheed/dsh-local-agent/src/client/settings-scope.ts';
2
2
  /**
3
3
  * Local-agent-dsh plugin, browser half: the dsh settings card — a section of
4
4
  * the family core's「本机 Agent 委派」family card on the Plugins page
@@ -45,7 +45,7 @@ export const inject = ['slots', 'remote', 'remote.commands', 'locale'];
45
45
  export function apply(ctx) {
46
46
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'local-agent-dsh: dictionaries');
47
47
  const t = ctx.locale.bind(NS);
48
- const scope = bindSettingsScope(ctx, 'local-agent-dsh');
48
+ const scope = bindCardSettingsScope(ctx, 'dsh', 'local-agent-dsh');
49
49
  // The auth block's copy lives in the family core's dictionary; binding is
50
50
  // stable per namespace and late dictionary registration still resolves.
51
51
  const authT = ctx.locale.bind('local-agent');