@x1a0f3n9/dsh-client-ui-commands 0.1.5-rc.3
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/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +95 -0
- package/README.zh.md +95 -0
- package/lib/client.js +1295 -0
- package/lib/index.js +12 -0
- package/lib/types/client/PopupSelectView.d.ts +16 -0
- package/lib/types/client/contract.d.ts +106 -0
- package/lib/types/client/directory.d.ts +75 -0
- package/lib/types/client/index.d.ts +37 -0
- package/lib/types/client/locales.d.ts +71 -0
- package/lib/types/client/popup.d.ts +154 -0
- package/lib/types/client/presentation.d.ts +23 -0
- package/lib/types/client/resolution.d.ts +35 -0
- package/lib/types/client/service.d.ts +152 -0
- package/lib/types/index.d.ts +10 -0
- package/package.json +72 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,1295 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@x1a0f3n9/dsh-client-ui-commands",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let _deepseek_ai_cordis = require("@deepseek-ai/cordis");
|
|
8
|
+
let _x1a0f3n9_dsh_client_ui_primitives = require("@x1a0f3n9/dsh-client-ui-primitives");
|
|
9
|
+
let _x1a0f3n9_dsh_client_store = require("@x1a0f3n9/dsh-client-store");
|
|
10
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
11
|
+
let react = require("react");
|
|
12
|
+
//#region lib/types/client/locales.js
|
|
13
|
+
/**
|
|
14
|
+
* `command` namespace dictionaries: the composer menu's section headings,
|
|
15
|
+
* the client face (title, description, claim token) of the built-in Host
|
|
16
|
+
* commands whose catalog descriptors carry English text only, and the
|
|
17
|
+
* popupSelect shell's copy.
|
|
18
|
+
*/
|
|
19
|
+
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
20
|
+
const zh = {
|
|
21
|
+
"section.add": "添加",
|
|
22
|
+
"section.commands": "指令",
|
|
23
|
+
"label.goal": "目标",
|
|
24
|
+
"label.plan": "计划",
|
|
25
|
+
"label.feedback": "反馈",
|
|
26
|
+
"label.compact": "压缩",
|
|
27
|
+
"label.permission": "权限",
|
|
28
|
+
"label.export": "下载日志",
|
|
29
|
+
"description.goal": "设置或查看长期任务目标",
|
|
30
|
+
"description.plan": "进入或退出计划模式",
|
|
31
|
+
"description.feedback": "发送关于当前会话的反馈",
|
|
32
|
+
"description.compact": "压缩以上对话内容",
|
|
33
|
+
"description.permission": "切换权限预设(沙箱模式与审批策略)",
|
|
34
|
+
"description.export": "将当前会话内容导出为 ZIP",
|
|
35
|
+
"token.goal": "目标",
|
|
36
|
+
"token.plan": "计划",
|
|
37
|
+
"token.feedback": "反馈",
|
|
38
|
+
"token.compact": "压缩",
|
|
39
|
+
"token.permission": "权限",
|
|
40
|
+
"token.export": "导出",
|
|
41
|
+
"search.placeholder": "搜索…",
|
|
42
|
+
"search.aria": "筛选选项",
|
|
43
|
+
"status.loading": "正在加载选项…",
|
|
44
|
+
"status.applying": "正在应用…",
|
|
45
|
+
"status.empty": "无选项",
|
|
46
|
+
"overlay.aria": "/{command} 选项",
|
|
47
|
+
"listbox.aria": "/{command} 匹配项",
|
|
48
|
+
"notice.attachmentsUnsupported": "/{command} 不接受附件,请先移除附件"
|
|
49
|
+
};
|
|
50
|
+
/** English dictionary, checked complete against the zh key set. */
|
|
51
|
+
const en = {
|
|
52
|
+
"section.add": "Add",
|
|
53
|
+
"section.commands": "Commands",
|
|
54
|
+
"label.goal": "Goal",
|
|
55
|
+
"label.plan": "Plan",
|
|
56
|
+
"label.feedback": "Feedback",
|
|
57
|
+
"label.compact": "Compact",
|
|
58
|
+
"label.permission": "Permission",
|
|
59
|
+
"label.export": "Export",
|
|
60
|
+
"description.goal": "Set or view the goal for a long-running task",
|
|
61
|
+
"description.plan": "Enter or leave plan mode",
|
|
62
|
+
"description.feedback": "Record feedback about this session",
|
|
63
|
+
"description.compact": "Compact older conversation history",
|
|
64
|
+
"description.permission": "Switch the permission preset (sandbox mode + approval policy)",
|
|
65
|
+
"description.export": "Download this Session log as a ZIP archive",
|
|
66
|
+
"token.goal": "goal",
|
|
67
|
+
"token.plan": "plan",
|
|
68
|
+
"token.feedback": "feedback",
|
|
69
|
+
"token.compact": "compact",
|
|
70
|
+
"token.permission": "permission",
|
|
71
|
+
"token.export": "export",
|
|
72
|
+
"search.placeholder": "Search…",
|
|
73
|
+
"search.aria": "Filter options",
|
|
74
|
+
"status.loading": "Loading options…",
|
|
75
|
+
"status.applying": "Applying…",
|
|
76
|
+
"status.empty": "No options",
|
|
77
|
+
"overlay.aria": "/{command} options",
|
|
78
|
+
"listbox.aria": "/{command} matches",
|
|
79
|
+
"notice.attachmentsUnsupported": "/{command} does not accept attachments; remove them first"
|
|
80
|
+
};
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region lib/types/client/resolution.js
|
|
83
|
+
const BUILTINS = {
|
|
84
|
+
goal: "@x1a0f3n9/dsh-command-goal",
|
|
85
|
+
plan: "@x1a0f3n9/dsh-plan-mode",
|
|
86
|
+
feedback: "@x1a0f3n9/dsh-command-feedback",
|
|
87
|
+
compact: "@x1a0f3n9/dsh-command-compact",
|
|
88
|
+
permission: "@x1a0f3n9/dsh-permission-presets",
|
|
89
|
+
export: "@x1a0f3n9/dsh-session-log-export"
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Identify a first-party definition without interpreting its display copy.
|
|
93
|
+
* @param descriptor - effective Host descriptor after scoped shadowing.
|
|
94
|
+
* @returns its first-party name, or undefined for another definition.
|
|
95
|
+
*/
|
|
96
|
+
function builtinCommandName(descriptor) {
|
|
97
|
+
return Object.keys(BUILTINS).find((name) => descriptor.definitionId === BUILTINS[name]);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Select the input spelling for a menu-picked command.
|
|
101
|
+
* @param descriptor - effective Host descriptor.
|
|
102
|
+
* @param t - command-namespace translator.
|
|
103
|
+
* @returns localized spelling for a known definition, otherwise its registered name.
|
|
104
|
+
*/
|
|
105
|
+
function claimToken(descriptor, t) {
|
|
106
|
+
const name = builtinCommandName(descriptor);
|
|
107
|
+
return name === void 0 ? descriptor.name : t(`token.${name}`);
|
|
108
|
+
}
|
|
109
|
+
const TOKEN_ALIASES = new Map(Object.keys(BUILTINS).flatMap((name) => [zh[`token.${name}`], en[`token.${name}`]].map((token) => [token, name])));
|
|
110
|
+
/**
|
|
111
|
+
* Resolve typed spelling against the current Session's effective definitions.
|
|
112
|
+
* @param token - typed name without its leading slash.
|
|
113
|
+
* @param descriptors - effective descriptors in the Session's ready catalog.
|
|
114
|
+
* @returns the matching descriptor; aliases never select an unrelated scoped override.
|
|
115
|
+
*/
|
|
116
|
+
function resolveCommand(token, descriptors) {
|
|
117
|
+
const exact = descriptors.find((descriptor) => descriptor.name === token);
|
|
118
|
+
if (exact !== void 0) return exact;
|
|
119
|
+
const name = TOKEN_ALIASES.get(token);
|
|
120
|
+
if (name === void 0) return void 0;
|
|
121
|
+
return descriptors.find((descriptor) => descriptor.definitionId === BUILTINS[name]);
|
|
122
|
+
}
|
|
123
|
+
//#endregion
|
|
124
|
+
//#region lib/types/client/directory.js
|
|
125
|
+
/** One session key's cache cell. */
|
|
126
|
+
var Entry = class {
|
|
127
|
+
state = "cold";
|
|
128
|
+
commands = [];
|
|
129
|
+
/** Bumped at each pull start; only the latest pull may publish its outcome. */
|
|
130
|
+
epoch = 0;
|
|
131
|
+
lastError;
|
|
132
|
+
waiters = [];
|
|
133
|
+
};
|
|
134
|
+
/** The session-keyed directory cache. Plain class — the owning service wires events and RPC. */
|
|
135
|
+
var CommandDirectory = class {
|
|
136
|
+
fetchCommands;
|
|
137
|
+
entries = /* @__PURE__ */ new Map();
|
|
138
|
+
constructor(fetchCommands) {
|
|
139
|
+
this.fetchCommands = fetchCommands;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Current cache status for one session.
|
|
143
|
+
* @param sessionId - session key.
|
|
144
|
+
* @returns the entry status (cold when never touched).
|
|
145
|
+
*/
|
|
146
|
+
status(sessionId) {
|
|
147
|
+
return this.entries.get(sessionId)?.state ?? "cold";
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Synchronous command lookup over one Session's ready catalog; exact names precede localized aliases.
|
|
151
|
+
* @param sessionId - session key.
|
|
152
|
+
* @param name - typed command spelling without the leading slash.
|
|
153
|
+
* @returns the descriptor, or undefined when absent or the entry is not ready.
|
|
154
|
+
*/
|
|
155
|
+
resolve(sessionId, name) {
|
|
156
|
+
const entry = this.entries.get(sessionId);
|
|
157
|
+
if (entry === void 0 || entry.state !== "ready") return void 0;
|
|
158
|
+
return resolveCommand(name, entry.commands);
|
|
159
|
+
}
|
|
160
|
+
/** Soft invalidation (commands-changed): background repull on every touched key; ready snapshots keep serving. */
|
|
161
|
+
invalidateAll() {
|
|
162
|
+
for (const key of this.entries.keys()) this.refresh(key);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Drop one Session's obsolete composition-specific snapshot and prewarm its replacement.
|
|
166
|
+
* @param sessionId - Session whose effective command composition changed.
|
|
167
|
+
*/
|
|
168
|
+
resetSession(sessionId) {
|
|
169
|
+
const entry = this.entry(sessionId);
|
|
170
|
+
entry.state = "cold";
|
|
171
|
+
entry.commands = [];
|
|
172
|
+
entry.lastError = void 0;
|
|
173
|
+
this.refresh(sessionId);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Hard reset on reconnect: every entry drops its snapshot (the agent world
|
|
177
|
+
* may have changed shape across the generation) and prewarms.
|
|
178
|
+
*/
|
|
179
|
+
resetConnected() {
|
|
180
|
+
for (const [key, entry] of this.entries) {
|
|
181
|
+
entry.state = "cold";
|
|
182
|
+
entry.commands = [];
|
|
183
|
+
this.refresh(key);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Fire-and-forget prewarm of one session (the command source's scope-birth
|
|
188
|
+
* warm hook lands here).
|
|
189
|
+
* @param sessionId - session key.
|
|
190
|
+
*/
|
|
191
|
+
warm(sessionId) {
|
|
192
|
+
const entry = this.entry(sessionId);
|
|
193
|
+
if (entry.state === "cold" || entry.state === "failed") this.refresh(sessionId);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Start one pull for one session. Publishes ready/failed only while it is
|
|
197
|
+
* still the key's latest pull (epoch guard); a ready snapshot is not
|
|
198
|
+
* demoted while the pull flies.
|
|
199
|
+
* @param sessionId - session key.
|
|
200
|
+
* @returns settled when this pull's outcome is published or discarded.
|
|
201
|
+
*/
|
|
202
|
+
async refresh(sessionId) {
|
|
203
|
+
const entry = this.entry(sessionId);
|
|
204
|
+
const epoch = ++entry.epoch;
|
|
205
|
+
if (entry.state !== "ready") entry.state = "pending";
|
|
206
|
+
try {
|
|
207
|
+
const commands = await this.fetchCommands(sessionId);
|
|
208
|
+
if (epoch !== entry.epoch) return;
|
|
209
|
+
entry.commands = commands;
|
|
210
|
+
entry.state = "ready";
|
|
211
|
+
entry.lastError = void 0;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
if (epoch !== entry.epoch) return;
|
|
214
|
+
entry.commands = [];
|
|
215
|
+
entry.state = "failed";
|
|
216
|
+
entry.lastError = error;
|
|
217
|
+
} finally {
|
|
218
|
+
if (epoch === entry.epoch) notifyWaiters(entry);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Strong-wait until one session's catalog is servable (the enter-
|
|
223
|
+
* adjudication "directory must be reached" rule): ready returns at once;
|
|
224
|
+
* cold/failed launch a fresh pull; pending joins the flying one. Rejects
|
|
225
|
+
* when the awaited pull fails or the signal aborts.
|
|
226
|
+
* @param sessionId - session key.
|
|
227
|
+
* @param signal - attempt-scoped abort (the SubmitAttempt signal).
|
|
228
|
+
* @returns the hot command snapshot.
|
|
229
|
+
*/
|
|
230
|
+
async ensureReady(sessionId, signal) {
|
|
231
|
+
const entry = this.entry(sessionId);
|
|
232
|
+
while (true) {
|
|
233
|
+
if (entry.state === "ready") return entry.commands;
|
|
234
|
+
if (entry.state !== "pending") this.refresh(sessionId);
|
|
235
|
+
await settled(entry, signal);
|
|
236
|
+
if (entry.state === "failed") throw new Error(`command directory warmup failed: ${entry.lastError instanceof Error ? entry.lastError.message : String(entry.lastError)}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
entry(sessionId) {
|
|
240
|
+
let entry = this.entries.get(sessionId);
|
|
241
|
+
if (entry === void 0) {
|
|
242
|
+
entry = new Entry();
|
|
243
|
+
this.entries.set(sessionId, entry);
|
|
244
|
+
}
|
|
245
|
+
return entry;
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
/** One settlement tick for one entry: resolves at the next winning publish, rejects on abort. */
|
|
249
|
+
function settled(entry, signal) {
|
|
250
|
+
if (signal.aborted) return Promise.reject(abortReason(signal));
|
|
251
|
+
return new Promise((resolve, reject) => {
|
|
252
|
+
const waiter = () => {
|
|
253
|
+
signal.removeEventListener("abort", onAbort);
|
|
254
|
+
resolve();
|
|
255
|
+
};
|
|
256
|
+
const onAbort = () => {
|
|
257
|
+
entry.waiters = entry.waiters.filter((w) => w !== waiter);
|
|
258
|
+
reject(abortReason(signal));
|
|
259
|
+
};
|
|
260
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
261
|
+
entry.waiters.push(waiter);
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
function notifyWaiters(entry) {
|
|
265
|
+
const woken = entry.waiters;
|
|
266
|
+
entry.waiters = [];
|
|
267
|
+
for (const wake of woken) wake();
|
|
268
|
+
}
|
|
269
|
+
/** Normalize an abort into an Error rejection. */
|
|
270
|
+
function abortReason(signal) {
|
|
271
|
+
return signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("command directory wait aborted");
|
|
272
|
+
}
|
|
273
|
+
//#endregion
|
|
274
|
+
//#region lib/types/client/popup.js
|
|
275
|
+
/**
|
|
276
|
+
* Headless popupSelect shell state: one controller per client
|
|
277
|
+
* session, owned by CommandUiRuntime's per-session map and torn down by the
|
|
278
|
+
* session scope disposer. The shell is a transient layer (never in the input
|
|
279
|
+
* state machine): it loads options once, filters them locally against the
|
|
280
|
+
* shell's own search text, and settles a selection through the context
|
|
281
|
+
* captured at open time. Draft consumption and composer focus are injected
|
|
282
|
+
* callbacks — the session wiring dispatches the consume-token event (the
|
|
283
|
+
* Input side owns the span/bare-token CAS guard) and focuses the composer;
|
|
284
|
+
* the controller never touches the input machine.
|
|
285
|
+
*/
|
|
286
|
+
const CLOSED = {
|
|
287
|
+
open: false,
|
|
288
|
+
command: null,
|
|
289
|
+
status: "pending",
|
|
290
|
+
options: [],
|
|
291
|
+
search: "",
|
|
292
|
+
active: 0,
|
|
293
|
+
submitting: false,
|
|
294
|
+
confirming: null,
|
|
295
|
+
acknowledged: false,
|
|
296
|
+
error: null
|
|
297
|
+
};
|
|
298
|
+
/**
|
|
299
|
+
* Filter option rows against the shell's local search text (case-insensitive
|
|
300
|
+
* substring over label and detail; blank search keeps every row).
|
|
301
|
+
* @param options - the loaded rows.
|
|
302
|
+
* @param search - the shell's search text.
|
|
303
|
+
* @returns the rows the shell shows and highlights over.
|
|
304
|
+
*/
|
|
305
|
+
function filterOptions(options, search) {
|
|
306
|
+
const query = search.trim().toLowerCase();
|
|
307
|
+
if (query === "") return options;
|
|
308
|
+
return options.filter((o) => o.label.toLowerCase().includes(query) || (o.detail?.toLowerCase().includes(query) ?? false));
|
|
309
|
+
}
|
|
310
|
+
/** The shell's error-strip line for a settlement failure. */
|
|
311
|
+
function errorText(error) {
|
|
312
|
+
return error instanceof Error ? error.message : String(error);
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Headless controller of one session's popupSelect shell. Late settlements
|
|
316
|
+
* lose their write rights through binding identity: dismiss/dispose/reopen
|
|
317
|
+
* swap the binding, so a settling options fetch or onSelect that no longer
|
|
318
|
+
* matches writes nothing and consumes nothing.
|
|
319
|
+
*/
|
|
320
|
+
var PopupSelectController = class {
|
|
321
|
+
deps;
|
|
322
|
+
/** Shell state store (the overlay component subscribes here). */
|
|
323
|
+
state = (0, _x1a0f3n9_dsh_client_store.createSnapshotStore)(CLOSED);
|
|
324
|
+
binding = null;
|
|
325
|
+
/**
|
|
326
|
+
* @param deps - session-wiring callbacks (token consumption + composer focus).
|
|
327
|
+
*/
|
|
328
|
+
constructor(deps) {
|
|
329
|
+
this.deps = deps;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Open the shell for one command: publish pending state and fetch options
|
|
333
|
+
* once through the business spec. A reopen supersedes the previous shell
|
|
334
|
+
* (its options fetch is aborted, its late settlements are dropped).
|
|
335
|
+
* @param command - command name the shell serves.
|
|
336
|
+
* @param spec - the registered popupSelect spec.
|
|
337
|
+
* @param context - open-time context snapshot, handed verbatim to options/onSelect.
|
|
338
|
+
* @param segment - open-time token segment snapshot for post-select consumption.
|
|
339
|
+
*/
|
|
340
|
+
open(command, spec, context, segment) {
|
|
341
|
+
this.binding?.abort.abort();
|
|
342
|
+
const binding = {
|
|
343
|
+
command,
|
|
344
|
+
spec,
|
|
345
|
+
context,
|
|
346
|
+
segment,
|
|
347
|
+
abort: new AbortController()
|
|
348
|
+
};
|
|
349
|
+
this.binding = binding;
|
|
350
|
+
this.state.set({
|
|
351
|
+
...CLOSED,
|
|
352
|
+
open: true,
|
|
353
|
+
command
|
|
354
|
+
});
|
|
355
|
+
this.load(binding);
|
|
356
|
+
}
|
|
357
|
+
/** Run the one options fetch of a binding; settlement rights die with the binding. */
|
|
358
|
+
load(binding) {
|
|
359
|
+
binding.spec.options(binding.context, binding.abort.signal).then((options) => {
|
|
360
|
+
if (this.binding !== binding) return;
|
|
361
|
+
this.state.set({
|
|
362
|
+
...this.state.getSnapshot(),
|
|
363
|
+
status: "ready",
|
|
364
|
+
options,
|
|
365
|
+
active: 0,
|
|
366
|
+
error: null
|
|
367
|
+
});
|
|
368
|
+
}, (error) => {
|
|
369
|
+
if (this.binding !== binding) return;
|
|
370
|
+
console.error(`[ui-commands] popupSelect options failed for /${binding.command}:`, error);
|
|
371
|
+
this.state.set({
|
|
372
|
+
...this.state.getSnapshot(),
|
|
373
|
+
status: "failed",
|
|
374
|
+
options: [],
|
|
375
|
+
active: 0,
|
|
376
|
+
error: errorText(error)
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
/** Re-run a failed options fetch (search survives; no-op unless status is 'failed'). */
|
|
381
|
+
retry() {
|
|
382
|
+
const binding = this.binding;
|
|
383
|
+
const s = this.state.getSnapshot();
|
|
384
|
+
if (binding === null || !s.open || s.status !== "failed") return;
|
|
385
|
+
this.state.set({
|
|
386
|
+
...s,
|
|
387
|
+
status: "pending",
|
|
388
|
+
error: null
|
|
389
|
+
});
|
|
390
|
+
this.load(binding);
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Replace the local search text (pure local filter — the provider is never
|
|
394
|
+
* re-queried) and rebase the highlight onto the new filtered list.
|
|
395
|
+
* @param search - the shell search input's text.
|
|
396
|
+
*/
|
|
397
|
+
setSearch(search) {
|
|
398
|
+
const s = this.state.getSnapshot();
|
|
399
|
+
if (!s.open || s.submitting || s.confirming !== null || search === s.search) return;
|
|
400
|
+
this.state.set({
|
|
401
|
+
...s,
|
|
402
|
+
search,
|
|
403
|
+
active: 0
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Move the highlight across the filtered rows (wraps around; no-op unless
|
|
408
|
+
* options are ready and no selection is in flight).
|
|
409
|
+
* @param dir - +1 down, -1 up.
|
|
410
|
+
*/
|
|
411
|
+
move(dir) {
|
|
412
|
+
const s = this.state.getSnapshot();
|
|
413
|
+
if (!s.open || s.status !== "ready" || s.submitting || s.confirming !== null) return;
|
|
414
|
+
const rows = filterOptions(s.options, s.search);
|
|
415
|
+
if (rows.length === 0) return;
|
|
416
|
+
const active = (s.active + dir + rows.length) % rows.length;
|
|
417
|
+
this.state.set({
|
|
418
|
+
...s,
|
|
419
|
+
active
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Set the highlight directly (pointer hover; no-op unless ready, idle, and
|
|
424
|
+
* in filtered range).
|
|
425
|
+
* @param index - filtered-row index.
|
|
426
|
+
*/
|
|
427
|
+
highlight(index) {
|
|
428
|
+
const s = this.state.getSnapshot();
|
|
429
|
+
if (!s.open || s.status !== "ready" || s.submitting || s.confirming !== null) return;
|
|
430
|
+
if (index < 0 || index >= filterOptions(s.options, s.search).length || index === s.active) return;
|
|
431
|
+
this.state.set({
|
|
432
|
+
...s,
|
|
433
|
+
active: index
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Select one filtered row: single-flight — the first call enters
|
|
438
|
+
* `submitting` and later calls no-op until it settles. Success consumes the
|
|
439
|
+
* open-time token segment (a false CAS answer is benign), closes, and
|
|
440
|
+
* returns focus to the composer. Failure keeps the shell open with search,
|
|
441
|
+
* highlight, and token intact, surfaces the error, and re-arms select as
|
|
442
|
+
* the retry.
|
|
443
|
+
* @param index - filtered-row index (callers pass the highlight or the clicked row).
|
|
444
|
+
* @returns settled when the attempt has closed the shell or surfaced its failure.
|
|
445
|
+
*/
|
|
446
|
+
async select(index) {
|
|
447
|
+
const binding = this.binding;
|
|
448
|
+
const s = this.state.getSnapshot();
|
|
449
|
+
if (binding === null || !s.open || s.status !== "ready" || s.submitting || s.confirming !== null) return;
|
|
450
|
+
const option = filterOptions(s.options, s.search)[index];
|
|
451
|
+
if (option === void 0) return;
|
|
452
|
+
if (option.confirmation !== void 0) {
|
|
453
|
+
this.state.set({
|
|
454
|
+
...s,
|
|
455
|
+
confirming: option,
|
|
456
|
+
acknowledged: false,
|
|
457
|
+
error: null
|
|
458
|
+
});
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
await this.settle(binding, option);
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Update the explicit checkbox for the currently pending risk gate.
|
|
465
|
+
* @param acknowledged - whether the user has acknowledged the displayed risk.
|
|
466
|
+
*/
|
|
467
|
+
acknowledge(acknowledged) {
|
|
468
|
+
const s = this.state.getSnapshot();
|
|
469
|
+
if (!s.open || s.submitting || s.confirming === null || s.acknowledged === acknowledged) return;
|
|
470
|
+
this.state.set({
|
|
471
|
+
...s,
|
|
472
|
+
acknowledged
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
/** Cancel only the risk gate and return to the still-open option picker. */
|
|
476
|
+
cancelConfirmation() {
|
|
477
|
+
const s = this.state.getSnapshot();
|
|
478
|
+
if (!s.open || s.submitting || s.confirming === null) return;
|
|
479
|
+
this.state.set({
|
|
480
|
+
...s,
|
|
481
|
+
confirming: null,
|
|
482
|
+
acknowledged: false
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
/** Settle the gated option only after the checkbox is acknowledged. */
|
|
486
|
+
async confirm() {
|
|
487
|
+
const binding = this.binding;
|
|
488
|
+
const s = this.state.getSnapshot();
|
|
489
|
+
if (binding === null || !s.open || s.submitting || s.confirming === null || !s.acknowledged) return;
|
|
490
|
+
await this.settle(binding, s.confirming);
|
|
491
|
+
}
|
|
492
|
+
/** Run the business settlement for an already admitted option. */
|
|
493
|
+
async settle(binding, option) {
|
|
494
|
+
const s = this.state.getSnapshot();
|
|
495
|
+
if (this.binding !== binding || !s.open || s.submitting) return;
|
|
496
|
+
this.state.set({
|
|
497
|
+
...s,
|
|
498
|
+
submitting: true,
|
|
499
|
+
confirming: null,
|
|
500
|
+
acknowledged: false,
|
|
501
|
+
error: null
|
|
502
|
+
});
|
|
503
|
+
try {
|
|
504
|
+
await binding.spec.onSelect(option, binding.context);
|
|
505
|
+
} catch (error) {
|
|
506
|
+
console.error(`[ui-commands] popupSelect onSelect failed for /${binding.command}:`, error);
|
|
507
|
+
if (this.binding !== binding) return;
|
|
508
|
+
this.state.set({
|
|
509
|
+
...this.state.getSnapshot(),
|
|
510
|
+
submitting: false,
|
|
511
|
+
error: errorText(error)
|
|
512
|
+
});
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
if (this.binding !== binding) return;
|
|
516
|
+
this.deps.consume(binding.segment);
|
|
517
|
+
this.binding = null;
|
|
518
|
+
this.state.set(CLOSED);
|
|
519
|
+
this.deps.focusComposer();
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Close the shell; aborts a flying options fetch and revokes settlement
|
|
523
|
+
* rights. An outside pointer interaction dismisses plainly (the click's own
|
|
524
|
+
* target takes focus); Escape passes focusComposer to return focus explicitly.
|
|
525
|
+
* @param opts - focusComposer: also restore composer focus (Escape path).
|
|
526
|
+
*/
|
|
527
|
+
dismiss(opts) {
|
|
528
|
+
if (this.binding === null) return;
|
|
529
|
+
this.binding.abort.abort();
|
|
530
|
+
this.binding = null;
|
|
531
|
+
this.state.set(CLOSED);
|
|
532
|
+
if (opts?.focusComposer === true) this.deps.focusComposer();
|
|
533
|
+
}
|
|
534
|
+
/** Scope-teardown disposer: abort in-flight work and clear state (no focus side effect). */
|
|
535
|
+
dispose() {
|
|
536
|
+
this.binding?.abort.abort();
|
|
537
|
+
this.binding = null;
|
|
538
|
+
this.state.set(CLOSED);
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
//#endregion
|
|
542
|
+
//#region lib/types/client/presentation.js
|
|
543
|
+
/** Row names per section, highest usage first; rows outside both lists close the Commands section in catalog order. */
|
|
544
|
+
const SECTION_ROWS = {
|
|
545
|
+
add: [
|
|
546
|
+
"file",
|
|
547
|
+
"goal",
|
|
548
|
+
"plan",
|
|
549
|
+
"feedback"
|
|
550
|
+
],
|
|
551
|
+
commands: [
|
|
552
|
+
"compact",
|
|
553
|
+
"permission",
|
|
554
|
+
"model",
|
|
555
|
+
"export"
|
|
556
|
+
]
|
|
557
|
+
};
|
|
558
|
+
/** One built-in Host command's face, keyed by its dictionary entries. */
|
|
559
|
+
function hostFace(name, icon) {
|
|
560
|
+
return [name, {
|
|
561
|
+
label: `label.${name}`,
|
|
562
|
+
description: `description.${name}`,
|
|
563
|
+
icon
|
|
564
|
+
}];
|
|
565
|
+
}
|
|
566
|
+
/** Built-in Host commands whose client face this package owns. */
|
|
567
|
+
const HOST_FACES = new Map([
|
|
568
|
+
hostFace("goal", _x1a0f3n9_dsh_client_ui_primitives.IconGoalOutline16),
|
|
569
|
+
hostFace("plan", _x1a0f3n9_dsh_client_ui_primitives.IconPlanOutline14),
|
|
570
|
+
hostFace("feedback", _x1a0f3n9_dsh_client_ui_primitives.IconSendOutline16),
|
|
571
|
+
hostFace("compact", _x1a0f3n9_dsh_client_ui_primitives.IconCompactOutline16),
|
|
572
|
+
hostFace("permission", _x1a0f3n9_dsh_client_ui_primitives.IconShieldOutline16),
|
|
573
|
+
hostFace("export", _x1a0f3n9_dsh_client_ui_primitives.IconDownloadOutline16)
|
|
574
|
+
]);
|
|
575
|
+
/**
|
|
576
|
+
* The localized menu face of a catalog row.
|
|
577
|
+
* @param descriptor - effective Host command descriptor.
|
|
578
|
+
* @param t - the `command` namespace translator.
|
|
579
|
+
* @returns title, description, and glyph for a built-in command; undefined
|
|
580
|
+
* for any other row, which keeps its catalog description.
|
|
581
|
+
*/
|
|
582
|
+
function builtinRowFace(descriptor, t) {
|
|
583
|
+
const name = builtinCommandName(descriptor);
|
|
584
|
+
const face = name === void 0 ? void 0 : HOST_FACES.get(name);
|
|
585
|
+
return face === void 0 ? void 0 : {
|
|
586
|
+
label: t(face.label),
|
|
587
|
+
description: t(face.description),
|
|
588
|
+
icon: face.icon
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Arrange the empty-query menu: the Add section, then the Commands section,
|
|
593
|
+
* each in usage order, with unlisted rows closing Commands in their input
|
|
594
|
+
* order; each row carries its section heading.
|
|
595
|
+
* @param rows - the visible candidates in catalog-then-contribution order.
|
|
596
|
+
* @param t - the `command` namespace translator.
|
|
597
|
+
* @returns the sectioned rows.
|
|
598
|
+
*/
|
|
599
|
+
function sectionRows(rows, t) {
|
|
600
|
+
const listed = new Set([...SECTION_ROWS.add, ...SECTION_ROWS.commands]);
|
|
601
|
+
const byName = new Map(rows.map((row) => [row.name, row]));
|
|
602
|
+
const pick = (names) => names.flatMap((name) => {
|
|
603
|
+
const row = byName.get(name);
|
|
604
|
+
return row === void 0 ? [] : [row];
|
|
605
|
+
});
|
|
606
|
+
const add = pick(SECTION_ROWS.add).map((row) => ({
|
|
607
|
+
...row,
|
|
608
|
+
section: t("section.add")
|
|
609
|
+
}));
|
|
610
|
+
const commands = [...pick(SECTION_ROWS.commands), ...rows.filter((row) => !listed.has(row.name))].map((row) => ({
|
|
611
|
+
...row,
|
|
612
|
+
section: t("section.commands")
|
|
613
|
+
}));
|
|
614
|
+
return [...add, ...commands];
|
|
615
|
+
}
|
|
616
|
+
//#endregion
|
|
617
|
+
//#region lib/types/client/service.js
|
|
618
|
+
/**
|
|
619
|
+
* CommandUiRuntime (`ctx.commandUi`): the '/' command source over the
|
|
620
|
+
* session-keyed directory, the client-contribution registry, and the
|
|
621
|
+
* per-session popupSelect controllers. Candidate synthesis merges the host
|
|
622
|
+
* catalog with contributions by availability, gives built-in Host rows their
|
|
623
|
+
* localized face (presentation.ts), then position-filters; an empty query
|
|
624
|
+
* lists the Add and Commands sections in usage order, a typed query ranks
|
|
625
|
+
* every row by the `/` menu's shared name-and-label ranking (ui-primitives
|
|
626
|
+
* `rankByName`). A host/contribution name collision fails loud. Every
|
|
627
|
+
* execute addresses the session's agent by sessionId — sessions are always
|
|
628
|
+
* agent-backed.
|
|
629
|
+
*/
|
|
630
|
+
/** Recover the command name from a line the Host confirmed as executed. */
|
|
631
|
+
function submittedCommandName(line) {
|
|
632
|
+
const trimmed = line.trim();
|
|
633
|
+
const separator = trimmed.search(/\s/u);
|
|
634
|
+
return (separator === -1 ? trimmed : trimmed.slice(0, separator)).slice(1);
|
|
635
|
+
}
|
|
636
|
+
/** Command surface: session-keyed directory + '/' source + contribution registry + per-session popups. */
|
|
637
|
+
var CommandUiRuntime = class extends _deepseek_ai_cordis.Service {
|
|
638
|
+
static inject = [
|
|
639
|
+
"inputTriggers",
|
|
640
|
+
"sessions",
|
|
641
|
+
"remote",
|
|
642
|
+
"remote.commands"
|
|
643
|
+
];
|
|
644
|
+
directory;
|
|
645
|
+
live = {
|
|
646
|
+
contributions: /* @__PURE__ */ new Map(),
|
|
647
|
+
decorations: /* @__PURE__ */ new Map(),
|
|
648
|
+
popups: /* @__PURE__ */ new Map()
|
|
649
|
+
};
|
|
650
|
+
/** `command`-namespace translator (composer refusal notices). */
|
|
651
|
+
t;
|
|
652
|
+
/**
|
|
653
|
+
* @param ctx - owning root context (plugin fiber; the service registers
|
|
654
|
+
* itself as `command` and follows that fiber's lifetime).
|
|
655
|
+
*/
|
|
656
|
+
constructor(ctx) {
|
|
657
|
+
super(ctx, "commandUi");
|
|
658
|
+
const locale = ctx.get("locale");
|
|
659
|
+
if (locale === void 0) throw new Error("ui-commands: locale service unavailable");
|
|
660
|
+
this.t = locale.bind("command");
|
|
661
|
+
this.directory = new CommandDirectory(async (sessionId) => {
|
|
662
|
+
if (this.sessions().subagentAddress(sessionId) !== void 0) return [];
|
|
663
|
+
const result = await ctx.remote.commands.list(sessionId);
|
|
664
|
+
if (!result.ok) throw new Error(`command.list failed: ${result.error.code}: ${result.error.message}`);
|
|
665
|
+
return result.value;
|
|
666
|
+
});
|
|
667
|
+
const inputTriggers = ctx.get("inputTriggers");
|
|
668
|
+
if (inputTriggers === void 0) throw new Error("ui-commands: slash service unavailable");
|
|
669
|
+
ctx.effect(() => inputTriggers.registerSource({
|
|
670
|
+
trigger: "/",
|
|
671
|
+
name: "command",
|
|
672
|
+
candidates: (session, req) => this.candidates(session, req),
|
|
673
|
+
onPick: (pick) => this.dispatch(pick),
|
|
674
|
+
matchSpace: (session, token) => this.matchSpace(session, token),
|
|
675
|
+
matchEnter: (session, line, signal, envelope) => this.matchEnter(session, line, signal, envelope),
|
|
676
|
+
warm: (session) => {
|
|
677
|
+
this.directory.warm(session.sessionId);
|
|
678
|
+
}
|
|
679
|
+
}), "command: slash source");
|
|
680
|
+
ctx.remote.$on("commands/change", () => {
|
|
681
|
+
this.directory.invalidateAll();
|
|
682
|
+
});
|
|
683
|
+
ctx.remote.$on("agent-preset/selected", (sessionId) => {
|
|
684
|
+
this.directory.resetSession(sessionId);
|
|
685
|
+
});
|
|
686
|
+
ctx.on("connection/reset", () => {
|
|
687
|
+
this.directory.resetConnected();
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Register one client command contribution; effect disposer (rides the
|
|
692
|
+
* caller's fiber). Duplicate names throw.
|
|
693
|
+
* @param contribution - the contribution (descriptor + availability + popup spec).
|
|
694
|
+
* @returns the disposer removing the registration.
|
|
695
|
+
*/
|
|
696
|
+
register(contribution) {
|
|
697
|
+
const dispose = this.ctx.effect(() => {
|
|
698
|
+
const { contributions } = this.live;
|
|
699
|
+
if (contributions.has(contribution.name)) throw new Error(`ui-commands: duplicate contribution for /${contribution.name}`);
|
|
700
|
+
contributions.set(contribution.name, contribution);
|
|
701
|
+
return () => {
|
|
702
|
+
contributions.delete(contribution.name);
|
|
703
|
+
};
|
|
704
|
+
}, "command.register()");
|
|
705
|
+
return () => {
|
|
706
|
+
dispose();
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Hang a bare-invocation decoration on one host command; effect disposer
|
|
711
|
+
* (rides the caller's fiber). Duplicate names throw.
|
|
712
|
+
* @param decoration - host command name + availability + popup spec.
|
|
713
|
+
* @returns the disposer removing the registration.
|
|
714
|
+
*/
|
|
715
|
+
decorate(decoration) {
|
|
716
|
+
const dispose = this.ctx.effect(() => {
|
|
717
|
+
const { decorations } = this.live;
|
|
718
|
+
if (decorations.has(decoration.name)) throw new Error(`ui-commands: duplicate decoration for /${decoration.name}`);
|
|
719
|
+
decorations.set(decoration.name, decoration);
|
|
720
|
+
return () => {
|
|
721
|
+
decorations.delete(decoration.name);
|
|
722
|
+
};
|
|
723
|
+
}, "command.decorate()");
|
|
724
|
+
return () => {
|
|
725
|
+
dispose();
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Resolve the per-session popup controller (lazy; dies with the session
|
|
730
|
+
* scope). The controller's consume callback dispatches the scoped
|
|
731
|
+
* consume-token event back to this session; focusComposer reaches the
|
|
732
|
+
* composer through the overlay slot currency.
|
|
733
|
+
* @param actx - session-scope ctx.
|
|
734
|
+
* @returns the resident controller.
|
|
735
|
+
*/
|
|
736
|
+
popupFor(actx) {
|
|
737
|
+
const id = this.sessions().scopeOf(actx);
|
|
738
|
+
if (id === void 0) throw new Error("command.popupFor requires a session scope");
|
|
739
|
+
const { popups } = this.live;
|
|
740
|
+
const existing = popups.get(id);
|
|
741
|
+
if (existing !== void 0) return existing;
|
|
742
|
+
const controller = new PopupSelectController({
|
|
743
|
+
consume: (segment) => actx.bail(actx, "slash/input-consume-token", { guard: segment.via === "menu" ? {
|
|
744
|
+
kind: "span",
|
|
745
|
+
span: segment.span
|
|
746
|
+
} : {
|
|
747
|
+
kind: "bare-token",
|
|
748
|
+
token: segment.token
|
|
749
|
+
} }) === true,
|
|
750
|
+
focusComposer: () => {
|
|
751
|
+
this.focusHooks.get(id)?.();
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
popups.set(id, controller);
|
|
755
|
+
actx.effect(() => () => {
|
|
756
|
+
controller.dispose();
|
|
757
|
+
popups.delete(id);
|
|
758
|
+
this.focusHooks.delete(id);
|
|
759
|
+
}, "command: session popup");
|
|
760
|
+
return controller;
|
|
761
|
+
}
|
|
762
|
+
/** Composer focus hooks by session (the overlay wiring binds the textarea focus here). */
|
|
763
|
+
focusHooks = /* @__PURE__ */ new Map();
|
|
764
|
+
/**
|
|
765
|
+
* Bind one session's composer-focus hook (overlay slot wiring; unbind on unmount).
|
|
766
|
+
* @param id - session id.
|
|
767
|
+
* @param focus - textarea focus callback.
|
|
768
|
+
* @returns the unbind disposer.
|
|
769
|
+
*/
|
|
770
|
+
bindComposerFocus(id, focus) {
|
|
771
|
+
this.focusHooks.set(id, focus);
|
|
772
|
+
return () => {
|
|
773
|
+
if (this.focusHooks.get(id) === focus) this.focusHooks.delete(id);
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Menu candidates: host catalog + contribution availability, built-in rows
|
|
778
|
+
* localized, then position filtering; sections for an empty query, the
|
|
779
|
+
* shared name-and-label ranking for a typed one.
|
|
780
|
+
*/
|
|
781
|
+
async candidates(session, req) {
|
|
782
|
+
const list = await this.directory.ensureReady(session.sessionId, req.signal);
|
|
783
|
+
const rows = [];
|
|
784
|
+
const seen = /* @__PURE__ */ new Set();
|
|
785
|
+
for (const c of list) {
|
|
786
|
+
seen.add(c.name);
|
|
787
|
+
rows.push({
|
|
788
|
+
name: c.name,
|
|
789
|
+
...builtinRowFace(c, this.t) ?? { description: c.description },
|
|
790
|
+
...c.input !== void 0 ? { hint: c.input.hint } : {}
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
for (const contribution of this.live.contributions.values()) {
|
|
794
|
+
if (!contribution.available(session)) continue;
|
|
795
|
+
if (seen.has(contribution.name)) throw new Error(`ui-commands: contribution /${contribution.name} collides with a host command`);
|
|
796
|
+
rows.push({
|
|
797
|
+
name: contribution.name,
|
|
798
|
+
...contribution.label === void 0 ? {} : { label: contribution.label() },
|
|
799
|
+
...contribution.description === void 0 ? {} : { description: contribution.description() },
|
|
800
|
+
...contribution.icon === void 0 ? {} : { icon: contribution.icon }
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
const visible = rows.filter((c) => req.position === "leading" || c.hint === void 0);
|
|
804
|
+
return req.query === "" ? sectionRows(visible, this.t) : (0, _x1a0f3n9_dsh_client_ui_primitives.rankByName)(visible, req.query);
|
|
805
|
+
}
|
|
806
|
+
/** Decision table, menu column: contribution/decorated-host → popup or action; host input → claim; host bare → detached execute. */
|
|
807
|
+
dispatch(pick) {
|
|
808
|
+
const name = pick.candidate.name;
|
|
809
|
+
const contribution = this.live.contributions.get(name);
|
|
810
|
+
if (contribution !== void 0 && contribution.available(pick.session)) {
|
|
811
|
+
this.invoke(name, contribution.ui, pick.session, {
|
|
812
|
+
via: "menu",
|
|
813
|
+
span: pick.span
|
|
814
|
+
});
|
|
815
|
+
return "handled";
|
|
816
|
+
}
|
|
817
|
+
const desc = this.directory.resolve(pick.session.sessionId, name);
|
|
818
|
+
if (desc === void 0) return void 0;
|
|
819
|
+
const decoration = this.live.decorations.get(name);
|
|
820
|
+
if (decoration !== void 0 && decoration.available(pick.session)) {
|
|
821
|
+
this.invoke(name, decoration.ui, pick.session, {
|
|
822
|
+
via: "menu",
|
|
823
|
+
span: pick.span
|
|
824
|
+
});
|
|
825
|
+
return "handled";
|
|
826
|
+
}
|
|
827
|
+
if (desc.input !== void 0) return { claim: this.leadingClaim(desc, pick.session, claimToken(desc, this.t)) };
|
|
828
|
+
this.consumeVia(pick.session.sessionId, {
|
|
829
|
+
via: "menu",
|
|
830
|
+
span: pick.span
|
|
831
|
+
});
|
|
832
|
+
this.runDetached(desc, pick.session, `/${name}`);
|
|
833
|
+
return "handled";
|
|
834
|
+
}
|
|
835
|
+
/** Decision table, space column: hot-key sync check; only host leadingInput claims. */
|
|
836
|
+
matchSpace(session, token) {
|
|
837
|
+
if (!token.startsWith("/")) return void 0;
|
|
838
|
+
if (this.live.contributions.has(token.slice(1))) return void 0;
|
|
839
|
+
const desc = this.directory.resolve(session.sessionId, token.slice(1));
|
|
840
|
+
if (desc === void 0 || desc.input === void 0) return void 0;
|
|
841
|
+
return { claim: this.leadingClaim(desc, session, token.slice(1)) };
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* Decision table, enter column. Strong-waits the session's catalog (a
|
|
845
|
+
* warmup failure rejects — never a silent downgrade). Contributions and
|
|
846
|
+
* bare host commands act on the bare token only; leadingInput claims
|
|
847
|
+
* args-tolerant.
|
|
848
|
+
*
|
|
849
|
+
* Envelope policy: an enter submission carrying attachments resolves only
|
|
850
|
+
* through a command declaring attachment acceptance. Every other submitting
|
|
851
|
+
* route — popup, non-accepting claim, bare detached execute — throws the
|
|
852
|
+
* refusal so the machine surfaces one composer notice and the draft and
|
|
853
|
+
* attachments stay in place; nothing executes and nothing is dropped. An
|
|
854
|
+
* action submits nothing and runs regardless.
|
|
855
|
+
*
|
|
856
|
+
* A typed token is resolved through the localized claim tokens, so a line
|
|
857
|
+
* written as `/计划` reaches the `plan` descriptor and executes as `/plan`.
|
|
858
|
+
*/
|
|
859
|
+
async matchEnter(session, line, signal, envelope) {
|
|
860
|
+
const trimmed = line.trim();
|
|
861
|
+
if (!trimmed.startsWith("/")) return void 0;
|
|
862
|
+
const ws = trimmed.search(/\s/);
|
|
863
|
+
const token = ws === -1 ? trimmed : trimmed.slice(0, ws);
|
|
864
|
+
const bare = ws === -1;
|
|
865
|
+
const typedName = token.slice(1);
|
|
866
|
+
if (typedName === "") return void 0;
|
|
867
|
+
const refuseAttachments = () => {
|
|
868
|
+
throw new Error(this.t("notice.attachmentsUnsupported", { command: typedName }));
|
|
869
|
+
};
|
|
870
|
+
const contribution = this.live.contributions.get(typedName);
|
|
871
|
+
if (contribution !== void 0 && contribution.available(session)) {
|
|
872
|
+
if (!bare) return void 0;
|
|
873
|
+
if (envelope.attachments > 0 && contribution.ui.kind !== "action") refuseAttachments();
|
|
874
|
+
this.invoke(typedName, contribution.ui, session, {
|
|
875
|
+
via: "enter",
|
|
876
|
+
token
|
|
877
|
+
});
|
|
878
|
+
return "handled";
|
|
879
|
+
}
|
|
880
|
+
await this.directory.ensureReady(session.sessionId, signal);
|
|
881
|
+
const desc = this.directory.resolve(session.sessionId, typedName);
|
|
882
|
+
if (desc === void 0) return void 0;
|
|
883
|
+
const name = desc.name;
|
|
884
|
+
const canonical = `/${name}${trimmed.slice(token.length)}`;
|
|
885
|
+
if (bare) {
|
|
886
|
+
const decoration = this.live.decorations.get(name);
|
|
887
|
+
if (decoration !== void 0 && decoration.available(session)) {
|
|
888
|
+
if (envelope.attachments > 0 && decoration.ui.kind !== "action") refuseAttachments();
|
|
889
|
+
this.invoke(name, decoration.ui, session, {
|
|
890
|
+
via: "enter",
|
|
891
|
+
token
|
|
892
|
+
});
|
|
893
|
+
return "handled";
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
if (desc.input !== void 0) {
|
|
897
|
+
if (envelope.attachments > 0 && desc.input.attachments !== true) refuseAttachments();
|
|
898
|
+
return { claim: this.leadingClaim(desc, session, token.slice(1)) };
|
|
899
|
+
}
|
|
900
|
+
if (!bare) return void 0;
|
|
901
|
+
if (envelope.attachments > 0) refuseAttachments();
|
|
902
|
+
this.consumeVia(session.sessionId, {
|
|
903
|
+
via: "enter",
|
|
904
|
+
token
|
|
905
|
+
});
|
|
906
|
+
this.runDetached(desc, session, canonical);
|
|
907
|
+
return "handled";
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* Invoke one contribution or decoration (menu pick / bare enter): open the
|
|
911
|
+
* session's popup, or consume the token and run the action.
|
|
912
|
+
*/
|
|
913
|
+
invoke(name, ui, session, segment) {
|
|
914
|
+
if (ui.kind === "action") {
|
|
915
|
+
this.consumeVia(session.sessionId, segment);
|
|
916
|
+
ui.run(session);
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
const actx = this.scopeFor(session.sessionId);
|
|
920
|
+
if (actx === void 0) return;
|
|
921
|
+
this.popupFor(actx).open(name, ui, session, segment);
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Build the leadingInput claim. The composer keeps the claimed token in
|
|
925
|
+
* the draft and reads the arguments after it, so the token is the spelling
|
|
926
|
+
* the draft will carry: the locale's token for a menu pick, the typed
|
|
927
|
+
* spelling for Space and Enter. The command.execute submit transaction
|
|
928
|
+
* always sends the catalog name.
|
|
929
|
+
*/
|
|
930
|
+
leadingClaim(desc, session, shown) {
|
|
931
|
+
const token = `/${shown} `;
|
|
932
|
+
const line = `/${desc.name} `;
|
|
933
|
+
return {
|
|
934
|
+
name: desc.name,
|
|
935
|
+
token,
|
|
936
|
+
...desc.input !== void 0 ? { hint: desc.input.hint } : {},
|
|
937
|
+
...desc.input?.attachments === true ? { attachments: true } : {},
|
|
938
|
+
submit: (args, _actx, attachments) => this.execute(session, line + args, attachments)
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* The command.execute transaction, addressed to the session's agent — pure
|
|
943
|
+
* admission semantics. An unmatched line reports an error outcome (the
|
|
944
|
+
* composer's immediate admission feedback); an admitted command reports
|
|
945
|
+
* plain success regardless of its handler outcome, because the host
|
|
946
|
+
* executor durably logged the lifecycle (`command/run`/`command/done`) and
|
|
947
|
+
* the outcome renders as a persistent flow node — the composer never
|
|
948
|
+
* echoes it. A handler error result reports an error outcome so the
|
|
949
|
+
* composer keeps the draft and attachments for correction.
|
|
950
|
+
* A refused call throws.
|
|
951
|
+
*/
|
|
952
|
+
async execute(session, line, attachments = []) {
|
|
953
|
+
const result = await this.ctx.remote.commands.execute(session.sessionId, line, attachments);
|
|
954
|
+
if (!result.ok) throw new Error(`command.execute failed: ${result.error.code}: ${result.error.message}`);
|
|
955
|
+
if (result.value === void 0) return {
|
|
956
|
+
kind: "error",
|
|
957
|
+
text: `unknown or malformed command: ${line}`
|
|
958
|
+
};
|
|
959
|
+
this.notifyExecuted(session.sessionId, submittedCommandName(line), result.value.result);
|
|
960
|
+
if (attachments.length > 0 && result.value.result.kind === "error") return {
|
|
961
|
+
kind: "error",
|
|
962
|
+
text: result.value.result.text
|
|
963
|
+
};
|
|
964
|
+
return { kind: "success" };
|
|
965
|
+
}
|
|
966
|
+
/** Publish the local acknowledgment without letting an observer change command admission. */
|
|
967
|
+
notifyExecuted(sessionId, name, result) {
|
|
968
|
+
const args = [
|
|
969
|
+
"command/executed",
|
|
970
|
+
sessionId,
|
|
971
|
+
name,
|
|
972
|
+
result
|
|
973
|
+
];
|
|
974
|
+
for (const listener of this.ctx.events.dispatch("emit", args)) try {
|
|
975
|
+
const returned = listener(sessionId, name, result);
|
|
976
|
+
if (returned != null && typeof returned.then === "function") Promise.resolve(returned).then(void 0, (error) => {
|
|
977
|
+
this.warnExecutedListenerFailure(name, error);
|
|
978
|
+
});
|
|
979
|
+
} catch (error) {
|
|
980
|
+
this.warnExecutedListenerFailure(name, error);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
/** Log one contained `command/executed` observer failure. */
|
|
984
|
+
warnExecutedListenerFailure(name, error) {
|
|
985
|
+
this.ctx.logger.warn("client command: a command/executed listener for \"%s\" failed", name);
|
|
986
|
+
this.ctx.logger.warn(error);
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Fire-and-forget execute for the internal ('handled') paths. Outcomes are
|
|
990
|
+
* NOT surfaced here: the host executor durably logs the command lifecycle
|
|
991
|
+
* (`command/run`/`command/done`), and the mux-broadcast events render as a
|
|
992
|
+
* persistent flow node on every tab. Only an admission failure — which never
|
|
993
|
+
* entered a handler and therefore never logged — falls back to the composer
|
|
994
|
+
* notice as immediate feedback.
|
|
995
|
+
*/
|
|
996
|
+
runDetached(desc, session, line) {
|
|
997
|
+
this.execute(session, line).then((outcome) => {
|
|
998
|
+
if (outcome.kind === "error") this.noticeFor(session.sessionId, "error", outcome.text ?? `/${desc.name} failed`);
|
|
999
|
+
}, (error) => {
|
|
1000
|
+
this.noticeFor(session.sessionId, "error", error instanceof Error ? error.message : String(error));
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
/** Dispatch a consume-token event to one session (menu-pick / bare-enter execute paths). */
|
|
1004
|
+
consumeVia(id, segment) {
|
|
1005
|
+
const actx = this.scopeFor(id);
|
|
1006
|
+
if (actx === void 0) return;
|
|
1007
|
+
actx.bail(actx, "slash/input-consume-token", { guard: segment.via === "menu" ? {
|
|
1008
|
+
kind: "span",
|
|
1009
|
+
span: segment.span
|
|
1010
|
+
} : {
|
|
1011
|
+
kind: "bare-token",
|
|
1012
|
+
token: segment.token
|
|
1013
|
+
} });
|
|
1014
|
+
}
|
|
1015
|
+
/** Route an admission failure to the session's composer notice channel (scope gone = attempt died with it). */
|
|
1016
|
+
noticeFor(id, level, text) {
|
|
1017
|
+
const actx = this.scopeFor(id);
|
|
1018
|
+
if (actx === void 0) return;
|
|
1019
|
+
const conversation = actx.get("conversation");
|
|
1020
|
+
if (conversation === void 0) return;
|
|
1021
|
+
conversation.input.for(actx).notify(level, text);
|
|
1022
|
+
}
|
|
1023
|
+
/** id → actx interchange (registered exchange point: this service coordinates for projection-only sources). */
|
|
1024
|
+
scopeFor(id) {
|
|
1025
|
+
return this.sessions().scope(id);
|
|
1026
|
+
}
|
|
1027
|
+
sessions() {
|
|
1028
|
+
const sessions = this.ctx.get("sessions");
|
|
1029
|
+
if (sessions === void 0) throw new Error("ui-commands: sessions service unavailable");
|
|
1030
|
+
return sessions;
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
//#endregion
|
|
1034
|
+
//#region ../../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
|
|
1035
|
+
function r(e) {
|
|
1036
|
+
var t, f, n = "";
|
|
1037
|
+
if ("string" == typeof e || "number" == typeof e) n += e;
|
|
1038
|
+
else if ("object" == typeof e) if (Array.isArray(e)) {
|
|
1039
|
+
var o = e.length;
|
|
1040
|
+
for (t = 0; t < o; t++) e[t] && (f = r(e[t])) && (n && (n += " "), n += f);
|
|
1041
|
+
} else for (f in e) e[f] && (n && (n += " "), n += f);
|
|
1042
|
+
return n;
|
|
1043
|
+
}
|
|
1044
|
+
function clsx() {
|
|
1045
|
+
for (var e, t, f = 0, n = "", o = arguments.length; f < o; f++) (e = arguments[f]) && (t = r(e)) && (n && (n += " "), n += t);
|
|
1046
|
+
return n;
|
|
1047
|
+
}
|
|
1048
|
+
//#endregion
|
|
1049
|
+
//#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-commands/src/client/PopupSelectView.module.css.mjs
|
|
1050
|
+
const css = ".mufS8W_card{z-index:100;--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);background:var(--dsw-specific-menu);--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);min-width:min(220px,100%);max-width:100%;max-height:320px;box-shadow:var(--dsw-elevation-prominent);border:0;border-radius:20px;outline:none;flex-direction:column;padding:4px;display:flex;position:absolute;bottom:calc(100% + 4px);left:0;overflow:hidden}.mufS8W_viewport{flex-direction:column;min-height:0;display:flex;overflow-y:auto}.mufS8W_row{cursor:pointer;color:var(--dsw-alias-label-primary);border-radius:8px;align-items:center;gap:8px;padding:6px 8px;font-size:13px;display:flex}.mufS8W_rowActive{background:var(--dsw-alias-interactive-bg-hover)}.mufS8W_label{white-space:nowrap;text-overflow:ellipsis;flex:auto;min-width:0;overflow:hidden}.mufS8W_detail{color:var(--dsw-alias-label-tertiary);white-space:nowrap;text-overflow:ellipsis;font-size:12px;overflow:hidden}.mufS8W_check{color:var(--dsw-alias-label-primary);flex:none;display:inline-flex}.mufS8W_status{color:var(--dsw-alias-label-tertiary);padding:8px 10px;font-size:13px}.mufS8W_search{border:.5px solid var(--dsw-alias-border-inverted);color:var(--dsw-alias-label-primary);background:0 0;border-radius:8px;outline:none;margin:2px 2px 4px;padding:6px 8px;font-size:13px}.mufS8W_error{color:var(--dsw-alias-state-error-primary);align-items:center;gap:8px;padding:6px 8px;font-size:12px;display:flex}.mufS8W_errorText{text-overflow:ellipsis;flex:1;overflow:hidden}.mufS8W_retry{border:.5px solid var(--dsw-alias-border-inverted);color:var(--dsw-alias-label-primary);cursor:pointer;background:0 0;border-radius:6px;padding:2px 8px;font-size:12px}";
|
|
1051
|
+
const tagId = "@x1a0f3n9/dsh-client-ui-commands/PopupSelectView.module.css";
|
|
1052
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
1053
|
+
const tag = document.createElement("style");
|
|
1054
|
+
tag.dataset.plugin = "@x1a0f3n9/dsh-client-ui-commands";
|
|
1055
|
+
tag.dataset.pluginCss = tagId;
|
|
1056
|
+
tag.textContent = css;
|
|
1057
|
+
document.head.appendChild(tag);
|
|
1058
|
+
}
|
|
1059
|
+
var PopupSelectView_module_css_default = {
|
|
1060
|
+
"card": "mufS8W_card",
|
|
1061
|
+
"check": "mufS8W_check",
|
|
1062
|
+
"detail": "mufS8W_detail",
|
|
1063
|
+
"error": "mufS8W_error",
|
|
1064
|
+
"errorText": "mufS8W_errorText",
|
|
1065
|
+
"label": "mufS8W_label",
|
|
1066
|
+
"retry": "mufS8W_retry",
|
|
1067
|
+
"row": "mufS8W_row",
|
|
1068
|
+
"rowActive": "mufS8W_rowActive",
|
|
1069
|
+
"search": "mufS8W_search",
|
|
1070
|
+
"status": "mufS8W_status",
|
|
1071
|
+
"viewport": "mufS8W_viewport"
|
|
1072
|
+
};
|
|
1073
|
+
//#endregion
|
|
1074
|
+
//#region lib/types/client/PopupSelectView.js
|
|
1075
|
+
/**
|
|
1076
|
+
* Official popupSelect shell: renders one session's PopupSelectController
|
|
1077
|
+
* store into the conversation.input.overlay anchor. Unlike the slash menu
|
|
1078
|
+
* (combobox — textarea keeps focus), this shell HOLDS focus while open: the
|
|
1079
|
+
* inner search input takes focus, plain typing filters the loaded options
|
|
1080
|
+
* locally, Enter/↑↓ drive the filtered highlight (scrolled into view), Escape
|
|
1081
|
+
* dismisses back to the composer, and ←→ keep the search input's native
|
|
1082
|
+
* caret. Any pointer interaction outside the box dismisses (the click's own
|
|
1083
|
+
* target takes focus). Closed state renders null; the overlay slot stays
|
|
1084
|
+
* mounted. The card height clamps to the space above the composer.
|
|
1085
|
+
*/
|
|
1086
|
+
/** Design cap on the card height (same MenuDropdown family as the slash menu). */
|
|
1087
|
+
const MAX_HEIGHT = 320;
|
|
1088
|
+
/**
|
|
1089
|
+
* Render the popupSelect shell overlay entry.
|
|
1090
|
+
* @param props - injected face: the session's shell controller; `t` rides the standard locale seat.
|
|
1091
|
+
* @returns the select card while open; null while closed.
|
|
1092
|
+
*/
|
|
1093
|
+
function PopupSelectView({ popup, t }) {
|
|
1094
|
+
const state = (0, react.useSyncExternalStore)((fn) => popup.state.subscribe(fn), () => popup.state.getSnapshot());
|
|
1095
|
+
const cardRef = (0, react.useRef)(null);
|
|
1096
|
+
const searchRef = (0, react.useRef)(null);
|
|
1097
|
+
const maxHeight = (0, _x1a0f3n9_dsh_client_ui_primitives.useAnchoredMaxHeight)(cardRef, MAX_HEIGHT, state);
|
|
1098
|
+
const active = state.open ? state.active : null;
|
|
1099
|
+
(0, react.useEffect)(() => {
|
|
1100
|
+
if (active === null) return;
|
|
1101
|
+
cardRef.current?.querySelector("[aria-selected=\"true\"]")?.scrollIntoView({ block: "nearest" });
|
|
1102
|
+
}, [active]);
|
|
1103
|
+
(0, react.useEffect)(() => {
|
|
1104
|
+
if (!state.open || state.confirming !== null) return;
|
|
1105
|
+
const onPointerDown = (ev) => {
|
|
1106
|
+
if (cardRef.current !== null && ev.target instanceof Node && cardRef.current.contains(ev.target)) return;
|
|
1107
|
+
popup.dismiss();
|
|
1108
|
+
};
|
|
1109
|
+
document.addEventListener("pointerdown", onPointerDown, true);
|
|
1110
|
+
return () => {
|
|
1111
|
+
document.removeEventListener("pointerdown", onPointerDown, true);
|
|
1112
|
+
};
|
|
1113
|
+
}, [
|
|
1114
|
+
state.open,
|
|
1115
|
+
state.confirming,
|
|
1116
|
+
popup
|
|
1117
|
+
]);
|
|
1118
|
+
(0, react.useEffect)(() => {
|
|
1119
|
+
if (state.open && state.confirming === null) searchRef.current?.focus();
|
|
1120
|
+
}, [state.open, state.confirming]);
|
|
1121
|
+
if (!state.open) return null;
|
|
1122
|
+
const rows = filterOptions(state.options, state.search);
|
|
1123
|
+
const confirmation = state.confirming?.confirmation;
|
|
1124
|
+
const onKeyDown = (ev) => {
|
|
1125
|
+
switch (ev.key) {
|
|
1126
|
+
case "ArrowDown":
|
|
1127
|
+
ev.preventDefault();
|
|
1128
|
+
popup.move(1);
|
|
1129
|
+
return;
|
|
1130
|
+
case "ArrowUp":
|
|
1131
|
+
ev.preventDefault();
|
|
1132
|
+
popup.move(-1);
|
|
1133
|
+
return;
|
|
1134
|
+
case "Enter":
|
|
1135
|
+
ev.preventDefault();
|
|
1136
|
+
popup.select(state.active);
|
|
1137
|
+
return;
|
|
1138
|
+
case "Escape":
|
|
1139
|
+
ev.preventDefault();
|
|
1140
|
+
popup.dismiss({ focusComposer: true });
|
|
1141
|
+
return;
|
|
1142
|
+
default:
|
|
1143
|
+
}
|
|
1144
|
+
};
|
|
1145
|
+
return (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [state.confirming === null && (0, react_jsx_runtime.jsxs)("div", {
|
|
1146
|
+
ref: cardRef,
|
|
1147
|
+
className: PopupSelectView_module_css_default.card,
|
|
1148
|
+
style: { maxHeight },
|
|
1149
|
+
"aria-label": t("overlay.aria", { command: String(state.command) }),
|
|
1150
|
+
onKeyDown,
|
|
1151
|
+
children: [
|
|
1152
|
+
(0, react_jsx_runtime.jsx)("input", {
|
|
1153
|
+
ref: searchRef,
|
|
1154
|
+
className: PopupSelectView_module_css_default.search,
|
|
1155
|
+
type: "text",
|
|
1156
|
+
placeholder: t("search.placeholder"),
|
|
1157
|
+
"aria-label": t("search.aria"),
|
|
1158
|
+
value: state.search,
|
|
1159
|
+
readOnly: state.submitting,
|
|
1160
|
+
onChange: (ev) => {
|
|
1161
|
+
popup.setSearch(ev.currentTarget.value);
|
|
1162
|
+
}
|
|
1163
|
+
}),
|
|
1164
|
+
state.error !== null && (0, react_jsx_runtime.jsxs)("div", {
|
|
1165
|
+
className: PopupSelectView_module_css_default.error,
|
|
1166
|
+
role: "alert",
|
|
1167
|
+
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
1168
|
+
className: PopupSelectView_module_css_default.errorText,
|
|
1169
|
+
children: state.error
|
|
1170
|
+
}), state.status === "failed" && (0, react_jsx_runtime.jsx)("button", {
|
|
1171
|
+
type: "button",
|
|
1172
|
+
className: PopupSelectView_module_css_default.retry,
|
|
1173
|
+
onClick: () => {
|
|
1174
|
+
popup.retry();
|
|
1175
|
+
},
|
|
1176
|
+
children: t("retry")
|
|
1177
|
+
})]
|
|
1178
|
+
}),
|
|
1179
|
+
state.status === "pending" && (0, react_jsx_runtime.jsx)("div", {
|
|
1180
|
+
className: PopupSelectView_module_css_default.status,
|
|
1181
|
+
children: t("status.loading")
|
|
1182
|
+
}),
|
|
1183
|
+
state.submitting && (0, react_jsx_runtime.jsx)("div", {
|
|
1184
|
+
className: PopupSelectView_module_css_default.status,
|
|
1185
|
+
children: t("status.applying")
|
|
1186
|
+
}),
|
|
1187
|
+
state.status === "ready" && rows.length === 0 && (0, react_jsx_runtime.jsx)("div", {
|
|
1188
|
+
className: PopupSelectView_module_css_default.status,
|
|
1189
|
+
children: t("status.empty")
|
|
1190
|
+
}),
|
|
1191
|
+
state.status === "ready" && (0, react_jsx_runtime.jsx)("div", {
|
|
1192
|
+
role: "listbox",
|
|
1193
|
+
"aria-label": t("listbox.aria", { command: String(state.command) }),
|
|
1194
|
+
className: PopupSelectView_module_css_default.viewport,
|
|
1195
|
+
children: rows.map((option, index) => (0, react_jsx_runtime.jsxs)("div", {
|
|
1196
|
+
role: "option",
|
|
1197
|
+
"aria-selected": index === state.active,
|
|
1198
|
+
className: clsx(PopupSelectView_module_css_default.row, index === state.active && PopupSelectView_module_css_default.rowActive),
|
|
1199
|
+
onClick: () => {
|
|
1200
|
+
popup.select(index);
|
|
1201
|
+
},
|
|
1202
|
+
onMouseEnter: () => {
|
|
1203
|
+
popup.highlight(index);
|
|
1204
|
+
},
|
|
1205
|
+
children: [
|
|
1206
|
+
(0, react_jsx_runtime.jsx)("span", {
|
|
1207
|
+
className: PopupSelectView_module_css_default.label,
|
|
1208
|
+
children: option.label
|
|
1209
|
+
}),
|
|
1210
|
+
option.detail !== void 0 && (0, react_jsx_runtime.jsx)("span", {
|
|
1211
|
+
className: PopupSelectView_module_css_default.detail,
|
|
1212
|
+
children: option.detail
|
|
1213
|
+
}),
|
|
1214
|
+
option.active === true && (0, react_jsx_runtime.jsx)("span", {
|
|
1215
|
+
className: PopupSelectView_module_css_default.check,
|
|
1216
|
+
children: (0, react_jsx_runtime.jsx)(_x1a0f3n9_dsh_client_ui_primitives.IconCheckOutline16, {})
|
|
1217
|
+
})
|
|
1218
|
+
]
|
|
1219
|
+
}, option.id))
|
|
1220
|
+
})
|
|
1221
|
+
]
|
|
1222
|
+
}), confirmation !== void 0 && (0, react_jsx_runtime.jsx)(_x1a0f3n9_dsh_client_ui_primitives.RiskConfirmation, {
|
|
1223
|
+
open: true,
|
|
1224
|
+
title: confirmation.title,
|
|
1225
|
+
description: confirmation.description,
|
|
1226
|
+
acknowledgeLabel: confirmation.acknowledgeLabel,
|
|
1227
|
+
cancelLabel: confirmation.cancelLabel,
|
|
1228
|
+
closeLabel: t("close"),
|
|
1229
|
+
confirmLabel: confirmation.confirmLabel,
|
|
1230
|
+
acknowledged: state.acknowledged,
|
|
1231
|
+
onAcknowledgedChange: (value) => {
|
|
1232
|
+
popup.acknowledge(value);
|
|
1233
|
+
},
|
|
1234
|
+
onCancel: () => {
|
|
1235
|
+
popup.cancelConfirmation();
|
|
1236
|
+
},
|
|
1237
|
+
onConfirm: () => {
|
|
1238
|
+
popup.confirm();
|
|
1239
|
+
}
|
|
1240
|
+
})] });
|
|
1241
|
+
}
|
|
1242
|
+
//#endregion
|
|
1243
|
+
//#region lib/types/client/index.js
|
|
1244
|
+
/** Dictionary namespace owned by this plugin. */
|
|
1245
|
+
const NS = "command";
|
|
1246
|
+
/** Required services: the '/' source registry, session scopes, commands Remote, and locale registry. */
|
|
1247
|
+
const inject = [
|
|
1248
|
+
"inputTriggers",
|
|
1249
|
+
"sessions",
|
|
1250
|
+
"remote",
|
|
1251
|
+
"remote.commands",
|
|
1252
|
+
"locale"
|
|
1253
|
+
];
|
|
1254
|
+
/**
|
|
1255
|
+
* Mount the command service and its per-session popupSelect overlay.
|
|
1256
|
+
* @param ctx - client root context.
|
|
1257
|
+
*/
|
|
1258
|
+
function apply(ctx) {
|
|
1259
|
+
ctx.effect(() => ctx.locale.register(NS, {
|
|
1260
|
+
zh,
|
|
1261
|
+
en
|
|
1262
|
+
}), "ui-commands: dictionaries");
|
|
1263
|
+
ctx.plugin(CommandUiRuntime);
|
|
1264
|
+
ctx.inject([
|
|
1265
|
+
"slots",
|
|
1266
|
+
"commandUi",
|
|
1267
|
+
"sessions"
|
|
1268
|
+
], (scope) => {
|
|
1269
|
+
const command = scope.commandUi;
|
|
1270
|
+
const sessions = scope.get("sessions");
|
|
1271
|
+
scope.slots.inject("conversation.input.overlay", () => scope.slots.register({
|
|
1272
|
+
name: "conversation.input.overlay",
|
|
1273
|
+
id: "command-popup",
|
|
1274
|
+
order: 1,
|
|
1275
|
+
locale: NS,
|
|
1276
|
+
inject: (sessionId) => {
|
|
1277
|
+
const actx = sessions.scope(sessionId);
|
|
1278
|
+
if (actx === void 0) throw new Error(`ui-commands: session "${String(sessionId)}" resolved no scope`);
|
|
1279
|
+
return { popup: command.popupFor(actx) };
|
|
1280
|
+
}
|
|
1281
|
+
}, PopupSelectView));
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
//#endregion
|
|
1285
|
+
exports.CommandDirectory = CommandDirectory;
|
|
1286
|
+
exports.CommandUiRuntime = CommandUiRuntime;
|
|
1287
|
+
exports.PopupSelectController = PopupSelectController;
|
|
1288
|
+
exports.apply = apply;
|
|
1289
|
+
exports.filterOptions = filterOptions;
|
|
1290
|
+
exports.inject = inject;
|
|
1291
|
+
return module.exports;
|
|
1292
|
+
}
|
|
1293
|
+
});
|
|
1294
|
+
|
|
1295
|
+
//# sourceMappingURL=client.js.map
|