@khorsheed/dsh-local-agent 0.2.0-rc.3 → 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.
@@ -15,7 +15,7 @@ let _khorsheed_dsh_local_agent_localAgentGateway_cancelMemberConfiguration_resul
15
15
  const _khorsheed_dsh_local_agent_localAgentGateway_cancelMemberConfiguration_result$schema = () => (_khorsheed_dsh_local_agent_localAgentGateway_cancelMemberConfiguration_result$schema$value ??= z.object({
16
16
  'requestId': z.string(),
17
17
  'revision': z.number(),
18
- 'status': z.union([z.literal("cancelled"), z.literal("failed"), z.literal("pending"), z.literal("applied"), z.literal("locked"), z.literal("applying"), z.literal("unsupported"), z.literal("conflict")]),
18
+ 'status': z.union([z.literal("cancelled"), z.literal("failed"), z.literal("pending"), z.literal("applied"), z.literal("conflict"), z.literal("unsupported"), z.literal("locked"), z.literal("applying")]),
19
19
  'error': z.string().optional(),
20
20
  }))
21
21
  let _khorsheed_dsh_local_agent_localAgentGateway_checkEndpoint_parameter_0$schema$value
@@ -1106,7 +1106,7 @@ let _khorsheed_dsh_local_agent_localAgentGateway_selectMemberConfiguration_resul
1106
1106
  const _khorsheed_dsh_local_agent_localAgentGateway_selectMemberConfiguration_result$schema = () => (_khorsheed_dsh_local_agent_localAgentGateway_selectMemberConfiguration_result$schema$value ??= z.object({
1107
1107
  'requestId': z.string(),
1108
1108
  'revision': z.number(),
1109
- 'status': z.union([z.literal("cancelled"), z.literal("failed"), z.literal("pending"), z.literal("applied"), z.literal("locked"), z.literal("applying"), z.literal("unsupported"), z.literal("conflict")]),
1109
+ 'status': z.union([z.literal("cancelled"), z.literal("failed"), z.literal("pending"), z.literal("applied"), z.literal("conflict"), z.literal("unsupported"), z.literal("locked"), z.literal("applying")]),
1110
1110
  'error': z.string().optional(),
1111
1111
  }))
1112
1112
  let _khorsheed_dsh_local_agent_localAgentGateway_sessions_parameter_0$schema$value
@@ -1363,7 +1363,7 @@ export const TYPERT_REMOTE = createTypertRemoteInfo({
1363
1363
  typeSymbol: '@khorsheed/dsh-local-agent/types#LocalAgentControlReceipt',
1364
1364
  create: _khorsheed_dsh_local_agent_localAgentGateway_cancelMemberConfiguration_result$schema,
1365
1365
  schema: _khorsheed_dsh_local_agent_localAgentGateway_cancelMemberConfiguration_result$schema(),
1366
- jsonSchema: {"type":"object","properties":{"requestId":{"type":"string"},"revision":{"type":"number"},"status":{"type":"string","enum":["cancelled","failed","pending","applied","locked","applying","unsupported","conflict"]},"error":{"type":"string"}},"required":["requestId","revision","status"],"additionalProperties":true},
1366
+ jsonSchema: {"type":"object","properties":{"requestId":{"type":"string"},"revision":{"type":"number"},"status":{"type":"string","enum":["cancelled","failed","pending","applied","conflict","unsupported","locked","applying"]},"error":{"type":"string"}},"required":["requestId","revision","status"],"additionalProperties":true},
1367
1367
  },
1368
1368
  sourceLocation: {"file":"packages/local-agent/src/gateway.ts","line":414,"column":9},
1369
1369
  },
@@ -2399,7 +2399,7 @@ export const TYPERT_REMOTE = createTypertRemoteInfo({
2399
2399
  typeSymbol: '@khorsheed/dsh-local-agent/types#LocalAgentControlReceipt',
2400
2400
  create: _khorsheed_dsh_local_agent_localAgentGateway_selectMemberConfiguration_result$schema,
2401
2401
  schema: _khorsheed_dsh_local_agent_localAgentGateway_selectMemberConfiguration_result$schema(),
2402
- jsonSchema: {"type":"object","properties":{"requestId":{"type":"string"},"revision":{"type":"number"},"status":{"type":"string","enum":["cancelled","failed","pending","applied","locked","applying","unsupported","conflict"]},"error":{"type":"string"}},"required":["requestId","revision","status"],"additionalProperties":true},
2402
+ jsonSchema: {"type":"object","properties":{"requestId":{"type":"string"},"revision":{"type":"number"},"status":{"type":"string","enum":["cancelled","failed","pending","applied","conflict","unsupported","locked","applying"]},"error":{"type":"string"}},"required":["requestId","revision","status"],"additionalProperties":true},
2403
2403
  },
2404
2404
  sourceLocation: {"file":"packages/local-agent/src/gateway.ts","line":409,"column":9},
2405
2405
  },
@@ -8,10 +8,30 @@ export interface HarnessModelPickerProps {
8
8
  face: ModelDirectoryFace;
9
9
  value: string;
10
10
  onChange(value: string): void;
11
+ /**
12
+ * Present = the menu also offers the model's reasoning-effort row
13
+ * ('' = follow the harness default). Absent = the picker stays model-only
14
+ * (settings cards' behavior).
15
+ */
16
+ effort?: {
17
+ value: string;
18
+ onChange(value: string): void;
19
+ } | undefined;
11
20
  disabled?: boolean | undefined;
12
21
  defaultLabel?: string | undefined;
22
+ /**
23
+ * Present = render as form fields (a labelled model select, plus an effort
24
+ * select when `effort` is present) in the host form's own classes, instead
25
+ * of the drill-down menu — for a dialog whose other fields are selects.
26
+ */
27
+ fields?: {
28
+ field?: string | undefined;
29
+ label?: string | undefined;
30
+ select?: string | undefined;
31
+ row?: string | undefined;
32
+ } | undefined;
13
33
  t: ConfigurationTranslate;
14
34
  }
