@khorsheed/dsh-local-agent-codex 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.
@@ -1,9 +1,7 @@
1
- import type { HarnessModelPickerInput } from '@khorsheed/dsh-local-agent/client';
2
- import type { ReactNode } from 'react';
3
1
  import type { SettingsScope } from '@khorsheed/dsh-local-agent/src/client/settings-scope.ts';
4
2
  import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
5
3
  import { type ProviderAuthBlockProps, type ProviderAuthInjected } from '@khorsheed/dsh-local-agent/src/client/ProviderAuthBlock.tsx';
6
- import type { LocalAgentModelInfo } from '@khorsheed/dsh-local-agent/types';
4
+ import type { LocalAgentEndpointAuthFields, LocalAgentEndpointAuthState, LocalAgentModelDirectory } from '@khorsheed/dsh-local-agent/types';
7
5
  import { NS } from './locales.ts';
8
6
  /** The `local-agent-codex` settings namespace section the card edits. */
9
7
  export interface CodexLiveSettings {
@@ -20,7 +18,6 @@ export interface CodexLiveSettings {
20
18
  }
21
19
  /** Injected face of the codex settings card. */
22
20
  export interface CodexSettingsCardInjected {
23
- renderModelPicker?: ((props: HarnessModelPickerInput) => ReactNode) | undefined;
24
21
  /** Bound settings scope for the local-agent-codex namespace. */
25
22
  scope: SettingsScope<CodexLiveSettings>;
26
23
  /** The auth block's query/command faces, backed by the family core Remote. */
@@ -28,34 +25,77 @@ export interface CodexSettingsCardInjected {
28
25
  /** Translate bound to the family core's `local-agent` namespace (the auth block's copy lives there). */
29
26
  authT: ProviderAuthBlockProps['t'];
30
27
  /**
31
- * The harness's memberless model surface (the core gateway's `harnessModel`
32
- * Remote): the inherited default an unset field displays and the menu's
33
- * choice vocabulary. Undefined or null when the gateway or the broker is
34
- * absent — the card then keeps the bare input exactly as before brokers
35
- * existed.
28
+ * The harness's curated model list (the family gateway's
29
+ * `modelList`/`configureModelList` Remotes): the editor's ordered
30
+ * shortlist — its first entry is the default model. Absent on a core that
31
+ * predates the Remotes — the card then hides the editor.
36
32
  */
37
- harnessModel: () => Promise<LocalAgentModelInfo | null | undefined>;
33
+ modelList?: {
34
+ read(): Promise<readonly string[] | null | undefined>;
35
+ write(models: readonly string[]): Promise<readonly string[] | null | undefined>;
36
+ };
37
+ /**
38
+ * The endpoint's served model catalog (the family gateway's
39
+ * `modelDirectory` Remote): the add dialog's candidate vocabulary. Absent
40
+ * on a core that predates the Remote — the dialog then shows its reason.
41
+ */
42
+ modelDirectory?: (refresh?: boolean) => Promise<LocalAgentModelDirectory | null | undefined>;
43
+ /**
44
+ * The harness's endpoint verification (the family gateway's `checkEndpoint`
45
+ * Remote): a token-free probe of the SAVED endpoint, answered as verdict
46
+ * text. Absent on a core that predates the Remote — the card then hides
47
+ * the verify button.
48
+ */
49
+ checkEndpoint?: () => Promise<string | null | undefined>;
50
+ /**
51
+ * The harness's endpoint credential face (the family gateway's
52
+ * `endpointAuth`/`configureEndpoint` Remotes): the card's credential form
53
+ * reads and writes the harness's OWN credential store through it — never
54
+ * the settings scope, so no key ever lands in the profile patch document.
55
+ * Absent on a core that predates the Remotes — the card then hides the
56
+ * form. `write` answers the post-write resolved state.
57
+ */
58
+ endpointAuth?: {
59
+ read(): Promise<LocalAgentEndpointAuthState | null | undefined>;
60
+ write(fields: LocalAgentEndpointAuthFields): Promise<LocalAgentEndpointAuthState | null | undefined>;
61
+ };
38
62
  hooks: {
39
63
  /** The same scope as a hooks source, so the card reacts to external writes. */
40
64
  settings: SettingsScope<CodexLiveSettings>;
41
65
  };
42
66
  }
43
67
  /**
44
- * Full props of the 0.1.5 settings.plugin.item card entry. The removed slot's
45
- * owner share was intentionally empty and its root scope delivered the global
46
- * seats; the structural type below is exact about the one seat the auth block
47
- * consumes (the slot name is gone from the alpha.2 SlotMap, so the
48
- * registration goes through a duck-typed narrow — see client/index.ts).
68
+ * Full props of the retired 0.1.5 settings.plugin.item card. The host removed
69
+ * that seat and client/index.ts no longer registers the component — the live
70
+ * settings surface is the plugins.item entry built on the BundleConfig
71
+ * component below; this export remains for the component spec and direct
72
+ * composers. The removed slot's owner share was intentionally empty and its
73
+ * root scope delivered the global seats; the structural type below is exact
74
+ * about the one seat the auth block consumes.
49
75
  */
50
76
  export type CodexSettingsCardProps = InjectFace<CodexSettingsCardInjected> & PropsLocale<typeof NS> & {
51
77
  /** The standing root-scope global seat (ui-session's merge), delivered to the 0.1.5 card by the renderer. */
52
78
  useSessions: ProviderAuthBlockProps['useSessions'];
53
79
  };
54
- /** Full props of the alpha.2 plugins.bundle.config entry (owner prop `view`). */
55
- export type CodexBundleConfigProps = PropsRuntime<'plugins.bundle.config'> & InjectFace<CodexSettingsCardInjected> & PropsLocale<typeof NS>;
80
+ /** Full props of the plugins.item entry (owner prop `view`). */
81
+ export type CodexBundleConfigProps = PropsRuntime<'plugins.item'> & InjectFace<CodexSettingsCardInjected> & PropsLocale<typeof NS>;
82
+ /**
83
+ * Full props of the harness's section inside the core's「本机 Agent 委派」
84
+ * family card: the same inject face minus the slot-owner share, plus the one
85
+ * global seat the body consumes, forwarded by the family card (the section
86
+ * render bypasses the slot runtime, so its remaining standard seats are not
87
+ * fabricable here and are deliberately absent).
88
+ */
89
+ export type CodexCardSectionProps = InjectFace<CodexSettingsCardInjected> & PropsLocale<typeof NS> & {
90
+ /** Whether the section is currently expanded (the family card mounts the body only while it is). */
91
+ readonly active: boolean;
92
+ /** The standing root-scope global seat (ui-session's merge), forwarded by the family card. */
93
+ readonly useSessions: ProviderAuthBlockProps['useSessions'];
94
+ };
56
95
  /**
57
- * The codex harness settings card in the 0.1.5 plugin configuration tab: the
58
- * same collapsible chrome the official plugin cards use, re-implemented
96
+ * The codex harness settings card of the retired 0.1.5 plugin configuration
97
+ * tab (no longer registered; the live surface is the plugins.item entry
98
+ * below): the same collapsible chrome the official plugin cards use, re-implemented
59
99
  * locally (the bundle-purity gate forbids value-importing the official chrome
60
100
  * — the context-guard pattern), with the at-a-glance credential dot in the
61
101
  * header. Explanation copy lives in hover/focus bubbles behind ⓘ anchors —
@@ -65,12 +105,21 @@ export type CodexBundleConfigProps = PropsRuntime<'plugins.bundle.config'> & Inj
65
105
  */
66
106
  export declare function CodexSettingsCard(props: CodexSettingsCardProps): import("react").JSX.Element;
67
107
  /**
68
- * The bundle's configuration on the alpha.2 Plugins page
69
- * (`plugins.bundle.config`, keyed by package name): the page draws the title,
70
- * the icon, and the crumb itself, so the entry renders the one-liner for the
71
- * `summary` view and the bare form — with its own save control — for `page`.
108
+ * The provider's card on the Plugins page (the `plugins.item` entry
109
+ * client/index.ts registers while the Host serves the namespace AND the core
110
+ * predates the family card): the page draws the title, the icon, and the
111
+ * crumb itself, so the entry renders the one-liner for the `summary` view and
112
+ * the bare form — with its own save control — for `page`.
72
113
  * @param props - the owner view, the injected scope/auth faces, and copy.
73
114
  * @returns the entry.
74
115
  */
75
- export declare function CodexBundleConfig({ view, useSettings, scope, auth, authT, renderModelPicker, harnessModel, useSessions, t }: CodexBundleConfigProps): import("react").JSX.Element;
116
+ export declare function CodexBundleConfig({ view, useSettings, scope, auth, authT, modelList, modelDirectory, checkEndpoint, endpointAuth, useSessions, t }: CodexBundleConfigProps): import("react").JSX.Element;
117
+ /**
118
+ * The harness's section inside the core's「本机 Agent 委派」family card: the
119
+ * same useCardState + CardBody the plugins.item page view renders — the
120
+ * collapsible chrome belongs to the family card now.
121
+ * @param props - the injected scope/auth faces, the forwarded session seat, and copy.
122
+ * @returns the section body.
123
+ */
124
+ export declare function CodexCardSection({ active, useSettings, scope, auth, authT, modelList, modelDirectory, checkEndpoint, endpointAuth, useSessions, t }: CodexCardSectionProps): import("react").JSX.Element;
76
125
  //# sourceMappingURL=SettingsCard.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useRef, useState } from 'react';
2
+ import { useEffect, useState } from 'react';
3
3
  import { Tooltip } from '@deepseek-ai/dsh-client-ui-primitives';
4
4
  import { IconChevronDownOutlineMedium } from './icons.js';
5
5
  // The shared auth block is bundled from the family core's source — the
@@ -7,162 +7,120 @@ import { IconChevronDownOutlineMedium } from './icons.js';
7
7
  // the block carries no runtime identity to share.
8
8
  import { ProviderAuthBlock, } from '@khorsheed/dsh-local-agent/src/client/ProviderAuthBlock.tsx';
9
9
  import { AuthStatusDot } from '@khorsheed/dsh-local-agent/src/client/AuthStatusDot.tsx';
10
- import { useHarnessAuthStatus } from '@khorsheed/dsh-local-agent/src/client/auth-status.ts';
10
+ import { publishAuthStatus, useHarnessAuthStatus } from '@khorsheed/dsh-local-agent/src/client/auth-status.ts';
11
+ import { ModelListSection } from '@khorsheed/dsh-local-agent/src/client/ModelListSection.tsx';
11
12
  import css from './SettingsCard.module.css';
12
- /** How many previously-saved model identifiers the input suggests. */
13
- const RECENT_MODEL_LIMIT = 5;
14
13
  /**
15
- * The staged card state both settings surfaces share: the resident-mode and
16
- * model edits write through the bound settingsScope immediately
14
+ * The staged card state both settings surfaces share: the resident-mode
15
+ * switch writes through the bound settingsScope immediately
17
16
  * (revision-fenced), so a flip takes effect on the next delegation round
18
- * without a reload; the harness's model surface is fetched while the surface
19
- * is active (the 0.1.5 card opened, the alpha.2 page mounted) and re-fetched
20
- * after every save.
21
- * @param active - whether the surface currently shows the body (gates the model-surface fetch).
17
+ * without a reload; the endpoint credential form talks to the harness's own
18
+ * store through the endpointAuth face. The「可用模型」section is the family
19
+ * core's shared ModelListSection, which owns its own state.
20
+ * @param active - whether the surface currently shows the body (gates the credential-form fetch).
22
21
  * @param useSettings - the bound settings hook from the inject hooks compartment.
23
22
  * @param scope - the bound settings scope the writes go through.
24
- * @param harnessModel - the harness's memberless model surface read.
25
- * @param t - copy.
23
+ * @param endpointAuth - the harness's endpoint credential face; absent hides the credential form.
26
24
  */
27
- function useCardState(active, useSettings, scope, harnessModel, t) {
25
+ function useCardState(active, useSettings, scope, endpointAuth) {
28
26
  const snapshot = useSettings(value => value);
29
27
  const [saved, setSaved] = useState(false);
30
28
  const [error, setError] = useState(false);
31
29
  const ready = snapshot.status === 'ready' && snapshot.writable;
32
30
  const live = snapshot.value?.live ?? false;
33
- // The model input is a DRAFT until saved: `null` means "showing whatever the
34
- // scope holds", so an external write (another tab, a YAML reload) still
35
- // reaches the field while the user is not typing in it.
36
- const storedModel = snapshot.value?.model ?? '';
37
- const recentModels = snapshot.value?.recentModels ?? [];
38
- const [modelDraft, setModelDraft] = useState(null);
39
- const [modelSaved, setModelSaved] = useState(false);
40
- const [modelError, setModelError] = useState(false);
41
- const modelValue = modelDraft ?? storedModel;
42
- // The harness's model surface, fetched while the surface is active (and
43
- // re-fetched after a save): undefined until the first answer, null when the
44
- // gateway or the broker is absent — the card then keeps the bare input with
45
- // its recent-models suggestions, exactly as before brokers existed.
46
- const [modelInfo, setModelInfo] = useState(undefined);
31
+ // The endpoint credential form talks to the harness's OWN credential store
32
+ // through the endpointAuth face — never the settings scope, so no key ever
33
+ // lands in the profile patch document. baseUrl echoes (it is not a
34
+ // secret); the key never does — a set key shows as a "configured"
35
+ // placeholder and the input holds only the draft of a REPLACEMENT. A blank
36
+ // write clears the field (the web login then answers for the scope again).
37
+ const [endpointState, setEndpointState] = useState(null);
38
+ const [baseUrlDraft, setBaseUrlDraft] = useState(null);
39
+ const [apiKeyDraft, setApiKeyDraft] = useState(null);
40
+ const [endpointSaved, setEndpointSaved] = useState(false);
41
+ const [endpointError, setEndpointError] = useState(false);
42
+ const endpointDirty = baseUrlDraft !== null || apiKeyDraft !== null;
43
+ const endpointUnavailable = endpointAuth === undefined || endpointState === 'unavailable';
44
+ const endpointLoading = !endpointUnavailable && endpointState === null;
45
+ const apiKeyConfigured = endpointState !== null && endpointState !== 'unavailable' && endpointState.apiKeySet;
47
46
  useEffect(() => {
48
- if (!active)
47
+ if (!active || endpointAuth === undefined)
49
48
  return;
50
49
  let stale = false;
51
- void harnessModel().then((info) => {
52
- if (!stale)
53
- setModelInfo(info ?? null);
54
- });
50
+ void endpointAuth.read().then((state) => { if (!stale)
51
+ setEndpointState(state ?? 'unavailable'); }, () => { if (!stale)
52
+ setEndpointState('unavailable'); });
55
53
  return () => { stale = true; };
56
- }, [active, harnessModel, modelSaved]);
57
- /**
58
- * Commit the model field. A blank value UNSETS the key rather than storing
59
- * an empty string, so the field re-inherits the YAML composition base — and
60
- * with no base, the harness is back to passing no model at all.
61
- */
62
- const saveModel = () => {
63
- const next = modelValue.trim();
64
- setModelSaved(false);
65
- setModelError(false);
66
- const write = next === '' ? scope.unset('model') : scope.set('model', next);
67
- void write.then(async () => {
68
- // Most-recent-first, deduplicated, capped: the suggestion list is a
69
- // memory of what this instance has actually run, never a catalog.
70
- if (next !== '') {
71
- await scope.set('recentModels', [next, ...recentModels.filter(value => value !== next)].slice(0, RECENT_MODEL_LIMIT));
54
+ }, [active, endpointAuth, endpointSaved]);
55
+ // The credential save: one `configureEndpoint` call carrying only the
56
+ // dirty fields (a blank draft clears its field); the answer is the
57
+ // post-write resolved state, so the chips and the auth probe flip without
58
+ // a re-read.
59
+ const saveEndpoint = () => {
60
+ if (endpointAuth === undefined)
61
+ return;
62
+ setEndpointSaved(false);
63
+ setEndpointError(false);
64
+ const fields = {
65
+ ...baseUrlDraft !== null ? { baseUrl: baseUrlDraft.trim() === '' ? null : baseUrlDraft.trim() } : {},
66
+ ...apiKeyDraft !== null ? { apiKey: apiKeyDraft.trim() === '' ? null : apiKeyDraft.trim() } : {},
67
+ };
68
+ void endpointAuth.write(fields).then((state) => {
69
+ if (state !== undefined && state !== null) {
70
+ setEndpointState(state);
71
+ // Flip the status dot with the write's own answer — but only in
72
+ // the authenticated direction: clearing the key may still leave
73
+ // the web login's auth.json, which the next scheduled probe sees.
74
+ if (state.apiKeySet)
75
+ publishAuthStatus('codex', 'authenticated');
72
76
  }
73
- setModelDraft(null);
74
- setModelSaved(true);
75
- }, () => { setModelError(true); });
77
+ setBaseUrlDraft(null);
78
+ setApiKeyDraft(null);
79
+ setEndpointSaved(true);
80
+ }, () => { setEndpointError(true); });
76
81
  };
77
82
  const write = (field, value) => {
78
83
  setSaved(false);
79
84
  setError(false);
80
85
  void scope.set(field, value).then(() => { setSaved(true); }, () => { setError(true); });
81
86
  };
82
- // The suggestion vocabulary: the broker's deduped union (settings + scoped
83
- // config + discovered + recent) when the surface answered, the card's own
84
- // recent-models memory when it did not. Never a hardcoded catalog. A
85
- // non-empty list turns the field into a select-like control: the
86
- // always-visible chevron opens the same vocabulary as a menu.
87
- const choices = modelInfo?.choices ?? recentModels;
88
- const [modelMenuOpen, setModelMenuOpen] = useState(false);
89
- const [modelMenuUp, setModelMenuUp] = useState(false);
90
- const modelFieldRef = useRef(null);
91
- // Click-outside and Esc close the model menu (the member composer's picker
92
- // pattern); the upward flip is measured when the menu opens.
93
- useEffect(() => {
94
- if (!modelMenuOpen)
95
- return;
96
- const onPointerDown = (event) => {
97
- if (modelFieldRef.current?.contains(event.target) === true)
98
- return;
99
- setModelMenuOpen(false);
100
- };
101
- const onKeyDown = (event) => {
102
- if (event.key === 'Escape')
103
- setModelMenuOpen(false);
104
- };
105
- document.addEventListener('mousedown', onPointerDown);
106
- document.addEventListener('keydown', onKeyDown);
107
- return () => {
108
- document.removeEventListener('mousedown', onPointerDown);
109
- document.removeEventListener('keydown', onKeyDown);
110
- };
111
- }, [modelMenuOpen]);
112
- const toggleModelMenu = () => {
113
- if (!modelMenuOpen) {
114
- const rect = modelFieldRef.current?.getBoundingClientRect();
115
- if (rect !== undefined) {
116
- const spaceBelow = window.innerHeight - rect.bottom;
117
- setModelMenuUp(spaceBelow < 260 && rect.top > spaceBelow);
118
- }
119
- }
120
- setModelMenuOpen(open => !open);
121
- };
122
- // An unset field DISPLAYS the default it follows (dimmed, as the input's
123
- // placeholder — inherited, never pinned), down the chain the broker
124
- // reports: the scoped config's model, then the account catalog's built-in
125
- // default (source cli-builtin WITH an effective model — the one case
126
- // cli-builtin names one), then the last observed model the records know,
127
- // and last the generic copy.
128
- const inheritedModel = modelInfo != null && modelInfo.cliDefault !== undefined && modelInfo.cliDefault !== ''
129
- ? modelInfo.cliDefault
130
- : modelInfo != null && modelInfo.source === 'cli-builtin' && modelInfo.effective !== undefined && modelInfo.effective !== ''
131
- ? modelInfo.effective
132
- : modelInfo != null && modelInfo.lastObserved !== undefined && modelInfo.lastObserved !== ''
133
- ? modelInfo.lastObserved
134
- : undefined;
135
- const modelPlaceholder = inheritedModel ?? t('model.placeholder');
136
- // The menu's leading item: the same chain, spelled as the follow-default
137
- // choice. It reads checked while the field is unset, and picking it clears
138
- // the draft back to follow-default (a save then unsets the key).
139
- const modelDefaultItem = modelInfo != null && modelInfo.cliDefault !== undefined && modelInfo.cliDefault !== ''
140
- ? t('model.menuDefault.cli-config', { model: modelInfo.cliDefault })
141
- : modelInfo != null && modelInfo.source === 'cli-builtin' && modelInfo.effective !== undefined && modelInfo.effective !== ''
142
- ? t('model.menuDefault.cli-builtin', { model: modelInfo.effective })
143
- : modelInfo != null && modelInfo.lastObserved !== undefined && modelInfo.lastObserved !== ''
144
- ? t('model.menuDefault.last-observed', { model: modelInfo.lastObserved })
145
- : t('model.menuDefault');
146
87
  return {
147
- snapshot, saved, error, ready, live, storedModel, modelValue, modelSaved,
148
- modelError, choices, modelMenuOpen, modelMenuUp, modelFieldRef,
149
- modelPlaceholder, modelDefaultItem, saveModel, write, toggleModelMenu,
150
- setModelDraft, setModelMenuOpen,
88
+ snapshot, saved, error, ready, live, write,
89
+ endpointState, endpointUnavailable, endpointLoading,
90
+ baseUrlValue: baseUrlDraft ?? (endpointState !== null && endpointState !== 'unavailable' ? endpointState.baseUrl ?? '' : ''),
91
+ apiKeyConfigured, apiKeyDraft,
92
+ endpointSaved, endpointError, endpointDirty,
93
+ saveEndpoint, setBaseUrlDraft, setApiKeyDraft,
151
94
  };
152
95
  }
153
- /** The card body both settings surfaces share: the auth block, the default-model block, and the resident-mode block. */
154
- function CardBody({ state, auth, authT, renderModelPicker, useSessions, t }) {
155
- const { snapshot, saved, error, ready, live, storedModel, modelValue, modelSaved, modelError, choices, modelMenuOpen, modelMenuUp, modelFieldRef, modelPlaceholder, modelDefaultItem, saveModel, write, toggleModelMenu, setModelDraft, setModelMenuOpen, } = state;
156
- // The free-text input, single-sourced for both presentations: bare when no
157
- // vocabulary exists (a fresh install degrades to exactly the pre-picker
158
- // field), or inside the select-like field next to its chevron. No datalist:
159
- // the chevron menu is the single choice list.
160
- const modelInputElement = (_jsx("input", { type: "text", className: css.modelInput, value: modelValue, placeholder: modelPlaceholder, "aria-label": t('model.title'), disabled: !ready, onChange: (event) => { setModelDraft(event.target.value); } }));
161
- return (_jsxs("div", { className: css.body, children: [_jsxs("section", { className: css.block, children: [_jsx("h3", { className: css.blockTitle, children: t('auth.title') }), _jsx(ProviderAuthBlock, { harness: { id: 'codex', label: 'Codex' }, useSessions: useSessions, status: auth.status, runCommand: auth.runCommand, t: authT })] }), _jsxs("section", { className: css.block, children: [_jsxs("h3", { className: css.blockTitle, children: [t('model.title'), _jsx(Tooltip, { label: t('model.info'), side: "bottom", maxWidth: 360, children: _jsx("button", { type: "button", className: css.info, "aria-label": t('model.info.aria'), children: "\u24D8" }) })] }), _jsxs("div", { className: css.row, children: [renderModelPicker?.({ value: modelValue, onChange: setModelDraft, disabled: !ready, defaultLabel: modelDefaultItem }) ?? (choices.length > 0 ? (_jsxs("div", { className: css.modelField, ref: modelFieldRef, children: [modelInputElement, _jsx("button", { type: "button", className: css.modelMenuButton, "aria-label": t('model.menu'), "aria-haspopup": "menu", "aria-expanded": modelMenuOpen, disabled: !ready, onClick: () => { toggleModelMenu(); }, children: _jsx(IconChevronDownOutlineMedium, { className: modelMenuOpen ? `${css.modelMenuChevron} ${css.modelMenuChevronOpen}` : css.modelMenuChevron }) }), modelMenuOpen && (_jsxs("div", { className: modelMenuUp ? `${css.modelMenu} ${css.modelMenuUp}` : css.modelMenu, role: "menu", "aria-label": t('model.menu'), children: [_jsxs("button", { type: "button", role: "menuitemradio", "aria-checked": modelValue.trim() === '', className: modelValue.trim() === '' ? `${css.modelItemDefault} ${css.modelItemCurrent}` : css.modelItemDefault, onClick: () => { setModelDraft(''); setModelMenuOpen(false); }, children: [_jsx("span", { className: css.modelItemLabel, children: modelDefaultItem }), _jsx("span", { className: css.modelItemCheck, "aria-hidden": true, children: modelValue.trim() === '' ? '✓' : '' })] }), choices.map(choice => (_jsxs("button", { type: "button", role: "menuitemradio", "aria-checked": choice === modelValue, className: choice === modelValue ? css.modelItemCurrent : css.modelItem, onClick: () => { setModelDraft(choice); setModelMenuOpen(false); }, children: [_jsx("span", { className: css.modelItemLabel, children: choice }), _jsx("span", { className: css.modelItemCheck, "aria-hidden": true, children: choice === modelValue ? '✓' : '' })] }, choice)))] }))] })) : modelInputElement), _jsx("button", { type: "button", className: css.modelSave, disabled: !ready || modelValue.trim() === storedModel, onClick: () => { saveModel(); }, children: t('model.save') })] }), modelSaved && _jsx("span", { className: css.saved, children: t('model.applied') }), modelError && _jsx("span", { className: css.errorText, children: t('model.error') })] }), _jsxs("section", { className: css.block, children: [_jsxs("div", { className: css.row, children: [_jsxs("span", { className: css.rowLabel, children: [t('live.title'), _jsx(Tooltip, { label: t('live.info'), side: "bottom", maxWidth: 360, children: _jsx("button", { type: "button", className: css.info, "aria-label": t('live.info.aria'), children: "\u24D8" }) })] }), _jsx("button", { type: "button", role: "switch", "aria-checked": live, "aria-label": t('live.title'), className: live ? `${css.switch} ${css.switchOn}` : css.switch, disabled: !ready, onClick: () => { write('live', !live); }, children: _jsx("span", { className: css.knob }) })] }), saved && _jsx("span", { className: css.saved, children: t('live.applied') }), error && _jsx("span", { className: css.errorText, children: t('live.error') }), snapshot.status === 'unavailable' && (_jsx("span", { className: css.hint, children: t('live.unavailable') }))] })] }));
96
+ /** The card body both settings surfaces share: the auth block, the model-list section, and the resident-mode block. */
97
+ function CardBody({ state, active, auth, authT, modelList, modelDirectory, checkEndpoint, useSessions, t }) {
98
+ const { snapshot, saved, error, ready, live, write, endpointUnavailable, endpointLoading, baseUrlValue, apiKeyConfigured, apiKeyDraft, endpointSaved, endpointError, endpointDirty, saveEndpoint, setBaseUrlDraft, setApiKeyDraft, } = state;
99
+ // The verify button: runs the gateway's `checkEndpoint` Remote against the
100
+ // SAVED endpoint (unsaved drafts deliberately disable it — what it verifies
101
+ // is the saved configuration) and shows the verdict inline. Session-less:
102
+ // a settings card may have no main agent session to run a slash command.
103
+ const [checking, setChecking] = useState(false);
104
+ const [checkText, setCheckText] = useState(null);
105
+ const runCheck = () => {
106
+ if (checkEndpoint === undefined)
107
+ return;
108
+ setChecking(true);
109
+ setCheckText(null);
110
+ void checkEndpoint().then((text) => {
111
+ setCheckText(text ?? t('endpoint.checkUnavailable'));
112
+ setChecking(false);
113
+ }, () => {
114
+ setCheckText(t('endpoint.checkFailed'));
115
+ setChecking(false);
116
+ });
117
+ };
118
+ return (_jsxs("div", { className: css.body, children: [_jsxs("section", { className: css.block, children: [_jsxs("h3", { className: css.blockTitle, children: [t('auth.title'), _jsx(Tooltip, { label: t('endpoint.info'), side: "bottom", maxWidth: 360, children: _jsx("button", { type: "button", className: css.info, "aria-label": t('endpoint.info.aria'), children: "\u24D8" }) })] }), _jsx(ProviderAuthBlock, { harness: { id: 'codex', label: 'Codex' }, useSessions: useSessions, status: auth.status, runCommand: auth.runCommand, t: authT }), endpointUnavailable && (_jsx("span", { className: css.hint, children: t('endpoint.unavailable') })), !endpointUnavailable && (_jsxs("div", { className: css.endpointFields, children: [_jsxs("label", { className: css.endpointField, children: [_jsx("span", { className: css.endpointLabel, children: t('endpoint.baseUrl') }), _jsx("input", { type: "text", className: css.modelInput, value: baseUrlValue, placeholder: t('endpoint.baseUrl.placeholder'), "aria-label": t('endpoint.baseUrl'), disabled: endpointLoading, onChange: (event) => { setBaseUrlDraft(event.currentTarget.value); } })] }), _jsxs("label", { className: css.endpointField, children: [_jsxs("span", { className: css.endpointLabel, children: [t('endpoint.apiKey'), apiKeyConfigured && _jsx("span", { className: css.configuredChip, children: t('endpoint.configured') })] }), _jsx("input", { type: "password", className: css.modelInput, value: apiKeyDraft ?? '', placeholder: apiKeyConfigured ? t('endpoint.secret.configured') : t('endpoint.secret.placeholder'), "aria-label": t('endpoint.apiKey'), autoComplete: "off", disabled: endpointLoading, onChange: (event) => { setApiKeyDraft(event.currentTarget.value); } })] }), _jsxs("div", { className: css.row, children: [_jsx("button", { type: "button", className: css.modelSave, disabled: endpointLoading || !endpointDirty, onClick: () => { saveEndpoint(); }, children: t('endpoint.save') }), checkEndpoint !== undefined && (_jsx("button", { type: "button", className: css.modelSave, disabled: endpointLoading || endpointDirty || checking, title: endpointDirty ? t('endpoint.checkDirtyHint') : undefined, onClick: () => { runCheck(); }, children: checking ? t('endpoint.checking') : t('endpoint.check') })), endpointSaved && _jsx("span", { className: css.saved, children: t('endpoint.applied') }), endpointError && _jsx("span", { className: css.errorText, children: t('endpoint.error') })] }), checkText !== null && _jsx("span", { className: css.hint, children: checkText })] }))] }), _jsx(ModelListSection, { active: active, modelList: modelList, modelDirectory: modelDirectory, directoryRefresh: endpointSaved, t: authT }), _jsxs("section", { className: css.block, children: [_jsxs("div", { className: css.row, children: [_jsxs("span", { className: css.rowLabel, children: [t('live.title'), _jsx(Tooltip, { label: t('live.info'), side: "bottom", maxWidth: 360, children: _jsx("button", { type: "button", className: css.info, "aria-label": t('live.info.aria'), children: "\u24D8" }) })] }), _jsx("button", { type: "button", role: "switch", "aria-checked": live, "aria-label": t('live.title'), className: live ? `${css.switch} ${css.switchOn}` : css.switch, disabled: !ready, onClick: () => { write('live', !live); }, children: _jsx("span", { className: css.knob }) })] }), saved && _jsx("span", { className: css.saved, children: t('live.applied') }), error && _jsx("span", { className: css.errorText, children: t('live.error') }), snapshot.status === 'unavailable' && (_jsx("span", { className: css.hint, children: t('live.unavailable') }))] })] }));
162
119
  }
163
120
  /**
164
- * The codex harness settings card in the 0.1.5 plugin configuration tab: the
165
- * same collapsible chrome the official plugin cards use, re-implemented
121
+ * The codex harness settings card of the retired 0.1.5 plugin configuration
122
+ * tab (no longer registered; the live surface is the plugins.item entry
123
+ * below): the same collapsible chrome the official plugin cards use, re-implemented
166
124
  * locally (the bundle-purity gate forbids value-importing the official chrome
167
125
  * — the context-guard pattern), with the at-a-glance credential dot in the
168
126
  * header. Explanation copy lives in hover/focus bubbles behind ⓘ anchors —
@@ -171,9 +129,9 @@ function CardBody({ state, auth, authT, renderModelPicker, useSessions, t }) {
171
129
  * @returns the card.
172
130
  */
173
131
  export function CodexSettingsCard(props) {
174
- const { useSettings, scope, auth, authT, renderModelPicker, harnessModel, useSessions, t } = props;
132
+ const { useSettings, scope, auth, authT, modelList, modelDirectory, checkEndpoint, endpointAuth, useSessions, t } = props;
175
133
  const [open, setOpen] = useState(false);
176
- const state = useCardState(open, useSettings, scope, harnessModel, t);
134
+ const state = useCardState(open, useSettings, scope, endpointAuth);
177
135
  // The at-a-glance credential dot in the collapsed header: every mount
178
136
  // re-probes (a credential expiring mid-session must flip the dot at the
179
137
  // next view); the bus dedupes identical results, so no re-render storm.
@@ -182,19 +140,31 @@ export function CodexSettingsCard(props) {
182
140
  : authStatus === 'anonymous' ? 'settings.notAuthenticated'
183
141
  : authStatus === 'checking' ? 'loading' : 'error');
184
142
  const title = t('card.title');
185
- return (_jsxs("li", { className: open ? `${css.card} ${css.cardOpen}` : css.card, children: [_jsxs("button", { type: "button", className: css.header, "aria-expanded": open, "aria-label": `${t(open ? 'card.collapse' : 'card.expand')}: ${title}`, onClick: () => { setOpen(!open); }, children: [_jsxs("span", { className: css.headText, children: [_jsxs("span", { className: css.nameRow, children: [_jsx("span", { className: css.name, children: title }), _jsx(AuthStatusDot, { status: authStatus, label: dotLabel })] }), _jsx("span", { className: css.description, children: t('card.description') })] }), _jsx(IconChevronDownOutlineMedium, { className: open ? `${css.chevron} ${css.chevronOpen}` : css.chevron })] }), open && _jsx(CardBody, { state: state, auth: auth, authT: authT, renderModelPicker: renderModelPicker, useSessions: useSessions, t: t })] }));
143
+ return (_jsxs("li", { className: open ? `${css.card} ${css.cardOpen}` : css.card, children: [_jsxs("button", { type: "button", className: css.header, "aria-expanded": open, "aria-label": `${t(open ? 'card.collapse' : 'card.expand')}: ${title}`, onClick: () => { setOpen(!open); }, children: [_jsxs("span", { className: css.headText, children: [_jsxs("span", { className: css.nameRow, children: [_jsx("span", { className: css.name, children: title }), _jsx(AuthStatusDot, { status: authStatus, label: dotLabel })] }), _jsx("span", { className: css.description, children: t('card.description') })] }), _jsx(IconChevronDownOutlineMedium, { className: open ? `${css.chevron} ${css.chevronOpen}` : css.chevron })] }), open && _jsx(CardBody, { state: state, active: open, auth: auth, authT: authT, modelList: modelList, modelDirectory: modelDirectory, checkEndpoint: checkEndpoint, useSessions: useSessions, t: t })] }));
186
144
  }
187
145
  /**
188
- * The bundle's configuration on the alpha.2 Plugins page
189
- * (`plugins.bundle.config`, keyed by package name): the page draws the title,
190
- * the icon, and the crumb itself, so the entry renders the one-liner for the
191
- * `summary` view and the bare form — with its own save control — for `page`.
146
+ * The provider's card on the Plugins page (the `plugins.item` entry
147
+ * client/index.ts registers while the Host serves the namespace AND the core
148
+ * predates the family card): the page draws the title, the icon, and the
149
+ * crumb itself, so the entry renders the one-liner for the `summary` view and
150
+ * the bare form — with its own save control — for `page`.
192
151
  * @param props - the owner view, the injected scope/auth faces, and copy.
193
152
  * @returns the entry.
194
153
  */
195
- export function CodexBundleConfig({ view, useSettings, scope, auth, authT, renderModelPicker, harnessModel, useSessions, t }) {
196
- const state = useCardState(view === 'page', useSettings, scope, harnessModel, t);
154
+ export function CodexBundleConfig({ view, useSettings, scope, auth, authT, modelList, modelDirectory, checkEndpoint, endpointAuth, useSessions, t }) {
155
+ const state = useCardState(view === 'page', useSettings, scope, endpointAuth);
197
156
  if (view === 'summary')
198
157
  return _jsx("span", { className: css.description, children: t('card.description') });
199
- return _jsx(CardBody, { state: state, auth: auth, authT: authT, renderModelPicker: renderModelPicker, useSessions: useSessions, t: t });
158
+ return _jsx(CardBody, { state: state, active: view === 'page', auth: auth, authT: authT, modelList: modelList, modelDirectory: modelDirectory, checkEndpoint: checkEndpoint, useSessions: useSessions, t: t });
159
+ }
160
+ /**
161
+ * The harness's section inside the core's「本机 Agent 委派」family card: the
162
+ * same useCardState + CardBody the plugins.item page view renders — the
163
+ * collapsible chrome belongs to the family card now.
164
+ * @param props - the injected scope/auth faces, the forwarded session seat, and copy.
165
+ * @returns the section body.
166
+ */
167
+ export function CodexCardSection({ active, useSettings, scope, auth, authT, modelList, modelDirectory, checkEndpoint, endpointAuth, useSessions, t }) {
168
+ const state = useCardState(active, useSettings, scope, endpointAuth);
169
+ return _jsx(CardBody, { state: state, active: active, auth: auth, authT: authT, modelList: modelList, modelDirectory: modelDirectory, checkEndpoint: checkEndpoint, useSessions: useSessions, t: t });
200
170
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Generated by scripts/sync-icon-artwork.mts from @deepseek-ai/dsh-client-ui-primitives
3
- * (v0.1.7-rc.1, harness 46a7f68b09) — do not edit by hand. The 0.1.5 and rc.1 host lines
3
+ * (v0.2.1-rc.1, harness 1600d8a1e1) — do not edit by hand. The 0.1.5 and rc.1 host lines
4
4
  * share no icon export name, so the rc.1 artwork is inlined here: pure SVG,
5
5
  * no host runtime identity, identical on every line. Re-run the generator
6
6
  * after adding an icon import or bumping the harness checkout.
@@ -1,28 +1,7 @@
1
- /**
2
- * Local-agent-codex plugin, browser half: the codex settings surface, one
3
- * face per install shape and host line — a standalone install renders the
4
- * bundle's own configuration on its Plugins-page detail view
5
- * (`plugins.bundle.config`, keyed by package name), a family-bundle install
6
- * renders the same card through the row-level configure entry on the
7
- * BUNDLE's detail view (`plugins.row.config`, keyed
8
- * `@khorsheed/dsh-bundle-local-agent#local-agent-codex`), and 0.1.5 renders
9
- * the `settings.plugin.item` card (keyed to the `local-agent-codex`
10
- * settings namespace the host half registers) in the official
11
- * Plugins → 可配置插件 tab. The card carries the family core's shared
12
- * ProviderAuthBlock (login/logout/device-code, backed by the core's
13
- * localAgentGateway Remote and the commands channel) plus the resident-mode
14
- * block (live switch) and the default-model block (free-text input, the
15
- * broker's effective-model line and suggestion vocabulary), whose writes ride
16
- * the bound settingsScope — the host watcher hot-applies them, no reload.
17
- * Every read degrades: an absent gateway renders the auth block's
18
- * 'unavailable' state and drops the model surface to the bare input, an
19
- * unregistered namespace disables the live controls.
20
- * @module @khorsheed/dsh-local-agent-codex/client
21
- */
22
1
  import type { Context } from '@deepseek-ai/cordis';
23
2
  import { en, NS, zh, type LocalAgentCodexKey } from './locales.ts';
24
- export { CodexBundleConfig, CodexSettingsCard } from './SettingsCard.tsx';
25
- export type { CodexLiveSettings, CodexBundleConfigProps, CodexSettingsCardInjected, CodexSettingsCardProps, } from './SettingsCard.tsx';
3
+ export { CodexBundleConfig, CodexCardSection, CodexSettingsCard } from './SettingsCard.tsx';
4
+ export type { CodexLiveSettings, CodexBundleConfigProps, CodexCardSectionProps, CodexSettingsCardInjected, CodexSettingsCardProps, } from './SettingsCard.tsx';
26
5
  export { en, NS, zh };
27
6
  export type { LocalAgentCodexKey };
28
7
  /**
@@ -31,12 +10,13 @@ export type { LocalAgentCodexKey };
31
10
  * line (`settingsScope` on 0.1.5, `configForms` on rc.1), so a static entry
32
11
  * would pend the whole client plugin on the other line; bindSettingsScope
33
12
  * probes the serving one instead (the settings client package is a declared
34
- * client inject, so one of them is always already up).
13
+ * client inject, so one of them is always already up). The card track probes
14
+ * `configForms` through the same kind of deferred inject (see apply).
35
15
  */
36
16
  export declare const inject: string[];
37
17
  /**
38
- * Client plugin body: register the dictionaries and the codex settings
39
- * surfaces bound to the local-agent-codex namespace.
18
+ * Client plugin body: register the dictionaries and the codex settings card
19
+ * bound to the local-agent-codex namespace.
40
20
  * @param ctx - client root context.
41
21
  */
42
22
  export declare function apply(ctx: Context): void;