@khorsheed/dsh-local-agent-claude-code 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,8 +1,6 @@
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
- import type { LocalAgentModelInfo } from '@khorsheed/dsh-local-agent/types';
3
+ import type { LocalAgentModelDirectory, LocalAgentEndpointAuthFields, LocalAgentEndpointAuthState } from '@khorsheed/dsh-local-agent/types';
6
4
  import { type ProviderAuthBlockProps, type ProviderAuthInjected } from '@khorsheed/dsh-local-agent/src/client/ProviderAuthBlock.tsx';
7
5
  import { NS } from './locales.ts';
8
6
  /** The `local-agent-claude-code` settings namespace section the card edits. */
@@ -22,7 +20,6 @@ export interface ClaudeLiveSettings {
22
20
  }
23
21
  /** Injected face of the claude-code settings card. */
24
22
  export interface ClaudeCodeSettingsCardInjected {
25
- renderModelPicker?: ((props: HarnessModelPickerInput) => ReactNode) | undefined;
26
23
  /** Bound settings scope for the local-agent-claude-code namespace. */
27
24
  scope: SettingsScope<ClaudeLiveSettings>;
28
25
  /** The auth block's query/command faces, backed by the family core Remote. */
@@ -30,34 +27,78 @@ export interface ClaudeCodeSettingsCardInjected {
30
27
  /** Translate bound to the family core's `local-agent` namespace (the auth block's copy lives there). */
31
28
  authT: ProviderAuthBlockProps['t'];
32
29
  /**
33
- * The harness's memberless model surface (the family gateway's
34
- * `harnessModel` Remote): the inherited default the unset field displays
35
- * and the menu's choice vocabulary. Absent on a core that predates the
36
- * model broker — the card then keeps the bare free-text input exactly as
37
- * before brokers existed.
30
+ * The harness's curated model list (the family gateway's
31
+ * `modelList`/`configureModelList` Remotes): the editor's ordered
32
+ * shortlist — its first entry is the default model. Absent on a core that
33
+ * predates the Remotes — the card then hides the editor.
38
34
  */
39
- modelInfo?: () => Promise<LocalAgentModelInfo | null | undefined>;
35
+ modelList?: {
36
+ read(): Promise<readonly string[] | null | undefined>;
37
+ write(models: readonly string[]): Promise<readonly string[] | null | undefined>;
38
+ };
39
+ /**
40
+ * The endpoint's served model catalog (the family gateway's
41
+ * `modelDirectory` Remote): the add dialog's candidate vocabulary. Absent
42
+ * on a core that predates the Remote — the dialog then shows its reason.
43
+ */
44
+ modelDirectory?: (refresh?: boolean) => Promise<LocalAgentModelDirectory | null | undefined>;
45
+ /**
46
+ * The harness's endpoint verification (the family gateway's `checkEndpoint`
47
+ * Remote): a token-free probe of the SAVED endpoint, answered as verdict
48
+ * text. Absent on a core that predates the Remote — the card then hides
49
+ * the verify button.
50
+ */
51
+ checkEndpoint?: () => Promise<string | null | undefined>;
52
+ /**
53
+ * The harness's endpoint credential face (the family gateway's
54
+ * `endpointAuth`/`configureEndpoint` Remotes): the card's credential form
55
+ * reads and writes the harness's OWN credential store through it — never
56
+ * the settings scope, so no key ever lands in the profile patch document.
57
+ * Absent on a core that predates the Remotes — the card then hides the
58
+ * form. `write` answers the post-write resolved state.
59
+ */
60
+ endpointAuth?: {
61
+ read(): Promise<LocalAgentEndpointAuthState | null | undefined>;
62
+ write(fields: LocalAgentEndpointAuthFields): Promise<LocalAgentEndpointAuthState | null | undefined>;
63
+ };
40
64
  hooks: {
41
65
  /** The same scope as a hooks source, so the card reacts to external writes. */
42
66
  settings: SettingsScope<ClaudeLiveSettings>;
43
67
  };
44
68
  }
45
69
  /**
46
- * Full props of the 0.1.5 settings.plugin.item card entry. The removed slot's
47
- * owner share was intentionally empty and its root scope delivered the global
48
- * seats; the structural type below is exact about the one seat the auth block
49
- * consumes (the slot name is gone from the alpha.2 SlotMap, so the
50
- * registration goes through a duck-typed narrow — see client/index.ts).
70
+ * Full props of the retired 0.1.5 settings.plugin.item card. The host removed
71
+ * that seat and client/index.ts no longer registers the component — the live
72
+ * settings surface is the plugins.item entry built on the BundleConfig
73
+ * component below; this export remains for the component spec and direct
74
+ * composers. The removed slot's owner share was intentionally empty and its
75
+ * root scope delivered the global seats; the structural type below is exact
76
+ * about the one seat the auth block consumes.
51
77
  */
52
78
  export type ClaudeCodeSettingsCardProps = InjectFace<ClaudeCodeSettingsCardInjected> & PropsLocale<typeof NS> & {
53
79
  /** The standing root-scope global seat (ui-session's merge), delivered to the 0.1.5 card by the renderer. */
54
80
  useSessions: ProviderAuthBlockProps['useSessions'];
55
81
  };
56
- /** Full props of the alpha.2 plugins.bundle.config entry (owner prop `view`). */
57
- export type ClaudeCodeBundleConfigProps = PropsRuntime<'plugins.bundle.config'> & InjectFace<ClaudeCodeSettingsCardInjected> & PropsLocale<typeof NS>;
82
+ /** Full props of the plugins.item entry (owner prop `view`). */
83
+ export type ClaudeCodeBundleConfigProps = PropsRuntime<'plugins.item'> & InjectFace<ClaudeCodeSettingsCardInjected> & PropsLocale<typeof NS>;
84
+ /**
85
+ * Full props of the harness's section inside the core's「本机 Agent 委派」
86
+ * family card: the same inject face minus the slot-owner share, plus the one
87
+ * global seat the body consumes, forwarded by the family card (the section
88
+ * render bypasses the slot runtime, so its remaining standard seats are not
89
+ * fabricable here and are deliberately absent).
90
+ */
91
+ export type ClaudeCodeCardSectionProps = InjectFace<ClaudeCodeSettingsCardInjected> & PropsLocale<typeof NS> & {
92
+ /** Whether the section is currently expanded (the family card mounts the body only while it is). */
93
+ readonly active: boolean;
94
+ /** The standing root-scope global seat (ui-session's merge), forwarded by the family card. */
95
+ readonly useSessions: ProviderAuthBlockProps['useSessions'];
96
+ };
58
97
  /**
59
- * The claude-code harness settings card in the 0.1.5 plugin configuration
60
- * tab: the same collapsible chrome the official plugin cards use,
98
+ * The claude-code harness settings card of the retired 0.1.5 plugin
99
+ * configuration tab (no longer registered; the live surface is the
100
+ * plugins.item entry below): the same collapsible chrome the official plugin
101
+ * cards use,
61
102
  * re-implemented locally (the bundle-purity gate forbids value-importing the
62
103
  * official chrome — the context-guard pattern), with the at-a-glance
63
104
  * credential dot in the header. Explanation copy lives in hover/focus bubbles
@@ -67,12 +108,22 @@ export type ClaudeCodeBundleConfigProps = PropsRuntime<'plugins.bundle.config'>
67
108
  */
68
109
  export declare function ClaudeCodeSettingsCard(props: ClaudeCodeSettingsCardProps): import("react").JSX.Element;
69
110
  /**
70
- * The bundle's configuration on the alpha.2 Plugins page
71
- * (`plugins.bundle.config`, keyed by package name): the page draws the title,
72
- * the icon, and the crumb itself, so the entry renders the one-liner for the
73
- * `summary` view and the bare form — with its own save control — for `page`.
111
+ * The provider's card on the Plugins page (the `plugins.item` entry
112
+ * client/index.ts registers while the Host serves the namespace AND the core
113
+ * predates the family card): the page
114
+ * draws the title, the icon, and the crumb itself, so the entry renders the
115
+ * one-liner for the `summary` view and the bare form — with its own save
116
+ * control — for `page`.
74
117
  * @param props - the owner view, the injected scope/auth faces, and copy.
75
118
  * @returns the entry.
76
119
  */
77
- export declare function ClaudeCodeBundleConfig({ view, useSettings, scope, auth, authT, renderModelPicker, modelInfo, useSessions, t }: ClaudeCodeBundleConfigProps): import("react").JSX.Element;
120
+ export declare function ClaudeCodeBundleConfig({ view, useSettings, scope, auth, authT, modelList, modelDirectory, checkEndpoint, endpointAuth, useSessions, t }: ClaudeCodeBundleConfigProps): import("react").JSX.Element;
121
+ /**
122
+ * The harness's section inside the core's「本机 Agent 委派」family card: the
123
+ * same useCardState + CardBody the plugins.item page view renders — the
124
+ * collapsible chrome belongs to the family card now.
125
+ * @param props - the injected scope/auth faces, the forwarded session seat, and copy.
126
+ * @returns the section body.
127
+ */
128
+ export declare function ClaudeCodeCardSection({ active, useSettings, scope, auth, authT, modelList, modelDirectory, checkEndpoint, endpointAuth, useSessions, t }: ClaudeCodeCardSectionProps): import("react").JSX.Element;
78
129
  //# 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,10 +7,9 @@ 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
14
  * The staged card state both settings surfaces share: the resident-mode and
16
15
  * model edits write through the bound settingsScope immediately
@@ -22,145 +21,111 @@ const RECENT_MODEL_LIMIT = 5;
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
23
  * @param modelInfo - the harness's memberless model surface read; absent degrades to the bare input.
24
+ * @param endpointAuth - the harness's endpoint credential face; absent hides the credential form.
25
25
  * @param t - copy.
26
26
  */
27
- function useCardState(active, useSettings, scope, modelInfo, t) {
27
+ function useCardState(active, useSettings, scope, endpointAuth) {
28
28
  const snapshot = useSettings(value => value);
29
29
  const [saved, setSaved] = useState(false);
30
30
  const [error, setError] = useState(false);
31
31
  const ready = snapshot.status === 'ready' && snapshot.writable;
32
32
  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 broker's model surface: fetched while the surface is active and after
43
- // every save (the save changes the settings layer the surface reports). `null`
44
- // means the gateway answered null (a brokerless core) or the read was
45
- // never wired — the card then degrades to the bare input, no effective
46
- // line, the recent-models suggestions.
47
- const [modelSurface, setModelSurface] = useState(null);
33
+ // The endpoint credential form talks to the harness's OWN credential store
34
+ // through the endpointAuth face — never the settings scope, so no key ever
35
+ // lands in the profile patch document. baseUrl echoes (it is not a
36
+ // secret); the two keys never do — a set key shows as a "configured"
37
+ // placeholder and the input holds only the draft of a REPLACEMENT. A blank
38
+ // write clears the field (the config layer then answers for it again).
39
+ const [endpointState, setEndpointState] = useState(null);
40
+ const [baseUrlDraft, setBaseUrlDraft] = useState(null);
41
+ const [apiKeyDraft, setApiKeyDraft] = useState(null);
42
+ const [authTokenDraft, setAuthTokenDraft] = useState(null);
43
+ const [endpointSaved, setEndpointSaved] = useState(false);
44
+ const [endpointError, setEndpointError] = useState(false);
45
+ const endpointDirty = baseUrlDraft !== null || apiKeyDraft !== null || authTokenDraft !== null;
46
+ const endpointUnavailable = endpointAuth === undefined || endpointState === 'unavailable';
47
+ const endpointLoading = !endpointUnavailable && endpointState === null;
48
+ const apiKeyConfigured = endpointState !== null && endpointState !== 'unavailable' && endpointState.apiKeySet;
49
+ const authTokenConfigured = endpointState !== null && endpointState !== 'unavailable' && endpointState.authTokenSet;
48
50
  useEffect(() => {
49
- if (!active || modelInfo === undefined)
51
+ if (!active || endpointAuth === undefined)
50
52
  return;
51
53
  let stale = false;
52
- void modelInfo().then((info) => { if (!stale)
53
- setModelSurface(info ?? null); }, () => { if (!stale)
54
- setModelSurface(null); });
54
+ void endpointAuth.read().then((state) => { if (!stale)
55
+ setEndpointState(state ?? 'unavailable'); }, () => { if (!stale)
56
+ setEndpointState('unavailable'); });
55
57
  return () => { stale = true; };
56
- }, [active, modelSaved, modelInfo]);
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));
72
- }
73
- setModelDraft(null);
74
- setModelSaved(true);
75
- }, () => { setModelError(true); });
76
- };
58
+ }, [active, endpointAuth, endpointSaved]);
77
59
  const write = (field, value) => {
78
60
  setSaved(false);
79
61
  setError(false);
80
62
  void scope.set(field, value).then(() => { setSaved(true); }, () => { setError(true); });
81
63
  };