15
35
  /** Discover while expanded or resolving an empty choice; settings remain caller-owned drafts. */
16
- export declare function HarnessModelPicker({ face, value, onChange, disabled, t }: HarnessModelPickerProps): import("react").JSX.Element;
36
+ export declare function HarnessModelPicker({ face, value, onChange, effort, disabled, fields, t }: HarnessModelPickerProps): import("react").JSX.Element;
17
37
  //# sourceMappingURL=HarnessModelPicker.d.ts.map
@@ -4,9 +4,11 @@ import { ModelConfigurationFields } from './ModelConfigurationFields.js';
4
4
  import css from './MemberConfiguration.module.css';
5
5
  import { useConfigurationMenu } from './use-configuration-menu.js';
6
6
  /** Discover while expanded or resolving an empty choice; settings remain caller-owned drafts. */
7
- export function HarnessModelPicker({ face, value, onChange, disabled, t }) {
7
+ export function HarnessModelPicker({ face, value, onChange, effort, disabled, fields, t }) {
8
8
  const menu = useConfigurationMenu('below');
9
- const { open, setOpen } = menu;
9
+ const { setOpen } = menu;
10
+ // Form fields show the directory all the time: discovery runs as if open.
11
+ const open = menu.open || fields !== undefined;
10
12
  const [surface, setSurface] = useState();
11
13
  const directory = surface?.face === face ? surface.directory : undefined;
12
14
  const currentFace = useRef(face);
@@ -44,5 +46,31 @@ export function HarnessModelPicker({ face, value, onChange, disabled, t }) {
44
46
  onChange(first.value);
45
47
  }, [directory, value, disabled, onChange]);
46
48
  const entry = directory?.entries.find(entry => entry.value === value);
47
- return _jsxs("details", { ref: menu.root, className: `${css.root} ${css.settings}`, open: open, onToggle: event => setOpen(event.currentTarget.open), children: [_jsx("summary", { "aria-label": t('configuration.model'), "aria-expanded": open, children: _jsx("span", { className: css.triggerLabel, children: entry?.label ?? (value || t('configuration.choose')) }) }), open && _jsxs("div", { ref: menu.panel, className: css.panel, style: menu.style, children: [error && _jsx("div", { role: "alert", children: error }), !directory && _jsx("span", { role: "status", children: t('loading') }), _jsx(ModelConfigurationFields, { directory: directory, value: { model: value ? { mode: 'value', value } : { mode: 'default' }, effort: { mode: 'default' } }, onChange: selection => { onChange(selection.model.mode === 'value' ? selection.model.value : ''); setOpen(false); }, showEffort: false, disabled: disabled, t: t })] })] });
49
+ if (fields !== undefined)
50
+ return _jsx(ModelSelects, { directory: directory, error: error, value: value, onChange: onChange, effort: effort, disabled: disabled, classes: fields, t: t });
51
+ return _jsxs("details", { ref: menu.root, className: `${css.root} ${css.settings}`, open: open, onToggle: event => setOpen(event.currentTarget.open), children: [_jsx("summary", { "aria-label": t('configuration.model'), "aria-expanded": open, children: _jsx("span", { className: css.triggerLabel, children: entry?.label ?? (value || t('configuration.choose')) }) }), open && _jsxs("div", { ref: menu.panel, className: css.panel, style: menu.style, children: [error && _jsx("div", { role: "alert", children: error }), !directory && _jsx("span", { role: "status", children: t('loading') }), _jsx(ModelConfigurationFields, { directory: directory, value: { model: value ? { mode: 'value', value } : { mode: 'default' }, effort: effort !== undefined && effort.value !== '' ? { mode: 'value', value: effort.value } : { mode: 'default' } }, onChange: selection => {
52
+ onChange(selection.model.mode === 'value' ? selection.model.value : '');
53
+ effort?.onChange(selection.effort.mode === 'value' ? selection.effort.value : '');
54
+ // The effort-capable menu navigates between panes instead of
55
+ // closing on a pick (the MemberConfiguration card's behavior).
56
+ if (effort === undefined)
57
+ setOpen(false);
58
+ }, showEffort: effort !== undefined, disabled: disabled, t: t })] })] });
59
+ }
60
+ /** The form-field face: a model select and, when asked, the model's effort select ('' = the model's default). */
61
+ function ModelSelects({ directory, error, value, onChange, effort, disabled, classes, t }) {
62
+ const entries = directory?.entries ?? [];
63
+ // The chosen model stays listed even when the directory hides it.
64
+ const shown = entries.filter(entry => !entry.hidden || entry.value === value);
65
+ const reasoning = entries.find(entry => entry.value === value)?.reasoning;
66
+ const options = reasoning?.options ?? [];
67
+ const defaultEffort = options.find(option => option.value === reasoning?.default)?.label ?? reasoning?.default;
68
+ const pickModel = (next) => {
69
+ onChange(next);
70
+ // An effort the next model does not offer falls back to its default.
71
+ const nextOptions = entries.find(entry => entry.value === next)?.reasoning?.options ?? [];
72
+ if (effort !== undefined && effort.value !== '' && !nextOptions.some(option => option.value === effort.value))
73
+ effort.onChange('');
74
+ };
75
+ return _jsxs("div", { className: classes.row, children: [_jsxs("label", { className: classes.field, children: [_jsx("span", { className: classes.label, children: t('configuration.model') }), _jsxs("select", { className: classes.select, "aria-label": t('configuration.model'), value: value, disabled: disabled || directory === undefined, onChange: event => { pickModel(event.target.value); }, children: [directory === undefined && _jsx("option", { value: value, children: value || t('loading') }), directory !== undefined && value === '' && _jsx("option", { value: "", children: t('configuration.choose') }), directory !== undefined && value !== '' && !shown.some(entry => entry.value === value) && _jsx("option", { value: value, children: value }), shown.map(entry => _jsx("option", { value: entry.value, children: entry.label }, entry.value))] }), error !== undefined && _jsx("span", { role: "alert", children: error })] }), effort !== undefined && _jsxs("label", { className: classes.field, children: [_jsx("span", { className: classes.label, children: t('configuration.effort') }), _jsxs("select", { className: classes.select, "aria-label": t('configuration.effort'), value: effort.value, disabled: disabled || options.length === 0, onChange: event => { effort.onChange(event.target.value); }, children: [_jsx("option", { value: "", children: defaultEffort === undefined ? t('configuration.effortDefault') : `${t('configuration.effortDefault')}(${defaultEffort})` }), options.map(option => _jsx("option", { value: option.value, children: option.label }, option.value))] })] })] });
48
76
  }
