@khorsheed/dsh-local-agent-dsh 0.2.0-rc.1 → 0.2.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.
@@ -43,21 +43,37 @@ export interface DshSettingsCardInjected {
43
43
  };
44
44
  }
45
45
  /**
46
- * Full props of the 0.1.5 settings.plugin.item card entry. The removed slot's
47
- * owner share was intentionally empty and its root scope delivered the global
48
- * seats; the structural type below is exact about the one seat the auth block
49
- * consumes (the slot name is gone from the alpha.2 SlotMap, so the
50
- * registration goes through a duck-typed narrow — see client/index.ts).
46
+ * Full props of the retired 0.1.5 settings.plugin.item card. The host removed
47
+ * that seat and client/index.ts no longer registers the component — the live
48
+ * settings surface is the plugins.item entry built on the BundleConfig
49
+ * component below; this export remains for the component spec and direct
50
+ * composers. The removed slot's owner share was intentionally empty and its
51
+ * root scope delivered the global seats; the structural type below is exact
52
+ * about the one seat the auth block consumes.
51
53
  */
52
54
  export type DshSettingsCardProps = InjectFace<DshSettingsCardInjected> & PropsLocale<typeof NS> & {
53
55
  /** The standing root-scope global seat (ui-session's merge), delivered to the 0.1.5 card by the renderer. */
54
56
  useSessions: ProviderAuthBlockProps['useSessions'];
55
57
  };
56
- /** Full props of the alpha.2 plugins.bundle.config entry (owner prop `view`). */
57
- export type DshBundleConfigProps = PropsRuntime<'plugins.bundle.config'> & InjectFace<DshSettingsCardInjected> & PropsLocale<typeof NS>;
58
+ /** Full props of the plugins.item entry (owner prop `view`). */
59
+ export type DshBundleConfigProps = PropsRuntime<'plugins.item'> & InjectFace<DshSettingsCardInjected> & PropsLocale<typeof NS>;
58
60
  /**
59
- * The dsh harness settings card in the 0.1.5 plugin configuration tab: the
60
- * same collapsible chrome the official plugin cards use, re-implemented
61
+ * Full props of the harness's section inside the core's「本机 Agent 委派」
62
+ * family card: the same inject face minus the slot-owner share, plus the one
63
+ * global seat the body consumes, forwarded by the family card (the section
64
+ * render bypasses the slot runtime, so its remaining standard seats are not
65
+ * fabricable here and are deliberately absent).
66
+ */
67
+ export type DshCardSectionProps = InjectFace<DshSettingsCardInjected> & PropsLocale<typeof NS> & {
68
+ /** Whether the section is currently expanded (the family card mounts the body only while it is). */
69
+ readonly active: boolean;
70
+ /** The standing root-scope global seat (ui-session's merge), forwarded by the family card. */
71
+ readonly useSessions: ProviderAuthBlockProps['useSessions'];
72
+ };
73
+ /**
74
+ * The dsh harness settings card of the retired 0.1.5 plugin configuration
75
+ * tab (no longer registered; the live surface is the plugins.item entry
76
+ * below): the same collapsible chrome the official plugin cards use, re-implemented
61
77
  * locally (the bundle-purity gate forbids value-importing the official chrome
62
78
  * — the context-guard pattern), with the at-a-glance credential dot in the
63
79
  * header. Explanation copy lives in hover/focus bubbles behind ⓘ anchors —
@@ -67,12 +83,24 @@ export type DshBundleConfigProps = PropsRuntime<'plugins.bundle.config'> & Injec
67
83
  */
68
84
  export declare function DshSettingsCard(props: DshSettingsCardProps): import("react").JSX.Element;
69
85
  /**
70
- * The bundle's configuration on the alpha.2 Plugins page
71
- * (`plugins.bundle.config`, keyed by package name): the page draws the title,
72
- * the icon, and the crumb itself, so the entry renders the one-liner for the
73
- * `summary` view and the bare form — with its own save control — for `page`.
86
+ * The provider's card on the Plugins page (the `plugins.item` entry
87
+ * client/index.ts registers while the Host serves the namespace AND the core
88
+ * predates the family card): the page
89
+ * draws the title, the icon, and the crumb itself, so the entry renders the
90
+ * one-liner for the `summary` view and the bare form — with its own save
91
+ * control — for `page`.
74
92
  * @param props - the owner view, the injected scope/auth faces, and copy.
75
93
  * @returns the entry.
76
94
  */
77
95
  export declare function DshBundleConfig({ view, useSettings, scope, auth, authT, renderModelPicker, harnessModel, useSessions, t }: DshBundleConfigProps): import("react").JSX.Element;
