@khorsheed/dsh-local-agent 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.
Files changed (57) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.en.md +23 -164
  3. package/README.i18n.yaml +2 -2
  4. package/README.md +23 -164
  5. package/lib/client.js +23707 -4768
  6. package/lib/index.js +827 -62
  7. package/lib/member-bridge.js +195 -25
  8. package/lib/tool-factory.js +52 -19
  9. package/lib/typert.host.js +1440 -353
  10. package/lib/typert.remote-client.d.ts +43 -3
  11. package/lib/typert.remote-client.js +973 -335
  12. package/lib/types/client/FamilyCard.d.ts +37 -0
  13. package/lib/types/client/FamilyCard.js +55 -0
  14. package/lib/types/client/HarnessModelPicker.d.ts +21 -1
  15. package/lib/types/client/HarnessModelPicker.js +31 -3
  16. package/lib/types/client/MemberLiveNode.js +38 -4
  17. package/lib/types/client/ModelListSection.d.ts +49 -0
  18. package/lib/types/client/ModelListSection.js +132 -0
  19. package/lib/types/client/ProviderAuthBlock.d.ts +10 -2
  20. package/lib/types/client/ProviderAuthBlock.js +13 -6
  21. package/lib/types/client/auth-status.d.ts +6 -0
  22. package/lib/types/client/auth-status.js +8 -0
  23. package/lib/types/client/harness-cards.d.ts +72 -0
  24. package/lib/types/client/harness-cards.js +57 -0
  25. package/lib/types/client/icons.d.ts +2 -1
  26. package/lib/types/client/icons.js +1 -0
  27. package/lib/types/client/index.d.ts +32 -7
  28. package/lib/types/client/index.js +66 -9
  29. package/lib/types/client/locales.d.ts +25 -1
  30. package/lib/types/client/locales.js +50 -2
  31. package/lib/types/client/model-directory-scope.d.ts +14 -0
  32. package/lib/types/client/model-directory-scope.js +20 -0
  33. package/lib/types/gateway.d.ts +49 -1
  34. package/lib/types/gateway.js +89 -0
  35. package/lib/types/index.d.ts +91 -38
  36. package/lib/types/index.js +190 -58
  37. package/lib/types/live-stream.d.ts +11 -0
  38. package/lib/types/live-stream.js +26 -0
  39. package/lib/types/member-bridge.d.ts +11 -1
  40. package/lib/types/member-bridge.js +81 -13
  41. package/lib/types/member-channel.d.ts +44 -1
  42. package/lib/types/member-channel.js +65 -2
  43. package/lib/types/member-controls.d.ts +8 -0
  44. package/lib/types/member-controls.js +15 -2
  45. package/lib/types/mode-skills.d.ts +18 -0
  46. package/lib/types/mode-skills.js +56 -0
  47. package/lib/types/model-list.d.ts +55 -0
  48. package/lib/types/model-list.js +101 -0
  49. package/lib/types/preset-tool-host.d.ts +70 -0
  50. package/lib/types/preset-tool-host.js +210 -0
  51. package/lib/types/tool-factory.d.ts +9 -3
  52. package/lib/types/tool-factory.js +55 -18
  53. package/lib/types/types.d.ts +62 -5
  54. package/package.json +27 -23
  55. package/lib/invariant.js +0 -85
  56. package/lib/types/invariant.d.ts +0 -16
  57. package/lib/types/invariant.js +0 -105
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The「本机 Agent 委派」family card: the core's single `plugins.item` entry on
3
+ * the Plugins page, replacing the four per-harness cards. Each delegation
4
+ * companion registers one section through the localAgentUi service's
5
+ * `registerHarnessCard`; the page view renders the sections in their retired
6
+ * standalone-card order, each under the collapsible chrome those cards used
7
+ * (header button with aria-expanded, the credential dot, a chevron), plus a
8
+ * grey「未检测到 CLI」tag when the harness's status probe answers without a
9
+ * cliVersion. Sections mount their bodies only while expanded, exactly like
10
+ * the standalone cards did.
11
+ * @module @khorsheed/dsh-local-agent/client — family card
12
+ */
13
+ import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
14
+ import type { LocalAgentStatus } from '@khorsheed/dsh-local-agent/types';
15
+ import type { HarnessCardRegistry } from './harness-cards.ts';
16
+ import { NS } from './locales.ts';
17
+ /** Injected face of the family card. */
18
+ export interface FamilyCardInjected {
19
+ /** The registry the companions' sections live in (the localAgentUi service's registerHarnessCard writes here). */
20
+ readonly harnessCards: HarnessCardRegistry;
21
+ /**
22
+ * Per-harness status probe over the read-only gateway Remote. The Remote
23
+ * declares `status(name, scope?)` and the api-gateway client enforces exact
24
+ * arity, so the default scope is passed explicitly at the call site.
25
+ */
26
+ readonly harnessStatus: (name: string) => Promise<LocalAgentStatus | undefined>;
27
+ }
28
+ /** Full props of the family card entry (owner prop `view`). */
29
+ export type LocalAgentFamilyCardProps = PropsRuntime<'plugins.item'> & InjectFace<FamilyCardInjected> & PropsLocale<typeof NS>;
30
+ /**
31
+ * The family card entry: the one-liner for the `summary` view, the section
32
+ * stack for `page`.
33
+ * @param props - the owner view, the injected registry/status faces, and copy.
34
+ * @returns the entry.
35
+ */
36
+ export declare function LocalAgentFamilyCard({ view, harnessCards, harnessStatus, useSessions, t }: LocalAgentFamilyCardProps): import("react").JSX.Element;
37
+ //# sourceMappingURL=FamilyCard.d.ts.map
@@ -0,0 +1,55 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The「本机 Agent 委派」family card: the core's single `plugins.item` entry on
4
+ * the Plugins page, replacing the four per-harness cards. Each delegation
5
+ * companion registers one section through the localAgentUi service's
6
+ * `registerHarnessCard`; the page view renders the sections in their retired
7
+ * standalone-card order, each under the collapsible chrome those cards used
8
+ * (header button with aria-expanded, the credential dot, a chevron), plus a
9
+ * grey「未检测到 CLI」tag when the harness's status probe answers without a
10
+ * cliVersion. Sections mount their bodies only while expanded, exactly like
11
+ * the standalone cards did.
12
+ * @module @khorsheed/dsh-local-agent/client — family card
13
+ */
14
+ import { useMemo, useState, useSyncExternalStore } from 'react';
15
+ import { AuthStatusDot } from './AuthStatusDot.js';
16
+ import { useHarnessAuthStatus } from './auth-status.js';
17
+ import { IconChevronDownOutlineMedium } from './icons.js';
18
+ import css from './FamilyCard.module.css';
19
+ /** One registered harness's collapsible section: the header row plus its body. */
20
+ function HarnessSection({ entry, harnessStatus, useSessions, t }) {
21
+ const [open, setOpen] = useState(false);
22
+ // One mount probe feeds both the header dot and the CLI-missing tag: the
23
+ // wrapper keeps the answered payload (its effectiveSettings.cliVersion)
24
+ // while the bus hook publishes the kind the dot reads — the same single
25
+ // probe the retired standalone card headers ran.
26
+ const [answered, setAnswered] = useState(null);
27
+ const probe = useMemo(() => async (name) => {
28
+ const status = await harnessStatus(name);
29
+ setAnswered(status ?? undefined);
30
+ return status;
31
+ }, [harnessStatus]);
32
+ const authStatus = useHarnessAuthStatus(entry.name, probe);
33
+ const dotLabel = t(authStatus === 'authenticated' ? 'settings.authenticated'
34
+ : authStatus === 'anonymous' ? 'settings.notAuthenticated'
35
+ : authStatus === 'checking' ? 'loading' : 'error');
36
+ const label = entry.label();
37
+ // The tag fires only on an answered probe: an unanswered one (the harness
38
+ // is absent host-side) already reads as the dot's unavailable state, and
39
+ // "CLI not detected" would misreport it.
40
+ const cliMissing = answered !== null && answered !== undefined && answered.effectiveSettings?.cliVersion === undefined;
41
+ return (_jsxs("section", { className: open ? `${css.section} ${css.sectionOpen}` : css.section, children: [_jsxs("button", { type: "button", className: css.header, "aria-expanded": open, "aria-label": `${t(open ? 'family.collapse' : 'family.expand')}: ${label}`, onClick: () => { setOpen(!open); }, children: [_jsx("span", { className: css.headText, children: _jsxs("span", { className: css.nameRow, children: [_jsx("span", { className: css.name, children: label }), _jsx(AuthStatusDot, { status: authStatus, label: dotLabel }), cliMissing && _jsx("span", { className: css.cliMissing, children: t('family.cliMissing') })] }) }), _jsx(IconChevronDownOutlineMedium, { className: open ? `${css.chevron} ${css.chevronOpen}` : css.chevron })] }), open && entry.render({ active: true, useSessions })] }));
42
+ }
43
+ /**
44
+ * The family card entry: the one-liner for the `summary` view, the section
45
+ * stack for `page`.
46
+ * @param props - the owner view, the injected registry/status faces, and copy.
47
+ * @returns the entry.
48
+ */
49
+ export function LocalAgentFamilyCard({ view, harnessCards, harnessStatus, useSessions, t }) {
50
+ useSyncExternalStore(harnessCards.subscribe, harnessCards.snapshotVersion);
51
+ const sections = harnessCards.cards();
52
+ if (view === 'summary')
53
+ return _jsx("span", { className: css.description, children: t('family.description') });
54
+ return (_jsxs("div", { className: css.sections, children: [sections.length === 0 && _jsx("span", { className: css.empty, children: t('family.empty') }), sections.map(entry => (_jsx(HarnessSection, { entry: entry, harnessStatus: harnessStatus, useSessions: useSessions, t: t }, entry.name)))] }));
55
+ }
@@ -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
  }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The family's shared「可用模型」section: an ORDERED shortlist of
