@khorsheed/dsh-local-agent 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 +181 -0
- package/README.i18n.yaml +2 -0
- package/README.md +181 -0
- package/cordis.patch.yml +21 -0
- package/lib/client.js +8832 -0
- package/lib/index.js +5281 -0
- package/lib/invariant.js +85 -0
- package/lib/member-bridge.js +298 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +3137 -0
- package/lib/typert.remote-client.d.ts +67 -0
- package/lib/typert.remote-client.js +1971 -0
- package/lib/types/cli-version.d.ts +82 -0
- package/lib/types/cli-version.js +165 -0
- package/lib/types/client/AuthStatusDot.d.ts +17 -0
- package/lib/types/client/AuthStatusDot.js +9 -0
- package/lib/types/client/HarnessModelPicker.d.ts +17 -0
- package/lib/types/client/HarnessModelPicker.js +48 -0
- package/lib/types/client/LocalAgentRecordsAction.d.ts +41 -0
- package/lib/types/client/LocalAgentRecordsAction.js +74 -0
- package/lib/types/client/MemberComposer.d.ts +79 -0
- package/lib/types/client/MemberComposer.js +308 -0
- package/lib/types/client/MemberConfiguration.d.ts +9 -0
- package/lib/types/client/MemberConfiguration.js +46 -0
- package/lib/types/client/MemberInboxView.d.ts +12 -0
- package/lib/types/client/MemberInboxView.js +38 -0
- package/lib/types/client/MemberLiveNode.d.ts +14 -0
- package/lib/types/client/MemberLiveNode.js +74 -0
- package/lib/types/client/MemberPaintDiagnostics.d.ts +7 -0
- package/lib/types/client/MemberPaintDiagnostics.js +20 -0
- package/lib/types/client/ModelConfigurationFields.d.ts +15 -0
- package/lib/types/client/ModelConfigurationFields.js +23 -0
- package/lib/types/client/ProviderAuthBlock.d.ts +59 -0
- package/lib/types/client/ProviderAuthBlock.js +164 -0
- package/lib/types/client/auth-status.d.ts +28 -0
- package/lib/types/client/auth-status.js +56 -0
- package/lib/types/client/icons.d.ts +22 -0
- package/lib/types/client/icons.js +6 -0
- package/lib/types/client/index.d.ts +62 -0
- package/lib/types/client/index.js +135 -0
- package/lib/types/client/live-node.d.ts +14 -0
- package/lib/types/client/live-node.js +34 -0
- package/lib/types/client/live-output.d.ts +34 -0
- package/lib/types/client/live-output.js +94 -0
- package/lib/types/client/live-paint.d.ts +40 -0
- package/lib/types/client/live-paint.js +74 -0
- package/lib/types/client/locales.d.ts +120 -0
- package/lib/types/client/locales.js +227 -0
- package/lib/types/client/member-configuration.d.ts +41 -0
- package/lib/types/client/member-configuration.js +114 -0
- package/lib/types/client/member-dock.d.ts +94 -0
- package/lib/types/client/member-dock.js +102 -0
- package/lib/types/client/member-feed.d.ts +24 -0
- package/lib/types/client/member-feed.js +134 -0
- package/lib/types/client/request-id.d.ts +3 -0
- package/lib/types/client/request-id.js +6 -0
- package/lib/types/client/settings-scope.d.ts +74 -0
- package/lib/types/client/settings-scope.js +98 -0
- package/lib/types/client/use-configuration-menu.d.ts +10 -0
- package/lib/types/client/use-configuration-menu.js +61 -0
- package/lib/types/container.d.ts +69 -0
- package/lib/types/container.js +129 -0
- package/lib/types/env.d.ts +17 -0
- package/lib/types/env.js +89 -0
- package/lib/types/gateway.d.ts +162 -0
- package/lib/types/gateway.js +470 -0
- package/lib/types/index.d.ts +1463 -0
- package/lib/types/index.js +2704 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.js +105 -0
- package/lib/types/live-flush.d.ts +21 -0
- package/lib/types/live-flush.js +59 -0
- package/lib/types/live-stream.d.ts +41 -0
- package/lib/types/live-stream.js +178 -0
- package/lib/types/member-bridge.d.ts +50 -0
- package/lib/types/member-bridge.js +216 -0
- package/lib/types/member-channel.d.ts +86 -0
- package/lib/types/member-channel.js +232 -0
- package/lib/types/member-control-storage.d.ts +10 -0
- package/lib/types/member-control-storage.js +100 -0
- package/lib/types/member-control.d.ts +62 -0
- package/lib/types/member-control.js +359 -0
- package/lib/types/member-controls.d.ts +23 -0
- package/lib/types/member-controls.js +143 -0
- package/lib/types/member-feed.d.ts +4 -0
- package/lib/types/member-feed.js +45 -0
- package/lib/types/member-inbox.d.ts +30 -0
- package/lib/types/member-inbox.js +225 -0
- package/lib/types/model-directory.d.ts +43 -0
- package/lib/types/model-directory.js +192 -0
- package/lib/types/prepared-members.d.ts +15 -0
- package/lib/types/prepared-members.js +61 -0
- package/lib/types/settings-face.d.ts +61 -0
- package/lib/types/settings-face.js +61 -0
- package/lib/types/types.d.ts +959 -0
- package/lib/types/types.js +24 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +112 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Writable composer for family CLI member sessions, elected through the
|
|
4
|
+
* 'conversation.composer' chain at priority -20 (ui-subagent's read-only
|
|
5
|
+
* takeover sits at -10; chain election is ascending, first non-null selector
|
|
6
|
+
* wins). The chain-slot contract requires a selector to be a PURE function of
|
|
7
|
+
* the owner props, so membership is decided in two stages: the selector coarse-
|
|
8
|
+
* filters on the session snapshot (one-shot subagent sessions elect), and the
|
|
9
|
+
* component confirms real family membership through the `memberOf` Remote —
|
|
10
|
+
* a session the family never delegated renders the same read-only panel the
|
|
11
|
+
* official composer shows, never a writable box. The verification window is
|
|
12
|
+
* a NEUTRAL checking state (no read-only semantics), an RPC failure is never
|
|
13
|
+
* read as "not a member", and a confirmed membership is cached per session so
|
|
14
|
+
* a re-enter renders the writable box on the first frame. Send goes to the
|
|
15
|
+
* `promptMember` Remote (the facade resume), never to the official input
|
|
16
|
+
* machine; Stop goes to `stopMember`.
|
|
17
|
+
*/
|
|
18
|
+
import { requestId } from './request-id.js';
|
|
19
|
+
import { useEffect, useRef, useState } from 'react';
|
|
20
|
+
import { IconChevronDownOutlineMedium } from './icons.js';
|
|
21
|
+
import { memberDockLines } from './member-dock.js';
|
|
22
|
+
import css from './MemberComposer.module.css';
|
|
23
|
+
/**
|
|
24
|
+
* Pure coarse filter for the composer chain: elect any one-shot subagent
|
|
25
|
+
* session. Deliberately wide (selector purity allows no async membership
|
|
26
|
+
* check) — the component degrades to the read-only panel when the delegation
|
|
27
|
+
* registry has no record for the session.
|
|
28
|
+
* @param owner - the composer chain currency dispatched by ConversationRoot.
|
|
29
|
+
* @returns the match carrying the child session id, or null to pass the
|
|
30
|
+
* election down the chain.
|
|
31
|
+
*/
|
|
32
|
+
export function selectCliMember(owner) {
|
|
33
|
+
// Pending interactions (questions/approvals) belong to the official
|
|
34
|
+
// ApprovalPanel — a chain entry at priority 1. Election runs ascending, so
|
|
35
|
+
// this -20 entry would shadow it: decline and let the interaction render.
|
|
36
|
+
// The owner prop moved between host lines: rc carries an `interactions`
|
|
37
|
+
// array, 0.1.2 a singular `pendingInteraction` — probe both (feature
|
|
38
|
+
// detection, not a version check).
|
|
39
|
+
const legacy = owner.interactions;
|
|
40
|
+
if (legacy !== undefined) {
|
|
41
|
+
if (legacy.length > 0)
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
else if (owner.pendingInteraction != null) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const session = owner.session;
|
|
48
|
+
const subagent = session?.subagent;
|
|
49
|
+
if (session === undefined || subagent === undefined || subagent === null)
|
|
50
|
+
return null;
|
|
51
|
+
if (subagent.address.mode !== 'one-shot')
|
|
52
|
+
return null;
|
|
53
|
+
return { childSessionId: session.sessionId };
|
|
54
|
+
}
|
|
55
|
+
/** RPC-failure retry budget for the membership probe (see the probe effect). */
|
|
56
|
+
const MEMBER_PROBE_RETRIES = 2;
|
|
57
|
+
/** Delay between membership probe retries. */
|
|
58
|
+
const MEMBER_PROBE_RETRY_MS = 300;
|
|
59
|
+
/** In-flight delegation poll cadence (the taskpilot dock's proven cadence). */
|
|
60
|
+
const ACTIVE_DELEGATIONS_POLL_MS = 1_500;
|
|
61
|
+
/** The locale key of each model-source layer's label (the chip's title). */
|
|
62
|
+
const MODEL_SOURCE_KEYS = {
|
|
63
|
+
'override': 'member.model.source.override',
|
|
64
|
+
'delegation': 'member.model.source.delegation',
|
|
65
|
+
'settings': 'member.model.source.settings',
|
|
66
|
+
'cli-config': 'member.model.source.cli-config',
|
|
67
|
+
'cli-builtin': 'member.model.source.cli-builtin',
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Session-level positive membership cache: a recorded delegation is immutable
|
|
71
|
+
* for the session's lifetime, so a confirmed member re-enter renders the
|
|
72
|
+
* writable box on the first frame instead of re-flashing the checking state.
|
|
73
|
+
* Null answers (not yet delegated) are never cached — the record lands with
|
|
74
|
+
* the first round's settle, and the probe effect re-checks on running flips.
|
|
75
|
+
*/
|
|
76
|
+
const membershipCache = new Map();
|
|
77
|
+
/** Test hook: drop every cached membership answer. */
|
|
78
|
+
export function resetMembershipCache() {
|
|
79
|
+
membershipCache.clear();
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The member composer: a writable box for family member sessions and the
|
|
83
|
+
* official-looking read-only panel for every other one-shot session. Drafts
|
|
84
|
+
* stay in local state (the official input machine is deliberately NOT wired —
|
|
85
|
+
* its send path is hardwired to the host prompt pipeline this channel must
|
|
86
|
+
* bypass). A run in flight disables the input and swaps Send for Stop — the
|
|
87
|
+
* running bit is DUAL-SOURCE: the session summary's flag (agent-based; the
|
|
88
|
+
* dsh member's sub-instance drives it natively) OR the family's in-flight
|
|
89
|
+
* delegation registry polled through `activeDelegations` (the only source
|
|
90
|
+
* that lights up for external CLI runs, which have no live host agent).
|
|
91
|
+
* @param props - selector match, standard slot props, locale seat, and the
|
|
92
|
+
* injected gateway face.
|
|
93
|
+
* @returns the composer, the neutral checking state while probing, or the
|
|
94
|
+
* read-only panel when not a member.
|
|
95
|
+
*/
|
|
96
|
+
export function MemberComposer({ matched, useSession, useProjection, memberOf, promptMember, stopMember, activeDelegations, memberModel, setMemberModel, renderMemberConfiguration, renderMemberInbox, t }) {
|
|
97
|
+
const [membership, setMembership] = useState(() => membershipCache.get(matched.childSessionId));
|
|
98
|
+
const [draft, setDraft] = useState('');
|
|
99
|
+
const [sending, setSending] = useState(false);
|
|
100
|
+
const inputRequest = useRef();
|
|
101
|
+
/** The last structured promptMember failure, rendered inline. */
|
|
102
|
+
const [error, setError] = useState(null);
|
|
103
|
+
const sessionRunning = useSession(snapshot => snapshot.running) ?? false;
|
|
104
|
+
/** The polled in-flight bit for this child (second running source). */
|
|
105
|
+
const [memberActive, setMemberActive] = useState(false);
|
|
106
|
+
/** The member's model surface; undefined = not fetched yet, null = no picker. */
|
|
107
|
+
const [modelSurface, setModelSurface] = useState(undefined);
|
|
108
|
+
/** The picker's dropdown open bit. */
|
|
109
|
+
const [modelMenuOpen, setModelMenuOpen] = useState(false);
|
|
110
|
+
/** The picker's root node (click-outside closes the dropdown). */
|
|
111
|
+
const modelPickerRef = useRef(null);
|
|
112
|
+
/** The child the current membership answer belongs to (re-probes on switch). */
|
|
113
|
+
const membershipFor = useRef(null);
|
|
114
|
+
/** Mirror of `membership` for the probe effect (kept out of its deps: a null answer must not self-trigger). */
|
|
115
|
+
const membershipRef = useRef(undefined);
|
|
116
|
+
membershipRef.current = membership;
|
|
117
|
+
// The member dock self-renders ambient state from the projection seat: the
|
|
118
|
+
// official panels (StatsLine on 'conversation.composer.dock', TodoPanel on
|
|
119
|
+
// 'conversation.input.dock') live INSIDE the fallback InputBar that
|
|
120
|
+
// `overlay: true` hides on election, so no official dock row can reach a
|
|
121
|
+
// member session. One bag entry per projection a contributor reads.
|
|
122
|
+
const projections = {
|
|
123
|
+
tokenUsage: useProjection('tokenUsage'),
|
|
124
|
+
todos: useProjection('todos'),
|
|
125
|
+
};
|
|
126
|
+
// Second running source: poll the family's in-flight delegation registry.
|
|
127
|
+
// The official summary flag never lights up for external CLI runs (no live
|
|
128
|
+
// host agent), so without this the Stop button and the running badge never
|
|
129
|
+
// appear and the membership re-probe below never fires in production. An
|
|
130
|
+
// RPC failure keeps the last known bit instead of flapping Stop off.
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
let cancelled = false;
|
|
133
|
+
const childId = matched.childSessionId;
|
|
134
|
+
const tick = () => {
|
|
135
|
+
void activeDelegations().then((ids) => {
|
|
136
|
+
if (cancelled || ids === undefined)
|
|
137
|
+
return;
|
|
138
|
+
setMemberActive(ids.includes(childId));
|
|
139
|
+
}, () => { });
|
|
140
|
+
};
|
|
141
|
+
tick();
|
|
142
|
+
const timer = setInterval(tick, ACTIVE_DELEGATIONS_POLL_MS);
|
|
143
|
+
return () => { cancelled = true; clearInterval(timer); };
|
|
144
|
+
}, [activeDelegations, matched.childSessionId]);
|
|
145
|
+
const running = sessionRunning || memberActive;
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
const childId = matched.childSessionId;
|
|
148
|
+
if (membershipFor.current !== childId) {
|
|
149
|
+
// A different child: seed from the positive cache (a member re-enter
|
|
150
|
+
// skips the probe entirely) and reset to checking when uncached.
|
|
151
|
+
membershipFor.current = childId;
|
|
152
|
+
const cached = membershipCache.get(childId);
|
|
153
|
+
setMembership(cached);
|
|
154
|
+
if (cached !== undefined)
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
else if (membershipRef.current !== null && membershipRef.current !== undefined) {
|
|
158
|
+
return; // already resolved for this child
|
|
159
|
+
}
|
|
160
|
+
// A null answer (not yet a member) re-probes on every running flip: the
|
|
161
|
+
// delegation record lands with the first round's settle (exec) or the
|
|
162
|
+
// live handshake, exactly when running changes — so an open panel flips
|
|
163
|
+
// from the one-shot read-only fallback to the writable member box on its
|
|
164
|
+
// own, without a session re-enter. An UNDEFINED answer is an RPC failure,
|
|
165
|
+
// never membership evidence: it keeps the neutral checking state and
|
|
166
|
+
// retries within a bounded budget before degrading to the read-only
|
|
167
|
+
// panel, so a transient failure never flashes "one-shot" at a member.
|
|
168
|
+
let cancelled = false;
|
|
169
|
+
let timer;
|
|
170
|
+
let attempts = 0;
|
|
171
|
+
const settle = (view) => {
|
|
172
|
+
if (cancelled)
|
|
173
|
+
return;
|
|
174
|
+
if (view === undefined && attempts < MEMBER_PROBE_RETRIES) {
|
|
175
|
+
attempts += 1;
|
|
176
|
+
timer = setTimeout(probe, MEMBER_PROBE_RETRY_MS);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const resolved = view === undefined ? null : view;
|
|
180
|
+
if (resolved !== null)
|
|
181
|
+
membershipCache.set(childId, resolved);
|
|
182
|
+
setMembership(resolved);
|
|
183
|
+
};
|
|
184
|
+
const probe = () => {
|
|
185
|
+
// A rejected RPC is the same transient failure as an undefined value.
|
|
186
|
+
void memberOf(childId).then(settle, () => settle(undefined));
|
|
187
|
+
};
|
|
188
|
+
probe();
|
|
189
|
+
return () => {
|
|
190
|
+
cancelled = true;
|
|
191
|
+
if (timer !== undefined)
|
|
192
|
+
clearTimeout(timer);
|
|
193
|
+
};
|
|
194
|
+
}, [memberOf, matched.childSessionId, running]);
|
|
195
|
+
// The model surface: fetched once membership resolves, and re-fetched on
|
|
196
|
+
// every running flip — a switch is refused while a round is in flight and a
|
|
197
|
+
// live runtime respawns on the next round, so run edges are exactly when
|
|
198
|
+
// `switchable`/`effective` can move. Undefined answers (RPC failure) keep
|
|
199
|
+
// the last known surface, mirroring the activeDelegations poll's rule.
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
if (membership === undefined || membership === null)
|
|
202
|
+
return;
|
|
203
|
+
const childId = matched.childSessionId;
|
|
204
|
+
let cancelled = false;
|
|
205
|
+
void memberModel(childId).then((info) => {
|
|
206
|
+
if (cancelled || info === undefined)
|
|
207
|
+
return;
|
|
208
|
+
setModelSurface({ child: childId, info });
|
|
209
|
+
}, () => { });
|
|
210
|
+
return () => { cancelled = true; };
|
|
211
|
+
}, [memberModel, matched.childSessionId, membership, running]);
|
|
212
|
+
// Click-outside closes the model dropdown (the dock capsules' pattern).
|
|
213
|
+
useEffect(() => {
|
|
214
|
+
if (!modelMenuOpen)
|
|
215
|
+
return;
|
|
216
|
+
const onPointerDown = (event) => {
|
|
217
|
+
if (modelPickerRef.current?.contains(event.target) === true)
|
|
218
|
+
return;
|
|
219
|
+
setModelMenuOpen(false);
|
|
220
|
+
};
|
|
221
|
+
document.addEventListener('mousedown', onPointerDown);
|
|
222
|
+
return () => { document.removeEventListener('mousedown', onPointerDown); };
|
|
223
|
+
}, [modelMenuOpen]);
|
|
224
|
+
if (membership === undefined) {
|
|
225
|
+
return _jsx("div", { className: css.frame, role: "status", children: _jsx("span", { children: t('member.checking') }) });
|
|
226
|
+
}
|
|
227
|
+
if (membership === null) {
|
|
228
|
+
// Not a family member: render the official one-shot read-only panel
|
|
229
|
+
// (visually aligned with ui-subagent's SubagentReadOnlyComposer), never a
|
|
230
|
+
// writable box.
|
|
231
|
+
return (_jsxs("div", { className: css.frame, role: "status", children: [_jsx("strong", { children: t('member.readonly.title') }), _jsx("span", { children: t('member.readonly.body') })] }));
|
|
232
|
+
}
|
|
233
|
+
const busy = sending || (running && renderMemberInbox === undefined);
|
|
234
|
+
const dockLines = memberDockLines(projections, t);
|
|
235
|
+
/** The chip label: the effective model id when any layer names one (codex's
|
|
236
|
+
catalog default counts — it names the CLI's own default); otherwise the
|
|
237
|
+
localized "Default", annotated with the last observed model when the
|
|
238
|
+
delegation records know what actually ran. */
|
|
239
|
+
const modelLabel = (info) => info.effective
|
|
240
|
+
?? (info.lastObserved === undefined
|
|
241
|
+
? t('member.model.default')
|
|
242
|
+
: t('member.model.defaultLastObserved', { model: info.lastObserved }));
|
|
243
|
+
// The surface stamped with another child (a session switch mid-fetch) never
|
|
244
|
+
// flashes its model at this member.
|
|
245
|
+
const modelInfo = modelSurface !== undefined && modelSurface.child === matched.childSessionId
|
|
246
|
+
? modelSurface.info
|
|
247
|
+
: null;
|
|
248
|
+
const send = () => {
|
|
249
|
+
const text = draft.trim();
|
|
250
|
+
if (text === '' || busy)
|
|
251
|
+
return;
|
|
252
|
+
setSending(true);
|
|
253
|
+
setError(null);
|
|
254
|
+
if (inputRequest.current?.child !== matched.childSessionId || inputRequest.current.text !== text)
|
|
255
|
+
inputRequest.current = { child: matched.childSessionId, text, id: requestId() };
|
|
256
|
+
const submit = renderMemberInbox === undefined ? promptMember(matched.childSessionId, text) : promptMember(matched.childSessionId, text, inputRequest.current.id);
|
|
257
|
+
void submit.then((result) => {
|
|
258
|
+
setSending(false);
|
|
259
|
+
if (result !== undefined && result.ok) {
|
|
260
|
+
setDraft('');
|
|
261
|
+
inputRequest.current = undefined;
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
setError(result !== undefined && !result.ok ? result.error : t('member.sendFailed'));
|
|
265
|
+
}
|
|
266
|
+
}).catch(() => { setSending(false); setError(t('member.sendFailed')); });
|
|
267
|
+
};
|
|
268
|
+
const stop = () => {
|
|
269
|
+
void stopMember(matched.childSessionId);
|
|
270
|
+
};
|
|
271
|
+
/**
|
|
272
|
+
* Apply a picker choice (undefined = clear the override). The displayed
|
|
273
|
+
* value only ever moves on an authoritative re-read after `{ ok: true }`,
|
|
274
|
+
* so a refused switch leaves the chip on the real model — the structured
|
|
275
|
+
* error rides the same inline line a promptMember failure uses.
|
|
276
|
+
*/
|
|
277
|
+
const chooseModel = (model) => {
|
|
278
|
+
setModelMenuOpen(false);
|
|
279
|
+
setError(null);
|
|
280
|
+
const childId = matched.childSessionId;
|
|
281
|
+
void setMemberModel(childId, model).then((result) => {
|
|
282
|
+
if (result !== undefined && result.ok) {
|
|
283
|
+
void memberModel(childId).then((info) => {
|
|
284
|
+
if (info !== undefined)
|
|
285
|
+
setModelSurface({ child: childId, info });
|
|
286
|
+
}, () => { });
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
setError(result !== undefined && !result.ok ? result.error : t('member.model.failed'));
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
};
|
|
293
|
+
const onChange = (event) => {
|
|
294
|
+
setDraft(event.target.value);
|
|
295
|
+
};
|
|
296
|
+
const onKeyDown = (event) => {
|
|
297
|
+
// Shift+Enter is the native newline; an IME-composition Enter picks a
|
|
298
|
+
// candidate and must not send.
|
|
299
|
+
if (event.key !== 'Enter' || event.shiftKey || event.nativeEvent.isComposing)
|
|
300
|
+
return;
|
|
301
|
+
event.preventDefault();
|
|
302
|
+
if (!event.repeat)
|
|
303
|
+
send(); // held-down Enter must not machine-gun sends
|
|
304
|
+
};
|
|
305
|
+
return (_jsxs("div", { className: css.root, children: [error !== null && _jsx("div", { className: css.error, role: "alert", children: error }), renderMemberInbox?.(matched.childSessionId), _jsxs("div", { className: css.card, children: [_jsxs("div", { className: css.header, children: [_jsx("span", { children: t('member.title', { harness: membership.harnessDisplayName ?? membership.provider }) }), running && _jsx("span", { className: css.running, children: t('member.running') })] }), _jsx("textarea", { className: css.input, value: draft, rows: 2, disabled: busy, placeholder: t('member.placeholder'), onChange: onChange, onKeyDown: onKeyDown }), _jsxs("div", { className: css.row, children: [renderMemberConfiguration !== undefined ? renderMemberConfiguration(matched.childSessionId) : modelInfo !== null && (_jsxs("div", { className: css.modelPicker, ref: modelPickerRef, children: [_jsxs("button", { type: "button", className: css.modelChip, disabled: busy || !modelInfo.switchable, title: !modelInfo.switchable && modelInfo.reason !== undefined
|
|
306
|
+
? modelInfo.reason
|
|
307
|
+
: t('member.model.title', { model: modelLabel(modelInfo), source: t(MODEL_SOURCE_KEYS[modelInfo.source]) }), "aria-label": t('member.model.picker'), "aria-haspopup": "menu", "aria-expanded": modelMenuOpen, onClick: () => { setModelMenuOpen(open => !open); }, children: [_jsx("span", { className: css.modelChipLabel, children: modelLabel(modelInfo) }), _jsx(IconChevronDownOutlineMedium, { className: modelMenuOpen ? css.chevronOpen : css.chevron })] }), modelMenuOpen && (_jsx("div", { className: css.modelMenu, role: "menu", "aria-label": t('member.model.picker'), children: modelInfo.choices.length === 0 && modelInfo.override === undefined ? (_jsx("span", { className: css.modelItemHint, role: "menuitem", "aria-disabled": "true", children: t('member.model.empty') })) : (_jsxs(_Fragment, { children: [modelInfo.choices.map(choice => (_jsxs("button", { type: "button", role: "menuitemradio", "aria-checked": choice === modelInfo.effective, className: choice === modelInfo.effective ? css.modelItemCurrent : css.modelItem, onClick: () => { chooseModel(choice); }, children: [_jsx("span", { className: css.modelItemLabel, children: choice }), _jsx("span", { className: css.modelItemCheck, "aria-hidden": true, children: choice === modelInfo.effective ? '✓' : '' })] }, choice))), modelInfo.override !== undefined && (_jsx("button", { type: "button", role: "menuitem", className: css.modelItemReset, onClick: () => { chooseModel(undefined); }, children: t('member.model.followSettings') }))] })) }))] })), running && (_jsx("button", { type: "button", className: css.primary, "aria-label": t('member.stop'), onClick: stop, children: _jsx("svg", { viewBox: "0 0 16 16", width: "16", height: "16", "aria-hidden": true, children: _jsx("rect", { x: "3", y: "3", width: "10", height: "10", rx: "3", fill: "currentColor" }) }) })), (!running || renderMemberInbox !== undefined) && (_jsx("button", { type: "button", className: css.primary, "aria-label": t('member.send'), disabled: draft.trim() === '' || busy, onClick: send, children: _jsx("svg", { viewBox: "0 0 16 16", width: "16", height: "16", "aria-hidden": true, children: _jsx("path", { d: "M8.3125 0.980183C8.66767 1.0531 8.97902 1.20418 9.2627 1.43233C9.48724 1.61297 9.73029 1.85793 9.97949 2.10714L14.707 6.83468L13.293 8.24874L9 3.95577V15.0417H7V3.95577L2.70703 8.24874L1.29297 6.83468L6.02051 2.10714C6.26971 1.85793 6.51277 1.61297 6.7373 1.43233C6.97662 1.23986 7.28445 1.04402 7.6875 0.980183C7.8973 0.947006 8.1031 0.95516 8.3125 0.980183Z", fill: "currentColor" }) }) }))] })] }), dockLines.length > 0 && (_jsx("div", { className: css.dock, "data-member-dock": true, children: dockLines.map(line => (_jsx("div", { className: css.dockRow, "data-member-stats": line.id === 'stats' ? '' : undefined, "data-member-dock-row": line.id, children: line.text }, line.id))) }))] }));
|
|
308
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { MemberConfigurationStore } from './member-configuration.ts';
|
|
2
|
+
import { type ConfigurationTranslate } from './ModelConfigurationFields.tsx';
|
|
3
|
+
import type { LivePaintDiagnostics } from './live-paint.ts';
|
|
4
|
+
export declare function MemberConfiguration({ store, t, diagnostics }: {
|
|
5
|
+
store: MemberConfigurationStore;
|
|
6
|
+
t: ConfigurationTranslate;
|
|
7
|
+
diagnostics?: LivePaintDiagnostics;
|
|
8
|
+
}): import("react").JSX.Element;
|
|
9
|
+
//# sourceMappingURL=MemberConfiguration.d.ts.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { requestId } from './request-id.js';
|
|
3
|
+
import { useState, useSyncExternalStore } from 'react';
|
|
4
|
+
import { ModelConfigurationFields } from './ModelConfigurationFields.js';
|
|
5
|
+
import css from './MemberConfiguration.module.css';
|
|
6
|
+
import { MemberPaintDiagnostics } from './MemberPaintDiagnostics.js';
|
|
7
|
+
import { useConfigurationMenu } from './use-configuration-menu.js';
|
|
8
|
+
export function MemberConfiguration({ store, t, diagnostics }) {
|
|
9
|
+
const snapshot = useSyncExternalStore(store.subscribe, store.getSnapshot);
|
|
10
|
+
const menu = useConfigurationMenu('above');
|
|
11
|
+
const [busy, setBusy] = useState(false);
|
|
12
|
+
const [error, setError] = useState();
|
|
13
|
+
const state = snapshot.state;
|
|
14
|
+
const current = state?.round?.configuration ?? state?.current;
|
|
15
|
+
const selection = state?.pending?.selection ?? state?.operation?.selection ?? state?.current.selection;
|
|
16
|
+
const label = (selection, model = current?.resolved.model, effort = current?.resolved.effort) => {
|
|
17
|
+
const value = selection?.model.mode === 'value' ? selection.model.value : model;
|
|
18
|
+
const entry = snapshot.directory?.entries.find(entry => entry.value === value || entry.resolvedModel === value);
|
|
19
|
+
const strength = selection?.effort.mode === 'value' ? selection.effort.value : effort;
|
|
20
|
+
return [entry?.label ?? value ?? t('loading'), entry?.reasoning?.options.find(option => option.value === strength)?.label ?? strength].filter(Boolean).join(' · ');
|
|
21
|
+
};
|
|
22
|
+
const currentLabel = label(current?.selection);
|
|
23
|
+
const disabled = busy || !snapshot.connected || state?.lockedReason !== undefined;
|
|
24
|
+
const act = async (operation, next) => {
|
|
25
|
+
if (!state || disabled)
|
|
26
|
+
return;
|
|
27
|
+
setBusy(true);
|
|
28
|
+
setError(undefined);
|
|
29
|
+
try {
|
|
30
|
+
const request = requestId();
|
|
31
|
+
const receipt = operation === 'retry' ? (await store.face.retry(store.id, state.revision), undefined)
|
|
32
|
+
: operation === 'cancel' ? await store.face.cancel(store.id, request, state.revision)
|
|
33
|
+
: await store.face.select(store.id, request, state.revision, next);
|
|
34
|
+
if (receipt && ['failed', 'conflict', 'locked', 'unsupported'].includes(receipt.status))
|
|
35
|
+
setError(receipt.error ?? t('configuration.failed'));
|
|
36
|
+
await store.refresh();
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
setError(error instanceof Error ? error.message : String(error));
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
setBusy(false);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
return _jsxs("details", { className: css.root, ref: menu.root, open: menu.open, onToggle: event => menu.setOpen(event.currentTarget.open), children: [_jsx("summary", { "aria-label": t('configuration.title'), "aria-expanded": menu.open, children: _jsxs("span", { className: css.triggerLabel, children: [currentLabel, state?.pending || state?.operation ? ` · ${t('configuration.pending')}` : ''] }) }), diagnostics && _jsx(MemberPaintDiagnostics, { sessionId: store.id, diagnostics: diagnostics }), _jsxs("div", { ref: menu.panel, className: css.panel, style: menu.style, children: [selection && _jsx(ModelConfigurationFields, { directory: snapshot.directory, value: selection, resolvedModel: state?.current.resolved.model, resolvedEffort: state?.current.resolved.effort, disabled: disabled, t: t, onChange: selection => { void act('select', selection); } }), state?.pending && _jsxs("div", { role: "status", children: [t('configuration.pending'), ": ", label(state.pending.selection)] }), state?.operation && _jsxs("div", { role: "status", children: [t('configuration.applying'), ": ", label(state.operation.selection)] }), state?.round && _jsx("small", { children: t('configuration.boundary') }), !snapshot.connected && _jsx("div", { role: "status", children: t('configuration.reconnecting') }), (error ?? state?.error ?? snapshot.error) && _jsx("div", { role: "alert", children: error ?? state?.error ?? snapshot.error }), state?.lockedReason && _jsx("div", { role: "status", children: state.lockedReason }), (state?.pending || state?.operation || state?.status === 'failed') && _jsxs("div", { className: css.actions, children: [(state?.pending || state?.operation) && _jsx("button", { type: "button", disabled: disabled, onClick: () => { void act('cancel'); }, children: t('configuration.cancel') }), state?.status === 'failed' && _jsx("button", { type: "button", disabled: disabled, onClick: () => { void act('retry'); }, children: t('configuration.retry') })] })] })] });
|
|
46
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
|
|
2
|
+
import type { LocalAgentMemberInbox, LocalAgentPromptResult } from '../types.ts';
|
|
3
|
+
import type { NS } from './locales.ts';
|
|
4
|
+
export interface MemberInboxFace {
|
|
5
|
+
read(): Promise<LocalAgentMemberInbox | undefined>;
|
|
6
|
+
control(action: 'pause' | 'resume' | 'cancel' | 'reconcile', requestId?: string, outcome?: 'done' | 'cancelled', evidence?: string): Promise<LocalAgentPromptResult | undefined>;
|
|
7
|
+
}
|
|
8
|
+
/** Core owns admission; the dock displays only inputs still waiting for it. */
|
|
9
|
+
export declare function MemberInboxView({ face, t }: {
|
|
10
|
+
face: MemberInboxFace;
|
|
11
|
+
} & PropsLocale<typeof NS>): import("react").JSX.Element | null;
|
|
12
|
+
//# sourceMappingURL=MemberInboxView.d.ts.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useId, useState } from 'react';
|
|
3
|
+
import { IconChevronDownOutlineMedium, IconChevronUpOutlineMedium, IconQueueOutlineMedium, IconTrashOutlineMedium } from './icons.js';
|
|
4
|
+
import css from './MemberInboxView.module.css';
|
|
5
|
+
/** Core owns admission; the dock displays only inputs still waiting for it. */
|
|
6
|
+
export function MemberInboxView({ face, t }) {
|
|
7
|
+
const [state, setState] = useState();
|
|
8
|
+
const [error, setError] = useState();
|
|
9
|
+
const [evidence, setEvidence] = useState('');
|
|
10
|
+
const [expanded, setExpanded] = useState(false);
|
|
11
|
+
const listId = useId();
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
let closed = false;
|
|
14
|
+
const refresh = () => { void face.read().then(value => { if (!closed && value !== undefined)
|
|
15
|
+
setState(value); }, () => { }); };
|
|
16
|
+
refresh();
|
|
17
|
+
const timer = setInterval(refresh, 750);
|
|
18
|
+
return () => { closed = true; clearInterval(timer); };
|
|
19
|
+
}, [face]);
|
|
20
|
+
if (state === undefined)
|
|
21
|
+
return null;
|
|
22
|
+
const queued = state.messages.filter(row => row.status === 'queued');
|
|
23
|
+
const recovery = state.messages.filter(row => row.status === 'uncertain' || row.status === 'failed').slice(-8);
|
|
24
|
+
if (queued.length === 0 && recovery.length === 0 && !state.paused && error === undefined && state.error === undefined)
|
|
25
|
+
return null;
|
|
26
|
+
const control = (action, requestId, outcome) => {
|
|
27
|
+
setError(undefined);
|
|
28
|
+
void face.control(action, requestId, outcome, evidence).then(async (result) => {
|
|
29
|
+
if (result?.ok !== true)
|
|
30
|
+
setError(result?.error ?? t('member.sendFailed'));
|
|
31
|
+
const next = await face.read();
|
|
32
|
+
if (next !== undefined)
|
|
33
|
+
setState(next);
|
|
34
|
+
}).catch(failure => setError(String(failure)));
|
|
35
|
+
};
|
|
36
|
+
const pause = _jsx("button", { className: css.action, type: "button", onClick: () => control(state.paused ? 'resume' : 'pause'), children: t(state.paused ? 'inbox.resume' : 'inbox.pause') });
|
|
37
|
+
return _jsx("section", { className: css.root, "aria-label": t('inbox.title'), children: _jsxs("div", { className: css.panel, children: [queued.length > 1 && _jsxs("div", { className: css.header, children: [_jsxs("button", { className: css.toggle, type: "button", "aria-expanded": expanded, "aria-controls": listId, onClick: () => setExpanded(value => !value), children: [_jsx(IconQueueOutlineMedium, {}), _jsxs("span", { children: [t('inbox.title'), " \u00B7 ", queued.length] }), expanded ? _jsx(IconChevronUpOutlineMedium, {}) : _jsx(IconChevronDownOutlineMedium, {})] }), pause] }), (queued.length === 1 || expanded) && queued.length > 0 && _jsx("ul", { className: css.list, id: listId, children: queued.map(row => _jsxs("li", { className: css.row, children: [_jsx("span", { className: css.lead, "aria-label": t('inbox.status.queued'), children: _jsx(IconQueueOutlineMedium, {}) }), _jsx("span", { className: css.preview, title: row.text, children: row.text }), queued.length === 1 && pause, _jsx("button", { className: css.iconAction, type: "button", "aria-label": t('inbox.cancel'), title: t('inbox.cancel'), onClick: () => control('cancel', row.id), children: _jsx(IconTrashOutlineMedium, {}) })] }, row.id)) }), state.paused && _jsxs("div", { className: css.notice, children: [_jsx("span", { children: t('inbox.paused') }), queued.length === 0 && pause] }), (error ?? state.error) !== undefined && _jsx("p", { className: css.notice, role: "alert", children: error ?? state.error }), recovery.map(row => _jsxs("div", { className: css.recovery, children: [_jsxs("span", { children: [t(`inbox.status.${row.status}`), " \u00B7 ", row.text.slice(0, 160)] }), row.error !== undefined && _jsx("p", { children: row.error }), row.status === 'uncertain' && _jsxs("div", { className: css.reconcile, children: [_jsx("input", { value: evidence, "aria-label": t('inbox.evidence'), placeholder: t('inbox.evidence'), onChange: event => setEvidence(event.target.value) }), _jsx("button", { className: css.action, type: "button", disabled: evidence.trim() === '', onClick: () => control('reconcile', row.id, 'done'), children: t('inbox.confirmDone') }), _jsx("button", { className: css.action, type: "button", disabled: evidence.trim() === '', onClick: () => control('reconcile', row.id, 'cancelled'), children: t('inbox.confirmCancelled') })] })] }, row.id))] }) });
|
|
38
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
2
|
+
import type { MemberLiveOutputs } from './live-output.ts';
|
|
3
|
+
import { NS } from './locales.ts';
|
|
4
|
+
export type MemberLiveNodeProps = PropsRuntime<'conversation.chat.node', 'local-agent-stream'> & PropsLocale<typeof NS> & {
|
|
5
|
+
outputs: MemberLiveOutputs;
|
|
6
|
+
};
|
|
7
|
+
export declare function MemberLiveNode({ node, outputs, t }: MemberLiveNodeProps): import("react").JSX.Element;
|
|
8
|
+
/** Same subscription in Room: run boundary excludes retained output from earlier turns. */
|
|
9
|
+
export declare function MemberLiveOutputView({ sessionId, startedAt, outputs, t }: {
|
|
10
|
+
sessionId: string;
|
|
11
|
+
startedAt: number;
|
|
12
|
+
outputs: MemberLiveOutputs;
|
|
13
|
+
} & PropsLocale<typeof NS>): import("react").JSX.Element;
|
|
14
|
+
//# sourceMappingURL=MemberLiveNode.d.ts.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/** Generation-time text rendered through the public Markdown component. */
|
|
3
|
+
import { useEffect, useLayoutEffect, useRef, useSyncExternalStore } from 'react';
|
|
4
|
+
import { MarkdownText } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
5
|
+
export function MemberLiveNode({ node, outputs, t }) {
|
|
6
|
+
const data = node.data;
|
|
7
|
+
const store = outputs.get(data.sessionId);
|
|
8
|
+
const snapshot = useSyncExternalStore(store.subscribe, store.getSnapshot);
|
|
9
|
+
const live = snapshot.items.get(data.id);
|
|
10
|
+
const text = live?.text ?? data.text;
|
|
11
|
+
return _jsx(StreamText, { id: data.id, kind: live?.kind ?? data.kind, text: text, receivedAt: live?.receivedAt, sessionId: data.sessionId, turn: live?.turn ?? data.turn, revision: live?.revision, baseline: live?.baseline ?? true, surface: "member", diagnostics: outputs.diagnostics, recovering: live === undefined || !snapshot.connected, t: t });
|
|
12
|
+
}
|
|
13
|
+
/** Same subscription in Room: run boundary excludes retained output from earlier turns. */
|
|
14
|
+
export function MemberLiveOutputView({ sessionId, startedAt, outputs, t }) {
|
|
15
|
+
const store = outputs.get(sessionId);
|
|
16
|
+
const snapshot = useSyncExternalStore(store.subscribe, store.getSnapshot);
|
|
17
|
+
return _jsx("div", { "data-member-output": sessionId, children: [...snapshot.items.values()].filter(item => item.receivedAt >= startedAt).map(item => (_jsx(StreamText, { ...item, sessionId: sessionId, surface: "room", diagnostics: outputs.diagnostics, recovering: !snapshot.connected, t: t }, item.id))) });
|
|
18
|
+
}
|
|
19
|
+
/** Local diagnostics only. Two animation frames bound a foreground paint from above.
|
|
20
|
+
* The wall-clock subtraction is meaningful only when browser and host share a clock.
|
|
21
|
+
* Keep independent callbacks across updates so slow paints cannot disappear from P95.
|
|
22
|
+
*/
|
|
23
|
+
function StreamText({ id, kind, text, receivedAt, recovering, sessionId, turn, revision, baseline, surface, diagnostics, t }) {
|
|
24
|
+
const element = useRef(null);
|
|
25
|
+
const samples = useRef([]);
|
|
26
|
+
const frames = useRef(new Set());
|
|
27
|
+
const unfinished = useRef(new Set());
|
|
28
|
+
useEffect(() => () => {
|
|
29
|
+
for (const frame of frames.current)
|
|
30
|
+
cancelAnimationFrame(frame);
|
|
31
|
+
frames.current.clear();
|
|
32
|
+
for (const complete of unfinished.current)
|
|
33
|
+
complete('unmounted', Date.now());
|
|
34
|
+
unfinished.current.clear();
|
|
35
|
+
}, []);
|
|
36
|
+
useLayoutEffect(() => {
|
|
37
|
+
if (receivedAt === undefined || revision === undefined || baseline)
|
|
38
|
+
return;
|
|
39
|
+
const complete = diagnostics.begin(sessionId, turn, revision, surface);
|
|
40
|
+
if (complete === undefined)
|
|
41
|
+
return;
|
|
42
|
+
if (document.visibilityState !== 'visible') {
|
|
43
|
+
complete('hidden', Date.now());
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
unfinished.current.add(complete);
|
|
47
|
+
const first = requestAnimationFrame(() => {
|
|
48
|
+
frames.current.delete(first);
|
|
49
|
+
const second = requestAnimationFrame(() => {
|
|
50
|
+
frames.current.delete(second);
|
|
51
|
+
unfinished.current.delete(complete);
|
|
52
|
+
if (element.current === null) {
|
|
53
|
+
complete('unmounted', Date.now());
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (document.visibilityState !== 'visible') {
|
|
57
|
+
complete('hidden', Date.now());
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
complete('painted', Date.now());
|
|
61
|
+
const elapsed = Date.now() - receivedAt;
|
|
62
|
+
if (elapsed < 0)
|
|
63
|
+
return; // Different clocks cannot establish a latency claim.
|
|
64
|
+
samples.current.push(elapsed);
|
|
65
|
+
if (samples.current.length > 256)
|
|
66
|
+
samples.current.shift();
|
|
67
|
+
element.current.dataset.livePaintSamples = JSON.stringify(samples.current);
|
|
68
|
+
});
|
|
69
|
+
frames.current.add(second);
|
|
70
|
+
});
|
|
71
|
+
frames.current.add(first);
|
|
72
|
+
}, [receivedAt, revision, baseline, diagnostics, sessionId, turn, surface]);
|
|
73
|
+
return _jsxs("section", { ref: element, "data-member-live": id, "data-live-received-at": receivedAt, children: [_jsxs("small", { children: [kind === 'think' ? t('stream.thinking') : t('stream.writing'), recovering ? ` · ${t('stream.recovering')}` : ''] }), _jsx(MarkdownText, { text: text, labels: { code: { copyLabel: t('stream.copy'), copiedLabel: t('stream.copied') }, footnotes: t('stream.footnotes') } })] });
|
|
74
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { LivePaintDiagnostics } from './live-paint.ts';
|
|
2
|
+
/** Read-only DOM probe retained in the composer after transient output settles. */
|
|
3
|
+
export declare function MemberPaintDiagnostics({ diagnostics, sessionId }: {
|
|
4
|
+
diagnostics: LivePaintDiagnostics;
|
|
5
|
+
sessionId: string;
|
|
6
|
+
}): import("react").JSX.Element;
|
|
7
|
+
//# sourceMappingURL=MemberPaintDiagnostics.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useLayoutEffect, useRef } from 'react';
|
|
3
|
+
/** Read-only DOM probe retained in the composer after transient output settles. */
|
|
4
|
+
export function MemberPaintDiagnostics({ diagnostics, sessionId }) {
|
|
5
|
+
const element = useRef(null);
|
|
6
|
+
useLayoutEffect(() => {
|
|
7
|
+
let timer;
|
|
8
|
+
const update = () => {
|
|
9
|
+
timer = undefined;
|
|
10
|
+
if (element.current)
|
|
11
|
+
element.current.dataset.livePaintReport = JSON.stringify(diagnostics.report(sessionId));
|
|
12
|
+
};
|
|
13
|
+
update();
|
|
14
|
+
// Exporting a long round must not serialize its samples on every token.
|
|
15
|
+
const unsubscribe = diagnostics.subscribe(() => { timer ??= setTimeout(update, 250); });
|
|
16
|
+
return () => { unsubscribe(); if (timer !== undefined)
|
|
17
|
+
clearTimeout(timer); };
|
|
18
|
+
}, [diagnostics, sessionId]);
|
|
19
|
+
return _jsx("span", { hidden: true, ref: element, "data-member-paint-diagnostics": sessionId });
|
|
20
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { LocalAgentMemberConfiguration, LocalAgentModelDirectory } from '../types.ts';
|
|
2
|
+
import type { LocalAgentKey } from './locales.ts';
|
|
3
|
+
export type ConfigurationTranslate = (key: LocalAgentKey) => string;
|
|
4
|
+
/** User choices are concrete; legacy inheritance stays a backend compatibility concern. */
|
|
5
|
+
export declare function ModelConfigurationFields({ directory, value, onChange, resolvedModel, resolvedEffort, disabled, showEffort, t }: {
|
|
6
|
+
directory?: LocalAgentModelDirectory | undefined;
|
|
7
|
+
value: LocalAgentMemberConfiguration;
|
|
8
|
+
onChange(value: LocalAgentMemberConfiguration): void;
|
|
9
|
+
resolvedModel?: string | undefined;
|
|
10
|
+
resolvedEffort?: string | undefined;
|
|
11
|
+
disabled?: boolean | undefined;
|
|
12
|
+
showEffort?: boolean | undefined;
|
|
13
|
+
t: ConfigurationTranslate;
|
|
14
|
+
}): import("react").JSX.Element;
|
|
15
|
+
//# sourceMappingURL=ModelConfigurationFields.d.ts.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import css from './MemberConfiguration.module.css';
|
|
4
|
+
/** User choices are concrete; legacy inheritance stays a backend compatibility concern. */
|
|
5
|
+
export function ModelConfigurationFields({ directory, value, onChange, resolvedModel, resolvedEffort, disabled, showEffort = true, t }) {
|
|
6
|
+
const [search, setSearch] = useState('');
|
|
7
|
+
const [pane, setPane] = useState(showEffort ? 'root' : 'model');
|
|
8
|
+
const entries = directory?.entries ?? [];
|
|
9
|
+
const model = value.model.mode === 'value' ? value.model.value : resolvedModel ?? entries.find(entry => !entry.hidden)?.value;
|
|
10
|
+
const entry = entries.find(entry => entry.value === model || entry.resolvedModel === model);
|
|
11
|
+
const options = entry?.reasoning?.options ?? [];
|
|
12
|
+
const effort = value.effort.mode === 'value' ? value.effort.value : resolvedEffort ?? entry?.reasoning?.default ?? options[0]?.value;
|
|
13
|
+
const query = search.trim().toLowerCase();
|
|
14
|
+
const candidates = entries.filter(entry => !query || `${entry.label} ${entry.value} ${entry.resolvedModel ?? ''}`.toLowerCase().includes(query));
|
|
15
|
+
const selectModel = (next) => {
|
|
16
|
+
const reasoning = entries.find(entry => entry.value === next)?.reasoning;
|
|
17
|
+
const nextEffort = next === model || reasoning?.options.some(option => option.value === effort) ? effort : reasoning?.default ?? reasoning?.options[0]?.value;
|
|
18
|
+
onChange({ model: { mode: 'value', value: next }, effort: nextEffort === undefined ? { mode: 'default' } : { mode: 'value', value: nextEffort } });
|
|
19
|
+
setPane(showEffort ? 'root' : 'model');
|
|
20
|
+
};
|
|
21
|
+
return _jsxs("fieldset", { className: css.fields, disabled: disabled, children: [pane === 'root' ? _jsxs(_Fragment, { children: [_jsxs("button", { type: "button", className: css.menuRow, onClick: () => setPane('model'), children: [_jsx("span", { children: t('configuration.model') }), ' ', _jsxs("span", { children: [entry?.label ?? model ?? t('loading'), " \u203A"] })] }), _jsxs("button", { type: "button", className: css.menuRow, disabled: options.length === 0, onClick: () => setPane('effort'), children: [_jsx("span", { children: t('configuration.effort') }), ' ', _jsxs("span", { children: [options.find(option => option.value === effort)?.label ?? effort ?? t('configuration.unavailable'), options.length ? ' ›' : ''] })] }), options.length === 0 && _jsx("small", { role: "status", children: t('configuration.effortUnknown') })] }) : _jsxs(_Fragment, { children: [showEffort && _jsxs("button", { type: "button", className: css.back, onClick: () => setPane('root'), children: ["\u2039 ", t('configuration.back')] }), pane === 'model' ? _jsxs(_Fragment, { children: [_jsx("input", { "aria-label": t('configuration.search'), placeholder: t('configuration.search'), value: search, onChange: event => setSearch(event.target.value) }), _jsxs("div", { className: css.candidates, role: "listbox", "aria-label": t('configuration.model'), children: [candidates.map(candidate => _jsxs("button", { type: "button", role: "option", "aria-selected": candidate.value === model, onClick: () => selectModel(candidate.value), children: [_jsxs("span", { className: css.optionLabel, children: [_jsx("strong", { children: candidate.label }), candidate.value === model && _jsx("span", { "aria-hidden": "true", className: css.check, children: "\u2713" })] }), _jsxs("small", { children: [candidate.value, candidate.resolvedModel ? ` → ${candidate.resolvedModel}` : ''] })] }, candidate.value)), candidates.length === 0 && _jsx("span", { children: t('configuration.empty') })] })] }) : _jsx("div", { className: css.candidates, role: "listbox", "aria-label": t('configuration.effort'), children: options.map(option => _jsxs("button", { type: "button", role: "option", "aria-selected": option.value === effort, onClick: () => { if (model)
|
|
22
|
+
onChange({ model: { mode: 'value', value: model }, effort: { mode: 'value', value: option.value } }); setPane('root'); }, children: [_jsxs("span", { className: css.optionLabel, children: [_jsx("strong", { children: option.label }), option.value === effort && _jsx("span", { "aria-hidden": "true", className: css.check, children: "\u2713" })] }), option.description && _jsx("small", { children: option.description })] }, option.value)) })] }), directory && directory.status !== 'ready' && _jsx("small", { role: "status", children: t(`configuration.directory.${directory.status}`) })] });
|
|
23
|
+
}
|