96
+ /**
97
+ * The harness's section inside the core's「本机 Agent 委派」family card: the
98
+ * same useCardState + CardBody the plugins.item page view renders — the
99
+ * collapsible chrome belongs to the family card now. The delegation enable
100
+ * switch keeps its semantics: it writes the scope, the host mounts/unmounts
101
+ * the harness on the served value, and the section itself stays put.
102
+ * @param props - the injected scope/auth faces, the forwarded session seat, and copy.
103
+ * @returns the section body.
104
+ */
105
+ export declare function DshCardSection({ active, useSettings, scope, auth, authT, renderModelPicker, harnessModel, useSessions, t }: DshCardSectionProps): import("react").JSX.Element;
78
106
  //# sourceMappingURL=SettingsCard.d.ts.map
@@ -161,8 +161,9 @@ function CardBody({ state, auth, authT, renderModelPicker, useSessions, t }) {
161
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') })] }));
162
162
  }
163
163
  /**
164
- * The dsh harness settings card in the 0.1.5 plugin configuration tab: the
165
- * same collapsible chrome the official plugin cards use, re-implemented
164
+ * The dsh harness settings card of the retired 0.1.5 plugin configuration
165
+ * tab (no longer registered; the live surface is the plugins.item entry
166
+ * below): the same collapsible chrome the official plugin cards use, re-implemented
166
167
  * locally (the bundle-purity gate forbids value-importing the official chrome
167
168
  * — the context-guard pattern), with the at-a-glance credential dot in the
168
169
  * header. Explanation copy lives in hover/focus bubbles behind ⓘ anchors —
@@ -185,10 +186,12 @@ export function DshSettingsCard(props) {
185
186
  return (_jsxs("li", { className: open ? `${css.card} ${css.cardOpen}` : css.card, children: [_jsxs("button", { type: "button", className: css.header, "aria-expanded": open, "aria-label": `${t(open ? 'card.collapse' : 'card.expand')}: ${title}`, onClick: () => { setOpen(!open); }, children: [_jsxs("span", { className: css.headText, children: [_jsxs("span", { className: css.nameRow, children: [_jsx("span", { className: css.name, children: title }), _jsx(AuthStatusDot, { status: authStatus, label: dotLabel })] }), _jsx("span", { className: css.description, children: t('card.description') })] }), _jsx(IconChevronDownOutlineMedium, { className: open ? `${css.chevron} ${css.chevronOpen}` : css.chevron })] }), open && _jsx(CardBody, { state: state, auth: auth, authT: authT, renderModelPicker: renderModelPicker, useSessions: useSessions, t: t })] }));
186
187
  }
187
188
  /**
188
- * The bundle's configuration on the alpha.2 Plugins page
189
- * (`plugins.bundle.config`, keyed by package name): the page draws the title,
190
- * the icon, and the crumb itself, so the entry renders the one-liner for the
191
- * `summary` view and the bare form — with its own save control — for `page`.
189
+ * The provider's card on the Plugins page (the `plugins.item` entry
190
+ * client/index.ts registers while the Host serves the namespace AND the core
191
+ * predates the family card): the page
192
+ * draws the title, the icon, and the crumb itself, so the entry renders the
193
+ * one-liner for the `summary` view and the bare form — with its own save
194
+ * control — for `page`.
192
195
  * @param props - the owner view, the injected scope/auth faces, and copy.
193
196
  * @returns the entry.
194
197
  */