3
+ * endpoint-served models — the first entry is the default model every
4
+ * delegation round starts with. Rows drag to reorder and delete per row;
5
+ * "add model" opens a dialog over the endpoint's served catalog (search +
6
+ * checkboxes), so the list can only ever contain what the endpoint actually
7
+ * serves. An empty list falls back to the pre-list chain (the settings
8
+ * model key, the scoped config file, then the CLI's own default). Every
9
+ * edit persists immediately through the harness's model-list face (the
10
+ * gateway's `modelList`/`configureModelList` Remotes).
11
+ *
12
+ * One implementation serves every companion card (claude-code, codex,
13
+ * kimi): the section owns its state (list read on activate, immediate
14
+ * writes, the catalog read behind the add dialog) so each card's
15
+ * integration is the two faces plus copy — the interaction cannot drift
16
+ * between harnesses. Copy lives in the family core's own dictionary.
17
+ * @module @khorsheed/dsh-local-agent/client — model list section
18
+ */
19
+ import type { LocalAgentModelDirectory } from '@khorsheed/dsh-local-agent/types';
20
+ import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
21
+ import { NS } from './locales.ts';
22
+ /** The harness's curated-list face the section edits through. */
23
+ export interface ModelListSectionListFace {
24
+ /** Read the current ordered list (absent/malformed reads as empty). */
25
+ read(): Promise<readonly string[] | null | undefined>;
26
+ /** Persist a new ordered list; the answer replaces the section's state. */
27
+ write(models: readonly string[]): Promise<readonly string[] | null | undefined>;
28
+ }
29
+ /** Full props of the shared model-list section. */
30
+ export interface ModelListSectionProps extends PropsLocale<typeof NS> {
31
+ /** Whether the section's surface is currently visible (gates the reads — a collapsed card never fetches). */
32
+ readonly active: boolean;
33
+ /** The harness's curated-list face; absent hides the editor (a core predating the Remotes). */
34
+ readonly modelList?: ModelListSectionListFace | undefined;
35
+ /** The endpoint's served catalog read — the add dialog's candidate vocabulary. */
36
+ readonly modelDirectory?: ((refresh?: boolean) => Promise<LocalAgentModelDirectory | null | undefined>) | undefined;
37
+ /** Re-read the catalog whenever this value changes (a credential save: a fresh credential's models belong in the dialog now). */
38
+ readonly directoryRefresh?: unknown;
39
+ }
40
+ /**
41
+ * The「可用模型」section every companion's settings card renders: the block
42
+ * chrome (title + ⓘ bubble), the editor, and the save verdict. Reads fire
43
+ * only while `active`; a missing list face renders the header without the
44
+ * editor (declare-and-degrade).
45
+ * @param props - the faces, the activity gate, and copy.
46
+ * @returns the section.
47
+ */
48
+ export declare function ModelListSection({ active, modelList, modelDirectory, directoryRefresh, t }: ModelListSectionProps): import("react").JSX.Element;
49
+ //# sourceMappingURL=ModelListSection.d.ts.map
@@ -0,0 +1,132 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The family's shared「可用模型」section: an ORDERED shortlist of
4
+ * endpoint-served models — the first entry is the default model every
5
+ * delegation round starts with. Rows drag to reorder and delete per row;
6
+ * "add model" opens a dialog over the endpoint's served catalog (search +
7
+ * checkboxes), so the list can only ever contain what the endpoint actually
8
+ * serves. An empty list falls back to the pre-list chain (the settings
9
+ * model key, the scoped config file, then the CLI's own default). Every
10
+ * edit persists immediately through the harness's model-list face (the
11
+ * gateway's `modelList`/`configureModelList` Remotes).
12
+ *
13
+ * One implementation serves every companion card (claude-code, codex,
14
+ * kimi): the section owns its state (list read on activate, immediate
15
+ * writes, the catalog read behind the add dialog) so each card's
16
+ * integration is the two faces plus copy — the interaction cannot drift
17
+ * between harnesses. Copy lives in the family core's own dictionary.
18
+ * @module @khorsheed/dsh-local-agent/client — model list section
19
+ */
20
+ import { useEffect, useMemo, useState } from 'react';
21
+ import { Tooltip } from '@deepseek-ai/dsh-client-ui-primitives';
22
+ import css from './ModelListSection.module.css';
23
+ /** The section's staged state: the list and the catalog behind the add dialog. */
24
+ function useModelListState(active, modelList, modelDirectory, directoryRefresh) {
25
+ const unavailable = modelList === undefined;
26
+ const [models, setModels] = useState([]);
27
+ const [saved, setSaved] = useState(false);
28
+ const [error, setError] = useState(false);
29
+ useEffect(() => {
30
+ if (!active || modelList === undefined)
31
+ return;
32
+ let stale = false;
33
+ void modelList.read().then((list) => { if (!stale)
34
+ setModels(list ?? []); }, () => { if (!stale)
35
+ setModels([]); });
36
+ return () => { stale = true; };
37
+ }, [active, modelList]);
38
+ // The endpoint's served catalog: the add dialog's candidate vocabulary.
39
+ // Refreshed while the surface is active and whenever directoryRefresh
40
+ // moves (a fresh credential's models belong in the dialog now).
41
+ const [directory, setDirectory] = useState(null);
42
+ useEffect(() => {
43
+ if (!active || modelDirectory === undefined)
44
+ return;
45
+ let stale = false;
46
+ void modelDirectory(true).then((result) => { if (!stale)
47
+ setDirectory(result ?? null); }, () => { if (!stale)
48
+ setDirectory(null); });
49
+ return () => { stale = true; };
50
+ }, [active, modelDirectory, directoryRefresh]);
51
+ const saveModels = (next) => {
52
+ if (modelList === undefined)
53
+ return;
54
+ setSaved(false);
55
+ setError(false);
56
+ void modelList.write(next).then((written) => {
57
+ setModels(written ?? next);
58
+ setSaved(true);
59
+ }, () => { setError(true); });
60
+ };
61
+ return { models, unavailable, saved, error, directory, saveModels };
62
+ }
63
+ /** The add dialog: search + checkboxes over the endpoint's served catalog. */
64
+ function AddModelsDialog({ models, directory, onAdd, onClose, t }) {
65
+ const [query, setQuery] = useState('');
66
+ const [checked, setChecked] = useState(new Set());
67
+ const candidates = useMemo(() => {
68
+ const entries = directory?.entries ?? [];
69
+ const needle = query.trim().toLowerCase();
70
+ return entries
71
+ .filter(entry => needle === '' || entry.value.toLowerCase().includes(needle) || entry.label.toLowerCase().includes(needle))
72
+ .map(entry => ({ ...entry, added: models.includes(entry.value) }));
73
+ }, [directory, query, models]);
74
+ const selectable = candidates.filter(entry => !entry.added);
75
+ const allChecked = selectable.length > 0 && selectable.every(entry => checked.has(entry.value));
76
+ const toggleAll = () => {
77
+ setChecked(allChecked ? new Set() : new Set(selectable.map(entry => entry.value)));
78
+ };
79
+ const toggle = (value) => {
80
+ setChecked(prev => {
81
+ const next = new Set(prev);
82
+ if (next.has(value))
83
+ next.delete(value);
84
+ else
85
+ next.add(value);
86
+ return next;
87
+ });
88
+ };
89
+ return (_jsx("div", { className: css.dialogBackdrop, role: "presentation", onClick: () => { onClose(); }, children: _jsxs("div", { className: css.dialog, role: "dialog", "aria-label": t('models.dialog.title'), onClick: event => { event.stopPropagation(); }, children: [_jsxs("div", { className: css.dialogHead, children: [_jsx("h4", { className: css.dialogTitle, children: t('models.dialog.title') }), _jsx("button", { type: "button", className: css.dialogClose, "aria-label": t('models.dialog.cancel'), onClick: () => { onClose(); }, children: "\u2715" })] }), _jsx("p", { className: css.dialogHint, children: t('models.dialog.hint') }), _jsxs("div", { className: css.dialogToolbar, children: [_jsx("input", { type: "text", className: css.modelInput, value: query, placeholder: t('models.dialog.search'), "aria-label": t('models.dialog.search'), onChange: event => { setQuery(event.currentTarget.value); } }), _jsx("button", { type: "button", className: css.dialogSelectAll, disabled: selectable.length === 0, onClick: () => { toggleAll(); }, children: t('models.dialog.selectAll') })] }), _jsxs("div", { className: css.dialogList, children: [directory === null && _jsx("span", { className: css.hint, children: t('models.dialog.loading') }), directory !== null && directory.entries.length === 0 && (_jsx("span", { className: css.hint, children: directory.reason ?? t('models.dialog.empty') })), candidates.map(entry => (_jsxs("label", { className: entry.added ? `${css.dialogItem} ${css.dialogItemAdded}` : css.dialogItem, children: [_jsx("input", { type: "checkbox", checked: entry.added || checked.has(entry.value), disabled: entry.added, onChange: () => { toggle(entry.value); } }), _jsx("span", { className: css.dialogItemValue, children: entry.value }), entry.label !== entry.value && _jsx("span", { className: css.dialogItemLabel, children: entry.label })] }, entry.value)))] }), _jsxs("div", { className: css.dialogActions, children: [_jsx("button", { type: "button", className: css.modelSave, onClick: () => { onClose(); }, children: t('models.dialog.cancel') }), _jsx("button", { type: "button", className: css.dialogConfirm, disabled: checked.size === 0, onClick: () => { onAdd([...checked]); }, children: t('models.dialog.confirm') })] })] }) }));
90
+ }
91
+ /**
92
+ * The editor body: the ordered rows, the per-row delete, and the add button.
93
+ * Reorders persist on drop — every gesture writes immediately.
94
+ */
95
+ function ModelListEditor({ models, directory, onChange, t }) {
96
+ const [dialogOpen, setDialogOpen] = useState(false);
97
+ const [dragIndex, setDragIndex] = useState(null);
98
+ const move = (from, to) => {
99
+ if (from === to)
100
+ return;
101
+ const item = models[from];
102
+ if (item === undefined)
103
+ return;
104
+ const next = [...models];
105
+ next.splice(from, 1);
106
+ next.splice(to, 0, item);
107
+ onChange(next);
108
+ };
109
+ return (_jsxs("div", { className: css.modelList, children: [models.length === 0 && _jsx("span", { className: css.hint, children: t('models.empty') }), models.map((model, index) => {
110
+ const entry = directory?.entries.find(candidate => candidate.value === model);
111
+ return (_jsxs("div", { className: dragIndex === index ? `${css.modelRow} ${css.modelRowDragging}` : css.modelRow, draggable: true, onDragStart: () => { setDragIndex(index); }, onDragOver: event => { event.preventDefault(); }, onDrop: () => {
112
+ if (dragIndex !== null)
113
+ move(dragIndex, index);
114
+ setDragIndex(null);
115
+ }, onDragEnd: () => { setDragIndex(null); }, children: [_jsx("span", { className: css.modelDrag, "aria-hidden": true, children: "\u283F" }), _jsx("span", { className: css.modelRowValue, children: model }), entry !== undefined && entry.label !== model && _jsx("span", { className: css.modelRowLabel, children: entry.label }), index === 0 && _jsx("span", { className: css.modelDefaultChip, children: t('models.defaultChip') }), _jsx("button", { type: "button", className: css.modelDelete, "aria-label": `${t('models.delete')}: ${model}`, onClick: () => { onChange(models.filter((_, other) => other !== index)); }, children: "\u2715" })] }, model));
116
+ }), _jsxs("div", { className: css.row, children: [_jsx("button", { type: "button", className: css.modelAdd, onClick: () => { setDialogOpen(true); }, children: t('models.add') }), models.length > 1 && _jsx("span", { className: css.hint, children: t('models.orderHint') })] }), dialogOpen && (_jsx(AddModelsDialog, { models: models, directory: directory, onAdd: (added) => {
117
+ onChange([...models, ...added]);
118
+ setDialogOpen(false);
119
+ }, onClose: () => { setDialogOpen(false); }, t: t }))] }));
120
+ }
121
+ /**
122
+ * The「可用模型」section every companion's settings card renders: the block
123
+ * chrome (title + ⓘ bubble), the editor, and the save verdict. Reads fire
124
+ * only while `active`; a missing list face renders the header without the
125
+ * editor (declare-and-degrade).
126
+ * @param props - the faces, the activity gate, and copy.
127
+ * @returns the section.
128
+ */
129
+ export function ModelListSection({ active, modelList, modelDirectory, directoryRefresh, t }) {
130
+ const { models, unavailable, saved, error, directory, saveModels } = useModelListState(active, modelList, modelDirectory, directoryRefresh);
131
+ return (_jsxs("section", { className: css.block, children: [_jsxs("h3", { className: css.blockTitle, children: [t('models.title'), _jsx(Tooltip, { label: t('models.info'), side: "bottom", maxWidth: 360, children: _jsx("button", { type: "button", className: css.info, "aria-label": t('models.info.aria'), children: "\u24D8" }) })] }), !unavailable && (_jsxs(_Fragment, { children: [_jsx(ModelListEditor, { models: models, directory: directory, onChange: saveModels, t: t }), saved && _jsx("span", { className: css.saved, children: t('models.applied') }), error && _jsx("span", { className: css.errorText, children: t('models.error') })] }))] }));
132
+ }
@@ -13,6 +13,14 @@ export declare const LOGIN_POLL_MS = 4000;
13
13
  export declare const LOGIN_POLL_LIMIT_MS: number;
14
14
  /** The first http(s) URL in a login prompt, if any. */
15
15
  export declare function parseLoginUrl(text: string): string | undefined;
16
+ /**
17
+ * The main-view session: host 0.1.6-alpha.2 dropped SessionListState.current
18
+ * for the retainedBy.mainView count on each summary (the label is ui-session's
19
+ * declaration merge, read here as a plain duck-typed count so this package
20
+ * needs no ui-session edge), with the legacy field as the older-host fallback.
21
+ * Exported for sibling cards that run slash commands from their own rows.
22
+ */
23
+ export declare const mainSessionId: (list: SessionListState) => SessionId | undefined;
16
24
  /**
17
25
  * Injected business face of the shared auth block — the subset of the
18
26
  * settings section's injected face one harness row consumes.
@@ -43,8 +51,8 @@ export interface ProviderAuthBlockProps extends ProviderAuthInjected, PropsLocal
43
51
  }
44
52
  /**
45
53
  * One harness's auth interactions, shared between the per-provider settings
46
- * cards (the alpha.2 `plugins.bundle.config` pages and the 0.1.5
47
- * `settings.plugin.item` tab): auth status through
54
+ * cards (each a `plugins.item` entry on the Plugins page, registered while
55
+ * the Host serves the provider's namespace): auth status through
48
56
  * the read-only Remote channel and a web-login action that runs
49
57
  * `/<harness> login`, surfaces the device-code prompt, and offers a link to
50
58
  * the authorization page. The login command returns at prompt time while the
@@ -2,7 +2,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
2
2
  import { useEffect, useRef, useState } from 'react';
3
3
  import { Toast } from '@deepseek-ai/dsh-client-ui-primitives';
4
4
  import { IconCheckOutlineMedium } from './icons.js';
5
- import { publishAuthStatus } from './auth-status.js';
5
+ import { publishAuthStatus, usePublishedAuthStatus } from './auth-status.js';
6
6
  import css from './ProviderAuthBlock.module.css';
7
7
  /** Known harnesses the family plans to support; unregistered ones render as pending. */
8
8
  export const KNOWN_HARNESSES = [
@@ -27,13 +27,14 @@ function harnessLabel(id) {
27
27
  * for the retainedBy.mainView count on each summary (the label is ui-session's
28
28
  * declaration merge, read here as a plain duck-typed count so this package
29
29
  * needs no ui-session edge), with the legacy field as the older-host fallback.
30
+ * Exported for sibling cards that run slash commands from their own rows.
30
31
  */
31
- const mainSessionId = (list) => Object.values(list.byId ?? {}).find(session => (session.retainedBy?.mainView ?? 0) > 0)?.id
32
+ export const mainSessionId = (list) => Object.values(list.byId ?? {}).find(session => (session.retainedBy?.mainView ?? 0) > 0)?.id
32
33
  ?? list.current;
33
34
  /**
34
35
  * One harness's auth interactions, shared between the per-provider settings
35
- * cards (the alpha.2 `plugins.bundle.config` pages and the 0.1.5
36
- * `settings.plugin.item` tab): auth status through
36
+ * cards (each a `plugins.item` entry on the Plugins page, registered while
37
+ * the Host serves the provider's namespace): auth status through
37
38
  * the read-only Remote channel and a web-login action that runs
38
39
  * `/<harness> login`, surfaces the device-code prompt, and offers a link to
39
40
  * the authorization page. The login command returns at prompt time while the
@@ -69,6 +70,7 @@ export function ProviderAuthBlock({ harness, useSessions, status, runCommand, ac
69
70
  loginable: probe.loginable ?? true,
70
71
  logoutable: probe.logoutable ?? true,
71
72
  ...probe.loginAwaitingCode === true ? { loginAwaitingCode: true } : {},
73
+ ...probe.effectiveSettings?.cliVersion === undefined ? {} : { cliVersion: probe.effectiveSettings.cliVersion },
72
74
  };
73
75
  setView(prev =>
74
76
  // A completed login drops the stale device prompt.
@@ -138,7 +140,12 @@ export function ProviderAuthBlock({ harness, useSessions, status, runCommand, ac
138
140
  setView({ status: 'unavailable' });
139
141
  });
140
142
  };
141
- const statusKind = view.status;
143
+ // The bus carries the same harness's status from every writer: this
144
+ // block's own probes publish into it, and so does a sibling surface that
145
+ // WRITES the credential (the endpoint form's save). Following it keeps the
146
+ // row truthful without a re-probe; undefined until the first probe lands.
147
+ const published = usePublishedAuthStatus(harness.id);
148
+ const statusKind = published ?? view.status;
142
149
  const authenticated = statusKind === 'authenticated';
143
150
  const loginPending = pendingSince !== undefined;
144
151
  // A harness without a login/logout flow (dsh authenticates through the host
@@ -146,7 +153,7 @@ export function ProviderAuthBlock({ harness, useSessions, status, runCommand, ac
146
153
  // with an error.
147
154
  const loginable = view.loginable ?? true;
148
155
  const logoutable = view.logoutable ?? true;
149
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: css.rowHead, children: [_jsxs("span", { className: css.rowIdentity, children: [_jsx("span", { className: css.rowName, children: harness.label }), _jsx("span", { className: `${css.credentialDot} ${authenticated ? css.dotOn : css.dotOff}`, role: "img", "aria-label": t(authenticated ? 'settings.authenticated' : 'settings.notAuthenticated') }), _jsx("span", { className: css.rowTag, children: harness.id })] }), _jsxs("span", { className: css.status, children: [statusKind === 'checking' && t('loading'), statusKind === 'authenticated' && t('settings.authenticated'), statusKind === 'anonymous' && t('settings.notAuthenticated'), statusKind === 'unavailable' && t('error')] }), _jsxs("span", { className: css.rowActions, children: [authenticated && logoutable && (_jsx("button", { type: "button", className: css.logoutButton, onClick: () => { signOut(); }, children: t('settings.logout') })), loginable && (_jsx("button", { type: "button", className: css.loginButton, disabled: loginPending || sessionId === undefined, onClick: () => { startLogin(); }, children: authenticated ? t('settings.reauthorize') : t('settings.login') })), actions] })] }), view.loginText !== undefined && (_jsxs("div", { className: css.loginPrompt, children: [_jsx("span", { className: css.promptText, children: view.loginText }), view.loginUrl !== undefined && (_jsx("a", { className: css.openPage, href: view.loginUrl, target: "_blank", rel: "noreferrer", children: t('settings.openPage') }))] })), view.loginAwaitingCode === true && (_jsxs("div", { className: css.loginPrompt, children: [_jsx("input", { className: css.codeInput, value: codeDraft, placeholder: t('settings.pasteCode'), onChange: (event) => {
156
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: css.rowHead, children: [_jsxs("span", { className: css.rowIdentity, children: [_jsx("span", { className: css.rowName, children: harness.label }), _jsx("span", { className: `${css.credentialDot} ${authenticated ? css.dotOn : css.dotOff}`, role: "img", "aria-label": t(authenticated ? 'settings.authenticated' : 'settings.notAuthenticated') }), _jsx("span", { className: css.rowTag, children: harness.id }), view.cliVersion !== undefined && _jsx("span", { className: css.rowTag, children: view.cliVersion })] }), _jsxs("span", { className: css.status, children: [statusKind === 'checking' && t('loading'), statusKind === 'authenticated' && t('settings.authenticated'), statusKind === 'anonymous' && t('settings.notAuthenticated'), statusKind === 'unavailable' && t('error')] }), _jsxs("span", { className: css.rowActions, children: [authenticated && logoutable && (_jsx("button", { type: "button", className: css.logoutButton, onClick: () => { signOut(); }, children: t('settings.logout') })), loginable && (_jsx("button", { type: "button", className: css.loginButton, disabled: loginPending || sessionId === undefined, onClick: () => { startLogin(); }, children: authenticated ? t('settings.reauthorize') : t('settings.login') })), actions] })] }), view.loginText !== undefined && (_jsxs("div", { className: css.loginPrompt, children: [_jsx("span", { className: css.promptText, children: view.loginText }), view.loginUrl !== undefined && (_jsx("a", { className: css.openPage, href: view.loginUrl, target: "_blank", rel: "noreferrer", children: t('settings.openPage') }))] })), view.loginAwaitingCode === true && (_jsxs("div", { className: css.loginPrompt, children: [_jsx("input", { className: css.codeInput, value: codeDraft, placeholder: t('settings.pasteCode'), onChange: (event) => {
150
157
  setCodeDraft(event.currentTarget.value);
151
158
  } }), _jsx("button", { type: "button", className: css.loginButton, disabled: codeDraft.trim() === '', onClick: () => {
152
159
  const code = codeDraft.trim();
@@ -15,6 +15,12 @@ export declare function publishAuthStatus(harnessId: string, status: HarnessAuth
15
15
  export declare function resetAuthStatuses(): void;
16
16
  /** Read one harness's latest published status without subscribing (tests, non-React callers). */
17
17
  export declare function readAuthStatus(harnessId: string): HarnessAuthStatusKind | undefined;
18
+ /**
19
+ * Subscribe one harness's published status (no probe of its own): a sibling
20
+ * surface that WRITES the credential (the endpoint form's save) publishes
21
+ * through the same bus, and every block showing the status flips at once.
22
+ */
23
+ export declare function usePublishedAuthStatus(harnessId: string): HarnessAuthStatusKind | undefined;
18
24
  /**
19
25
  * Read one harness's latest known auth status. Every mount probes once and
20
26
  * publishes — a published value only snapshots the past (a credential expiring
@@ -29,6 +29,14 @@ export function resetAuthStatuses() {
29
29
  export function readAuthStatus(harnessId) {
30
30
  return statuses.get(harnessId);
31
31
  }
32
+ /**
33
+ * Subscribe one harness's published status (no probe of its own): a sibling
34
+ * surface that WRITES the credential (the endpoint form's save) publishes
35
+ * through the same bus, and every block showing the status flips at once.
36
+ */
37
+ export function usePublishedAuthStatus(harnessId) {
38
+ return useSyncExternalStore(subscribe, () => statuses.get(harnessId));
39
+ }
32
40
  /**
33
41
  * Read one harness's latest known auth status. Every mount probes once and
34
42
  * publishes — a published value only snapshots the past (a credential expiring
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The harness-card registry behind the「本机 Agent 委派」family card: each
3
+ * delegation companion's client registers one section (its settings card
4
+ * body) through the localAgentUi service's `registerHarnessCard`, and the
5
+ * core's single `plugins.item` entry renders every registered section sorted
6
+ * by order. Sections come from THIS registry, never the gateway roster — a
7
+ * registered harness absent host-side still renders, its body degrading
8
+ * exactly like the retired standalone card did.
9
+ *
10
+ * The registry lives on the cordis service (not a module export) because every
11
+ * plugin's client bundle inlines its own copy of the core's sources: module
12
+ * identity is per bundle, the service is the one shared instance.
13
+ * @module @khorsheed/dsh-local-agent/client — harness card registry
14
+ */
15
+ import type { ReactNode } from 'react';
16
+ import type { SessionListState } from '@deepseek-ai/dsh-api-session-controller/client';
17
+ /** Render props the family card hands each harness section. */
18
+ export interface HarnessCardRenderProps {
19
+ /**
20
+ * Whether the section is currently expanded. The family card requests the
21
+ * body only while the section is open, so a mounted section always reads
22
+ * true; the prop keeps the contract honest for a keep-alive variant.
23
+ */
24
+ readonly active: boolean;
25
+ /**
26
+ * The standing root-scope global seat (ui-session's GlobalStandardProps
27
+ * merge), forwarded verbatim from the family card's own plugins.item
28
+ * runtime props — the same seat the retired standalone cards received from
29
+ * the slot renderer.
30
+ */
31
+ readonly useSessions: <T>(select: (state: SessionListState) => T) => T;
32
+ }
33
+ /** One harness's section registration in the family card. */
34
+ export interface HarnessCardRegistration {
35
+ /** The harness name the gateway answers for ('kimi', 'codex', …). */
36
+ readonly name: string;
37
+ /** Section order — the harness's retired standalone plugins.item order. */
38
+ readonly order: number;
39
+ /** The registering companion's locale namespace (mirrors the slot registration vocabulary; the label binding reads it live). */
40
+ readonly locale: string;
41
+ /** Section title (the companion's `card.title` copy). */
42
+ readonly label: () => string;
43
+ /** The section body (the companion's card body minus its standalone chrome). */
44
+ readonly render: (props: HarnessCardRenderProps) => ReactNode;
45
+ }
46
+ /**
47
+ * In-memory registry with the useSyncExternalStore face the family card
48
+ * subscribes through: a version counter as the snapshot (mutations bump it)
49
+ * and a cached sorted list readers re-read after a bump.
50
+ */
51
+ export declare class HarnessCardRegistry {
52
+ private readonly entries;
53
+ private readonly listeners;
54
+ private version;
55
+ private sorted;
56
+ /**
57
+ * Register one harness's section; re-registering the same name replaces the
58
+ * entry. The returned disposer removes only the entry it registered (a stale
59
+ * disposer never evicts a newer registration of the same name).
60
+ * @param entry - the section registration.
61
+ * @returns the unregister.
62
+ */
63
+ readonly register: (entry: HarnessCardRegistration) => (() => void);
64
+ /** The registered sections in display order (order, then name). */
65
+ readonly cards: () => readonly HarnessCardRegistration[];
66
+ /** The mutation counter — the useSyncExternalStore snapshot. */
67
+ readonly snapshotVersion: () => number;
68
+ /** Subscribe a listener fired after every mutation; returns the unsubscribe. */
69
+ readonly subscribe: (listener: () => void) => (() => void);
70
+ private bump;
71
+ }
72
+ //# sourceMappingURL=harness-cards.d.ts.map