dsh-loop-engine 0.1.5-rc3 → 0.1.5-rc4
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.md +44 -187
- package/README.zh.md +45 -103
- package/lib/client.js +887 -266
- package/lib/index.js +2212 -914
- package/lib/invariant.js +43 -45
- package/lib/types/agent-preset-ids.d.ts +303 -0
- package/lib/types/client/LoopEngineBadge.d.ts +44 -17
- package/lib/types/client/LoopEngineComposerSelect.d.ts +79 -13
- package/lib/types/client/LoopEngineSection.d.ts +5 -4
- package/lib/types/client/locales.d.ts +133 -7
- package/lib/types/client/reload.d.ts +135 -0
- package/lib/types/client/session-engine.d.ts +474 -0
- package/lib/types/client/store.d.ts +1 -1
- package/lib/types/client/turn-status.d.ts +112 -10
- package/lib/types/client/use-session-engine.d.ts +66 -0
- package/lib/types/commands.d.ts +11 -3
- package/lib/types/driver-core/host-servers.d.ts +106 -0
- package/lib/types/driver-core/hosted-engine-runtime.d.ts +190 -0
- package/lib/types/driver-core/hosted-tool-vocabulary.d.ts +72 -0
- package/lib/types/driver-core/model-handover.d.ts +116 -0
- package/lib/types/driver-core/ownership.d.ts +6 -5
- package/lib/types/driver-core/prompt.d.ts +32 -0
- package/lib/types/driver-core/session-lifetime.d.ts +62 -0
- package/lib/types/driver-core/session-model.d.ts +82 -0
- package/lib/types/engine-claude/agent.d.ts +23 -3
- package/lib/types/engine-claude/loop.d.ts +16 -15
- package/lib/types/engine-codex/agent.d.ts +22 -3
- package/lib/types/engine-codex/appserver/client.d.ts +15 -2
- package/lib/types/engine-codex/loop.d.ts +13 -15
- package/lib/types/engine-codex/model-handover.d.ts +44 -0
- package/lib/types/engine-kimi/acp/client.d.ts +10 -0
- package/lib/types/engine-kimi/agent.d.ts +19 -2
- package/lib/types/engine-kimi/commands.d.ts +18 -14
- package/lib/types/engine-kimi/loop.d.ts +14 -16
- package/lib/types/engine-kimi/model-handover.d.ts +32 -0
- package/lib/types/engine-kimi/process.d.ts +2 -2
- package/lib/types/engine-kimi/types.d.ts +1 -1
- package/lib/types/engine-of-session.d.ts +97 -0
- package/lib/types/engine-pi/agent.d.ts +25 -23
- package/lib/types/engine-pi/loop.d.ts +13 -23
- package/lib/types/engine-pi/model-handover.d.ts +35 -0
- package/lib/types/engine-pi/types.d.ts +2 -2
- package/lib/types/engine-remote.d.ts +192 -0
- package/lib/types/engine-surface.d.ts +36 -0
- package/lib/types/index.d.ts +51 -50
- package/lib/types/invariant.d.ts +8 -5
- package/lib/types/model-selection-reset.d.ts +271 -0
- package/lib/types/patch-manager.d.ts +57 -39
- package/lib/types/preset.d.ts +39 -26
- package/lib/types/provider-route.d.ts +83 -36
- package/lib/types/router-loop.d.ts +406 -0
- package/lib/types/session-engine-store.d.ts +138 -0
- package/lib/types/settings.d.ts +12 -11
- package/package.json +109 -104
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-session engine, browser half: read the engine a session actually runs
|
|
3
|
+
* from the plugin's OWN Remote, and switch it through that same Remote.
|
|
4
|
+
*
|
|
5
|
+
* The engine a session runs is NOT a client-cache fact, and reading one as if it
|
|
6
|
+
* were is the bug this module exists to prevent. The harness hands a page a
|
|
7
|
+
* projection HINT per session (`@deepseek-ai/dsh-api-session-controller`'s
|
|
8
|
+
* `projectionsFor()` returns `SessionProjectionHints`: "every currently cached
|
|
9
|
+
* wire value", partial by its own documentation), and its `agentPreset` cell
|
|
10
|
+
* answers a different question than "what does this session run": a session may
|
|
11
|
+
* switch engine while it is blank (and, with this plugin, at any time), and that
|
|
12
|
+
* choice lives in the plugin's own record while the session header keeps naming
|
|
13
|
+
* the preset the session was CREATED with. A page reading the hint therefore
|
|
14
|
+
* shows a claude-code session that switched to pi — and the engine it actually
|
|
15
|
+
* runs — as Claude Code, the misreport this plugin shipped a production incident
|
|
16
|
+
* for.
|
|
17
|
+
*
|
|
18
|
+
* So the browser half asks the host instead:
|
|
19
|
+
* `remote.loopEngine.engine({ sessionId })` to read, and
|
|
20
|
+
* `remote.loopEngine.select({ sessionId, engine })` to switch — the endpoints
|
|
21
|
+
* `src/engine-remote.ts` serves from the same read the router routes on. Nothing
|
|
22
|
+
* is derived here: the three-state arrives decided, and a refused switch arrives
|
|
23
|
+
* with the host's own reason.
|
|
24
|
+
*
|
|
25
|
+
* This module is deliberately React-free: the cache, the contribution, and the
|
|
26
|
+
* switch are plain logic, so they run in node under its own spec
|
|
27
|
+
* (`tests/session-engine-cache.spec.ts`). The one part that needs a component to
|
|
28
|
+
* render — the hook that subscribes a surface to the cache — lives in
|
|
29
|
+
* `./use-session-engine.ts`.
|
|
30
|
+
*
|
|
31
|
+
* The cache owns NO document-level side effect, deliberately. The chat
|
|
32
|
+
* turn-status row is painted through a document-level attribute
|
|
33
|
+
* (`./turn-status.ts`), so the only writer entitled to it is the session on
|
|
34
|
+
* screen — and this cache publishes for every session a surface has ever
|
|
35
|
+
* watched, the session the user just left included. Reflecting from
|
|
36
|
+
* `publish` therefore let a background session's (or a left session's late)
|
|
37
|
+
* answer take the attribute over from the session on screen, which painted ITS
|
|
38
|
+
* engine onto the row beside it — a session running pi drew Kimi's moon. The
|
|
39
|
+
* reflection is driven from `./use-session-engine.ts` instead: that hook is
|
|
40
|
+
* where a component knows which session it is the surface of, and the reflection
|
|
41
|
+
* it makes carries a focus guard. Here the cache answers one question only —
|
|
42
|
+
* what one session's engine is.
|
|
43
|
+
*
|
|
44
|
+
* One member of the framework's standard seat is still read from the slot props:
|
|
45
|
+
* the session-scope `sessionId`. The harness declares it by declaration-merging
|
|
46
|
+
* `SessionStandardProps` in `@deepseek-ai/dsh-client-ui-session/client`, and its
|
|
47
|
+
* published declarations elide the type-only imports that carry the merge — so a
|
|
48
|
+
* half compiled against the installed artifacts (this package's
|
|
49
|
+
* `tsconfig.build.json` pins `@deepseek-ai/*` to the artifact plane) never sees
|
|
50
|
+
* it. It is therefore restated structurally here, in the shape the harness
|
|
51
|
+
* declares (`packages/client/ui-session/src/client/index.ts`), and the
|
|
52
|
+
* components assert it off their slot props. The `useSessions` hook needs no
|
|
53
|
+
* such restatement any more: nothing here reads the session list.
|
|
54
|
+
*
|
|
55
|
+
* The Remote's client-side types are restated structurally for the same reason
|
|
56
|
+
* and one more: a third-party plugin cannot reach the main repository's
|
|
57
|
+
* generated `typert.remote-client.d.ts`, and it does not need to — the protocol
|
|
58
|
+
* has no schema registry, so a contribution is plain data (see
|
|
59
|
+
* `LOOP_ENGINE_REMOTE_CONTRIBUTION`).
|
|
60
|
+
*
|
|
61
|
+
* @module dsh-loop-engine/client/session-engine
|
|
62
|
+
*/
|
|
63
|
+
import type { Context as ClientContext } from '@deepseek-ai/cordis';
|
|
64
|
+
import { type LoopEngineId, type LoopEngineRefusalCode, type LoopEngineSelectResult, type SessionEngine, type SessionEngineReport } from '../agent-preset-ids.ts';
|
|
65
|
+
import { type ReloadPage } from './reload.ts';
|
|
66
|
+
/**
|
|
67
|
+
* The three-state engine answer, re-exported so the client half names one type.
|
|
68
|
+
* It is DEFINED in the zero-import shared module: the node half decides it
|
|
69
|
+
* (`src/engine-of-session.ts`) and this half only renders what it is told.
|
|
70
|
+
*/
|
|
71
|
+
export type { SessionEngine } from '../agent-preset-ids.ts';
|
|
72
|
+
/**
|
|
73
|
+
* What one session's engine read answers: the engine it ACTUALLY runs, plus the
|
|
74
|
+
* engine a committed switch has recorded for it but not yet reached.
|
|
75
|
+
*
|
|
76
|
+
* Re-exported for the same reason as the three-state: the split between the two
|
|
77
|
+
* facts is decided on the host (`src/engine-of-session.ts`) and this half renders
|
|
78
|
+
* both without ever merging them — the pending engine belongs in a "not in force
|
|
79
|
+
* yet" marker, never in the place that names what the session runs.
|
|
80
|
+
*/
|
|
81
|
+
export type { SessionEngineReport } from '../agent-preset-ids.ts';
|
|
82
|
+
/**
|
|
83
|
+
* The standard seat members this half reads off a session-scope slot.
|
|
84
|
+
*
|
|
85
|
+
* Only the identity: the engine itself is no longer read from a framework hook,
|
|
86
|
+
* it is asked of the host through this plugin's own Remote.
|
|
87
|
+
*/
|
|
88
|
+
export interface SessionSeat {
|
|
89
|
+
/**
|
|
90
|
+
* Current Session identity. Undefined only off a seat rendered without a
|
|
91
|
+
* session: the composer then picks the new-session default, and the header chip
|
|
92
|
+
* (which only ever renders with a session) shows nothing.
|
|
93
|
+
*/
|
|
94
|
+
sessionId?: string;
|
|
95
|
+
}
|
|
96
|
+
/** Cordis service key AND wire namespace of the plugin's own Remote. */
|
|
97
|
+
export declare const LOOP_ENGINE_REMOTE_NAMESPACE = "loopEngine";
|
|
98
|
+
/** Reporting endpoint of the plugin's own Remote (mirrors `src/engine-remote.ts`). */
|
|
99
|
+
export declare const LOOP_ENGINE_REMOTE_METHOD = "engine";
|
|
100
|
+
/** Switching endpoint of the plugin's own Remote (mirrors `src/engine-remote.ts`). */
|
|
101
|
+
export declare const LOOP_ENGINE_REMOTE_SELECT_METHOD = "select";
|
|
102
|
+
/** One refused Remote call (harness: the error branch of `RemoteResult`). */
|
|
103
|
+
export interface RemoteFailure {
|
|
104
|
+
/** The host's own framing of the refusal. */
|
|
105
|
+
readonly message: string;
|
|
106
|
+
/** The refusal's detail map; a `reason` entry carries the unframed cause. */
|
|
107
|
+
readonly details?: Readonly<Record<string, unknown>>;
|
|
108
|
+
}
|
|
109
|
+
/** What `remote.loopEngine.engine` resolves to (harness: `RemoteResult<SessionEngineReport>`). */
|
|
110
|
+
export type SessionEngineResult = {
|
|
111
|
+
readonly ok: true;
|
|
112
|
+
readonly value: SessionEngineReport;
|
|
113
|
+
} | {
|
|
114
|
+
readonly ok: false;
|
|
115
|
+
readonly error: RemoteFailure;
|
|
116
|
+
};
|
|
117
|
+
/** What `remote.loopEngine.select` resolves to (harness: `RemoteResult<LoopEngineSelectResult>`). */
|
|
118
|
+
export type EngineSelectResult = {
|
|
119
|
+
readonly ok: true;
|
|
120
|
+
readonly value: LoopEngineSelectResult;
|
|
121
|
+
} | {
|
|
122
|
+
readonly ok: false;
|
|
123
|
+
readonly error: RemoteFailure;
|
|
124
|
+
};
|
|
125
|
+
/** This plugin's own Remote namespace, as the browser half calls it. */
|
|
126
|
+
export interface SessionEngineRemote {
|
|
127
|
+
/** Report the engine one session runs (and the recorded engine it is not running, if any). */
|
|
128
|
+
engine(request: {
|
|
129
|
+
readonly sessionId: string;
|
|
130
|
+
}): Promise<SessionEngineResult>;
|
|
131
|
+
/** Move one session to another engine, or answer why it was not moved. */
|
|
132
|
+
select(request: {
|
|
133
|
+
readonly sessionId: string;
|
|
134
|
+
readonly engine: LoopEngineId;
|
|
135
|
+
}): Promise<EngineSelectResult>;
|
|
136
|
+
}
|
|
137
|
+
/** Outcome of one per-session engine switch, ready to render. */
|
|
138
|
+
export type SessionSwitchResult = {
|
|
139
|
+
readonly ok: true;
|
|
140
|
+
/**
|
|
141
|
+
* Present when the host made the switch land by RELEASING the session's
|
|
142
|
+
* agent — a change with the harness loop on either side of it. This page has
|
|
143
|
+
* already been reloaded (and the session remembered for the page that
|
|
144
|
+
* replaces it) by the time a caller sees this, so a surface renders it as
|
|
145
|
+
* "the switch is happening" rather than as a session that can be typed into.
|
|
146
|
+
*/
|
|
147
|
+
readonly reload?: true;
|
|
148
|
+
}
|
|
149
|
+
/** This page cannot reach the plugin's own Remote at all. */
|
|
150
|
+
| {
|
|
151
|
+
readonly ok: false;
|
|
152
|
+
readonly kind: 'unavailable';
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The host refused the switch. `code` says which refusal it was, for the
|
|
156
|
+
* surface to render in the user's language, and `reason` is the host's own
|
|
157
|
+
* sentence about this session — detail under that copy, or the whole message
|
|
158
|
+
* when the code is absent (an unknown code, a rejected CALL, a lost
|
|
159
|
+
* connection: none of those has a refusal code, and none of them may leave the
|
|
160
|
+
* surface with nothing to show).
|
|
161
|
+
*/
|
|
162
|
+
| {
|
|
163
|
+
readonly ok: false;
|
|
164
|
+
readonly kind: 'refused';
|
|
165
|
+
readonly reason: string;
|
|
166
|
+
readonly code?: LoopEngineRefusalCode;
|
|
167
|
+
};
|
|
168
|
+
/** Move one session to another engine. */
|
|
169
|
+
export type SessionEngineSwitcher = (sessionId: string, engine: LoopEngineId) => Promise<SessionSwitchResult>;
|
|
170
|
+
/**
|
|
171
|
+
* Whether a pick on one session's engine can be committed at all — that is,
|
|
172
|
+
* whether the host has answered what that session runs.
|
|
173
|
+
*
|
|
174
|
+
* This is the composer's usability gate, and it is hostile to guessing on
|
|
175
|
+
* purpose. The reload judgement below is a statement about two engines, and with
|
|
176
|
+
* no answer there is nothing honest to state: an unknown session may reload in
|
|
177
|
+
* EITHER direction. Answering the question anyway would cost the user something
|
|
178
|
+
* either way — `true` raises a confirmation for a switch that may not reload
|
|
179
|
+
* (and the confirmation is the only thing keeping an UNEXPLAINED reload from
|
|
180
|
+
* looking like a glitch), while `false` commits a pick that reloads and takes
|
|
181
|
+
* this page's unsent draft with it. So the picker is simply not usable until the
|
|
182
|
+
* answer lands: the trigger is greyed out with its menu unopenable, and it keeps
|
|
183
|
+
* reading「读取中…」so the user can see what it is waiting for. The instant the
|
|
184
|
+
* answer arrives the control becomes usable, unchanged.
|
|
185
|
+
*
|
|
186
|
+
* It is a TYPE PREDICATE as well as the runtime gate, so "no pick is ever judged
|
|
187
|
+
* against an unknown engine" is enforced by the compiler: the composer narrows a
|
|
188
|
+
* session's report through this before it asks {@link switchNeedsReload}, whose
|
|
189
|
+
* parameter therefore cannot be undefined.
|
|
190
|
+
*
|
|
191
|
+
* A seat with NO session is not this function's business and never calls it: it
|
|
192
|
+
* writes the default for sessions created later, moves nothing, and reloads
|
|
193
|
+
* nothing, so there is no engine to wait for.
|
|
194
|
+
* @param report - the session's engine report, or undefined while the host has
|
|
195
|
+
* not answered yet (or cannot answer at all).
|
|
196
|
+
* @returns whether the engine is known — the one condition under which a pick on
|
|
197
|
+
* that session may be judged and sent.
|
|
198
|
+
*/
|
|
199
|
+
export declare function engineSwitchReady(report: SessionEngineReport | undefined): report is SessionEngineReport;
|
|
200
|
+
/**
|
|
201
|
+
* Whether moving a session from `current` to `target` has to land through a page
|
|
202
|
+
* reload — the one thing a surface must ask the user about BEFORE it commits,
|
|
203
|
+
* because the reload is what takes this page's unsent draft and scroll position
|
|
204
|
+
* with it.
|
|
205
|
+
*
|
|
206
|
+
* The rule is the host's own split (`src/router-loop.ts` `move`): a move with the
|
|
207
|
+
* harness loop on EITHER side cannot be handed over in place — `AgentLoop` neither
|
|
208
|
+
* hands a live session over nor accepts one it did not create — so the session's
|
|
209
|
+
* agent is released, the session goes cold, and the page reloads to rebuild it on
|
|
210
|
+
* the recorded engine. Between two hosted engines nothing reloads: the agent is
|
|
211
|
+
* swapped in place and the conversation stays open. So the move reloads exactly
|
|
212
|
+
* when the two engines differ in being the in-process one.
|
|
213
|
+
*
|
|
214
|
+
* `current` is the engine the session ACTUALLY runs — the report's `engine`, never
|
|
215
|
+
* the engine its record names, which is why this takes a {@link SessionEngine} and
|
|
216
|
+
* not a bare id:
|
|
217
|
+
*
|
|
218
|
+
* - a state that names an engine answers the rule above directly;
|
|
219
|
+
* - `legacy` and `unset` name no engine, and both are built on the in-process
|
|
220
|
+
* loop (`src/agent-preset-ids.ts` `hostedEngineOf` — the router falls back to
|
|
221
|
+
* the harness loop for a preset this plugin does not own), so they are read as
|
|
222
|
+
* the in-process side of the rule rather than as "unknown": picking any hosted
|
|
223
|
+
* engine on such a session really does reload.
|
|
224
|
+
*
|
|
225
|
+
* There is deliberately NO third branch for "the host has not answered yet": that
|
|
226
|
+
* state never reaches here, because the composer gates every pick on
|
|
227
|
+
* {@link engineSwitchReady} first (the picker is disabled until the answer lands),
|
|
228
|
+
* and the parameter's non-optional type is what holds that down. A judgement made
|
|
229
|
+
* without an answer would be a guess in one direction or the other, which is
|
|
230
|
+
* exactly what the gate exists to avoid.
|
|
231
|
+
*
|
|
232
|
+
* It is a pure function of the two engines on purpose: the composer's confirmation
|
|
233
|
+
* is only as trustworthy as this judgement, and this way it is pinned by a test
|
|
234
|
+
* rather than by reading the component.
|
|
235
|
+
* @param current - the engine the session runs now, as the host reports it. Never
|
|
236
|
+
* unknown: callers hold a report that passed {@link engineSwitchReady}.
|
|
237
|
+
* @param target - the engine the user picked.
|
|
238
|
+
* @returns whether committing that pick releases the session's agent and reloads
|
|
239
|
+
* this page.
|
|
240
|
+
*/
|
|
241
|
+
export declare function switchNeedsReload(current: SessionEngine, target: LoopEngineId): boolean;
|
|
242
|
+
/** A codec of one wire field (harness: `TypertCodec`, strict branch). */
|
|
243
|
+
interface RemoteCodec {
|
|
244
|
+
readonly mode: 'strict';
|
|
245
|
+
/** Stable identity of the field's declared type, for diagnostics. */
|
|
246
|
+
readonly typeSymbol: string;
|
|
247
|
+
readonly schema: {
|
|
248
|
+
parse(value: unknown): unknown;
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
/** One ordered business parameter (harness: `InvocationParameterDescriptor`). */
|
|
252
|
+
interface RemoteParameter {
|
|
253
|
+
readonly name: string;
|
|
254
|
+
/** Wire field this parameter occupies in `args`. */
|
|
255
|
+
readonly wire: string;
|
|
256
|
+
readonly source: 'json';
|
|
257
|
+
readonly codec: RemoteCodec;
|
|
258
|
+
}
|
|
259
|
+
/** One exported method (harness: `InvocationDescriptor`). */
|
|
260
|
+
interface RemoteMethod {
|
|
261
|
+
readonly id: string;
|
|
262
|
+
readonly service: string;
|
|
263
|
+
readonly namespace: string;
|
|
264
|
+
readonly method: string;
|
|
265
|
+
readonly invocation: {
|
|
266
|
+
readonly kind: 'direct';
|
|
267
|
+
};
|
|
268
|
+
readonly parameters: readonly RemoteParameter[];
|
|
269
|
+
readonly result: RemoteCodec;
|
|
270
|
+
}
|
|
271
|
+
/** One package's exported methods (harness: `TypertRemoteContribution`). */
|
|
272
|
+
export interface RemoteContribution {
|
|
273
|
+
readonly package: string;
|
|
274
|
+
readonly descriptors: readonly RemoteMethod[];
|
|
275
|
+
}
|
|
276
|
+
/** The Client Remote service, as much of it as this half calls (harness: `ClientRemote`). */
|
|
277
|
+
export interface EngineRemoteHost {
|
|
278
|
+
/** Install one contribution's namespaces for the calling fiber. */
|
|
279
|
+
$mount(contribution: RemoteContribution): Promise<() => Promise<void>>;
|
|
280
|
+
}
|
|
281
|
+
/** The plugin's npm package name, as the contribution identifies itself. */
|
|
282
|
+
export declare const LOOP_ENGINE_PACKAGE = "dsh-loop-engine";
|
|
283
|
+
/** The plugin's own Remote, declared for the Gateway's client mount. */
|
|
284
|
+
export declare const LOOP_ENGINE_REMOTE_CONTRIBUTION: RemoteContribution;
|
|
285
|
+
/**
|
|
286
|
+
* What one session's engine is, cached per session.
|
|
287
|
+
*
|
|
288
|
+
* Every read goes to the host's authoritative answer and is cached by session id
|
|
289
|
+
* so the chip and the composer in the same header cannot disagree. Until the
|
|
290
|
+
* first answer arrives a session has NO engine here: {@link read} returns
|
|
291
|
+
* undefined, and the surfaces render nothing (chip) or a neutral loading state
|
|
292
|
+
* (composer) rather than a guess — a default or a list hint would be exactly the
|
|
293
|
+
* misreport this class exists to end.
|
|
294
|
+
*
|
|
295
|
+
* What is cached is the whole {@link SessionEngineReport}, the recorded engine
|
|
296
|
+
* that differs from the live one included, because the three surfaces read
|
|
297
|
+
* different halves of it: the chip, the composer's label, and the chat
|
|
298
|
+
* turn-status row all name the engine the session RUNS, and only the first two
|
|
299
|
+
* also carry the "recorded, not in force" marker. Merging the two here would
|
|
300
|
+
* make that distinction unavailable downstream; leaving the report intact lets
|
|
301
|
+
* each surface take the half it renders.
|
|
302
|
+
*
|
|
303
|
+
* A cached answer is never treated as final for the page's lifetime: a session
|
|
304
|
+
* the user comes back to is re-read when its surfaces appear again
|
|
305
|
+
* ({@link watch} → {@link refresh}). Coming back does not move the session — a
|
|
306
|
+
* switch is performed on the host, between hosted engines it swaps the agent in
|
|
307
|
+
* place, and the direction that has to release the session reloads this very
|
|
308
|
+
* page (`./reload.ts`) — but the answer this page holds is still only one this
|
|
309
|
+
* page took earlier: another window, a hot swap performed elsewhere, or a plugin
|
|
310
|
+
* reload can have moved the session since. The answer a surface holds is
|
|
311
|
+
* therefore correct for as long as that surface is on screen, and refreshed at
|
|
312
|
+
* every re-mount — not only when a switch this page performed invalidated it
|
|
313
|
+
* ({@link invalidate}).
|
|
314
|
+
*
|
|
315
|
+
* Its answer is also what the chat turn-status row draws (the third place a
|
|
316
|
+
* session's engine is shown), but through the surfaces rather than through this
|
|
317
|
+
* class: the row follows the session on screen because the hook both surfaces
|
|
318
|
+
* share reflects that session's answer with a focus guard — the same authority
|
|
319
|
+
* the chip and the composer read, where the settings default it used to follow
|
|
320
|
+
* was not. Nothing here writes to the document; see `./turn-status.ts` and
|
|
321
|
+
* `./use-session-engine.ts`.
|
|
322
|
+
*/
|
|
323
|
+
export declare class SessionEngineCache {
|
|
324
|
+
private readonly engines;
|
|
325
|
+
private readonly watchers;
|
|
326
|
+
private readonly reading;
|
|
327
|
+
/**
|
|
328
|
+
* Monotonic read generation per session. A read started before an
|
|
329
|
+
* invalidation describes the session as it was BEFORE the switch, so however
|
|
330
|
+
* late it settles it must not publish — publishing it would put the old engine
|
|
331
|
+
* back on screen, which is the very misreport this cache exists to prevent.
|
|
332
|
+
*/
|
|
333
|
+
private readonly generations;
|
|
334
|
+
private remote;
|
|
335
|
+
private disposed;
|
|
336
|
+
/**
|
|
337
|
+
* Attach the mounted Remote namespace. Values asked for before the mount
|
|
338
|
+
* settled are re-read, so a surface that mounted first is not left loading.
|
|
339
|
+
* @param remote - the namespace, or undefined when the mount failed.
|
|
340
|
+
*/
|
|
341
|
+
attach(remote: SessionEngineRemote | undefined): void;
|
|
342
|
+
/**
|
|
343
|
+
* The engine report cached for one session, or undefined while the first
|
|
344
|
+
* answer is still in flight (or when the host could not answer at all).
|
|
345
|
+
* @param sessionId - the session whose engine is asked for.
|
|
346
|
+
* @returns the authoritative report, cached by session id.
|
|
347
|
+
*/
|
|
348
|
+
read(sessionId: string): SessionEngineReport | undefined;
|
|
349
|
+
/**
|
|
350
|
+
* Follow one session's engine, re-reading it whenever a session's surfaces
|
|
351
|
+
* appear again.
|
|
352
|
+
*
|
|
353
|
+
* A watch is a surface appearing for this session, and that is the moment the
|
|
354
|
+
* cached answer can be stale in a way nothing else reports: a session whose
|
|
355
|
+
* surfaces all went away and came back — the page switched to another session
|
|
356
|
+
* and back — must not render what the page cached before. A re-mount is not
|
|
357
|
+
* what moves a session's engine (a switch is performed on the host, and either
|
|
358
|
+
* swaps the agent in place or releases the session and reloads this page), but
|
|
359
|
+
* the page's answer is old by then, and a surface that rendered it as current
|
|
360
|
+
* would be the one lying about what runs.
|
|
361
|
+
*
|
|
362
|
+
* Only the FIRST watcher of a session triggers the re-read, which is what
|
|
363
|
+
* makes this "the session's surfaces appeared" rather than "a component
|
|
364
|
+
* rendered": the chip and the composer are two watchers of one session, and
|
|
365
|
+
* the second joins the read the first one started instead of sending its own.
|
|
366
|
+
* While a session keeps its surfaces, nothing here asks again — the answer only
|
|
367
|
+
* changes through {@link invalidate} (a committed switch) or a later re-mount.
|
|
368
|
+
* @param sessionId - the session to follow.
|
|
369
|
+
* @param listener - called whenever that session's cached state changes.
|
|
370
|
+
* @returns the unsubscribe.
|
|
371
|
+
*/
|
|
372
|
+
watch(sessionId: string, listener: () => void): () => void;
|
|
373
|
+
/**
|
|
374
|
+
* Ask the host for one session's engine report again, superseding nothing.
|
|
375
|
+
*
|
|
376
|
+
* This is the way a surface that has just been rendered asks "is what this page
|
|
377
|
+
* cached still what runs?" — the answer that can go stale without anyone
|
|
378
|
+
* invalidating it, because the page is not the only thing that can move a
|
|
379
|
+
* session (another window, a hot swap elsewhere, a plugin reload).
|
|
380
|
+
* A read already in flight for that session is joined rather than replaced (it
|
|
381
|
+
* was started no earlier than this call and describes the same moment), and the
|
|
382
|
+
* cached answer is kept until the new one lands, so a re-read never blinks the
|
|
383
|
+
* chip back to "reading". Use {@link invalidate} instead for an answer that is
|
|
384
|
+
* known to be WRONG.
|
|
385
|
+
* @param sessionId - the session whose answer should be re-read.
|
|
386
|
+
*/
|
|
387
|
+
refresh(sessionId: string): void;
|
|
388
|
+
/**
|
|
389
|
+
* Forget one session's cached engine and ask the host again.
|
|
390
|
+
*
|
|
391
|
+
* A successful engine switch changes what the host answers for that session,
|
|
392
|
+
* so the picker MUST come through here: without it the composer would keep
|
|
393
|
+
* showing the engine the session no longer runs. Any read already in flight
|
|
394
|
+
* for that session is superseded and can no longer publish.
|
|
395
|
+
* @param sessionId - the session whose answer is now stale.
|
|
396
|
+
*/
|
|
397
|
+
invalidate(sessionId: string): void;
|
|
398
|
+
/** Drop every cached answer and notification (plugin unload). */
|
|
399
|
+
dispose(): void;
|
|
400
|
+
/**
|
|
401
|
+
* Ask unless the session is already answered, being read, or unanswerable.
|
|
402
|
+
*
|
|
403
|
+
* For the paths that need an answer and have none: the plugin's mount settling
|
|
404
|
+
* (a surface asked before the Remote existed), a session whose cached answer
|
|
405
|
+
* was just dropped — and never a re-read of an answer this page already holds,
|
|
406
|
+
* which is {@link refresh}'s job and only a freshly rendered surface may ask
|
|
407
|
+
* for.
|
|
408
|
+
*/
|
|
409
|
+
private readIfUnknown;
|
|
410
|
+
/**
|
|
411
|
+
* Start one read for a session, unless there is nothing to read it with.
|
|
412
|
+
* @param sessionId - the session to read.
|
|
413
|
+
*/
|
|
414
|
+
private start;
|
|
415
|
+
/**
|
|
416
|
+
* One read, quiet on failure: a session the host cannot answer for stays
|
|
417
|
+
* unknown, and the surfaces keep their neutral state instead of claiming an
|
|
418
|
+
* engine. A refusal is not cached as an answer, so the next watch re-asks.
|
|
419
|
+
* @param remote - the mounted namespace.
|
|
420
|
+
* @param sessionId - the session asked about.
|
|
421
|
+
* @param generation - the read generation this answer belongs to.
|
|
422
|
+
*/
|
|
423
|
+
private ask;
|
|
424
|
+
/**
|
|
425
|
+
* Notify one session's watchers.
|
|
426
|
+
*
|
|
427
|
+
* Nothing is painted here. The session's engine shows up in the chat
|
|
428
|
+
* turn-status row too, but that row hangs off a DOCUMENT-level attribute, whose
|
|
429
|
+
* only entitled owner is the session on screen — and a publish says nothing
|
|
430
|
+
* about what is on screen (this is called for every session a surface has ever
|
|
431
|
+
* watched, and for a session the user has already left, when its late answer
|
|
432
|
+
* lands). So painting here let a background session take the attribute over
|
|
433
|
+
* from the session on screen. The surfaces reflect instead, with the focus
|
|
434
|
+
* guard in `./turn-status.ts`; this is the notification the chip and the
|
|
435
|
+
* composer wake up on.
|
|
436
|
+
*/
|
|
437
|
+
private publish;
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Mount this plugin's own engine Remote and return the cache the session
|
|
441
|
+
* surfaces render from.
|
|
442
|
+
*
|
|
443
|
+
* `remote` is taken lazily rather than declared in the client half's `inject`:
|
|
444
|
+
* the settings page must keep working on a page that composes no Gateway, and
|
|
445
|
+
* the two session surfaces degrade to "engine not recorded" — never to a guess.
|
|
446
|
+
* @param ctx - the client root context.
|
|
447
|
+
* @returns the cache both session surfaces and the switcher share.
|
|
448
|
+
*/
|
|
449
|
+
export declare function createSessionEngineCache(ctx: ClientContext): SessionEngineCache;
|
|
450
|
+
/**
|
|
451
|
+
* Build the per-session switch over this plugin's own Remote.
|
|
452
|
+
*
|
|
453
|
+
* Every refusal is reported rather than thrown: the host's own reason when it
|
|
454
|
+
* refused (the session is not open, it is mid-turn, it belongs to subagent
|
|
455
|
+
* routing, or the engine record could not be written), and this page's own words
|
|
456
|
+
* when the namespace is not reachable. A switch that DID land drops the cached
|
|
457
|
+
* answer, because the host's read for that session just changed.
|
|
458
|
+
*
|
|
459
|
+
* A switch the host had to make by RELEASING the session's agent is finished
|
|
460
|
+
* here rather than reported: the page is reloaded — the only action that clears
|
|
461
|
+
* the client state a `session/disposed` leaves behind — and the session is
|
|
462
|
+
* stashed for the page that replaces it, so the reload lands back on it
|
|
463
|
+
* (`./reload.ts` explains why each half is necessary). That is why this function
|
|
464
|
+
* owns the reload instead of leaving it to a component: it is the one place that
|
|
465
|
+
* knows the host released the agent, and it keeps the sequence (invalidate →
|
|
466
|
+
* stash → reload) in one testable step.
|
|
467
|
+
* @param ctx - the client context carrying the Remote namespace.
|
|
468
|
+
* @param onSwitched - called with the session id once the host accepted the
|
|
469
|
+
* switch, so the caller can drop the cached engine it just changed.
|
|
470
|
+
* @param page - the page to reload and to stash the return in.
|
|
471
|
+
* @returns the switch the composer's picker commits through.
|
|
472
|
+
*/
|
|
473
|
+
export declare function sessionEngineSwitcher(ctx: ClientContext, onSwitched?: (sessionId: string) => void, page?: ReloadPage): SessionEngineSwitcher;
|
|
474
|
+
//# sourceMappingURL=session-engine.d.ts.map
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { SettingsScope } from '@deepseek-ai/dsh-client-ui-settings/client';
|
|
7
7
|
import type { SnapshotStore } from '@deepseek-ai/dsh-client-store';
|
|
8
|
-
import type { LoopEngineId } from '../
|
|
8
|
+
import type { LoopEngineId } from '../agent-preset-ids.ts';
|
|
9
9
|
/** State rendered by the loop engine section. */
|
|
10
10
|
export interface LoopEngineState {
|
|
11
11
|
status: 'loading' | 'ready' | 'unavailable' | 'saving';
|
|
@@ -6,8 +6,46 @@
|
|
|
6
6
|
* namespace (a second `locale.register` for the same namespace throws), so a
|
|
7
7
|
* plugin cannot change the text. What it CAN do is restyle the element, and
|
|
8
8
|
* that is all this module does — it paints an engine-specific glyph and color
|
|
9
|
-
* onto the row while a hosted engine
|
|
10
|
-
* alone otherwise.
|
|
9
|
+
* onto the row while the session on screen runs a hosted engine, and leaves the
|
|
10
|
+
* stock look alone otherwise.
|
|
11
|
+
*
|
|
12
|
+
* WHICH engine it paints is the SESSION ON SCREEN's, not the settings default.
|
|
13
|
+
* {@link reflectTurnStatusEngine} is driven from the per-session engine cache
|
|
14
|
+
* (`./session-engine.ts`) — the same authoritative answer the header chip and
|
|
15
|
+
* the composer render from, the plugin's own Remote read off the session's
|
|
16
|
+
* durable log — and it is driven by the ONE hook those two surfaces share
|
|
17
|
+
* (`./use-session-engine.ts`). So this row cannot disagree with the two surfaces
|
|
18
|
+
* beside it; while it was keyed off the settings store it did, and painted
|
|
19
|
+
* Claude Code's glyph onto every session's row whenever claude-code was the
|
|
20
|
+
* default, a session running pi included.
|
|
21
|
+
*
|
|
22
|
+
* The reflection's SUBJECT is still a document-level attribute rather than the
|
|
23
|
+
* row's own element: the sheet has to reach a class the harness hashes
|
|
24
|
+
* (`[class$="_turnStatus"]`), which takes an attribute selector on an ancestor,
|
|
25
|
+
* and the row offers no session-scoped hook for a plugin to hang it on. A
|
|
26
|
+
* document-level attribute has exactly ONE owner at a time, though — and on this
|
|
27
|
+
* page the WRITERS outnumber the reader: the chip and the composer of every
|
|
28
|
+
* session that has been rendered carry an answer, the session the user has just
|
|
29
|
+
* left included, and the cache publishes a session's answer regardless of what is
|
|
30
|
+
* on screen. So "whoever reflected last" is not "the session on screen", and an
|
|
31
|
+
* implementation that read it as one shipped the bug this guard ends: a
|
|
32
|
+
* background session's answer — or the late answer of the session the user just
|
|
33
|
+
* left — painted ITS engine onto the row of the session on screen, and a session
|
|
34
|
+
* running pi showed Kimi's moon.
|
|
35
|
+
*
|
|
36
|
+
* The owner is therefore DECLARED, not inferred: a reflection names the session
|
|
37
|
+
* it speaks for and writes only while that session is the row's focus
|
|
38
|
+
* ({@link focusTurnStatusSession}); a reflection whose subject is not the focused
|
|
39
|
+
* session is a NO-OP, so only the session on screen can paint and only it can
|
|
40
|
+
* un-paint. The chip and the composer of one session reflect the same value (the
|
|
41
|
+
* write is idempotent); the focus is withdrawn by
|
|
42
|
+
* {@link blurTurnStatusSession}, which acts only while the focus is still the
|
|
43
|
+
* departing session's (React runs a departing component's cleanup in no
|
|
44
|
+
* guaranteed order against an arriving one's, so an unconditional withdrawal
|
|
45
|
+
* would un-focus the session that just took over); and the attribute itself is
|
|
46
|
+
* never withdrawn on unmount: the sibling surface of the same session still
|
|
47
|
+
* paints by it, and a leftover attribute is inert on a page whose turn-status row
|
|
48
|
+
* is not rendered.
|
|
11
49
|
*
|
|
12
50
|
* Three facts about the harness markup make that safe and specific:
|
|
13
51
|
* - the row carries exactly one class whose `[hash]_turnStatus` suffix is
|
|
@@ -28,16 +66,80 @@
|
|
|
28
66
|
* @module dsh-loop-engine/client/turn-status
|
|
29
67
|
*/
|
|
30
68
|
import type { Context as ClientContext } from '@deepseek-ai/cordis';
|
|
31
|
-
import type {
|
|
32
|
-
|
|
69
|
+
import type { LoopEngineId } from '../agent-preset-ids.ts';
|
|
70
|
+
/**
|
|
71
|
+
* Reflect the engine of the session ON SCREEN; `undefined` restores the stock
|
|
72
|
+
* row.
|
|
73
|
+
*
|
|
74
|
+
* The caller names the session it speaks for, and that is the row's SUBJECT: a
|
|
75
|
+
* reflection paints its session's engine only while that session is the focused
|
|
76
|
+
* one ({@link focusTurnStatusSession}), and is a NO-OP otherwise — the whole
|
|
77
|
+
* guard, and the reason the attribute can no longer be taken over by a session
|
|
78
|
+
* the user is not looking at. The subject is an explicit argument rather than
|
|
79
|
+
* ambient state, so a caller cannot reflect without saying whose engine it is,
|
|
80
|
+
* and cannot say "whoever, I don't know".
|
|
81
|
+
*
|
|
82
|
+
* `in-process` is the harness's own row, and `undefined` is every answer that
|
|
83
|
+
* names no engine at all: a session the host reports as `legacy` (it ran a
|
|
84
|
+
* hosted engine, the id never said which) or `unset`, a session whose first
|
|
85
|
+
* answer has not landed yet, and an answer dropped in preparation for a re-read.
|
|
86
|
+
* None of them paints — naming an engine for an unknown one is the misreport
|
|
87
|
+
* this module was fixed for — so each clears the attribute. The clear is as
|
|
88
|
+
* authoritative as the paint: a session that takes the focus with no answer yet
|
|
89
|
+
* takes the previous session's paint off with it.
|
|
90
|
+
*
|
|
91
|
+
* The write is idempotent and never a withdrawal: the chip and the composer of
|
|
92
|
+
* one session both come through here with the same value, and unmounting never
|
|
93
|
+
* clears the attribute, because the sibling surface of the same session is still
|
|
94
|
+
* on screen painting through it.
|
|
95
|
+
*
|
|
96
|
+
* The NO-OP covers the late reflection as well — the one from the session the
|
|
97
|
+
* user has just left, whose own answer landing after the switch used to paint
|
|
98
|
+
* over the session that replaced it.
|
|
99
|
+
* @param sessionId - the session this reflection speaks for, or undefined for a
|
|
100
|
+
* page with no session at all (the new-session page, which has no turn-status
|
|
101
|
+
* row): such a page has nothing to say about the row, so nothing is written and
|
|
102
|
+
* nothing is withdrawn — it leaves the row it found alone.
|
|
103
|
+
* @param engine - the engine that session runs, when it is known.
|
|
104
|
+
*/
|
|
105
|
+
export declare function reflectTurnStatusEngine(sessionId: string | undefined, engine: LoopEngineId | undefined): void;
|
|
106
|
+
/**
|
|
107
|
+
* Declare which session's surfaces are on screen — the row's one subject.
|
|
108
|
+
*
|
|
109
|
+
* Only the focused session may paint ({@link reflectTurnStatusEngine}), which is
|
|
110
|
+
* what keeps the row on the session the user is looking at: the alternative,
|
|
111
|
+
* "whoever writes last owns the row", was the bug this guard ends, because the
|
|
112
|
+
* chip and the composer of EVERY session that has been rendered write here, and
|
|
113
|
+
* a session's answer can land after the user has left it.
|
|
114
|
+
*
|
|
115
|
+
* The caller is the component that renders that session: only a component knows
|
|
116
|
+
* which session is the one on screen, and the hook both of that session's
|
|
117
|
+
* surfaces share (`./use-session-engine.ts`) is the only place that declares it.
|
|
118
|
+
* @param sessionId - the session whose surfaces are now on screen.
|
|
119
|
+
*/
|
|
120
|
+
export declare function focusTurnStatusSession(sessionId: string): void;
|
|
121
|
+
/**
|
|
122
|
+
* Withdraw the focus — but only while it is still this session's.
|
|
123
|
+
*
|
|
124
|
+
* The guard is the point: React offers no ordering guarantee between a departing
|
|
125
|
+
* component's cleanup and an arriving one's, so an unconditional withdrawal would
|
|
126
|
+
* let the session the user has just left un-focus the session that replaced it
|
|
127
|
+
* and silence the row. A session that is no longer the focus has nothing to
|
|
128
|
+
* withdraw; unmounting also never clears the attribute itself, which the sibling
|
|
129
|
+
* surface of the same session (and, for a session that is still on screen, the
|
|
130
|
+
* next focus) still paints by.
|
|
131
|
+
* @param sessionId - the session whose surface is going away.
|
|
132
|
+
*/
|
|
133
|
+
export declare function blurTurnStatusSession(sessionId: string): void;
|
|
33
134
|
/**
|
|
34
|
-
* Install the per-engine turn-status
|
|
135
|
+
* Install the per-engine turn-status stylesheet for the lifetime of `ctx`.
|
|
35
136
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
137
|
+
* This is the sheet and nothing else: which engine it selects is written by
|
|
138
|
+
* {@link reflectTurnStatusEngine}, driven from the session on screen's own
|
|
139
|
+
* authoritative answer (`./session-engine.ts`), through the hook the chip and
|
|
140
|
+
* the composer share (`./use-session-engine.ts`) — not from the settings
|
|
141
|
+
* default.
|
|
39
142
|
* @param ctx - the client root context.
|
|
40
|
-
* @param store - the loop-engine controller's snapshot source.
|
|
41
143
|
*/
|
|
42
|
-
export declare function installTurnStatusStyles(ctx: ClientContext
|
|
144
|
+
export declare function installTurnStatusStyles(ctx: ClientContext): void;
|
|
43
145
|
//# sourceMappingURL=turn-status.d.ts.map
|