@@ -198,3 +201,16 @@ export function DshBundleConfig({ view, useSettings, scope, auth, authT, renderM
198
201
  return _jsx("span", { className: css.description, children: t('card.description') });
199
202
  return _jsx(CardBody, { state: state, auth: auth, authT: authT, renderModelPicker: renderModelPicker, useSessions: useSessions, t: t });
200
203
  }
204
+ /**
205
+ * The harness's section inside the core's「本机 Agent 委派」family card: the
206
+ * same useCardState + CardBody the plugins.item page view renders — the
207
+ * collapsible chrome belongs to the family card now. The delegation enable
208
+ * switch keeps its semantics: it writes the scope, the host mounts/unmounts
209
+ * the harness on the served value, and the section itself stays put.
210
+ * @param props - the injected scope/auth faces, the forwarded session seat, and copy.
211
+ * @returns the section body.
212
+ */
213
+ export function DshCardSection({ active, useSettings, scope, auth, authT, renderModelPicker, harnessModel, useSessions, t }) {
214
+ const state = useCardState(active, useSettings, scope, harnessModel, t);
215
+ return _jsx(CardBody, { state: state, auth: auth, authT: authT, renderModelPicker: renderModelPicker, useSessions: useSessions, t: t });
216
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Generated by scripts/sync-icon-artwork.mts from @deepseek-ai/dsh-client-ui-primitives
3
- * (v0.1.7-rc.1, harness 46a7f68b09) — do not edit by hand. The 0.1.5 and rc.1 host lines
3
+ * (v0.2.1-rc.1, harness 1600d8a1e1) — do not edit by hand. The 0.1.5 and rc.1 host lines
4
4
  * share no icon export name, so the rc.1 artwork is inlined here: pure SVG,
5
5
  * no host runtime identity, identical on every line. Re-run the generator
6
6
  * after adding an icon import or bumping the harness checkout.
@@ -1,30 +1,7 @@
1
- /**
2
- * Local-agent-dsh plugin, browser half: the dsh settings surface, one face
3
- * per install shape and host line — a standalone install renders the
4
- * bundle's own configuration on its Plugins-page detail view
5
- * (`plugins.bundle.config`, keyed by package name), a family-bundle install
6
- * renders the same card through the row-level configure entry on the
7
- * BUNDLE's detail view (`plugins.row.config`, keyed
8
- * `@khorsheed/dsh-bundle-local-agent#local-agent-dsh`), and 0.1.5 renders
9
- * the `settings.plugin.item` card (keyed to the `local-agent-dsh`
10
- * settings namespace the host half registers) in the official
11
- * Plugins → 可配置插件 tab. The card carries the family core's shared
12
- * ProviderAuthBlock (dsh authenticates through the host credentials, so the
13
- * block renders status only), the DeepSeek delegation switch — migrated from
14
- * the core section's `local-agent.settings.row-action` seat, which this
15
- * plugin no longer contributes to — and the resident-mode block (live switch)
16
- * plus the default-model block (free-text input, the broker's effective-model
17
- * line and suggestion vocabulary), whose writes ride the bound settingsScope:
18
- * the host watcher hot-applies them, no reload. Every read degrades: an
19
- * absent gateway renders the auth block's 'unavailable' state and drops the
20
- * model surface to the bare input, an unregistered namespace disables the
21
- * controls.
22
- * @module @khorsheed/dsh-local-agent-dsh/client
23
- */
24
1
  import type { Context } from '@deepseek-ai/cordis';
25
2
  import { en, NS, zh, type LocalAgentDshKey } from './locales.ts';
26
- export { DshBundleConfig, DshSettingsCard } from './SettingsCard.tsx';
27
- export type { DshCardSettings, DshBundleConfigProps, DshSettingsCardInjected, DshSettingsCardProps, } from './SettingsCard.tsx';
3
+ export { DshBundleConfig, DshCardSection, DshSettingsCard } from './SettingsCard.tsx';
4
+ export type { DshCardSettings, DshBundleConfigProps, DshCardSectionProps, DshSettingsCardInjected, DshSettingsCardProps, } from './SettingsCard.tsx';
28
5
  export { en, NS, zh };
29
6
  export type { LocalAgentDshKey };
30
7
  /**
@@ -33,11 +10,12 @@ export type { LocalAgentDshKey };
33
10
  * line (`settingsScope` on 0.1.5, `configForms` on rc.1), so a static entry
34
11
  * would pend the whole client plugin on the other line; bindSettingsScope
35
12
  * probes the serving one instead (the settings client package is a declared
36
- * client inject, so one of them is always already up).
13
+ * client inject, so one of them is always already up). The card track probes
14
+ * `configForms` through the same kind of deferred inject (see apply).
37
15
  */
38
16
  export declare const inject: string[];
39
17
  /**
40
- * Client plugin body: register the dictionaries and the dsh settings surfaces
18
+ * Client plugin body: register the dictionaries and the dsh settings card
41
19
  * bound to the local-agent-dsh namespace.
42
20
  * @param ctx - client root context.
43
21
  */
@@ -1,30 +1,50 @@
1
1
  import { bindSettingsScope } from '@khorsheed/dsh-local-agent/src/client/settings-scope.ts';
2
- import { en, NS, zh } from './locales.js';
3
- import { DshBundleConfig, DshSettingsCard, } from './SettingsCard.js';
4
- export { DshBundleConfig, DshSettingsCard } from './SettingsCard.js';
5
- export { en, NS, zh };
6
2
  /**
7
- * This bundle's package name — the key the Plugins page dispatches
8
- * `plugins.bundle.config` entries on (identity triangle: cordis.patch.yml,
9
- * tsdown.config.ts, invariant.ts).
3
+ * Local-agent-dsh plugin, browser half: the dsh settings card — a section of
4
+ * the family core's「本机 Agent 委派」family card on the Plugins page
5
+ * (registered through the core's `localAgentUi.registerHarnessCard` while the
6
+ * Host serves the `local-agent-dsh` namespace), or this package's own
7
+ * `plugins.item` entry when the core predates the family card (the
8
+ * declare-and-degrade fallback). Every
9
+ * install shape serves the row's volatile Config under the row id —
10
+ * standalone install, family bundle, or mode pack — so the card follows the
11
+ * served namespace rather than any carrier bundle, and a composition without
12
+ * the row shows no trace of it. The card carries the family core's shared
13
+ * ProviderAuthBlock (dsh authenticates through the host credentials, so the
14
+ * block renders status only), the DeepSeek delegation switch — migrated from
15
+ * the core section's `local-agent.settings.row-action` seat, which this
16
+ * plugin no longer contributes to — and the resident-mode block (live switch)
17
+ * plus the default-model block (free-text input, the broker's effective-model
18
+ * line and suggestion vocabulary), whose writes ride the bound settingsScope:
19
+ * the host watcher hot-applies them, no reload. Every read degrades: an
20
+ * absent gateway renders the auth block's 'unavailable' state and drops the
21
+ * model surface to the bare input, an unregistered namespace disables the
22
+ * controls.
23
+ * @module @khorsheed/dsh-local-agent-dsh/client
10
24
  */
11
- const PACKAGE_NAME = '@khorsheed/dsh-local-agent-dsh';
25
+ import { createElement, useSyncExternalStore } from 'react';
26
+ import { en, NS, zh } from './locales.js';
27
+ import { DshBundleConfig, DshCardSection, } from './SettingsCard.js';
28
+ export { DshBundleConfig, DshCardSection, DshSettingsCard } from './SettingsCard.js';
29
+ export { en, NS, zh };
12
30
  /**
13
31
  * Required services: slot registry, command Remote, and locale registry. The
14
32
  * settings scope is NOT a static inject — its service name differs per host
15
33
  * line (`settingsScope` on 0.1.5, `configForms` on rc.1), so a static entry
16
34
  * would pend the whole client plugin on the other line; bindSettingsScope
17
35
  * probes the serving one instead (the settings client package is a declared
18
- * client inject, so one of them is always already up).
36
+ * client inject, so one of them is always already up). The card track probes
37
+ * `configForms` through the same kind of deferred inject (see apply).
19
38
  */
20
39
  export const inject = ['slots', 'remote', 'remote.commands', 'locale'];
21
40
  /**
22
- * Client plugin body: register the dictionaries and the dsh settings surfaces
41
+ * Client plugin body: register the dictionaries and the dsh settings card
23
42
  * bound to the local-agent-dsh namespace.
24
43
  * @param ctx - client root context.
25
44
  */
26
45
  export function apply(ctx) {
27
46
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'local-agent-dsh: dictionaries');
47
+ const t = ctx.locale.bind(NS);
28
48
  const scope = bindSettingsScope(ctx, 'local-agent-dsh');
29
49
  // The auth block's copy lives in the family core's dictionary; binding is
30
50
  // stable per namespace and late dictionary registration still resolves.
@@ -34,10 +54,11 @@ export function apply(ctx) {
34
54
  // never matters, and degrade to the auth block's 'unavailable' state when
35
55
  // the core client is absent.
36
56
  const gateway = () => ctx.get('remote.localAgentGateway');
37
- // Both settings surfaces inject the same face.
38
- const cardInject = () => ({
57
+ // The card's inject face minus the hooks compartment — the family-card
58
+ // section consumes this verbatim; the standalone card's slot registration
59
+ // adds the hooks compartment back below.
60
+ const cardFace = () => ({
39
61
  scope,
40
- hooks: { settings: scope },
41
62
  authT,
42
63
  renderModelPicker: props => ctx.get('localAgentUi')?.renderHarnessModelPicker('dsh', props),
43
64
  // The harness's memberless model surface; absent gateway/broker
@@ -54,35 +75,57 @@ export function apply(ctx) {
54
75
  .then(result => (result.ok ? result.value?.result.text : undefined)),
55
76
  },
56
77
  });
57
- // The settings surface follows the host line AND the install shape — three
58
- // tracks, all riding slots.inject so each fires only where its declaration
59
- // exists. Standalone install: the package's own Plugins-page detail view
60
- // (`plugins.bundle.config`, keyed by package name). Installed as a family
61
- // member: the member is no longer the profile's direct dependency, so only
62
- // the bundle gets a detail view — the card rides the row-level slot
63
- // (`plugins.row.config`, keyed `<bundle package>#<row id>` with the row id
64
- // the bundle's patch declares) and the bundle's page gains a configure
65
- // entry opening it. 0.1.5: the legacy configurable-plugins tab card
66
- // (`settings.plugin.item`, keyed by the settings namespace) through a
67
- // string-keyed duck narrow of the same service (the slot name is gone from
68
- // the alpha.2 SlotMap).
69
- ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({
70
- name: 'plugins.bundle.config',
71
- key: PACKAGE_NAME,
72
- locale: NS,
73
- inject: cardInject,
74
- }, DshBundleConfig));
75
- ctx.slots.inject('plugins.row.config', () => ctx.slots.register({
76
- name: 'plugins.row.config',
77
- key: '@khorsheed/dsh-bundle-local-agent#local-agent-dsh',
78
- locale: NS,
79
- inject: cardInject,
80
- }, DshBundleConfig));
81
- const legacy = ctx.slots;
82
- legacy.inject('settings.plugin.item', () => legacy.register({
83
- name: 'settings.plugin.item',
84
- key: 'local-agent-dsh',
85
- locale: NS,
86
- inject: cardInject,
87
- }, DshSettingsCard));
78
+ // The settings card injects this face.
79
+ const cardInject = () => ({ ...cardFace(), hooks: { settings: scope } });
80
+ // The family-card section render bypasses the slot renderer's inject.hooks
81
+ // materialization, so bind the scope's selector hook here with the same
82
+ // semantics (a uSES subscription over the scope's own subscribe/getSnapshot).
83
+ const subscribeScope = (listener) => scope.subscribe(listener);
84
+ const readScope = () => scope.getSnapshot();
85
+ const useSettings = (select) => select(useSyncExternalStore(subscribeScope, readScope));
86
+ // One Plugins-page seat gated on the SERVED NAMESPACE, never on the install
87
+ // shape: whichever composition carries the `local-agent-dsh` row —
88
+ // standalone install, family bundle, or mode pack — serves the row's
89
+ // volatile Config under the row id, and `whileServed` keeps the
90
+ // registration alive for exactly that span (the official companion
91
+ // convention), so a composition without the row shows no trace of the card.
92
+ // This replaces the retired per-shape seats: `plugins.row.config` hardcoded
93
+ // one carrier bundle into its key, and the host deleted
94
+ // `settings.plugin.item`. The probe rides a deferred inject because
95
+ // configForms exists only on the alpha.2+ host line; a 0.1.5 host never
96
+ // fires it, and a configForms predating whileServed (0.1.6-alpha.2) skips
97
+ // the card.
98
+ ctx.inject(['configForms'], (formsCtx) => {
99
+ const forms = formsCtx.configForms;
100
+ if (typeof forms.whileServed !== 'function')
101
+ return;
102
+ return forms.whileServed(['local-agent-dsh'], () => {
103
+ // The family card owns the Plugins-page seat when the core's client
104
+ // provides it: a core new enough answers registerHarnessCard, and this
105
+ // package renders as a section of the「本机 Agent 委派」card instead of
106
+ // its own entry. The probe runs at serve time — a host round-trip past
107
+ // client plugin boot — so a composed core has already provided the
108
+ // service; an old or absent core keeps the standalone card below.
109
+ const ui = ctx.get('localAgentUi');
110
+ if (typeof ui?.registerHarnessCard === 'function') {
111
+ return ui.registerHarnessCard({
112
+ name: 'dsh',
113
+ // The retired standalone card's order, kept as the section order.
114
+ order: 140,
115
+ locale: NS,
116
+ label: () => t('card.title'),
117
+ render: ({ active, useSessions }) => createElement(DshCardSection, { active, useSessions, ...cardFace(), useSettings, t }),
118
+ });
119
+ }
120
+ return ctx.slots.inject('plugins.item', () => ctx.slots.register({
121
+ name: 'plugins.item',
122
+ id: 'local-agent-dsh',
123
+ // Community cards sort after the official settings pages (orders 10-40).
124
+ order: 140,
125
+ label: () => t('card.title'),
126
+ locale: NS,
127
+ inject: cardInject,
128
+ }, DshBundleConfig));
129
+ });
130
+ });
88
131
  }
@@ -3,7 +3,7 @@
3
3
  export const NS = 'local-agent-dsh';
4
4
  /** Simplified Chinese dictionary (the key-set source of truth). */
5
5
  export const zh = {
6
- 'card.title': 'Local Agent · dsh',
6
+ 'card.title': '本机 DSH 委派',
7
7
  'card.description': 'dsh 自委派:开关、认证状态与常驻模式(live)',
8
8
  'card.expand': '展开',
9
9
  'card.collapse': '收起',
@@ -35,7 +35,7 @@ export const zh = {
35
35
  };
36
36
  /** English dictionary, checked complete against the zh key set. */
37
37
  export const en = {
38
- 'card.title': 'Local Agent · dsh',
38
+ 'card.title': 'Local DSH Delegation',
39
39
  'card.description': 'dsh self-delegation: toggle, credential status, and resident (live) mode',
40
40
  'card.expand': 'Expand',
41
41
  'card.collapse': 'Collapse',
@@ -10,15 +10,16 @@
10
10
  * that session. No stdout parsing — the sub-dsh session id is known a priori.
11
11
  * @module @khorsheed/dsh-local-agent-dsh/dsh-cli-provider
12
12
  */
13
+ import { localModelLaunch } from './model-launch.js';
13
14
  import { randomUUID } from 'node:crypto';
14
15
  import { credentialRef } from '@deepseek-ai/dsh-credentials';
15
16
  import { SessionId } from '@deepseek-ai/dsh-session';
16
- import { NO_START_CAPABILITIES, settleRunResult, subprocessRunHandle, } from '@deepseek-ai/dsh-subagent';
17
- import { assertResumeCwdUnchanged, assertResumeScopeUnchanged, resolveRoundModel, containerExecSpawn, containerScopedHome, delegationEnv, establishSubagentCatalogChild, probeCliVersion, resolveChildCwd, subagentDelegationLabel, } from '@khorsheed/dsh-local-agent';
17
+ import { NO_START_CAPABILITIES, SUBAGENT_DESCRIPTOR_VERSION, settleRunResult, subprocessRunHandle, } from '@deepseek-ai/dsh-subagent';
18
+ import { assertResumeCwdUnchanged, assertResumeScopeUnchanged, resolveRoundModel, containerExecSpawn, containerScopedHome, delegationEnv, establishSubagentCatalogChild, persistChildSession, probeCliVersion, resolveChildCwd, subagentDelegationLabel, } from '@khorsheed/dsh-local-agent';
18
19
  import { MEMBER_BRIDGE_SOCKET_ENV, MEMBER_BRIDGE_TOKEN_ENV } from '@khorsheed/dsh-local-agent/types';
19
20
  import { LiveChannelUnavailableError } from './live-driver.js';
20
- import { DEFAULT_SUB_PROFILE_NAME, provisionDshScope } from './provision.js';
21
- import { mirrorDshSession } from './session-mirror.js';
21
+ import { DEFAULT_SUB_PROFILE_NAME, provisionDshScope, unitSubProfileName } from './provision.js';
22
+ import { closeMirroredOpenStep, mirrorDshSession } from './session-mirror.js';
22
23
  /**
23
24
  * Spawn-env name carrying the bridge entry path into the sub-dsh process,
24
25
  * where the headless bundle's mcp-client row reads it (`!!js`). The name is a
@@ -168,7 +169,7 @@ export class DshCliProvider {
168
169
  ...record?.configurationLock === undefined ? {} : { configurationLock: record.configurationLock },
169
170
  }, configuration => this.startDshResume(request, intent, cwd, homeDir, exec, scope, record?.model, configuration), request.signal, intent?.onAdmitted);
170
171
  }
171
- const childSessionId = SessionId(intent?.preparedMemberId ?? randomUUID());
172
+ const childSessionId = SessionId(intent?.preparedMemberId ?? intent?.childSessionId ?? randomUUID());
172
173
  if (typeof this.ctx.localAgent.withMemberConfigurationRound !== 'function') {
173
174
  if (intent?.effort !== undefined)
174
175
  throw new Error('DSH effort requires the configuration admission core');
@@ -202,20 +203,25 @@ export class DshCliProvider {
202
203
  delegationDepth: (request.parent.session.header.delegationDepth ?? 0) + 1,
203
204
  },
204
205
  });
205
- // The durable one-shot descriptor the runtime resolved marks this child
206
- // as session-backed; without it the 子代理 projection cannot classify
207
- // the session and the delegation stays invisible. The label carries the
208
- // harness display name as the source marker so the dropdown shows which
209
- // local agent produced the conversation.
206
+ // The durable one-shot descriptor marks this child as session-backed;
207
+ // without it the 子代理 projection cannot classify the session and the
208
+ // delegation stays invisible. Host 0.2.1 no longer resolves a descriptor
209
+ // onto the request (the runtime's one-shot snapshot is gone), so the
210
+ // provider stamps the same payload itself. The label carries the harness
211
+ // display name as the source marker so the dropdown shows which local
212
+ // agent produced the conversation; the activation spec's required label
213
+ // arrives as the empty string when the caller named none.
210
214
  const harness = this.ctx.localAgent.get('dsh');
211
- const label = subagentDelegationLabel(harness?.displayName ?? 'dsh', request.descriptor.label);
215
+ const label = subagentDelegationLabel(harness?.displayName ?? 'dsh', request.label || undefined);
212
216
  childSession.append('subagent/descriptor', {
213
- ...request.descriptor,
217
+ version: SUBAGENT_DESCRIPTOR_VERSION,
218
+ mode: 'one-shot',
219
+ provider: this.name,
214
220
  label,
215
221
  });
216
- // The official runtime appends the parent-side subagent/catalog
217
- // discovery row only for in-process children (run.localAgent); this run
218
- // is remote, so the provider appends it here — once per child, where
222
+ // The 0.2.1 manager appends the parent-side subagent/catalog discovery
223
+ // row only for parent-delivered children; the family runs caller
224
+ // delivery, so the provider appends it here — once per child, where
219
225
  // the descriptor lands. A failure degrades to the warn below; a missing
220
226
  // row never blocks the delegation.
221
227
  establishSubagentCatalogChild(request.parent.session, childSession.header, label);
@@ -488,20 +494,25 @@ export async function startDshCliRun(request, spec) {
488
494
  if (request.signal.aborted) {
489
495
  throw new Error('subagent-dsh: request was aborted before the CLI started');
490
496
  }
491
- const profileName = config.profileName ?? DEFAULT_SUB_PROFILE_NAME;
497
+ // A container round launches the `-unit` twin when one is configured: its
498
+ // headless link names the image's own install, while the host profile
499
+ // links this instance's (`unitHeadlessBundleDir`).
500
+ const profileName = spec.exec === undefined ? config.profileName ?? DEFAULT_SUB_PROFILE_NAME : unitSubProfileName(config);
492
501
  // Provisioning is idempotent and cheap; re-running heals a deleted or
493
502
  // drifted sub-profile before every round. A CONTAINER round skips it: the
494
- // sub-profile's node_modules symlink points at the host installation of the
495
- // headless bundle, which resolves to nothing inside a unit — and the scoped
503
+ // sub-profile's node_modules carries the HOST installation's headless
504
+ // bundle (a copy of the host line's build, or a link that resolves to
505
+ // nothing inside a unit) — and the scoped
496
506
  // home is bind-mounted, so writing it would plant a broken profile in the
497
507
  // directory the unit actually reads. A containerized caller names the
498
508
  // unit's own profile through `profileName` (the image's in-box `headless`)
499
509
  // and its own entry through `cliLaunch`.
500
510
  if (spec.exec === undefined)
501
- provisionDshScope(spec.homeDir, config);
511
+ provisionDshScope(spec.homeDir, { ...config, inheritModelConfiguration: true });
502
512
  // Resolve the sub-dsh credential BEFORE spawning: the key travels in the
503
513
  // explicit env layer, which is the only way past the shared env scrub.
504
- const apiKey = await resolveApiKey(ctx, config);
514
+ const modelEnv = spec.exec === undefined ? (await localModelLaunch(ctx, config, spec.model)).env
515
+ : { DEEPSEEK_API_KEY: await resolveApiKey(ctx, config) };
505
516
  const turn = spec.resume?.turn ?? 1;
506
517
  // The turn opens at the real spawn moment so the timing projection
507
518
  // measures actual CLI runtime.
@@ -525,7 +536,7 @@ export async function startDshCliRun(request, spec) {
525
536
  // sessions into the parent instance's store.
526
537
  const env = delegationEnv({
527
538
  DSH_HOME: spec.homeDir,
528
- DEEPSEEK_API_KEY: apiKey,
539
+ ...modelEnv,
529
540
  // The launch replicates the parent's tsx ESM hook (dshLaunchArgv), so
530
541
  // the hook's tsconfig path is a hard launch dependency: without it tsx
531
542
  // compiles the harness source with default options and the child dies
@@ -674,47 +685,71 @@ export async function startDshCliRun(request, spec) {
674
685
  // reached its endpoint and produced an answer.
675
686
  if (settled.stopReason === 'completed')
676
687
  markCredentialVerified(ctx, spec.scope);
677
- // The turn closes at the real settle moment, so the timing projection's
678
- // duration equals the actual CLI runtime. Every terminal path closes the
679
- // window — a failed or cancelled run settles 'error'/'aborted' instead of
680
- // leaving the turn open with a distorted tiny duration.
681
- if (spec.childSession !== undefined) {
682
- if (settled.stopReason === 'completed') {
683
- spec.childSession.append('turn/end', { turn, reason: { kind: 'completed' } });
684
- }
685
- else if (settled.stopReason === 'aborted') {
686
- spec.childSession.append('turn/end', { turn, reason: { kind: 'aborted', reason: { kind: 'parent' } } });
687
- }
688
- else {
689
- spec.childSession.append('turn/end', {
690
- turn,
691
- reason: { kind: 'error', error: { message: 'dsh exited before the run completed', code: 'UNKNOWN' } },
692
- });
693
- }
694
- }
688
+ // The turn does NOT close here: turn/end lands in the mirror queue after
689
+ // the settle pass (see below), so every mirrored event precedes it in the
690
+ // stored log — the released-log reader rejects content outside an open
691
+ // turn on every cold read.
695
692
  return settled;
696
693
  });
697
694
  // After the child EXITS — however it ended (completed, killed by dispose, or
698
695
  // crashed) — mirror whatever the sub-dsh session already produced into the
699
696
  // dsh subagent session, so every round preserves its conversation and real
700
697
  // token usage instead of showing turn boundaries only. Waits for the settle
701
- // chain first (so turn/end is already appended) AND for the process to
702
- // actually exit (the sub-dsh flushed its session before exiting) before
703
- // reading.
698
+ // chain first AND for the process to actually exit (the sub-dsh flushed its
699
+ // session before exiting) before reading. The turn closes INSIDE the mirror
700
+ // queue, after the settle pass: the copied tail's open step first receives
701
+ // its closure (an interrupted sub-dsh leaves its step — and any advertised
702
+ // tool calls — open, and the released-log reader rejects a turn/end over
703
+ // it), then turn/end lands and is persisted explicitly (the mirror's own
704
+ // persist skips a no-op pass, which is exactly the case that needs the
705
+ // boundary written). Every terminal path closes the window — a failed or
706
+ // cancelled run settles 'error'/'aborted' instead of leaving the turn open.
704
707
  void result.then(() => child.done).then(() => {
705
708
  if (spec.childSession !== undefined) {
706
709
  const childSession = spec.childSession;
707
710
  // The settle pass reports the final mirror count even when polling
708
711
  // already covered the round (empty delta): it is authoritative.
709
- return enqueueMirror(() => mirrorAndReport(childSession, true));
712
+ return enqueueMirror(async () => {
713
+ const settled = await result;
714
+ try {
715
+ await mirrorAndReport(childSession, true);
716
+ }
717
+ catch (error) {
718
+ // The mirror degrades to a warn; the turn boundary still closes —
719
+ // an open turn at EOF reads as an interrupted tail only until the
720
+ // next resume round tries to open its turn over it.
721
+ ctx.logger.warn(`subagent-dsh: settle mirror failed: ${error instanceof Error ? error.message : String(error)}`);
722
+ }
723
+ closeMirroredOpenStep(childSession);
724
+ // The turn closes at the real settle moment (plus the settle-mirror
725
+ // quiescence), so the timing projection's duration equals the actual
726
+ // CLI runtime.
727
+ if (settled.stopReason === 'completed') {
728
+ childSession.append('turn/end', { turn, reason: { kind: 'completed' } });
729
+ }
730
+ else if (settled.stopReason === 'aborted') {
731
+ childSession.append('turn/end', { turn, reason: { kind: 'aborted', reason: { kind: 'parent' } } });
732
+ }
733
+ else {
734
+ childSession.append('turn/end', {
735
+ turn,
736
+ reason: { kind: 'error', error: { message: 'dsh exited before the run completed', code: 'UNKNOWN' } },
737
+ });
738
+ }
739
+ await persistChildSession(ctx, childSession).catch((error) => {
740
+ ctx.logger.warn(`subagent-dsh: turn/end persistence failed: ${error instanceof Error ? error.message : String(error)}`);
741
+ });
742
+ });
710
743
  }
711
744
  return undefined;
712
745
  }, () => { });
713
746
  return Promise.resolve(subprocessRunHandle({
714
- // A session-backed run's id is the child session id (the seam's local-run
715
- // contract), so the tool's resume self-description names the same handle
716
- // the delegation registry records.
717
- id: spec.childSession?.id ?? SessionId(randomUUID()),
747
+ // Host 0.2.1: the published run id is a fresh PER-EXECUTION identity —
748
+ // the activation manager rejects a run id naming a live Session (the
749
+ // mirrored child session is exactly that), and resume rounds reuse the
750
+ // child session id. The durable child identity the family tracks rides
751
+ // the staged intent instead (spec.childSession.id == its childSessionId).
752
+ id: SessionId(randomUUID()),
718
753
  result,
719
754
  signal: request.signal,
720
755
  onAbort,
@@ -4,7 +4,7 @@
4
4
  * namespace (default off); while ON, the controller registers the `dsh`
5
5
  * harness into the core registry (scoped home under the shared homes root,
6
6
  * `$DSH_HOME`), the `dsh-cli` delegation provider (sub-dsh headless runs with
7
- * the parent's resolved DeepSeek key), and the family delegation tool for it.
7
+ * the parent's selected API-key model route), and the family delegation tool for it.
8
8
  * While OFF — the default — nothing model-visible mounts, so the instance
9
9
  * keeps exactly the current behavior (official in-process subagent tools) and
10
10
  * the model never sees two overlapping delegation tools at once.
@@ -42,6 +42,14 @@ export interface LocalAgentDshConfig {
42
42
  cliLaunch?: string[];
43
43
  /** Override the headless bundle directory the sub-profile symlinks to. */
44
44
  headlessBundleDir?: string;
45
+ /**
46
+ * The headless bundle directory as an evaluation unit (container round)
47
+ * sees it — the image's own install. Present, provisioning also writes a
48
+ * `<profileName>-unit` sub-profile linking it, and container rounds launch
49
+ * that one, while host rounds keep `headlessBundleDir` (default: this
50
+ * instance's own headless). Absent, both share one sub-profile.
51
+ */
52
+ unitHeadlessBundleDir?: string;
45
53
  /**
46
54
  * The sub-dsh's permission boundary: the sandbox mode every confined call
47
55
  * runs under, paired with the approval policy `dsh-base`'s own preset table