82
- // The suggestion vocabulary: the broker's deduped union when the surface
83
- // answered, the card's own recent-models memory when it did not. Never a
84
- // hardcoded catalog. A non-empty list turns the field into a select-like
85
- // control: the always-visible chevron opens the same vocabulary as a menu.
86
- const choices = modelSurface !== null ? modelSurface.choices : recentModels;
87
- const [modelMenuOpen, setModelMenuOpen] = useState(false);
88
- const [modelMenuUp, setModelMenuUp] = useState(false);
89
- const modelFieldRef = useRef(null);
90
- // Click-outside and Esc close the model menu (the member composer's picker
91
- // pattern); the upward flip is measured when the menu opens.
92
- useEffect(() => {
93
- if (!modelMenuOpen)
64
+ // The credential save: one `configureEndpoint` call carrying only the
65
+ // dirty fields (a blank draft clears its field); the answer is the
66
+ // post-write resolved state, so the chips and the auth probe flip without
67
+ // a re-read.
68
+ const saveEndpoint = () => {
69
+ if (endpointAuth === undefined)
94
70
  return;
95
- const onPointerDown = (event) => {
96
- if (modelFieldRef.current?.contains(event.target) === true)
97
- return;
98
- setModelMenuOpen(false);
99
- };
100
- const onKeyDown = (event) => {
101
- if (event.key === 'Escape')
102
- setModelMenuOpen(false);
103
- };
104
- document.addEventListener('mousedown', onPointerDown);
105
- document.addEventListener('keydown', onKeyDown);
106
- return () => {
107
- document.removeEventListener('mousedown', onPointerDown);
108
- document.removeEventListener('keydown', onKeyDown);
71
+ setEndpointSaved(false);
72
+ setEndpointError(false);
73
+ const fields = {
74
+ ...baseUrlDraft !== null ? { baseUrl: baseUrlDraft.trim() === '' ? null : baseUrlDraft.trim() } : {},
75
+ ...apiKeyDraft !== null ? { apiKey: apiKeyDraft.trim() === '' ? null : apiKeyDraft.trim() } : {},
76
+ ...authTokenDraft !== null ? { authToken: authTokenDraft.trim() === '' ? null : authTokenDraft.trim() } : {},
109
77
  };
110
- }, [modelMenuOpen]);
111
- const toggleModelMenu = () => {
112
- if (!modelMenuOpen) {
113
- const rect = modelFieldRef.current?.getBoundingClientRect();
114
- if (rect !== undefined) {
115
- const spaceBelow = window.innerHeight - rect.bottom;
116
- setModelMenuUp(spaceBelow < 260 && rect.top > spaceBelow);
78
+ void endpointAuth.write(fields).then((state) => {
79
+ if (state !== undefined && state !== null) {
80
+ setEndpointState(state);
81
+ // Flip the status dot with the write's own answer — the next
82
+ // scheduled probe confirms it, but the save reads as done NOW.
83
+ publishAuthStatus('claude-code', state.apiKeySet || state.authTokenSet ? 'authenticated' : 'anonymous');
117
84
  }
118
- }
119
- setModelMenuOpen(open => !open);
85
+ setBaseUrlDraft(null);
86
+ setApiKeyDraft(null);
87
+ setAuthTokenDraft(null);
88
+ setEndpointSaved(true);
89
+ }, () => { setEndpointError(true); });
120
90
  };
121
- // An unset field DISPLAYS the default it follows (dimmed, as the input's
122
- // placeholder — inherited, never pinned), down the chain the broker
123
- // reports: the scoped config's model, then a cli-builtin source that still
124
- // names its default (a harness catalog's isDefault entry), then the last
125
- // observed model the records know, and last the generic copy.
126
- const inheritedModel = modelSurface !== null && modelSurface.cliDefault !== undefined && modelSurface.cliDefault !== ''
127
- ? modelSurface.cliDefault
128
- : modelSurface !== null && modelSurface.source === 'cli-builtin' && modelSurface.effective !== undefined && modelSurface.effective !== ''
129
- ? modelSurface.effective
130
- : modelSurface !== null && modelSurface.lastObserved !== undefined && modelSurface.lastObserved !== ''
131
- ? modelSurface.lastObserved
132
- : undefined;
133
- const modelPlaceholder = inheritedModel ?? t('model.placeholder');
134
- // The menu's leading item: the same chain, spelled as the follow-default
135
- // choice. It reads checked while the field is unset, and picking it clears
136
- // the draft back to follow-default (a save then unsets the key).
137
- const modelDefaultItem = modelSurface !== null && modelSurface.cliDefault !== undefined && modelSurface.cliDefault !== ''
138
- ? t('model.menuDefaultCliConfig', { model: modelSurface.cliDefault })
139
- : modelSurface !== null && modelSurface.source === 'cli-builtin' && modelSurface.effective !== undefined && modelSurface.effective !== ''
140
- ? t('model.menuDefaultCliBuiltin', { model: modelSurface.effective })
141
- : modelSurface !== null && modelSurface.lastObserved !== undefined && modelSurface.lastObserved !== ''
142
- ? t('model.menuDefaultLastObserved', { model: modelSurface.lastObserved })
143
- : t('model.menuDefault');
144
91
  return {
145
- snapshot, saved, error, ready, live, storedModel, modelValue, modelSaved,
146
- modelError, choices, modelMenuOpen, modelMenuUp, modelFieldRef,
147
- modelPlaceholder, modelDefaultItem, saveModel, write, toggleModelMenu,
148
- setModelDraft, setModelMenuOpen,
92
+ snapshot, saved, error, ready, live, write,
93
+ endpointState, endpointUnavailable, endpointLoading,
94
+ baseUrlValue: baseUrlDraft ?? (endpointState !== null && endpointState !== 'unavailable' ? endpointState.baseUrl ?? '' : ''),
95
+ apiKeyConfigured, authTokenConfigured, apiKeyDraft, authTokenDraft,
96
+ endpointSaved, endpointError, endpointDirty,
97
+ saveEndpoint, setBaseUrlDraft, setApiKeyDraft, setAuthTokenDraft,
149
98
  };
150
99
  }
151
- /** The card body both settings surfaces share: the auth block, the default-model block, and the resident-mode block. */
152
- function CardBody({ state, auth, authT, renderModelPicker, useSessions, t }) {
153
- const { snapshot, saved, error, ready, live, storedModel, modelValue, modelSaved, modelError, choices, modelMenuOpen, modelMenuUp, modelFieldRef, modelPlaceholder, modelDefaultItem, saveModel, write, toggleModelMenu, setModelDraft, setModelMenuOpen, } = state;
154
- // The free-text input, single-sourced for both presentations: bare when no
155
- // vocabulary exists (a fresh install degrades to exactly the pre-picker
156
- // field), or inside the select-like field next to its chevron. No datalist:
157
- // the chevron menu is the single choice list.
158
- 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); } }));
159
- 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: 'claude-code', label: 'Claude Code' }, 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') }))] })] }));
100
+ /** The card body both settings surfaces share: the auth block, the model-list section, and the resident-mode block. */
101
+ function CardBody({ state, active, auth, authT, modelList, modelDirectory, checkEndpoint, useSessions, t }) {
102
+ const { snapshot, saved, error, ready, live, write, endpointUnavailable, endpointLoading, baseUrlValue, apiKeyConfigured, authTokenConfigured, apiKeyDraft, authTokenDraft, endpointSaved, endpointError, endpointDirty, saveEndpoint, setBaseUrlDraft, setApiKeyDraft, setAuthTokenDraft, } = state;
103
+ // The verify button: runs the gateway's `checkEndpoint` Remote against the
104
+ // SAVED endpoint (unsaved drafts deliberately disable it — what it verifies
105
+ // is the saved configuration) and shows the verdict inline. Session-less:
106
+ // a settings card may have no main agent session to run a slash command.
107
+ const [checking, setChecking] = useState(false);
108
+ const [checkText, setCheckText] = useState(null);
109
+ const runCheck = () => {
110
+ if (checkEndpoint === undefined)
111
+ return;
112
+ setChecking(true);
113
+ setCheckText(null);
114
+ void checkEndpoint().then((text) => {
115
+ setCheckText(text ?? t('endpoint.checkUnavailable'));
116
+ setChecking(false);
117
+ }, () => {
118
+ setCheckText(t('endpoint.checkFailed'));
119
+ setChecking(false);
120
+ });
121
+ };
122
+ 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: 'claude-code', label: 'Claude Code' }, 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("label", { className: css.endpointField, children: [_jsxs("span", { className: css.endpointLabel, children: [t('endpoint.authToken'), authTokenConfigured && _jsx("span", { className: css.configuredChip, children: t('endpoint.configured') })] }), _jsx("input", { type: "password", className: css.modelInput, value: authTokenDraft ?? '', placeholder: authTokenConfigured ? t('endpoint.secret.configured') : t('endpoint.secret.placeholder'), "aria-label": t('endpoint.authToken'), autoComplete: "off", disabled: endpointLoading, onChange: (event) => { setAuthTokenDraft(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') }))] })] }));
160
123
  }
161
124
  /**
162
- * The claude-code harness settings card in the 0.1.5 plugin configuration
163
- * tab: the same collapsible chrome the official plugin cards use,
125
+ * The claude-code harness settings card of the retired 0.1.5 plugin
126
+ * configuration tab (no longer registered; the live surface is the
127
+ * plugins.item entry below): the same collapsible chrome the official plugin
128
+ * cards use,
164
129
  * re-implemented locally (the bundle-purity gate forbids value-importing the
165
130
  * official chrome — the context-guard pattern), with the at-a-glance
166
131
  * credential dot in the header. Explanation copy lives in hover/focus bubbles
@@ -169,9 +134,9 @@ function CardBody({ state, auth, authT, renderModelPicker, useSessions, t }) {
169
134
  * @returns the card.
170
135
  */
171
136
  export function ClaudeCodeSettingsCard(props) {
172
- const { useSettings, scope, auth, authT, renderModelPicker, modelInfo, useSessions, t } = props;
137
+ const { useSettings, scope, auth, authT, modelList, modelDirectory, checkEndpoint, endpointAuth, useSessions, t } = props;
173
138
  const [open, setOpen] = useState(false);
174
- const state = useCardState(open, useSettings, scope, modelInfo, t);
139
+ const state = useCardState(open, useSettings, scope, endpointAuth);
175
140
  // The at-a-glance credential dot in the collapsed header: every mount
176
141
  // re-probes (a credential expiring mid-session must flip the dot at the
177
142
  // next view); the bus dedupes identical results, so no re-render storm.
@@ -180,19 +145,32 @@ export function ClaudeCodeSettingsCard(props) {
180
145
  : authStatus === 'anonymous' ? 'settings.notAuthenticated'
181
146
  : authStatus === 'checking' ? 'loading' : 'error');
182
147
  const title = t('card.title');
183
- 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 })] }));
148
+ 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 })] }));
184
149
  }
