@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.
- package/CHANGELOG.md +31 -0
- package/README.en.md +23 -164
- package/README.i18n.yaml +2 -2
- package/README.md +23 -164
- package/lib/client.js +23707 -4768
- package/lib/index.js +827 -62
- package/lib/member-bridge.js +195 -25
- package/lib/tool-factory.js +52 -19
- package/lib/typert.host.js +1440 -353
- package/lib/typert.remote-client.d.ts +43 -3
- package/lib/typert.remote-client.js +973 -335
- package/lib/types/client/FamilyCard.d.ts +37 -0
- package/lib/types/client/FamilyCard.js +55 -0
- package/lib/types/client/HarnessModelPicker.d.ts +21 -1
- package/lib/types/client/HarnessModelPicker.js +31 -3
- package/lib/types/client/MemberLiveNode.js +38 -4
- package/lib/types/client/ModelListSection.d.ts +49 -0
- package/lib/types/client/ModelListSection.js +132 -0
- package/lib/types/client/ProviderAuthBlock.d.ts +10 -2
- package/lib/types/client/ProviderAuthBlock.js +13 -6
- package/lib/types/client/auth-status.d.ts +6 -0
- package/lib/types/client/auth-status.js +8 -0
- package/lib/types/client/harness-cards.d.ts +72 -0
- package/lib/types/client/harness-cards.js +57 -0
- package/lib/types/client/icons.d.ts +2 -1
- package/lib/types/client/icons.js +1 -0
- package/lib/types/client/index.d.ts +32 -7
- package/lib/types/client/index.js +66 -9
- package/lib/types/client/locales.d.ts +25 -1
- package/lib/types/client/locales.js +50 -2
- package/lib/types/client/model-directory-scope.d.ts +14 -0
- package/lib/types/client/model-directory-scope.js +20 -0
- package/lib/types/gateway.d.ts +49 -1
- package/lib/types/gateway.js +89 -0
- package/lib/types/index.d.ts +91 -38
- package/lib/types/index.js +190 -58
- package/lib/types/live-stream.d.ts +11 -0
- package/lib/types/live-stream.js +26 -0
- package/lib/types/member-bridge.d.ts +11 -1
- package/lib/types/member-bridge.js +81 -13
- package/lib/types/member-channel.d.ts +44 -1
- package/lib/types/member-channel.js +65 -2
- package/lib/types/member-controls.d.ts +8 -0
- package/lib/types/member-controls.js +15 -2
- package/lib/types/mode-skills.d.ts +18 -0
- package/lib/types/mode-skills.js +56 -0
- package/lib/types/model-list.d.ts +55 -0
- package/lib/types/model-list.js +101 -0
- package/lib/types/preset-tool-host.d.ts +70 -0
- package/lib/types/preset-tool-host.js +210 -0
- package/lib/types/tool-factory.d.ts +9 -3
- package/lib/types/tool-factory.js +55 -18
- package/lib/types/types.d.ts +62 -5
- package/package.json +27 -23
- package/lib/invariant.js +0 -85
- package/lib/types/invariant.d.ts +0 -16
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
47
|
-
*
|
|
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 (
|
|
36
|
-
*
|
|
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
|
-
|
|
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
|