@@ -1,7 +1,9 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /** Generation-time text rendered through the public Markdown component. */
3
- import { useEffect, useLayoutEffect, useRef, useSyncExternalStore } from 'react';
4
- import { MarkdownText } from '@deepseek-ai/dsh-client-ui-primitives';
3
+ import { useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
4
+ import { DisclosureRow, MarkdownText, TextShimmer } from '@deepseek-ai/dsh-client-ui-primitives';
5
+ import { IconThinkOutlineRegular } from './icons.js';
6
+ import css from './MemberLiveOutput.module.css';
5
7
  export function MemberLiveNode({ node, outputs, t }) {
6
8
  const data = node.data;
7
9
  const store = outputs.get(data.sessionId);
@@ -20,6 +22,35 @@ export function MemberLiveOutputView({ sessionId, startedAt, outputs, t }) {
20
22
  * The wall-clock subtraction is meaningful only when browser and host share a clock.
21
23
  * Keep independent callbacks across updates so slow paints cannot disappear from P95.
22
24
  */
25
+ /** The latest completed paragraph's first line — the host think row's running summary. */
26
+ function latestParagraphLine(text) {
27
+ let summary = '';
28
+ let start = 0;
29
+ const separator = /\r?\n(?:[\t ]*\r?\n)+/g;
30
+ while (true) {
31
+ const next = separator.exec(text);
32
+ const end = next === null ? text.length : next.index + next[0].indexOf('\n');
33
+ const newline = text.indexOf('\n', start);
34
+ if (newline !== -1 && newline <= end) {
35
+ const candidate = text.slice(start, newline).trim();
36
+ if (candidate !== '')
37
+ summary = candidate;
38
+ }
39
+ if (next === null)
40
+ return summary;
41
+ start = next.index + next[0].length;
42
+ }
43
+ }
44
+ /**
45
+ * A streaming think block drawn as the host's own think row: collapsed to one
46
+ * line (icon, title, the latest paragraph's first line shimmering), the full
47
+ * text on expand — the same row the settled message shows once it lands.
48
+ */
49
+ function LiveReasoning({ text, labels, title }) {
50
+ const [open, setOpen] = useState(false);
51
+ const summary = useMemo(() => (latestParagraphLine(text) || text.split('\n', 1)[0] || '').replaceAll('**', ''), [text]);
52
+ return (_jsx(DisclosureRow, { rowClassName: css.thinkRow, leadingClassName: css.leading, titleClassName: css.title, icon: _jsx(IconThinkOutlineRegular, { size: 14 }), title: title, running: true, open: open, expandable: true, expandOnRowClick: true, onToggle: () => { setOpen(value => !value); }, collapsedContent: open || summary === '' ? undefined : _jsxs(_Fragment, { children: [_jsx("span", { className: css.separator, "aria-hidden": true }), _jsx("span", { className: css.summary, children: _jsx(TextShimmer, { children: summary }) })] }), children: open ? _jsx("div", { className: css.thinkBody, children: _jsx(MarkdownText, { text: text, labels: labels }) }) : undefined }));
53
+ }
23
54
  function StreamText({ id, kind, text, receivedAt, recovering, sessionId, turn, revision, baseline, surface, diagnostics, t }) {
24
55
  const element = useRef(null);
25
56
  const samples = useRef([]);
@@ -70,5 +101,8 @@ function StreamText({ id, kind, text, receivedAt, recovering, sessionId, turn, r
70
101
  });
71
102
  frames.current.add(first);
72
103
  }, [receivedAt, revision, baseline, diagnostics, sessionId, turn, surface]);
73
- return _jsxs("section", { ref: element, "data-member-live": id, "data-live-received-at": receivedAt, children: [_jsxs("small", { children: [kind === 'think' ? t('stream.thinking') : t('stream.writing'), recovering ? ` · ${t('stream.recovering')}` : ''] }), _jsx(MarkdownText, { text: text, labels: { code: { copyLabel: t('stream.copy'), copiedLabel: t('stream.copied') }, footnotes: t('stream.footnotes') } })] });
104
+ const labels = { code: { copyLabel: t('stream.copy'), copiedLabel: t('stream.copied') }, footnotes: t('stream.footnotes') };
105
+ return _jsxs("section", { ref: element, className: css.item, "data-member-live": id, "data-live-received-at": receivedAt, children: [kind === 'think'
106
+ ? _jsx(LiveReasoning, { text: text, labels: labels, title: t('stream.thinking') })
107
+ : _jsx("div", { className: css.text, children: _jsx(MarkdownText, { text: text, labels: labels }) }), recovering && _jsx("span", { className: css.recovering, children: t('stream.recovering') })] });
74
108
  }
@@ -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.
@@ -18,5 +18,6 @@ export declare const IconCheckOutlineMedium: ({ size, className }: IconProps) =>
18
18
  export declare const IconChevronDownOutlineMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
19
19
  export declare const IconChevronUpOutlineMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
20
20
  export declare const IconQueueOutlineMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
21
+ export declare const IconThinkOutlineRegular: ({ size, className }: IconProps) => import("react").JSX.Element;
21
22
  export declare const IconTrashOutlineMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
22
23
  //# sourceMappingURL=icons.d.ts.map
@@ -3,4 +3,5 @@ export const IconCheckOutlineMedium = ({ size = 16, className }) => (_jsx("svg",
3
3
  export const IconChevronDownOutlineMedium = ({ size = 14, className }) => (_jsx("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: _jsx("path", { d: "M4 6L7.29289 9.29289C7.68342 9.68342 8.31658 9.68342 8.70711 9.29289L12 6", stroke: "currentColor" }) }));
4
4
  export const IconChevronUpOutlineMedium = ({ size = 14, className }) => (_jsx("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: _jsx("path", { d: "M12 10L8.70711 6.70711C8.31658 6.31658 7.68342 6.31658 7.29289 6.70711L4 10", stroke: "currentColor" }) }));
5
5
  export const IconQueueOutlineMedium = ({ size = 14, className }) => (_jsxs("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: [_jsx("path", { d: "M5 6.75H11", stroke: "currentColor" }), _jsx("path", { d: "M5 9H8", stroke: "currentColor" }), _jsx("path", { d: "M2.37067 11.2497C1.5872 9.89252 1.32042 8.29798 1.61945 6.7597C1.91847 5.22141 2.76317 3.84293 3.99801 2.87809C5.23285 1.91325 6.7747 1.427 8.33964 1.50888C9.90458 1.59076 11.3873 2.23526 12.5147 3.32369C13.6422 4.41232 14.3384 5.8717 14.4751 7.43304C14.6118 8.99438 14.1797 10.5525 13.2585 11.8205C12.3372 13.0885 10.9889 13.9809 9.4617 14.3334C8.18666 14.6277 6.8587 14.529 5.64964 14.0601C5.17095 13.8745 4.76937 13.4929 4.26509 13.3963C3.67389 13.2832 2.95232 13.5595 2.0377 14.3334", stroke: "currentColor" })] }));
6
+ export const IconThinkOutlineRegular = ({ size = 16, className }) => (_jsxs("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1, children: [_jsx("path", { d: "M10.2854 5.71481C12.9673 8.39663 14.1182 11.5938 12.8562 12.8559C11.5942 14.1179 8.39706 12.9669 5.71518 10.2851C3.03333 7.60323 1.88236 4.40608 3.14441 3.14403C4.40644 1.882 7.6036 3.03297 10.2854 5.71481Z", stroke: "currentColor" }), _jsx("path", { d: "M10.2854 10.2851C7.6036 12.9669 4.40644 14.1179 3.14441 12.8559C1.88236 11.5938 3.03333 8.39663 5.71518 5.71481C8.39706 3.03297 11.5942 1.882 12.8562 3.14403C14.1182 4.40608 12.9673 7.60323 10.2854 10.2851Z", stroke: "currentColor" }), _jsx("path", { d: "M8.86291 8.0002C8.86291 8.47549 8.47762 8.86087 8.00224 8.86087C7.52694 8.86087 7.1416 8.47549 7.1416 8.0002C7.1416 7.52485 7.52694 7.13953 8.00224 7.13953C8.47762 7.13953 8.86291 7.52485 8.86291 8.0002Z", fill: "currentColor" })] }));
6
7
  export const IconTrashOutlineMedium = ({ size = 16, className }) => (_jsxs("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: [_jsx("path", { d: "M1.28149 3.88831H14.7187", stroke: "currentColor" }), _jsx("path", { d: "M5.41602 3.88833V2.47962C5.41602 2.29282 5.52492 2.11366 5.71876 1.98157C5.9126 1.84948 6.17551 1.77527 6.44964 1.77527H9.55053C9.82466 1.77527 10.0876 1.84948 10.2814 1.98157C10.4753 2.11366 10.5842 2.29282 10.5842 2.47962V3.88833", stroke: "currentColor" }), _jsx("path", { d: "M2.57349 3.88831L3.19366 13.2943C3.21937 13.5502 3.33952 13.7872 3.53065 13.9593C3.72178 14.1313 3.97016 14.2259 4.22729 14.2246H11.7728C12.0299 14.2259 12.2783 14.1313 12.4694 13.9593C12.6605 13.7872 12.7807 13.5502 12.8064 13.2943L13.4266 3.88831", stroke: "currentColor" }), _jsx("path", { d: "M6.44946 6.98926V11.1238", stroke: "currentColor" }), _jsx("path", { d: "M9.55054 6.98926V11.1238", stroke: "currentColor" })] }));
@@ -23,6 +23,7 @@ import { MemberFeeds } from './member-feed.js';
23
23
  */
24
24
  import { createElement } from 'react';
25
25
  import { HarnessModelPicker } from './HarnessModelPicker.js';
26
+ import { scopeDirectoryToList } from './model-directory-scope.js';
26
27
  import { LocalAgentFamilyCard } from './FamilyCard.js';
27
28
  import { HarnessCardRegistry } from './harness-cards.js';
28
29
  import { MemberConfiguration } from './MemberConfiguration.js';
@@ -87,8 +88,21 @@ export async function apply(ctx) {
87
88
  let face = directoryFaces.get(name);
88
89
  if (face === undefined)
89
90
  directoryFaces.set(name, face = {
90
- read: async (refresh) => unwrap(await gateway.modelDirectory(name, undefined, refresh)),
91
- follow: signal => gateway.followModelDirectory(name, undefined, signal),
91
+ read: async (refresh) => {
92
+ const directory = unwrap(await gateway.modelDirectory(name, undefined, refresh));
93
+ return directory === null ? null : scopeDirectoryToList(directory, unwrap(await gateway.modelList(name)));
94
+ },
95
+ follow: async function* (signal) {
96
+ // The invite picker faces the curated list, not the raw served
97
+ // catalog; re-read the list on each directory revision.
98
+ let list = unwrap(await gateway.modelList(name));
99
+ for await (const directory of gateway.followModelDirectory(name, undefined, signal)) {
100
+ if (signal.aborted)
101
+ return;
102
+ list = unwrap(await gateway.modelList(name));
103
+ yield scopeDirectoryToList(directory, list);
104
+ }
105
+ },
92
106
  });
93
107
  return face;
94
108
  };
@@ -24,6 +24,7 @@ export declare const zh: {
24
24
  readonly "configuration.details": "模型目录详情";
25
25
  readonly "configuration.model": "模型";
26
26
  readonly "configuration.effort": "推理强度";
27
+ readonly "configuration.effortDefault": "默认";
27
28
  readonly "configuration.inherit": "继承成员创建配置";
28
29
  readonly "configuration.default": "跟随 harness 默认值";
29
30
  readonly "configuration.search": "搜索模型名称或 ID";
@@ -55,7 +56,6 @@ export declare const zh: {
55
56
  readonly "configuration.cancelled": "已接收撤销请求,正在恢复配置。";
56
57
  readonly "configuration.failed": "配置操作失败,请检查原因后重试。";
57
58
  readonly 'stream.thinking': '思考中';
58
- readonly 'stream.writing': '正在生成';
59
59
  readonly 'stream.recovering': '部分记录,等待实时连接';
60
60
  readonly 'stream.copy': '复制';
61
61
  readonly 'stream.copied': '已复制';
@@ -24,6 +24,7 @@ export const zh = {
24
24
  "configuration.details": "模型目录详情",
25
25
  "configuration.model": "模型",
26
26
  "configuration.effort": "推理强度",
27
+ "configuration.effortDefault": "默认",
27
28
  "configuration.inherit": "继承成员创建配置",
28
29
  "configuration.default": "跟随 harness 默认值",
29
30
  "configuration.search": "搜索模型名称或 ID",
@@ -55,7 +56,6 @@ export const zh = {
55
56
  "configuration.cancelled": "已接收撤销请求,正在恢复配置。",
56
57
  "configuration.failed": "配置操作失败,请检查原因后重试。",
57
58
  'stream.thinking': '思考中',
58
- 'stream.writing': '正在生成',
59
59
  'stream.recovering': '部分记录,等待实时连接',
60
60
  'stream.copy': '复制',
61
61
  'stream.copied': '已复制',
@@ -160,6 +160,7 @@ export const en = {
160
160
  "configuration.details": "Model directory details",
161
161
  "configuration.model": "Model",
162
162
  "configuration.effort": "Reasoning effort",
163
+ "configuration.effortDefault": "Default",
163
164
  "configuration.inherit": "Inherit member configuration",
164
165
  "configuration.default": "Follow harness defaults",
165
166
  "configuration.search": "Search model names or IDs",
@@ -191,7 +192,6 @@ export const en = {
191
192
  "configuration.cancelled": "Cancellation accepted; restoring configuration.",
192
193
  "configuration.failed": "Configuration failed; check the reason before retrying.",
193
194
  'stream.thinking': 'Thinking',
194
- 'stream.writing': 'Writing',
195
195
  'stream.recovering': 'Partial record, awaiting live connection',
196
196
  'stream.copy': 'Copy',
197
197
  'stream.copied': 'Copied',
@@ -0,0 +1,14 @@
1
+ import type { LocalAgentModelDirectory } from '../types.ts';
2
+ /**
3
+ * Scope a harness's served directory to its curated model list (models.json):
4
+ * the list IS the invite picker's vocabulary, in the list's own order — a
5
+ * listed value the endpoint no longer serves stays as a bare row (the member
6
+ * dialog shows it the same way). No list support, or none configured, keeps
7
+ * the directory as served. Mirrors the member dialog's scoping rule so the
8
+ * invite picker and the member configuration show the same candidates.
9
+ * @param directory - the harness's served directory.
10
+ * @param list - the curated shortlist values, or null when unsupported.
11
+ * @returns the scoped directory.
12
+ */
13
+ export declare function scopeDirectoryToList(directory: LocalAgentModelDirectory, list: readonly string[] | null | undefined): LocalAgentModelDirectory;
14
+ //# sourceMappingURL=model-directory-scope.d.ts.map
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Scope a harness's served directory to its curated model list (models.json):
3
+ * the list IS the invite picker's vocabulary, in the list's own order — a
4
+ * listed value the endpoint no longer serves stays as a bare row (the member
5
+ * dialog shows it the same way). No list support, or none configured, keeps
6
+ * the directory as served. Mirrors the member dialog's scoping rule so the
7
+ * invite picker and the member configuration show the same candidates.
8
+ * @param directory - the harness's served directory.
9
+ * @param list - the curated shortlist values, or null when unsupported.
10
+ * @returns the scoped directory.
11
+ */
12
+ export function scopeDirectoryToList(directory, list) {
13
+ if (list === undefined || list === null || list.length === 0)
14
+ return directory;
15
+ const byValue = new Map(directory.entries.flatMap(entry => entry.resolvedModel === undefined
16
+ ? [[entry.value, entry]]
17
+ : [[entry.value, entry], [entry.resolvedModel, entry]]));
18
+ const entries = list.map(value => byValue.get(value) ?? { value, label: value, source: 'configuration' });
19
+ return { ...directory, entries };
20
+ }
@@ -1190,8 +1190,12 @@ export declare class LocalAgentRegistry {
1190
1190
  * const handle = await ctx.sessionPersistence.open(SessionId(childSessionId), 'write')
1191
1191
  * try {
1192
1192
  * const cold = await handle.read(0)
1193
+ * // Close a turn a restart interrupted, or the next round's turn/start
1194
+ * // makes the stored log unreadable.
1195
+ * const closers = interruptedTurnClosers(cold.events)
1196
+ * if (closers.length > 0) await handle.append(closers)
1193
1197
  * const session = ctx.sessions.prepare(SessionId(childSessionId), {
1194
- * seed: [...cold.events],
1198
+ * seed: [...cold.events, ...closers],
1195
1199
  * meta: structuredClone(handle.header),
1196
1200
  * inheritedEventCount: handle.inheritedEventCount,
1197
1201
  * eventState: cold.eventState,
@@ -1199,7 +1203,7 @@ export declare class LocalAgentRegistry {
1199
1203
  * // The seeded constructor's session/end-seed marker lands before enter()
1200
1204
  * // installs publication hooks — push the unstored suffix through the
1201
1205
  * // handle directly, or the writer's contiguous cursor diverges.
1202
- * const unstored = session.snapshotEvents().slice(cold.events.length)
1206
+ * const unstored = session.snapshotEvents().slice(cold.events.length + closers.length)
1203
1207
  * if (unstored.length > 0) await handle.append(unstored)
1204
1208
  * const detach = ctx.sessions.enter(session)
1205
1209
  * try { ctx.sessions.announce(session) }
@@ -1336,6 +1340,18 @@ export declare class LocalAgentRegistry {
1336
1340
  * @param session - the live child session to sync.
1337
1341
  */
1338
1342
  syncChildSession(session: Session, strict?: boolean): Promise<void>;
1343
+ /**
1344
+ * One sync pass. While the write handle is open the host routes every live
1345
+ * append into the writer's buffer, so the routed events are drained first
1346
+ * (`sessions.flush`: drain + flush) and only what is STILL missing — events
1347
+ * from before the handle opened, or a host that routes nothing — goes in
1348
+ * explicitly, bounded to the events that existed before the drain. Appending
1349
+ * routed-but-undrained events explicitly as well would land them twice: the
1350
+ * explicit write wins, the buffered copy then fails contiguity forever, and
1351
+ * every later flush — the host's tool-execution checkpoint included —
1352
+ * throws "append seq mismatch".
1353
+ */
1354
+ private syncChildSessionOnce;
1339
1355
  /**
1340
1356
  * Get or open the child session's registry-owned WRITE handle (the
1341
1357
  * {@link childWriteHandles} cache). When the caller carries the session
@@ -30,6 +30,7 @@ import { appendFileSync, chmodSync, mkdirSync, readFileSync, statSync } from 'no
30
30
  import { join } from 'node:path';
31
31
  import { fileURLToPath } from 'node:url';
32
32
  import z from '@deepseek-ai/schemastery';
33
+ import * as sessionModule from '@deepseek-ai/dsh-session';
33
34
  import { SessionId } from '@deepseek-ai/dsh-session';
34
35
  // The durable reader validates plugin vocabulary before replay. Register in
35
36
  // both resolution planes: source-based host toolchains and installed bundles
@@ -46,6 +47,7 @@ for (const catalog of streamCatalogs) {
46
47
  }
47
48
  import LocalAgentGateway from './gateway.js';
48
49
  import { MemberChannel } from './member-channel.js';
50
+ import { PresetToolHost } from './preset-tool-host.js';
49
51
  import { MemberInbox } from './member-inbox.js';
50
52
  import { PreparedMembers } from './prepared-members.js';
51
53
  import { MemberControls } from './member-controls.js';
@@ -540,6 +542,11 @@ export function parseScopeFlag(input) {
540
542
  }
541
543
  return { input: rest.join(' ').trim(), ...scope === undefined ? {} : { scope } };
542
544
  }
545
+ /** A delegation record without its requested model. */
546
+ function withoutModel(record) {
547
+ const { model: _model, ...rest } = record;
548
+ return rest;
549
+ }
543
550
  /** Map a login failure to a command error, naming the swallowed signal. */
544
551
  function loginFailure(harness, exitCode, signal) {
545
552
  return `${harness.name} login exited with ${signal ?? `code ${exitCode ?? 'unknown'}`}; try /${harness.name} login again.`;
@@ -1171,7 +1178,12 @@ export class LocalAgentRegistry {
1171
1178
  const binding = this.memberControls.binding(record.childSessionId);
1172
1179
  if (binding !== undefined)
1173
1180
  record = {
1174
- ...record,
1181
+ // The model a resume re-requests is the bound one, verbatim: a provider
1182
+ // recording its resolved model ('opus' → 'claude-opus-5-5', or the
1183
+ // default a model-less start resolved to) would make the next round's
1184
+ // binding differ and the member refuse it ("identity changed: model").
1185
+ ...withoutModel(record),
1186
+ ...binding.model === undefined ? {} : { model: binding.model },
1175
1187
  ...binding.effort === undefined ? {} : { effort: binding.effort },
1176
1188
  ...binding.configurationLock === undefined ? {} : { configurationLock: binding.configurationLock },
1177
1189
  };
@@ -1720,8 +1732,12 @@ export class LocalAgentRegistry {
1720
1732
  * const handle = await ctx.sessionPersistence.open(SessionId(childSessionId), 'write')
1721
1733
  * try {
1722
1734
  * const cold = await handle.read(0)
1735
+ * // Close a turn a restart interrupted, or the next round's turn/start
1736
+ * // makes the stored log unreadable.
1737
+ * const closers = interruptedTurnClosers(cold.events)
1738
+ * if (closers.length > 0) await handle.append(closers)
1723
1739
  * const session = ctx.sessions.prepare(SessionId(childSessionId), {
1724
- * seed: [...cold.events],
1740
+ * seed: [...cold.events, ...closers],
1725
1741
  * meta: structuredClone(handle.header),
1726
1742
  * inheritedEventCount: handle.inheritedEventCount,
1727
1743
  * eventState: cold.eventState,
@@ -1729,7 +1745,7 @@ export class LocalAgentRegistry {
1729
1745
  * // The seeded constructor's session/end-seed marker lands before enter()
1730
1746
  * // installs publication hooks — push the unstored suffix through the
1731
1747
  * // handle directly, or the writer's contiguous cursor diverges.
1732
- * const unstored = session.snapshotEvents().slice(cold.events.length)
1748
+ * const unstored = session.snapshotEvents().slice(cold.events.length + closers.length)
1733
1749
  * if (unstored.length > 0) await handle.append(unstored)
1734
1750
  * const detach = ctx.sessions.enter(session)
1735
1751
  * try { ctx.sessions.announce(session) }
@@ -2019,8 +2035,18 @@ export class LocalAgentRegistry {
2019
2035
  const handle = await this.acquireChildWriteHandle(childSessionId);
2020
2036
  try {
2021
2037
  const cold = await handle.read(0);
2038
+ // A host restart mid-round leaves the child's last turn open on disk;
2039
+ // the next round's turn/start would then follow an unclosed turn and
2040
+ // the strict reader rejects the whole log. Close it the way the agent
2041
+ // loop's own resume does: synthetic closers (missing tool errors,
2042
+ // step/end, an `interrupted` turn/end) appended through the handle.
2043
+ // Older hosts without the helper keep the previous behavior.
2044
+ const closeOpenTurn = sessionModule.interruptedTurnClosers;
2045
+ const closers = closeOpenTurn === undefined ? [] : closeOpenTurn(cold.events);
2046
+ if (closers.length > 0)
2047
+ await handle.append(closers);
2022
2048
  const session = sessions.prepare(SessionId(childSessionId), {
2023
- seed: [...cold.events],
2049
+ seed: [...cold.events, ...closers],
2024
2050
  meta: structuredClone(handle.header),
2025
2051
  inheritedEventCount: handle.inheritedEventCount,
2026
2052
  eventState: cold.eventState,
@@ -2030,7 +2056,7 @@ export class LocalAgentRegistry {
2030
2056
  // — push the unstored suffix through the handle directly (the agent
2031
2057
  // loop's resume does the same via appendUnstoredSuffix), or the writer's
2032
2058
  // contiguous cursor and the session log diverge.
2033
- const unstored = session.snapshotEvents().slice(cold.events.length);
2059
+ const unstored = session.snapshotEvents().slice(cold.events.length + closers.length);
2034
2060
  if (unstored.length > 0)
2035
2061
  await handle.append(unstored);
2036
2062
  const detach = sessions.enter(session);
@@ -2065,20 +2091,48 @@ export class LocalAgentRegistry {
2065
2091
  * @param session - the live child session to sync.
2066
2092
  */
2067
2093
  async syncChildSession(session, strict = false) {
2094
+ const id = String(session.id);
2068
2095
  try {
2069
- const handle = await this.acquireChildWriteHandle(String(session.id), session.header);
2070
- const stored = await handle.read(0);
2071
- const suffix = session.snapshotEvents().slice(stored.events.length);
2072
- if (suffix.length > 0)
2073
- await handle.append(suffix);
2074
- await handle.flush();
2096
+ try {
2097
+ await this.syncChildSessionOnce(session);
2098
+ }
2099
+ catch (error) {
2100
+ if (!/append seq mismatch/.test(error instanceof Error ? error.message : String(error)))
2101
+ throw error;
2102
+ // A routed live buffer the explicit suffix raced (see
2103
+ // syncChildSessionOnce) stays poisoned for the handle's lifetime:
2104
+ // drop the handle with it and catch up from disk on a fresh one.
2105
+ await this.releaseChildWriteHandle(id);
2106
+ await this.syncChildSessionOnce(session);
2107
+ }
2075
2108
  }
2076
2109
  catch (error) {
2077
2110
  if (strict)
2078
2111
  throw error;
2079
- this.ctx.logger.warn(`localAgent: syncing child session ${String(session.id)} to persistence failed: ${error instanceof Error ? error.message : String(error)}`);
2112
+ this.ctx.logger.warn(`localAgent: syncing child session ${id} to persistence failed: ${error instanceof Error ? error.message : String(error)}`);
2080
2113
  }
2081
2114
  }
2115
+ /**
2116
+ * One sync pass. While the write handle is open the host routes every live
2117
+ * append into the writer's buffer, so the routed events are drained first
2118
+ * (`sessions.flush`: drain + flush) and only what is STILL missing — events
2119
+ * from before the handle opened, or a host that routes nothing — goes in
2120
+ * explicitly, bounded to the events that existed before the drain. Appending
2121
+ * routed-but-undrained events explicitly as well would land them twice: the
2122
+ * explicit write wins, the buffered copy then fails contiguity forever, and
2123
+ * every later flush — the host's tool-execution checkpoint included —
2124
+ * throws "append seq mismatch".
2125
+ */
2126
+ async syncChildSessionOnce(session) {
2127
+ const handle = await this.acquireChildWriteHandle(String(session.id), session.header);
2128
+ const known = session.snapshotEvents().length;
2129
+ await this.ctx.get('sessions')?.flush(session);
2130
+ const stored = await handle.read(0);
2131
+ const missing = session.snapshotEvents().slice(stored.events.length, known);
2132
+ if (missing.length > 0)
2133
+ await handle.append(missing);
2134
+ await handle.flush();
2135
+ }
2082
2136
  /**
2083
2137
  * Get or open the child session's registry-owned WRITE handle (the
2084
2138
  * {@link childWriteHandles} cache). When the caller carries the session
@@ -2735,9 +2789,22 @@ export async function apply(ctx, config) {
2735
2789
  // The member channel's loopback listener (bridge MCP servers call back here).
2736
2790
  // Degrade, don't explode: a failed bind leaves member_message failing with a
2737
2791
  // tool error while everything else works.
2738
- const memberChannel = new MemberChannel(ctx, registry, registry.memberBridgeSocketPath());
2792
+ // Mode tools: a member with a mode reaches that preset's plugin tools
2793
+ // through the same bridge (see preset-tool-host.ts).
2794
+ const modeTools = new PresetToolHost(ctx, join(config.homesRoot, 'mode-tools.jsonl'));
2795
+ const memberChannel = new MemberChannel(ctx, registry, registry.memberBridgeSocketPath(), modeTools);
2739
2796
  void memberChannel.start();
2740
- ctx.effect(() => () => { void memberChannel.dispose(); });
2797
+ ctx.on('approval/request', async (request, next) => {
2798
+ const child = modeTools.memberOf(request.agent);
2799
+ const delegation = child === undefined ? undefined : registry.getDelegation(child);
2800
+ const room = ctx.get.call(ctx, 'room');
2801
+ if (child === undefined || delegation === undefined || room?.askMemberApproval === undefined)
2802
+ return next();
2803
+ const reason = request.displayReason ?? request.reason;
2804
+ const outcome = await room.askMemberApproval({ childSessionId: child, parentSessionId: delegation.parentSessionId, provider: delegation.provider }, { toolName: request.toolName, ...reason === undefined ? {} : { reason }, ...request.signal === undefined ? {} : { signal: request.signal } });
2805
+ return outcome === 'unavailable' ? next() : outcome;
2806
+ }, { prepend: true });
2807
+ ctx.effect(() => () => { void memberChannel.dispose(); void modeTools.dispose(); });
2741
2808
  ctx.commands.register({
2742
2809
  name: 'local-agent',
2743
2810
  description: 'list the registered local code-agent harnesses, or stop an in-flight delegation',
@@ -27,7 +27,18 @@ export declare class LiveStreamPublisher {
27
27
  private readonly persist;
28
28
  private readonly onError;
29
29
  private readonly states;
30
+ /** Items shown live but not yet in the log: their step cannot open before the earlier steps close. */
31
+ private readonly previews;
30
32
  constructor(streams: LocalAgentStreams, session: Session, turn: number, persist: () => void, onError: (error: unknown) => void);
33
+ /**
34
+ * Show an item live without writing it to the session: a step may open in
35
+ * the log only after every earlier step has closed, so a later item that
36
+ * streams first waits here (browser-only) until the driver can open it.
37
+ */
38
+ preview(step: number, kind: 'think' | 'text', text: string, options?: {
39
+ itemId?: string;
40
+ receivedAt?: number;
41
+ }): void;
31
42
  update(step: number, kind: 'think' | 'text', text: string, options?: {
32
43
  itemId?: string;
33
44
  receivedAt?: number;
@@ -97,6 +97,8 @@ export class LiveStreamPublisher {
97
97
  persist;
98
98
  onError;
99
99
  states = new Map();
100
+ /** Items shown live but not yet in the log: their step cannot open before the earlier steps close. */
101
+ previews = new Map();
100
102
  constructor(streams, session, turn, persist, onError) {
101
103
  this.streams = streams;
102
104
  this.session = session;
@@ -104,8 +106,23 @@ export class LiveStreamPublisher {
104
106
  this.persist = persist;
105
107
  this.onError = onError;
106
108
  }
109
+ /**
110
+ * Show an item live without writing it to the session: a step may open in
111
+ * the log only after every earlier step has closed, so a later item that
112
+ * streams first waits here (browser-only) until the driver can open it.
113
+ */
114
+ preview(step, kind, text, options = {}) {
115
+ const id = `${this.turn}:${step}${options.itemId === undefined ? '' : `:${options.itemId}`}`;
116
+ const previous = this.previews.get(id);
117
+ if (previous?.text === text && previous.kind === kind)
118
+ return;
119
+ const item = { id, turn: this.turn, step, kind, text, receivedAt: previous?.receivedAt ?? options.receivedAt ?? Date.now() };
120
+ this.previews.set(id, item);
121
+ this.streams.publish(String(this.session.id), item);
122
+ }
107
123
  update(step, kind, text, options = {}) {
108
124
  const id = `${this.turn}:${step}${options.itemId === undefined ? '' : `:${options.itemId}`}`;
125
+ this.previews.delete(id);
109
126
  let state = this.states.get(id);
110
127
  if (state?.item.text === text && state.item.kind === kind)
111
128
  return;
@@ -150,6 +167,12 @@ export class LiveStreamPublisher {
150
167
  }
151
168
  /** Call only after the corresponding native final message has been appended. */
152
169
  finish(step) {
170
+ for (const [id, item] of this.previews) {
171
+ if (item.step !== step)
172
+ continue;
173
+ this.previews.delete(id);
174
+ this.streams.finish(String(this.session.id), id);
175
+ }
153
176
  for (const [id, state] of this.states) {
154
177
  if (state.item.step !== step)
155
178
  continue;
@@ -167,6 +190,9 @@ export class LiveStreamPublisher {
167
190
  }
168
191
  /** A producer failure keeps a durable partial, without claiming completion. */
169
192
  dispose() {
193
+ for (const id of this.previews.keys())
194
+ this.streams.finish(String(this.session.id), id);
195
+ this.previews.clear();
170
196
  for (const [id, state] of this.states) {
171
197
  this.checkpoint(id);
172
198
  if (state.timer !== undefined)