@khorsheed/dsh-local-agent-claude-code 0.1.0-rc.6
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/README.en.md +120 -0
- package/README.i18n.yaml +6 -0
- package/README.md +120 -0
- package/cordis.patch.yml +42 -0
- package/lib/client.js +1065 -0
- package/lib/index.js +3771 -0
- package/lib/invariant.js +31 -0
- package/lib/types/claude-cli-provider.d.ts +389 -0
- package/lib/types/claude-cli-provider.js +1509 -0
- package/lib/types/client/SettingsCard.d.ts +78 -0
- package/lib/types/client/SettingsCard.js +198 -0
- package/lib/types/client/icons.d.ts +18 -0
- package/lib/types/client/icons.js +2 -0
- package/lib/types/client/index.d.ts +36 -0
- package/lib/types/client/index.js +87 -0
- package/lib/types/client/locales.d.ts +64 -0
- package/lib/types/client/locales.js +55 -0
- package/lib/types/control-requests.d.ts +15 -0
- package/lib/types/control-requests.js +62 -0
- package/lib/types/index.d.ts +81 -0
- package/lib/types/index.js +300 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.js +33 -0
- package/lib/types/live-driver.d.ts +172 -0
- package/lib/types/live-driver.js +1057 -0
- package/lib/types/live-switch.d.ts +67 -0
- package/lib/types/live-switch.js +85 -0
- package/lib/types/model-broker.d.ts +131 -0
- package/lib/types/model-broker.js +248 -0
- package/lib/types/model-catalog.d.ts +27 -0
- package/lib/types/model-catalog.js +118 -0
- package/lib/types/provision.d.ts +63 -0
- package/lib/types/provision.js +132 -0
- package/lib/types/records.d.ts +120 -0
- package/lib/types/records.js +687 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +106 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { HarnessModelPickerInput } from '@khorsheed/dsh-local-agent/client';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
import type { SettingsScope } from '@khorsheed/dsh-local-agent/src/client/settings-scope.ts';
|
|
4
|
+
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
5
|
+
import type { LocalAgentModelInfo } from '@khorsheed/dsh-local-agent/types';
|
|
6
|
+
import { type ProviderAuthBlockProps, type ProviderAuthInjected } from '@khorsheed/dsh-local-agent/src/client/ProviderAuthBlock.tsx';
|
|
7
|
+
import { NS } from './locales.ts';
|
|
8
|
+
/** The `local-agent-claude-code` settings namespace section the card edits. */
|
|
9
|
+
export interface ClaudeLiveSettings {
|
|
10
|
+
/** Resident (live) mode: one resident stream-json process per member. */
|
|
11
|
+
live?: boolean;
|
|
12
|
+
/** Live mirror granularity: folded events or per-chunk streaming. */
|
|
13
|
+
liveMirrorGranularity?: 'event' | 'token';
|
|
14
|
+
/**
|
|
15
|
+
* The model every delegation round starts the CLI with. Absent (the field
|
|
16
|
+
* cleared) means the plugin passes no model at all — the scoped `settings.json`'s own `model` decides,
|
|
17
|
+
* exactly as before the key existed.
|
|
18
|
+
*/
|
|
19
|
+
model?: string;
|
|
20
|
+
/** Model identifiers saved before; the input's suggestions, capped at five. */
|
|
21
|
+
recentModels?: readonly string[];
|
|
22
|
+
}
|
|
23
|
+
/** Injected face of the claude-code settings card. */
|
|
24
|
+
export interface ClaudeCodeSettingsCardInjected {
|
|
25
|
+
renderModelPicker?: ((props: HarnessModelPickerInput) => ReactNode) | undefined;
|
|
26
|
+
/** Bound settings scope for the local-agent-claude-code namespace. */
|
|
27
|
+
scope: SettingsScope<ClaudeLiveSettings>;
|
|
28
|
+
/** The auth block's query/command faces, backed by the family core Remote. */
|
|
29
|
+
auth: ProviderAuthInjected;
|
|
30
|
+
/** Translate bound to the family core's `local-agent` namespace (the auth block's copy lives there). */
|
|
31
|
+
authT: ProviderAuthBlockProps['t'];
|
|
32
|
+
/**
|
|
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.
|
|
38
|
+
*/
|
|
39
|
+
modelInfo?: () => Promise<LocalAgentModelInfo | null | undefined>;
|
|
40
|
+
hooks: {
|
|
41
|
+
/** The same scope as a hooks source, so the card reacts to external writes. */
|
|
42
|
+
settings: SettingsScope<ClaudeLiveSettings>;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
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).
|
|
51
|
+
*/
|
|
52
|
+
export type ClaudeCodeSettingsCardProps = InjectFace<ClaudeCodeSettingsCardInjected> & PropsLocale<typeof NS> & {
|
|
53
|
+
/** The standing root-scope global seat (ui-session's merge), delivered to the 0.1.5 card by the renderer. */
|
|
54
|
+
useSessions: ProviderAuthBlockProps['useSessions'];
|
|
55
|
+
};
|
|
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>;
|
|
58
|
+
/**
|
|
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,
|
|
61
|
+
* re-implemented locally (the bundle-purity gate forbids value-importing the
|
|
62
|
+
* official chrome — the context-guard pattern), with the at-a-glance
|
|
63
|
+
* credential dot in the header. Explanation copy lives in hover/focus bubbles
|
|
64
|
+
* behind ⓘ anchors — the card's rows stay one line each.
|
|
65
|
+
* @param props - the injected scope/auth faces and copy.
|
|
66
|
+
* @returns the card.
|
|
67
|
+
*/
|
|
68
|
+
export declare function ClaudeCodeSettingsCard(props: ClaudeCodeSettingsCardProps): import("react").JSX.Element;
|
|
69
|
+
/**
|
|
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`.
|
|
74
|
+
* @param props - the owner view, the injected scope/auth faces, and copy.
|
|
75
|
+
* @returns the entry.
|
|
76
|
+
*/
|
|
77
|
+
export declare function ClaudeCodeBundleConfig({ view, useSettings, scope, auth, authT, renderModelPicker, modelInfo, useSessions, t }: ClaudeCodeBundleConfigProps): import("react").JSX.Element;
|
|
78
|
+
//# sourceMappingURL=SettingsCard.d.ts.map
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { Tooltip } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
4
|
+
import { IconChevronDownOutlineMedium } from './icons.js';
|
|
5
|
+
// The shared auth block is bundled from the family core's source — the
|
|
6
|
+
// sanctioned core/companion edge (the host half already depends on the core);
|
|
7
|
+
// the block carries no runtime identity to share.
|
|
8
|
+
import { ProviderAuthBlock, } from '@khorsheed/dsh-local-agent/src/client/ProviderAuthBlock.tsx';
|
|
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';
|
|
11
|
+
import css from './SettingsCard.module.css';
|
|
12
|
+
/** How many previously-saved model identifiers the input suggests. */
|
|
13
|
+
const RECENT_MODEL_LIMIT = 5;
|
|
14
|
+
/**
|
|
15
|
+
* The staged card state both settings surfaces share: the resident-mode and
|
|
16
|
+
* model edits write through the bound settingsScope immediately
|
|
17
|
+
* (revision-fenced), so a flip takes effect on the next delegation round
|
|
18
|
+
* without a reload; the broker'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).
|
|
22
|
+
* @param useSettings - the bound settings hook from the inject hooks compartment.
|
|
23
|
+
* @param scope - the bound settings scope the writes go through.
|
|
24
|
+
* @param modelInfo - the harness's memberless model surface read; absent degrades to the bare input.
|
|
25
|
+
* @param t - copy.
|
|
26
|
+
*/
|
|
27
|
+
function useCardState(active, useSettings, scope, modelInfo, t) {
|
|
28
|
+
const snapshot = useSettings(value => value);
|
|
29
|
+
const [saved, setSaved] = useState(false);
|
|
30
|
+
const [error, setError] = useState(false);
|
|
31
|
+
const ready = snapshot.status === 'ready' && snapshot.writable;
|
|
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);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!active || modelInfo === undefined)
|
|
50
|
+
return;
|
|
51
|
+
let stale = false;
|
|
52
|
+
void modelInfo().then((info) => { if (!stale)
|
|
53
|
+
setModelSurface(info ?? null); }, () => { if (!stale)
|
|
54
|
+
setModelSurface(null); });
|
|
55
|
+
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
|
+
};
|
|
77
|
+
const write = (field, value) => {
|
|
78
|
+
setSaved(false);
|
|
79
|
+
setError(false);
|
|
80
|
+
void scope.set(field, value).then(() => { setSaved(true); }, () => { setError(true); });
|
|
81
|
+
};
|
|
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)
|
|
94
|
+
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);
|
|
109
|
+
};
|
|
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);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
setModelMenuOpen(open => !open);
|
|
120
|
+
};
|
|
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
|
+
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,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
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') }))] })] }));
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
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,
|
|
164
|
+
* re-implemented locally (the bundle-purity gate forbids value-importing the
|
|
165
|
+
* official chrome — the context-guard pattern), with the at-a-glance
|
|
166
|
+
* credential dot in the header. Explanation copy lives in hover/focus bubbles
|
|
167
|
+
* behind ⓘ anchors — the card's rows stay one line each.
|
|
168
|
+
* @param props - the injected scope/auth faces and copy.
|
|
169
|
+
* @returns the card.
|
|
170
|
+
*/
|
|
171
|
+
export function ClaudeCodeSettingsCard(props) {
|
|
172
|
+
const { useSettings, scope, auth, authT, renderModelPicker, modelInfo, useSessions, t } = props;
|
|
173
|
+
const [open, setOpen] = useState(false);
|
|
174
|
+
const state = useCardState(open, useSettings, scope, modelInfo, t);
|
|
175
|
+
// The at-a-glance credential dot in the collapsed header: every mount
|
|
176
|
+
// re-probes (a credential expiring mid-session must flip the dot at the
|
|
177
|
+
// next view); the bus dedupes identical results, so no re-render storm.
|
|
178
|
+
const authStatus = useHarnessAuthStatus('claude-code', auth.status);
|
|
179
|
+
const dotLabel = authT(authStatus === 'authenticated' ? 'settings.authenticated'
|
|
180
|
+
: authStatus === 'anonymous' ? 'settings.notAuthenticated'
|
|
181
|
+
: authStatus === 'checking' ? 'loading' : 'error');
|
|
182
|
+
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 })] }));
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
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`.
|
|
190
|
+
* @param props - the owner view, the injected scope/auth faces, and copy.
|
|
191
|
+
* @returns the entry.
|
|
192
|
+
*/
|
|
193
|
+
export function ClaudeCodeBundleConfig({ view, useSettings, scope, auth, authT, renderModelPicker, modelInfo, useSessions, t }) {
|
|
194
|
+
const state = useCardState(view === 'page', useSettings, scope, modelInfo, t);
|
|
195
|
+
if (view === 'summary')
|
|
196
|
+
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 });
|
|
198
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
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
|
|
4
|
+
* share no icon export name, so the rc.1 artwork is inlined here: pure SVG,
|
|
5
|
+
* no host runtime identity, identical on every line. Re-run the generator
|
|
6
|
+
* after adding an icon import or bumping the harness checkout.
|
|
7
|
+
*
|
|
8
|
+
* @module @khorsheed/dsh-local-agent-claude-code/client
|
|
9
|
+
*/
|
|
10
|
+
/** Shared props for every product icon component (mirrors the upstream IconProps). */
|
|
11
|
+
export interface IconProps {
|
|
12
|
+
/** Square edge in px; defaults to the glyph's own drawn size. */
|
|
13
|
+
size?: number | undefined;
|
|
14
|
+
/** Extra class for layout placement; color rides currentColor. */
|
|
15
|
+
className?: string | undefined;
|
|
16
|
+
}
|
|
17
|
+
export declare const IconChevronDownOutlineMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
|
|
18
|
+
//# sourceMappingURL=icons.d.ts.map
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
export const IconChevronDownOutlineMedium = ({ size = 14, className }) => (_jsx("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: _jsx("path", { d: "M4 6L7.29289 9.29289C7.68342 9.68342 8.31658 9.68342 8.70711 9.29289L12 6", stroke: "currentColor" }) }));
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
23
|
+
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';
|
|
26
|
+
export { en, NS, zh };
|
|
27
|
+
export type { LocalAgentClaudeCodeKey };
|
|
28
|
+
/** Required services: slot registry, settings scope, command Remote, and locale registry. */
|
|
29
|
+
export declare const inject: string[];
|
|
30
|
+
/**
|
|
31
|
+
* Client plugin body: register the dictionaries and the claude-code settings
|
|
32
|
+
* surfaces bound to the local-agent-claude-code namespace.
|
|
33
|
+
* @param ctx - client root context.
|
|
34
|
+
*/
|
|
35
|
+
export declare function apply(ctx: Context): void;
|
|
36
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { bindSettingsScope } from '@khorsheed/dsh-local-agent/src/client/settings-scope.ts';
|
|
2
|
+
import { en, NS, zh } from './locales.js';
|
|
3
|
+
import { ClaudeCodeBundleConfig, ClaudeCodeSettingsCard, } from './SettingsCard.js';
|
|
4
|
+
export { ClaudeCodeBundleConfig, ClaudeCodeSettingsCard } from './SettingsCard.js';
|
|
5
|
+
export { en, NS, zh };
|
|
6
|
+
/**
|
|
7
|
+
* This bundle's package name — the key the Plugins page dispatches
|
|
8
|
+
* `plugins.bundle.config` entries on (identity triangle: cordis.patch.yml,
|
|
9
|
+
* tsdown.config.ts, invariant.ts).
|
|
10
|
+
*/
|
|
11
|
+
const PACKAGE_NAME = '@khorsheed/dsh-local-agent-claude-code';
|
|
12
|
+
/** Required services: slot registry, settings scope, command Remote, and locale registry. */
|
|
13
|
+
export const inject = ['slots', 'remote', 'remote.commands', 'locale'];
|
|
14
|
+
/**
|
|
15
|
+
* Client plugin body: register the dictionaries and the claude-code settings
|
|
16
|
+
* surfaces bound to the local-agent-claude-code namespace.
|
|
17
|
+
* @param ctx - client root context.
|
|
18
|
+
*/
|
|
19
|
+
export function apply(ctx) {
|
|
20
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'local-agent-claude-code: dictionaries');
|
|
21
|
+
const scope = bindSettingsScope(ctx, 'local-agent-claude-code');
|
|
22
|
+
// The auth block's copy lives in the family core's dictionary; binding is
|
|
23
|
+
// stable per namespace and late dictionary registration still resolves.
|
|
24
|
+
const authT = ctx.locale.bind('local-agent');
|
|
25
|
+
// The gateway namespace is mounted by the family core's client half; read it
|
|
26
|
+
// lazily from the global store so boot order between the two client plugins
|
|
27
|
+
// never matters, and degrade to the auth block's 'unavailable' state when
|
|
28
|
+
// the core client is absent.
|
|
29
|
+
const gateway = () => ctx.get('remote.localAgentGateway');
|
|
30
|
+
// Both settings surfaces inject the same face.
|
|
31
|
+
const cardInject = () => ({
|
|
32
|
+
scope,
|
|
33
|
+
hooks: { settings: scope },
|
|
34
|
+
authT,
|
|
35
|
+
renderModelPicker: props => ctx.get('localAgentUi')?.renderHarnessModelPicker('claude-code', props),
|
|
36
|
+
// The model surface read behind the default-model block's effective
|
|
37
|
+
// line and suggestions. 'claude-code' is the harness name the host
|
|
38
|
+
// half registers; a core predating the model broker (or an absent
|
|
39
|
+
// gateway) degrades to the bare free-text input.
|
|
40
|
+
modelInfo: () => {
|
|
41
|
+
const gw = gateway();
|
|
42
|
+
if (gw === undefined || typeof gw.harnessModel !== 'function')
|
|
43
|
+
return Promise.resolve(undefined);
|
|
44
|
+
return gw.harnessModel('claude-code').then(result => (result.ok ? result.value : undefined));
|
|
45
|
+
},
|
|
46
|
+
auth: {
|
|
47
|
+
status: name =>
|
|
48
|
+
// The Remote declares (name, scope?) and the client enforces exact arity:
|
|
49
|
+
// pass the default scope explicitly (undefined reads as omitted host-side).
|
|
50
|
+
gateway()?.status(name, undefined).then(result => (result.ok ? result.value : undefined))
|
|
51
|
+
?? Promise.resolve(undefined),
|
|
52
|
+
runCommand: (sessionId, line) => ctx.remote.commands.execute(sessionId, line, [])
|
|
53
|
+
.then(result => (result.ok ? result.value?.result.text : undefined)),
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
// The settings surface follows the host line AND the install shape — three
|
|
57
|
+
// tracks, all riding slots.inject so each fires only where its declaration
|
|
58
|
+
// exists. Standalone install: the package's own Plugins-page detail view
|
|
59
|
+
// (`plugins.bundle.config`, keyed by package name). Installed as a family
|
|
60
|
+
// member: the member is no longer the profile's direct dependency, so only
|
|
61
|
+
// the bundle gets a detail view — the card rides the row-level slot
|
|
62
|
+
// (`plugins.row.config`, keyed `<bundle package>#<row id>` with the row id
|
|
63
|
+
// the bundle's patch declares) and the bundle's page gains a configure
|
|
64
|
+
// entry opening it. 0.1.5: the legacy configurable-plugins tab card
|
|
65
|
+
// (`settings.plugin.item`, keyed by the settings namespace) through a
|
|
66
|
+
// string-keyed duck narrow of the same service (the slot name is gone from
|
|
67
|
+
// the alpha.2 SlotMap).
|
|
68
|
+
ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({
|
|
69
|
+
name: 'plugins.bundle.config',
|
|
70
|
+
key: PACKAGE_NAME,
|
|
71
|
+
locale: NS,
|
|
72
|
+
inject: cardInject,
|
|
73
|
+
}, ClaudeCodeBundleConfig));
|
|
74
|
+
ctx.slots.inject('plugins.row.config', () => ctx.slots.register({
|
|
75
|
+
name: 'plugins.row.config',
|
|
76
|
+
key: '@khorsheed/dsh-bundle-local-agent#local-agent-claude-code',
|
|
77
|
+
locale: NS,
|
|
78
|
+
inject: cardInject,
|
|
79
|
+
}, ClaudeCodeBundleConfig));
|
|
80
|
+
const legacy = ctx.slots;
|
|
81
|
+
legacy.inject('settings.plugin.item', () => legacy.register({
|
|
82
|
+
name: 'settings.plugin.item',
|
|
83
|
+
key: 'local-agent-claude-code',
|
|
84
|
+
locale: NS,
|
|
85
|
+
inject: cardInject,
|
|
86
|
+
}, ClaudeCodeSettingsCard));
|
|
87
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** `local-agent-claude-code` namespace dictionaries. */
|
|
2
|
+
/** Dictionary namespace owned by this plugin (matches the settings namespace). */
|
|
3
|
+
export declare const NS = "local-agent-claude-code";
|
|
4
|
+
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
5
|
+
export declare const zh: {
|
|
6
|
+
'card.title': string;
|
|
7
|
+
'card.description': string;
|
|
8
|
+
'card.expand': string;
|
|
9
|
+
'card.collapse': string;
|
|
10
|
+
'auth.title': string;
|
|
11
|
+
'model.title': string;
|
|
12
|
+
'model.info.aria': string;
|
|
13
|
+
'model.info': string;
|
|
14
|
+
'model.placeholder': string;
|
|
15
|
+
'model.menu': string;
|
|
16
|
+
'model.menuDefault': string;
|
|
17
|
+
'model.menuDefaultCliConfig': string;
|
|
18
|
+
'model.menuDefaultCliBuiltin': string;
|
|
19
|
+
'model.menuDefaultLastObserved': string;
|
|
20
|
+
'model.save': string;
|
|
21
|
+
'model.applied': string;
|
|
22
|
+
'model.error': string;
|
|
23
|
+
'live.title': string;
|
|
24
|
+
'live.info.aria': string;
|
|
25
|
+
'live.info': string;
|
|
26
|
+
'live.applied': string;
|
|
27
|
+
'live.error': string;
|
|
28
|
+
'live.unavailable': string;
|
|
29
|
+
};
|
|
30
|
+
/** The local-agent-claude-code namespace key union. */
|
|
31
|
+
export type LocalAgentClaudeCodeKey = keyof typeof zh;
|
|
32
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
33
|
+
interface LocaleNamespaceMap {
|
|
34
|
+
/** The claude-code harness settings card copy. */
|
|
35
|
+
'local-agent-claude-code': LocalAgentClaudeCodeKey;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** English dictionary, checked complete against the zh key set. */
|
|
39
|
+
export declare const en: {
|
|
40
|
+
'card.title': string;
|
|
41
|
+
'card.description': string;
|
|
42
|
+
'card.expand': string;
|
|
43
|
+
'card.collapse': string;
|
|
44
|
+
'auth.title': string;
|
|
45
|
+
'model.title': string;
|
|
46
|
+
'model.info.aria': string;
|
|
47
|
+
'model.info': string;
|
|
48
|
+
'model.placeholder': string;
|
|
49
|
+
'model.menu': string;
|
|
50
|
+
'model.menuDefault': string;
|
|
51
|
+
'model.menuDefaultCliConfig': string;
|
|
52
|
+
'model.menuDefaultCliBuiltin': string;
|
|
53
|
+
'model.menuDefaultLastObserved': string;
|
|
54
|
+
'model.save': string;
|
|
55
|
+
'model.applied': string;
|
|
56
|
+
'model.error': string;
|
|
57
|
+
'live.title': string;
|
|
58
|
+
'live.info.aria': string;
|
|
59
|
+
'live.info': string;
|
|
60
|
+
'live.applied': string;
|
|
61
|
+
'live.error': string;
|
|
62
|
+
'live.unavailable': string;
|
|
63
|
+
};
|
|
64
|
+
//# sourceMappingURL=locales.d.ts.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** `local-agent-claude-code` namespace dictionaries. */
|
|
2
|
+
/** Dictionary namespace owned by this plugin (matches the settings namespace). */
|
|
3
|
+
export const NS = 'local-agent-claude-code';
|
|
4
|
+
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
5
|
+
export const zh = {
|
|
6
|
+
'card.title': 'Local Agent · Claude Code',
|
|
7
|
+
'card.description': 'Claude Code 委派:登录授权与常驻模式(live)',
|
|
8
|
+
'card.expand': '展开',
|
|
9
|
+
'card.collapse': '收起',
|
|
10
|
+
'auth.title': '认证',
|
|
11
|
+
'model.title': '默认模型',
|
|
12
|
+
'model.info.aria': '默认模型说明',
|
|
13
|
+
'model.info': '留空 = 跟随默认(输入框里显示的就是当前跟随的模型)。从列表选择或手动输入一个模型名后保存,之后的每轮委派都以它起 CLI(`claude -p --model <模型>`,常驻模式同一个旗标)。成员会话里还可用作曲器按会话切换,不影响这里的默认。改动在下一轮委派生效,进行中的轮次不受影响;评测 run 的条件在建立时冻结,中途换模型会被下一轮的模型回读判为 misattributed 而拦下。',
|
|
14
|
+
'model.placeholder': '留空 = 不传模型,用 CLI 自己的默认',
|
|
15
|
+
'model.menu': '选择模型',
|
|
16
|
+
'model.menuDefault': '默认(跟随 CLI 内置默认)',
|
|
17
|
+
'model.menuDefaultCliConfig': '默认(跟随 CLI 配置:{model})',
|
|
18
|
+
'model.menuDefaultCliBuiltin': '默认(跟随 CLI 默认:{model})',
|
|
19
|
+
'model.menuDefaultLastObserved': '默认(跟随 CLI 内置默认(最近:{model}))',
|
|
20
|
+
'model.save': '保存',
|
|
21
|
+
'model.applied': '已保存,下一轮委派生效;进行中的轮次不受影响',
|
|
22
|
+
'model.error': '保存失败,请重试',
|
|
23
|
+
'live.title': '常驻模式(live)',
|
|
24
|
+
'live.info.aria': '常驻模式说明',
|
|
25
|
+
'live.info': '常驻模式 = 成员进程常驻:输出实时流入成员会话、取消不杀进程、崩溃后自动续上原会话;关闭则每轮独立进程,跑完一次性出结果。设置为全局偏好,对所有后续委派生效。',
|
|
26
|
+
'live.applied': '已保存,下一轮委派生效;进行中的轮次不受影响',
|
|
27
|
+
'live.error': '保存失败,请重试',
|
|
28
|
+
'live.unavailable': '设置服务不可用(宿主未注册 local-agent-claude-code 命名空间)',
|
|
29
|
+
};
|
|
30
|
+
/** English dictionary, checked complete against the zh key set. */
|
|
31
|
+
export const en = {
|
|
32
|
+
'card.title': 'Local Agent · Claude Code',
|
|
33
|
+
'card.description': 'Claude Code delegation: sign-in and resident (live) mode',
|
|
34
|
+
'card.expand': 'Expand',
|
|
35
|
+
'card.collapse': 'Collapse',
|
|
36
|
+
'auth.title': 'Authentication',
|
|
37
|
+
'model.title': 'Default model',
|
|
38
|
+
'model.info.aria': 'About the default model',
|
|
39
|
+
'model.info': "Blank = follow the default (the input shows the model it currently follows). Pick from the list or type a model name by hand and save, and every later delegation round starts the CLI with it (`claude -p --model <model>`, the same flag in resident mode). Inside a member session the composer can also switch the model per session, without touching this default. A change applies to the next round and never disturbs a round in flight; an evaluation run freezes its condition at setup, so switching mid-run is caught by the model read-back on the next round and fails the run as misattributed.",
|
|
40
|
+
'model.placeholder': "Blank = pass no model, keep the CLI's own default",
|
|
41
|
+
'model.menu': 'Pick a model',
|
|
42
|
+
'model.menuDefault': "Default (follow the CLI's built-in default)",
|
|
43
|
+
'model.menuDefaultCliConfig': 'Default (follow the CLI config: {model})',
|
|
44
|
+
'model.menuDefaultCliBuiltin': 'Default (follow the CLI default: {model})',
|
|
45
|
+
'model.menuDefaultLastObserved': "Default (follow the CLI's built-in default (last: {model}))",
|
|
46
|
+
'model.save': 'Save',
|
|
47
|
+
'model.applied': 'Saved — takes effect on the next delegation round; rounds in flight are unaffected',
|
|
48
|
+
'model.error': 'Save failed, please retry',
|
|
49
|
+
'live.title': 'Resident mode (live)',
|
|
50
|
+
'live.info.aria': 'About resident mode',
|
|
51
|
+
'live.info': 'Resident mode keeps the member process alive: output streams into the member session in real time, cancelling does not kill the process, and a crash resumes the same session. Off means every round runs as an independent one-shot process. The setting is global and applies to every later delegation.',
|
|
52
|
+
'live.applied': 'Saved — takes effect on the next delegation round; rounds in flight are unaffected',
|
|
53
|
+
'live.error': 'Save failed, please retry',
|
|
54
|
+
'live.unavailable': 'Settings service unavailable (the host did not register the local-agent-claude-code namespace)',
|
|
55
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
type JsonObject = Record<string, unknown>;
|
|
2
|
+
/** Bounded, correlated Claude stream-json controls, shared by discovery and live sessions. */
|
|
3
|
+
export declare class ClaudeControlRequests {
|
|
4
|
+
private readonly send;
|
|
5
|
+
private readonly timeoutMs;
|
|
6
|
+
private sequence;
|
|
7
|
+
private closed;
|
|
8
|
+
private readonly pending;
|
|
9
|
+
constructor(send: (message: JsonObject) => void, timeoutMs?: number);
|
|
10
|
+
accept(event: JsonObject): boolean;
|
|
11
|
+
request(request: JsonObject, signal?: AbortSignal): Promise<JsonObject>;
|
|
12
|
+
close(): void;
|
|
13
|
+
}
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=control-requests.d.ts.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** Bounded, correlated Claude stream-json controls, shared by discovery and live sessions. */
|
|
2
|
+
export class ClaudeControlRequests {
|
|
3
|
+
send;
|
|
4
|
+
timeoutMs;
|
|
5
|
+
sequence = 0;
|
|
6
|
+
closed = false;
|
|
7
|
+
pending = new Map();
|
|
8
|
+
constructor(send, timeoutMs = 5_000) {
|
|
9
|
+
this.send = send;
|
|
10
|
+
this.timeoutMs = timeoutMs;
|
|
11
|
+
}
|
|
12
|
+
accept(event) {
|
|
13
|
+
if (event['type'] !== 'control_response')
|
|
14
|
+
return false;
|
|
15
|
+
const response = event['response'];
|
|
16
|
+
const id = response?.['request_id'];
|
|
17
|
+
if (typeof id !== 'string')
|
|
18
|
+
return true;
|
|
19
|
+
const pending = this.pending.get(id);
|
|
20
|
+
if (pending === undefined)
|
|
21
|
+
return true;
|
|
22
|
+
if (response?.['subtype'] !== 'success') {
|
|
23
|
+
pending.reject(new Error(typeof response?.['error'] === 'string' ? response['error'] : 'Claude rejected the control request'));
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
const result = response['response'];
|
|
27
|
+
pending.resolve(typeof result === 'object' && result !== null ? result : {});
|
|
28
|
+
}
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
request(request, signal) {
|
|
32
|
+
if (this.closed || signal?.aborted)
|
|
33
|
+
return Promise.reject(new Error('Claude control channel is closed'));
|
|
34
|
+
const id = `dsh-control-${++this.sequence}`;
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const finish = () => {
|
|
37
|
+
this.pending.delete(id);
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
signal?.removeEventListener('abort', abort);
|
|
40
|
+
};
|
|
41
|
+
const fail = (error) => { finish(); reject(error); };
|
|
42
|
+
const abort = () => { fail(new Error('Claude control request cancelled')); };
|
|
43
|
+
const timer = setTimeout(() => fail(new Error(`Claude ${String(request['subtype'])} control timed out`)), this.timeoutMs);
|
|
44
|
+
timer.unref();
|
|
45
|
+
this.pending.set(id, { resolve: value => { finish(); resolve(value); }, reject: fail });
|
|
46
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
47
|
+
try {
|
|
48
|
+
this.send({ type: 'control_request', request_id: id, request });
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
fail(error instanceof Error ? error : new Error('Claude control write failed'));
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
close() {
|
|
56
|
+
if (this.closed)
|
|
57
|
+
return;
|
|
58
|
+
this.closed = true;
|
|
59
|
+
for (const request of [...this.pending.values()])
|
|
60
|
+
request.reject(new Error('Claude control channel closed before acknowledgement'));
|
|
61
|
+
}
|
|
62
|
+
}
|