185
150
  /**
186
- * The bundle's configuration on the alpha.2 Plugins page
187
- * (`plugins.bundle.config`, keyed by package name): the page draws the title,
188
- * the icon, and the crumb itself, so the entry renders the one-liner for the
189
- * `summary` view and the bare form — with its own save control — for `page`.
151
+ * The provider's card on the Plugins page (the `plugins.item` entry
152
+ * client/index.ts registers while the Host serves the namespace AND the core
153
+ * predates the family card): the page
154
+ * draws the title, the icon, and the crumb itself, so the entry renders the
155
+ * one-liner for the `summary` view and the bare form — with its own save
156
+ * control — for `page`.
190
157
  * @param props - the owner view, the injected scope/auth faces, and copy.
191
158
  * @returns the entry.
192
159
  */
193
- export function ClaudeCodeBundleConfig({ view, useSettings, scope, auth, authT, renderModelPicker, modelInfo, useSessions, t }) {
194
- const state = useCardState(view === 'page', useSettings, scope, modelInfo, t);
160
+ export function ClaudeCodeBundleConfig({ view, useSettings, scope, auth, authT, modelList, modelDirectory, checkEndpoint, endpointAuth, useSessions, t }) {
161
+ const state = useCardState(view === 'page', useSettings, scope, endpointAuth);
195
162
  if (view === 'summary')
196
163
  return _jsx("span", { className: css.description, children: t('card.description') });
197
- return _jsx(CardBody, { state: state, auth: auth, authT: authT, renderModelPicker: renderModelPicker, useSessions: useSessions, t: t });
164
+ return _jsx(CardBody, { state: state, active: view === 'page', auth: auth, authT: authT, modelList: modelList, modelDirectory: modelDirectory, checkEndpoint: checkEndpoint, useSessions: useSessions, t: t });
165
+ }
166
+ /**
167
+ * The harness's section inside the core's「本机 Agent 委派」family card: the
168
+ * same useCardState + CardBody the plugins.item page view renders — the
169
+ * collapsible chrome belongs to the family card now.
170
+ * @param props - the injected scope/auth faces, the forwarded session seat, and copy.
171
+ * @returns the section body.
172
+ */
173
+ export function ClaudeCodeCardSection({ active, useSettings, scope, auth, authT, modelList, modelDirectory, checkEndpoint, endpointAuth, useSessions, t }) {
174
+ const state = useCardState(active, useSettings, scope, endpointAuth);
175
+ return _jsx(CardBody, { state: state, active: active, auth: auth, authT: authT, modelList: modelList, modelDirectory: modelDirectory, checkEndpoint: checkEndpoint, useSessions: useSessions, t: t });
198
176
  }
@@ -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,35 +1,22 @@
1
- /**
2
- * Local-agent-claude-code plugin, browser half: the claude-code settings
3
- * surface, one face per install shape and host line — a standalone install
4
- * renders the 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-claude-code`, the row id the
9
- * bundle's patch declares), and 0.1.5 renders the `settings.plugin.item`
10
- * card (keyed to the `local-agent-claude-code` settings namespace the host
11
- * half registers) in the official Plugins → 可配置插件 tab. The card carries the family core's
12
- * shared ProviderAuthBlock (login/logout/manual-handoff, backed by the core's
13
- * localAgentGateway Remote and the commands channel), the default-model block
14
- * (free-text write plus the gateway's model surface: the effective-model line
15
- * and the suggestion vocabulary), and the resident-mode switch, whose writes
16
- * ride the bound settingsScope — the host watcher hot-applies them, no
17
- * reload. Every read degrades: an absent gateway renders the auth block's
18
- * 'unavailable' state and drops the model surface, an unregistered namespace
19
- * disables the live controls.
20
- * @module @khorsheed/dsh-local-agent-claude-code/client
21
- */
22
1
  import type { Context } from '@deepseek-ai/cordis';
23
2
  import { en, NS, zh, type LocalAgentClaudeCodeKey } from './locales.ts';
24
- export { ClaudeCodeBundleConfig, ClaudeCodeSettingsCard } from './SettingsCard.tsx';
25
- export type { ClaudeLiveSettings, ClaudeCodeBundleConfigProps, ClaudeCodeSettingsCardInjected, ClaudeCodeSettingsCardProps, } from './SettingsCard.tsx';
3
+ export { ClaudeCodeBundleConfig, ClaudeCodeCardSection, ClaudeCodeSettingsCard } from './SettingsCard.tsx';
4
+ export type { ClaudeLiveSettings, ClaudeCodeBundleConfigProps, ClaudeCodeCardSectionProps, ClaudeCodeSettingsCardInjected, ClaudeCodeSettingsCardProps, } from './SettingsCard.tsx';
26
5
  export { en, NS, zh };
27
6
  export type { LocalAgentClaudeCodeKey };
28
- /** Required services: slot registry, settings scope, command Remote, and locale registry. */
7
+ /**
8
+ * Required services: slot registry, command Remote, and locale registry. The
9
+ * settings scope is NOT a static inject — its service name differs per host
10
+ * line (`settingsScope` on 0.1.5, `configForms` on rc.1), so a static entry
11
+ * would pend the whole client plugin on the other line; bindSettingsScope
12
+ * probes the serving one instead (the settings client package is a declared
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).
15
+ */
29
16
  export declare const inject: string[];
30
17
  /**
31
18
  * Client plugin body: register the dictionaries and the claude-code settings
32
- * surfaces bound to the local-agent-claude-code namespace.
19
+ * card bound to the local-agent-claude-code namespace.
33
20
  * @param ctx - client root context.
34
21
  */
35
22
  export declare function apply(ctx: Context): void;