@qilin-agent/client-ui-input-trigger 0.0.0-stage → 3.1.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/lib/client.js ADDED
@@ -0,0 +1,1927 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@qilin-agent/client-ui-input-trigger",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let _qilin_agent_kylin = require("@qilin-agent/kylin");
8
+ let _qilin_agent_client_store = require("@qilin-agent/client-store");
9
+ let react_jsx_runtime = require("react/jsx-runtime");
10
+ let react = require("react");
11
+ let _qilin_agent_client_ui_primitives = require("@qilin-agent/client-ui-primitives");
12
+ //#region ../../util/values/src/partial-json.ts
13
+ /**
14
+ * Lazily scanned view of one JSON object's top-level fields, built from text
15
+ * that may still be streaming or from an already parsed object. Nothing is
16
+ * scanned until a reader asks; the view remembers every question it answered
17
+ * and reports changed answers when the owner refreshes for publication.
18
+ * Used for model tool-call arguments: a row reads the fields it
19
+ * cares about at whatever granularity it displays, at every stage of the call.
20
+ * @module @qilin-agent/util-values/src/partial-json
21
+ */
22
+ const SIMPLE_ESCAPES = {
23
+ "\"": "\"",
24
+ "\\": "\\",
25
+ "/": "/",
26
+ b: "\b",
27
+ f: "\f",
28
+ n: "\n",
29
+ r: "\r",
30
+ t: " "
31
+ };
32
+ const CONTENT_ESCAPE = /[\\\u0000-\u001f]/u;
33
+ function isWhitespace(c) {
34
+ return c === " " || c === "\n" || c === "\r" || c === " ";
35
+ }
36
+ function isHex(c) {
37
+ return c >= "0" && c <= "9" || c >= "a" && c <= "f" || c >= "A" && c <= "F";
38
+ }
39
+ (class PartialArguments {
40
+ /** The view of a call with no arguments available. */
41
+ static EMPTY = PartialArguments.fromObject({});
42
+ /**
43
+ * View finished argument text without scanning it until a reader asks.
44
+ * @param text - the complete argument JSON text.
45
+ * @returns a sealed view.
46
+ */
47
+ static fromText(text) {
48
+ const view = new PartialArguments();
49
+ view.append(text);
50
+ view.sealed = true;
51
+ return view;
52
+ }
53
+ /**
54
+ * View an already parsed argument payload, such as a PTC dispatch object.
55
+ * @param value - the parsed argument value.
56
+ * @returns a sealed view; a non-object payload has no fields.
57
+ */
58
+ static fromObject(value) {
59
+ const view = new PartialArguments();
60
+ view.object = typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
61
+ view.sealed = true;
62
+ return view;
63
+ }
64
+ /**
65
+ * The source: text so far or a parsed object, plus whether it can still grow.
66
+ * These are the only enumerable fields, so two views over the same source
67
+ * compare equal structurally however far each has been read.
68
+ */
69
+ chunks = [];
70
+ object;
71
+ sealed = false;
72
+ #ends = [];
73
+ #size = 0;
74
+ #consumed = 0;
75
+ #mode = "root";
76
+ #escape = false;
77
+ #keyStart = 0;
78
+ #keyEscaped = false;
79
+ #key = "";
80
+ #current = null;
81
+ #nestedEnds = [];
82
+ #nestedInString = false;
83
+ #invalidAt;
84
+ #invalidValue = false;
85
+ #entries = /* @__PURE__ */ new Map();
86
+ #order = [];
87
+ #reads = /* @__PURE__ */ new Map();
88
+ /** Whether this view rejects further appends; does not scan text or register reads. */
89
+ get isSealed() {
90
+ return this.sealed;
91
+ }
92
+ /** Whether indexing or a content read found invalid JSON; unread value contents are not validated. */
93
+ get invalid() {
94
+ this.scan();
95
+ return this.#mode === "invalid" || this.#invalidValue;
96
+ }
97
+ /**
98
+ * Retain streamed argument text without scanning or comparing observed answers.
99
+ * @param fragment - the text following every fragment appended before.
100
+ */
101
+ append(fragment) {
102
+ if (this.sealed) throw new Error("PartialArguments: cannot append to a sealed view");
103
+ if (fragment.length === 0) return;
104
+ this.chunks.push(fragment);
105
+ this.#size += fragment.length;
106
+ this.#ends.push(this.#size);
107
+ }
108
+ /**
109
+ * Reconcile a streamed prefix with authoritative complete text without joining the fragments.
110
+ * @param text - the final argument text, which replaces missing or conflicting deltas.
111
+ * @returns this view sealed with its caches retained when every character matches; otherwise a new sealed view.
112
+ */
113
+ settle(text) {
114
+ if (this.object !== void 0 || text.length !== this.#size) return PartialArguments.fromText(text);
115
+ let offset = 0;
116
+ for (const chunk of this.chunks) {
117
+ if (!text.startsWith(chunk, offset)) return PartialArguments.fromText(text);
118
+ offset += chunk.length;
119
+ }
120
+ this.chunks = text.length === 0 ? [] : [text];
121
+ this.#ends = text.length === 0 ? [] : [text.length];
122
+ this.sealed = true;
123
+ return this;
124
+ }
125
+ /**
126
+ * Compare observed answers and advance their publication baseline. Unread views remain unscanned.
127
+ * @returns whether any observed answer changed since its first read or the preceding refresh.
128
+ */
129
+ refresh() {
130
+ if (this.#reads.size === 0) return false;
131
+ this.scan();
132
+ let changed = false;
133
+ let completions = false;
134
+ for (const read of this.#reads.values()) {
135
+ if (read.completion) {
136
+ completions = true;
137
+ continue;
138
+ }
139
+ changed = this.refreshRead(read) || changed;
140
+ }
141
+ if (completions) {
142
+ for (const read of this.#reads.values()) if (read.completion) changed = this.refreshRead(read) || changed;
143
+ }
144
+ if (this.sealed) this.#reads.clear();
145
+ return changed;
146
+ }
147
+ refreshRead(read) {
148
+ const now = read.answer();
149
+ if (Object.is(now, read.last)) return false;
150
+ read.last = now;
151
+ return true;
152
+ }
153
+ /**
154
+ * Check whether no further fields can arrive.
155
+ * @returns whether the outer object closed, indexing failed, or the view is sealed; unread values are not validated.
156
+ */
157
+ closed() {
158
+ return this.remember("closed", "", () => this.closedNow());
159
+ }
160
+ /**
161
+ * List discovered fields in first-appearance order.
162
+ * @returns top-level keys seen so far, in first-appearance order.
163
+ */
164
+ keys() {
165
+ return this.remember("keys", "", () => this.keysNow(), (keys) => keys.length);
166
+ }
167
+ /**
168
+ * Check whether a top-level field has appeared.
169
+ * @param key - argument name.
170
+ * @returns whether the field has appeared (a string opened or another value began).
171
+ */
172
+ has(key) {
173
+ return this.remember("has", key, () => this.hasNow(key));
174
+ }
175
+ /**
176
+ * Check whether a field's closing delimiter has arrived, without validating its contents.
177
+ * @param key - argument name.
178
+ * @returns whether its delimiter arrived and no content reader has reported an error for this value.
179
+ */
180
+ complete(key) {
181
+ return this.remember("complete", key, () => this.completeNow(key));
182
+ }
183
+ /**
184
+ * Read string length without materializing its text.
185
+ * @param key - argument name.
186
+ * @param options - change granularity for a streaming string.
187
+ * @returns decoded UTF-16 length of the string field so far; undefined when absent or not a string.
188
+ */
189
+ stringLength(key, options) {
190
+ const step = Math.max(1, Math.floor(options?.step ?? 1));
191
+ const offset = options?.offset ?? 0;
192
+ return this.remember(`length:${step}:${offset}`, key, () => this.lengthNow(key), (length) => length === void 0 ? void 0 : Math.ceil((length + offset) / step));
193
+ }
194
+ /**
195
+ * Check a string against a decoded UTF-16 length limit without materializing it.
196
+ * @param key - argument name.
197
+ * @param maxLength - decoded UTF-16 limit, floored to at least zero.
198
+ * @returns whether the string is longer than the limit; false when absent or not a string.
199
+ */
200
+ stringExceeds(key, maxLength) {
201
+ const limit = Math.max(0, Math.floor(maxLength));
202
+ return this.remember(`exceeds:${limit}`, key, () => (this.lengthNow(key, limit + 1) ?? 0) > limit);
203
+ }
204
+ /**
205
+ * Read a decoded string, including a streaming prefix.
206
+ * @param key - argument name.
207
+ * @returns the string field's decoded text so far; undefined when absent or not a string.
208
+ */
209
+ text(key) {
210
+ return this.remember("text", key, () => this.textNow(key));
211
+ }
212
+ /**
213
+ * Read at most the first decoded UTF-16 units of a string.
214
+ * @param key - argument name.
215
+ * @param maxLength - maximum decoded UTF-16 length, floored to at least one.
216
+ * @returns the bounded string prefix; undefined when absent or not a string.
217
+ */
218
+ textPrefix(key, maxLength) {
219
+ const limit = Math.max(1, Math.floor(maxLength));
220
+ return this.remember(`prefix:${limit}`, key, () => this.textPrefixNow(key, limit));
221
+ }
222
+ /**
223
+ * Read a completed non-string argument.
224
+ * @param key - argument name.
225
+ * @returns the parsed non-string value once it closed; undefined while open, absent, or a string.
226
+ */
227
+ value(key) {
228
+ return this.remember("value", key, () => this.valueNow(key));
229
+ }
230
+ /** Answer a question and, on a streaming view, remember it for change detection. */
231
+ remember(kind, key, read, comparison) {
232
+ this.scan();
233
+ const result = read();
234
+ if (!this.sealed) {
235
+ const id = `${kind}/${key}`;
236
+ if (!this.#reads.has(id)) this.#reads.set(id, {
237
+ completion: kind === "complete",
238
+ answer: comparison === void 0 ? read : () => comparison(read()),
239
+ last: comparison === void 0 ? result : comparison(result)
240
+ });
241
+ }
242
+ return result;
243
+ }
244
+ closedNow() {
245
+ return this.sealed || this.#mode === "closed" || this.#mode === "invalid";
246
+ }
247
+ keysNow() {
248
+ return this.object === void 0 ? this.#order : Object.keys(this.object);
249
+ }
250
+ hasNow(key) {
251
+ return this.object === void 0 ? this.#entries.has(key) : Object.hasOwn(this.object, key);
252
+ }
253
+ completeNow(key) {
254
+ if (this.object !== void 0) return Object.hasOwn(this.object, key);
255
+ const entry = this.#entries.get(key);
256
+ return entry !== void 0 && entry.end >= 0 && (entry.kind === "string" ? entry.invalidAt === void 0 : !entry.invalid);
257
+ }
258
+ lengthNow(key, limit = Number.POSITIVE_INFINITY) {
259
+ if (this.object !== void 0) {
260
+ const field = Object.hasOwn(this.object, key) ? this.object[key] : void 0;
261
+ return typeof field === "string" ? field.length : void 0;
262
+ }
263
+ const entry = this.#entries.get(key);
264
+ if (entry?.kind !== "string") return void 0;
265
+ if (entry.text !== void 0 && entry.text.at === entry.end) return entry.text.length;
266
+ const read = entry.length ??= {
267
+ at: entry.start,
268
+ length: 0,
269
+ text: ""
270
+ };
271
+ this.readString(entry, read, limit, false);
272
+ return read.length;
273
+ }
274
+ textNow(key) {
275
+ if (this.object !== void 0) {
276
+ const field = Object.hasOwn(this.object, key) ? this.object[key] : void 0;
277
+ return typeof field === "string" ? field : void 0;
278
+ }
279
+ const entry = this.#entries.get(key);
280
+ if (entry?.kind !== "string") return void 0;
281
+ if (entry.text === void 0 && entry.end >= 0 && entry.needsDecoding && entry.invalidAt === void 0) {
282
+ let text;
283
+ try {
284
+ text = JSON.parse(`"${this.slice(entry.start, entry.end)}"`);
285
+ } catch (_error) {}
286
+ if (text !== void 0) entry.text = {
287
+ at: entry.end,
288
+ length: text.length,
289
+ text
290
+ };
291
+ }
292
+ const read = entry.text ??= {
293
+ at: entry.start,
294
+ length: 0,
295
+ text: ""
296
+ };
297
+ this.readString(entry, read, Number.POSITIVE_INFINITY, true);
298
+ return read.text;
299
+ }
300
+ textPrefixNow(key, maxLength) {
301
+ if (this.object !== void 0) {
302
+ const field = Object.hasOwn(this.object, key) ? this.object[key] : void 0;
303
+ return typeof field === "string" ? field.slice(0, maxLength) : void 0;
304
+ }
305
+ const entry = this.#entries.get(key);
306
+ if (entry?.kind !== "string") return void 0;
307
+ const prefixes = entry.prefixes ??= /* @__PURE__ */ new Map();
308
+ let read = prefixes.get(maxLength);
309
+ if (read === void 0) {
310
+ read = {
311
+ at: entry.start,
312
+ length: 0,
313
+ text: ""
314
+ };
315
+ prefixes.set(maxLength, read);
316
+ }
317
+ this.readString(entry, read, maxLength, true);
318
+ return read.text;
319
+ }
320
+ valueNow(key) {
321
+ if (this.object !== void 0) {
322
+ if (!Object.hasOwn(this.object, key)) return void 0;
323
+ const field = this.object[key];
324
+ return typeof field === "string" ? void 0 : field;
325
+ }
326
+ const entry = this.#entries.get(key);
327
+ if (entry?.kind !== "value" || entry.end < 0 || entry.invalid) return void 0;
328
+ if (entry.parsed === void 0) try {
329
+ entry.parsed = JSON.parse(this.slice(entry.start, entry.end));
330
+ } catch (_error) {
331
+ entry.invalid = true;
332
+ this.#invalidValue = true;
333
+ }
334
+ return entry.parsed;
335
+ }
336
+ chunkAt(at) {
337
+ let low = 0;
338
+ let high = this.#ends.length;
339
+ while (low < high) {
340
+ const mid = low + high >>> 1;
341
+ if (this.#ends[mid] <= at) low = mid + 1;
342
+ else high = mid;
343
+ }
344
+ return low;
345
+ }
346
+ /** Materialize only a requested range, never the cumulative source. */
347
+ slice(start, end) {
348
+ if (start >= end) return "";
349
+ const first = this.chunkAt(start);
350
+ const last = this.chunkAt(end - 1);
351
+ const base = first === 0 ? 0 : this.#ends[first - 1];
352
+ if (first === last) return this.chunks[first].slice(start - base, end - base);
353
+ const parts = [this.chunks[first].slice(start - base)];
354
+ for (let i = first + 1; i < last; i++) parts.push(this.chunks[i]);
355
+ parts.push(this.chunks[last].slice(0, end - this.#ends[last - 1]));
356
+ return parts.join("");
357
+ }
358
+ readString(entry, read, limit, materialize) {
359
+ const end = Math.min(entry.end < 0 ? this.#consumed : entry.end, entry.invalidAt ?? Number.POSITIVE_INFINITY, this.#invalidAt ?? Number.POSITIVE_INFINITY);
360
+ if (!entry.needsDecoding) {
361
+ const length = Math.min(end - read.at, limit - read.length);
362
+ if (length <= 0) return;
363
+ if (materialize) read.text += this.slice(read.at, read.at + length);
364
+ read.at += length;
365
+ read.length += length;
366
+ return;
367
+ }
368
+ let chunkIndex = this.chunkAt(read.at);
369
+ while (read.at < end && read.length < limit) {
370
+ const base = chunkIndex === 0 ? 0 : this.#ends[chunkIndex - 1];
371
+ const chunk = this.chunks[chunkIndex];
372
+ const remaining = chunk.slice(read.at - base, Math.min(chunk.length, end - base));
373
+ const boundary = remaining.search(CONTENT_ESCAPE);
374
+ const length = Math.min(boundary < 0 ? remaining.length : boundary, limit - read.length);
375
+ if (length > 0) {
376
+ if (materialize) read.text += remaining.slice(0, length);
377
+ read.at += length;
378
+ read.length += length;
379
+ if (read.at === base + chunk.length) chunkIndex++;
380
+ continue;
381
+ }
382
+ const type = remaining.length > 1 ? remaining[1] : read.at + 1 < end ? this.chunks[chunkIndex + 1][0] : void 0;
383
+ let decoded;
384
+ let width = 2;
385
+ if (remaining[0] === "\\" && type === void 0 && entry.end < 0) return;
386
+ if (remaining[0] === "\\" && type === "u") {
387
+ const hex = this.slice(read.at + 2, Math.min(end, read.at + 6));
388
+ let valid = true;
389
+ for (let i = 0; i < hex.length; i++) if (!isHex(hex[i])) valid = false;
390
+ if (valid) {
391
+ if (hex.length < 4 && entry.end < 0) return;
392
+ if (hex.length === 4) decoded = String.fromCharCode(Number.parseInt(hex, 16));
393
+ }
394
+ width = 6;
395
+ } else if (remaining[0] === "\\" && type !== void 0) decoded = SIMPLE_ESCAPES[type];
396
+ if (decoded === void 0) {
397
+ entry.invalidAt = read.at;
398
+ this.#invalidValue = true;
399
+ return;
400
+ }
401
+ if (materialize) read.text += decoded;
402
+ read.length++;
403
+ read.at += width;
404
+ while (chunkIndex < this.chunks.length && read.at >= this.#ends[chunkIndex]) chunkIndex++;
405
+ }
406
+ }
407
+ /** Locate new field ranges without decoding or parsing their contents. */
408
+ scan() {
409
+ if (this.object !== void 0 || this.#consumed === this.#size) return;
410
+ for (let i = this.chunkAt(this.#consumed); i < this.chunks.length && this.#invalidAt === void 0; i++) {
411
+ const pending = this.chunks[i];
412
+ const base = i === 0 ? 0 : this.#ends[i - 1];
413
+ for (let index = this.#consumed - base; index < pending.length && this.#mode !== "invalid"; index++) {
414
+ if (this.#mode === "string" || this.#mode === "nested" && this.#nestedInString) {
415
+ const end = this.stringBoundary(pending, index);
416
+ this.#consumed += end - index;
417
+ index = end;
418
+ if (index === pending.length) break;
419
+ }
420
+ this.step(pending[index], this.#consumed);
421
+ this.#consumed++;
422
+ }
423
+ }
424
+ }
425
+ /** Only raw quotes and their preceding backslash runs can terminate a string. */
426
+ stringBoundary(fragment, start) {
427
+ let at = start;
428
+ while (true) {
429
+ const quote = fragment.indexOf("\"", at);
430
+ const end = quote < 0 ? fragment.length : quote;
431
+ if (this.#mode === "string") {
432
+ const entry = this.#current;
433
+ if (!entry.needsDecoding && CONTENT_ESCAPE.test(fragment.slice(at, end))) entry.needsDecoding = true;
434
+ }
435
+ let slashStart = end;
436
+ while (slashStart > at && fragment[slashStart - 1] === "\\") slashStart--;
437
+ const escaped = (end - slashStart) % 2 === 1 !== (slashStart === at && this.#escape);
438
+ this.#escape = quote < 0 && escaped;
439
+ if (quote < 0 || !escaped) return end;
440
+ at = quote + 1;
441
+ }
442
+ }
443
+ step(c, at) {
444
+ switch (this.#mode) {
445
+ case "root":
446
+ if (isWhitespace(c)) return;
447
+ if (c === "{") {
448
+ this.#mode = "key-or-end";
449
+ return;
450
+ }
451
+ this.fail();
452
+ return;
453
+ case "key-or-end":
454
+ if (isWhitespace(c)) return;
455
+ if (c === "}") {
456
+ this.#mode = "closed";
457
+ return;
458
+ }
459
+ if (c === "\"") {
460
+ this.beginKey(at);
461
+ return;
462
+ }
463
+ this.fail();
464
+ return;
465
+ case "key-only":
466
+ if (isWhitespace(c)) return;
467
+ if (c === "\"") {
468
+ this.beginKey(at);
469
+ return;
470
+ }
471
+ this.fail();
472
+ return;
473
+ case "key":
474
+ this.stepKey(c, at);
475
+ return;
476
+ case "colon":
477
+ if (isWhitespace(c)) return;
478
+ if (c === ":") {
479
+ this.#mode = "value";
480
+ return;
481
+ }
482
+ this.fail();
483
+ return;
484
+ case "value":
485
+ this.beginValue(c, at);
486
+ return;
487
+ case "string": {
488
+ const entry = this.#current;
489
+ entry.end = at;
490
+ this.#current = null;
491
+ this.#mode = "comma-or-end";
492
+ return;
493
+ }
494
+ case "scalar":
495
+ this.stepScalar(c, at);
496
+ return;
497
+ case "nested":
498
+ this.stepNested(c, at);
499
+ return;
500
+ case "comma-or-end":
501
+ if (isWhitespace(c)) return;
502
+ if (c === ",") {
503
+ this.#mode = "key-only";
504
+ return;
505
+ }
506
+ if (c === "}") {
507
+ this.#mode = "closed";
508
+ return;
509
+ }
510
+ this.fail();
511
+ return;
512
+ case "closed":
513
+ if (isWhitespace(c)) return;
514
+ this.fail();
515
+ return;
516
+ /* v8 ignore next 2 -- scan() stops stepping once the view is invalid. */
517
+ case "invalid": return;
518
+ /* v8 ignore next 2 -- Every scanner mode has a handler above. */
519
+ default: assertNever(this.#mode);
520
+ }
521
+ }
522
+ fail() {
523
+ this.#invalidAt = this.#consumed;
524
+ this.#mode = "invalid";
525
+ this.#current = null;
526
+ }
527
+ beginKey(at) {
528
+ this.#mode = "key";
529
+ this.#keyStart = at + 1;
530
+ this.#keyEscaped = false;
531
+ this.#escape = false;
532
+ }
533
+ stepKey(c, at) {
534
+ if (c < " ") {
535
+ this.fail();
536
+ return;
537
+ }
538
+ if (this.#escape) {
539
+ this.#escape = false;
540
+ return;
541
+ }
542
+ if (c === "\\") {
543
+ this.#escape = true;
544
+ this.#keyEscaped = true;
545
+ return;
546
+ }
547
+ if (c !== "\"") return;
548
+ const raw = this.slice(this.#keyStart, at);
549
+ if (this.#keyEscaped) try {
550
+ this.#key = JSON.parse(`"${raw}"`);
551
+ } catch (_error) {
552
+ this.fail();
553
+ return;
554
+ }
555
+ else this.#key = raw;
556
+ this.#mode = "colon";
557
+ }
558
+ open(entry) {
559
+ if (!this.#entries.has(this.#key)) this.#order.push(this.#key);
560
+ this.#entries.set(this.#key, entry);
561
+ this.#current = entry;
562
+ }
563
+ beginValue(c, at) {
564
+ if (isWhitespace(c)) return;
565
+ if (c === "\"") {
566
+ this.open({
567
+ kind: "string",
568
+ start: at + 1,
569
+ end: -1,
570
+ needsDecoding: false,
571
+ invalidAt: void 0,
572
+ length: void 0,
573
+ text: void 0,
574
+ prefixes: void 0
575
+ });
576
+ this.#escape = false;
577
+ this.#mode = "string";
578
+ return;
579
+ }
580
+ if (c === "}" || c === "," || c === ":" || c === "]") {
581
+ this.fail();
582
+ return;
583
+ }
584
+ this.open({
585
+ kind: "value",
586
+ start: at,
587
+ end: -1,
588
+ parsed: void 0,
589
+ invalid: false
590
+ });
591
+ if (c === "{" || c === "[") {
592
+ this.#mode = "nested";
593
+ this.#nestedEnds = [c === "{" ? "}" : "]"];
594
+ this.#nestedInString = false;
595
+ this.#escape = false;
596
+ return;
597
+ }
598
+ this.#mode = "scalar";
599
+ }
600
+ stepScalar(c, at) {
601
+ if (c !== "," && c !== "}" && !isWhitespace(c)) return;
602
+ this.closeValue(at);
603
+ this.#mode = c === "," ? "key-only" : c === "}" ? "closed" : "comma-or-end";
604
+ }
605
+ stepNested(c, at) {
606
+ if (this.#nestedInString) {
607
+ this.#nestedInString = false;
608
+ return;
609
+ }
610
+ if (c === "\"") {
611
+ this.#nestedInString = true;
612
+ return;
613
+ }
614
+ if (c === "{" || c === "[") {
615
+ this.#nestedEnds.push(c === "{" ? "}" : "]");
616
+ return;
617
+ }
618
+ if (c === "}" || c === "]") {
619
+ if (this.#nestedEnds.pop() !== c) {
620
+ this.fail();
621
+ return;
622
+ }
623
+ if (this.#nestedEnds.length === 0) {
624
+ this.closeValue(at + 1);
625
+ this.#mode = "comma-or-end";
626
+ }
627
+ }
628
+ }
629
+ closeValue(end) {
630
+ const entry = this.#current;
631
+ entry.end = end;
632
+ this.#current = null;
633
+ }
634
+ });
635
+ //#endregion
636
+ //#region ../../util/values/src/index.ts
637
+ /**
638
+ * Mark an unreachable closed-union branch.
639
+ * @param value - impossible value; an unhandled typed variant fails at the call site.
640
+ * @param context - optional switch-site label included in the failure message.
641
+ * @returns never; a runtime value that escaped its type always throws.
642
+ */
643
+ function assertNever(value, context) {
644
+ const rendered = JSON.stringify(value) ?? String(value);
645
+ throw new Error(`unreachable variant${context ? ` in ${context}` : ""}: ${rendered}`);
646
+ }
647
+ /**
648
+ * Weak-key lookup with a strongly retained iterable set of associated values.
649
+ *
650
+ * Each value must belong to only one key. The container performs no automatic
651
+ * cleanup; owners delete associations or clear the container at lifecycle end.
652
+ */
653
+ var WeakMapWithValues = class {
654
+ keys = /* @__PURE__ */ new WeakMap();
655
+ valueSet = /* @__PURE__ */ new Set();
656
+ /** Live strongly retained values in insertion order. */
657
+ values = this.valueSet;
658
+ /**
659
+ * Read the value associated with a key.
660
+ * @param key - weakly held lookup key.
661
+ * @returns the associated value, or absence.
662
+ */
663
+ get(key) {
664
+ return this.keys.get(key);
665
+ }
666
+ /**
667
+ * Test whether a key has an association.
668
+ * @param key - weakly held lookup key.
669
+ * @returns whether the key is present.
670
+ */
671
+ has(key) {
672
+ return this.keys.has(key);
673
+ }
674
+ /**
675
+ * Associate one key with one caller-unique value.
676
+ * @param key - weakly held lookup key.
677
+ * @param value - strongly retained value that belongs to no other key.
678
+ * @returns this container.
679
+ */
680
+ set(key, value) {
681
+ if (this.keys.has(key)) {
682
+ const previous = this.keys.get(key);
683
+ if (previous === value) return this;
684
+ this.valueSet.delete(previous);
685
+ }
686
+ this.keys.set(key, value);
687
+ this.valueSet.add(value);
688
+ return this;
689
+ }
690
+ /**
691
+ * Remove one association and its strongly retained value.
692
+ * @param key - weakly held lookup key.
693
+ * @returns whether an association was removed.
694
+ */
695
+ delete(key) {
696
+ if (!this.keys.has(key)) return false;
697
+ const value = this.keys.get(key);
698
+ const deleted = this.keys.delete(key);
699
+ this.valueSet.delete(value);
700
+ return deleted;
701
+ }
702
+ /** Remove every association and strongly retained value. */
703
+ clear() {
704
+ this.keys = /* @__PURE__ */ new WeakMap();
705
+ this.valueSet.clear();
706
+ }
707
+ };
708
+ //#endregion
709
+ //#region ../../context/file-reference/src/grammar.ts
710
+ /**
711
+ * Extract an `@path` or `@"path with spaces` token at the cursor. An `@`
712
+ * inside another token, such as an email address, is not a completion trigger.
713
+ * @param line - current editor line.
714
+ * @param cursorCol - cursor column within that line.
715
+ * @returns the active token, or `undefined` outside an `@` token.
716
+ */
717
+ function activeAtToken(line, cursorCol) {
718
+ const beforeCursor = line.slice(0, cursorCol);
719
+ const quoted = /(?:^|\s)(@"([^"]*))$/u.exec(beforeCursor);
720
+ if (quoted?.[1] !== void 0 && quoted[2] !== void 0) return {
721
+ prefix: quoted[1],
722
+ query: quoted[2],
723
+ quoted: true
724
+ };
725
+ const plain = /(?:^|\s)(@([^\s]*))$/u.exec(beforeCursor);
726
+ if (plain?.[1] === void 0 || plain[2] === void 0) return void 0;
727
+ return {
728
+ prefix: plain[1],
729
+ query: plain[2],
730
+ quoted: false
731
+ };
732
+ }
733
+ //#endregion
734
+ //#region lib/types/core/detect.js
735
+ /**
736
+ * Trigger detection pure core. Scans backward from
737
+ * the caret for a live trigger char under the guard tier and applies the
738
+ * word-boundary rules. Zero React / DOM / cordis.
739
+ */
740
+ const WORD_CHAR = /[\p{L}\p{N}_]/u;
741
+ const WHITESPACE = /\s/u;
742
+ /**
743
+ * Word-boundary rule: a trigger char opens only at start-of-draft, after
744
+ * whitespace (newlines included), or after punctuation. Two URL carve-outs
745
+ * keep '/' dead inside URLs (both pinned by tests): '/' after a ':' that
746
+ * itself follows a non-whitespace char (scheme separator, `https:/…`), and
747
+ * '/' directly after another '/' (second slash of `//`).
748
+ */
749
+ function boundaryOk(draft, index, char) {
750
+ if (index === 0) return true;
751
+ const prev = draft.charAt(index - 1);
752
+ if (WHITESPACE.test(prev)) return true;
753
+ if (WORD_CHAR.test(prev)) return false;
754
+ if (char === "/") {
755
+ if (prev === "/") return false;
756
+ if (prev === ":" && index >= 2 && !WHITESPACE.test(draft.charAt(index - 2))) return false;
757
+ }
758
+ return true;
759
+ }
760
+ /**
761
+ * Detect a trigger token at the caret. `@` first uses the shared grammar,
762
+ * including an open quoted token that may span whitespace. Slash detection
763
+ * scans left to the first whitespace; slashes failing the word boundary are
764
+ * treated as ordinary token chars and the scan continues (URL slashes).
765
+ * Guard tiers: plain = both chars live; claimed = '/' fully suppressed,
766
+ * '@' live; frozen = none.
767
+ *
768
+ * @param draft - Full draft text.
769
+ * @param caret - Caret offset into `draft`.
770
+ * @param guard - Availability tier derived from the input phase.
771
+ * @returns The hit with `query` = trigger-to-caret slice and `span` =
772
+ * `{start: triggerIndex, end: caret}`; `span.draftRev` is a placeholder `0`
773
+ * — the calling shell stamps the real revision. Null when no trigger is
774
+ * live at the caret.
775
+ */
776
+ const detectTrigger = (draft, caret, guard) => {
777
+ if (guard.tier === "frozen") return null;
778
+ const at = activeAtToken(draft, caret);
779
+ if (at !== void 0) {
780
+ const start = caret - at.prefix.length;
781
+ return {
782
+ trigger: "@",
783
+ query: at.query,
784
+ quoted: at.quoted,
785
+ position: draft.search(/\S/) === start ? "leading" : "inline",
786
+ span: {
787
+ start,
788
+ end: caret,
789
+ draftRev: 0
790
+ }
791
+ };
792
+ }
793
+ for (let i = caret - 1; i >= 0; i--) {
794
+ const ch = draft.charAt(i);
795
+ if (WHITESPACE.test(ch)) return null;
796
+ if (ch !== "/") continue;
797
+ if (guard.tier === "claimed") continue;
798
+ if (!boundaryOk(draft, i, ch)) continue;
799
+ return {
800
+ trigger: ch,
801
+ query: draft.slice(i + 1, caret),
802
+ quoted: false,
803
+ position: draft.search(/\S/) === i ? "leading" : "inline",
804
+ span: {
805
+ start: i,
806
+ end: caret,
807
+ draftRev: 0
808
+ }
809
+ };
810
+ }
811
+ return null;
812
+ };
813
+ //#endregion
814
+ //#region lib/types/core/menu.js
815
+ /** Closed rest state with generation 0; store initializer and test seed. */
816
+ const MENU_CLOSED = {
817
+ open: false,
818
+ hit: null,
819
+ generation: 0,
820
+ groups: [],
821
+ highlight: null
822
+ };
823
+ /**
824
+ * Replace the group roster with pending groups for `sources`, in order.
825
+ * Shell-side step before dispatching `hit` on a fresh menu open.
826
+ *
827
+ * @param state - Current menu state.
828
+ * @param sources - Sources registered for the hit trigger, in menu order.
829
+ * @returns State carrying the new pending roster; highlight cleared.
830
+ */
831
+ function seedGroups(state, sources) {
832
+ return {
833
+ ...state,
834
+ groups: sources.map((source) => ({
835
+ source: source.name,
836
+ ...source.showGroupTitle === false ? { showGroupTitle: false } : {},
837
+ status: "pending",
838
+ items: []
839
+ })),
840
+ highlight: null
841
+ };
842
+ }
843
+ /** Close, preserving the generation so in-flight settlements stay droppable. */
844
+ const closed = (state) => state.open || state.hit !== null || state.groups.length > 0 || state.highlight !== null ? {
845
+ open: false,
846
+ hit: null,
847
+ generation: state.generation,
848
+ groups: [],
849
+ highlight: null
850
+ } : state;
851
+ /** First item of the first non-empty ready group, or null. */
852
+ function firstHighlight(groups) {
853
+ for (const g of groups) if (g.status === "ready" && g.items.length > 0) return {
854
+ source: g.source,
855
+ index: 0
856
+ };
857
+ return null;
858
+ }
859
+ /** The highlight itself when it still points at a ready item, else null. */
860
+ function validHighlight(highlight, groups) {
861
+ if (!highlight) return null;
862
+ const g = groups.find((x) => x.source === highlight.source);
863
+ return g && g.status === "ready" && highlight.index < g.items.length ? highlight : null;
864
+ }
865
+ /** Flatten ready items into (source, index) positions in group order. */
866
+ function positions(groups) {
867
+ const out = [];
868
+ for (const g of groups) {
869
+ if (g.status !== "ready") continue;
870
+ for (let i = 0; i < g.items.length; i++) out.push({
871
+ source: g.source,
872
+ index: i
873
+ });
874
+ }
875
+ return out;
876
+ }
877
+ /** True when every group is ready with zero items (the auto-close condition). */
878
+ const allReadyEmpty = (groups) => groups.every((g) => g.status === "ready" && g.items.length === 0);
879
+ /**
880
+ * Pure menu reducer. `hit` opens a new generation over the seeded roster
881
+ * (null hit closes); `source-settled` outside the current generation, the
882
+ * open menu, or the roster is dropped; a settlement or failure leaving every
883
+ * group ready-and-empty (or no groups) auto-closes; `source-failed` silently
884
+ * removes the group (the shell logs); `move` cycles the highlight across
885
+ * ready items; `hover` parks it on one ready item (pointer and keyboard
886
+ * share the single highlight — last input wins).
887
+ *
888
+ * @param state - Current menu state.
889
+ * @param ev - Menu event.
890
+ * @returns Next state; the same reference when stale or a no-op.
891
+ */
892
+ const menuReduce = (state, ev) => {
893
+ switch (ev.type) {
894
+ case "hit":
895
+ if (ev.hit === null) return closed(state);
896
+ return {
897
+ open: true,
898
+ hit: ev.hit,
899
+ generation: state.generation + 1,
900
+ groups: state.groups.map((g) => ({
901
+ ...g,
902
+ status: "pending"
903
+ })),
904
+ highlight: state.highlight
905
+ };
906
+ case "source-settled": {
907
+ if (!state.open || ev.generation !== state.generation) return state;
908
+ const idx = state.groups.findIndex((g) => g.source === ev.source);
909
+ if (idx < 0) return state;
910
+ const items = ev.items ?? [];
911
+ const groups = state.groups.map((g, i) => i === idx ? {
912
+ ...g,
913
+ status: "ready",
914
+ items
915
+ } : g);
916
+ if (allReadyEmpty(groups)) return closed(state);
917
+ const highlight = validHighlight(state.highlight, groups) ?? firstHighlight(groups);
918
+ return {
919
+ ...state,
920
+ groups,
921
+ highlight
922
+ };
923
+ }
924
+ case "source-failed": {
925
+ if (!state.open || ev.generation !== state.generation) return state;
926
+ if (!state.groups.some((g) => g.source === ev.source)) return state;
927
+ const groups = state.groups.filter((g) => g.source !== ev.source);
928
+ if (groups.length === 0 || allReadyEmpty(groups)) return closed(state);
929
+ const highlight = validHighlight(state.highlight, groups) ?? firstHighlight(groups);
930
+ return {
931
+ ...state,
932
+ groups,
933
+ highlight
934
+ };
935
+ }
936
+ case "move": {
937
+ if (!state.open) return state;
938
+ const pos = positions(state.groups);
939
+ if (pos.length === 0) return state;
940
+ const hl = state.highlight;
941
+ const at = hl ? pos.findIndex((p) => p.source === hl.source && p.index === hl.index) : -1;
942
+ const next = pos[at < 0 ? ev.dir === 1 ? 0 : pos.length - 1 : (at + ev.dir + pos.length) % pos.length];
943
+ if (next === void 0) return state;
944
+ if (hl && next.source === hl.source && next.index === hl.index) return state;
945
+ return {
946
+ ...state,
947
+ highlight: next
948
+ };
949
+ }
950
+ case "hover": {
951
+ if (!state.open) return state;
952
+ const target = validHighlight({
953
+ source: ev.source,
954
+ index: ev.index
955
+ }, state.groups);
956
+ if (target === null) return state;
957
+ const hl = state.highlight;
958
+ if (hl && hl.source === target.source && hl.index === target.index) return state;
959
+ return {
960
+ ...state,
961
+ highlight: target
962
+ };
963
+ }
964
+ case "close": return closed(state);
965
+ }
966
+ };
967
+ //#endregion
968
+ //#region lib/types/client/controller.js
969
+ /** Whether a tracked hit is the one the user just dismissed (same token, same query). */
970
+ function dismissedHit(dismissed, hit) {
971
+ return dismissed.trigger === hit.trigger && dismissed.query === hit.query && dismissed.quoted === hit.quoted && dismissed.start === hit.span.start && dismissed.end === hit.span.end;
972
+ }
973
+ /**
974
+ * Per-session trigger pipeline state and orchestration. All mutation stays
975
+ * inside; MenuView renders from {@link InputTriggerController.menu} and routes
976
+ * pointer picks back through {@link InputTriggerController.pick}.
977
+ */
978
+ var InputTriggerController = class {
979
+ deps;
980
+ /** Menu state store (per-session; survives session switches, dies with the scope). */
981
+ menu = (0, _qilin_agent_client_store.createSnapshotStore)(MENU_CLOSED);
982
+ /**
983
+ * Name of the source opened through the programmatic launcher, or null for
984
+ * trigger-detected/closed menus. Composer chrome subscribes to this store
985
+ * for the launcher's expanded state without owning a second menu model.
986
+ */
987
+ launcher = (0, _qilin_agent_client_store.createSnapshotStore)(null);
988
+ /**
989
+ * Crumbs published by each header-bearing source for the open menu, keyed
990
+ * by source name. A snapshot store like {@link InputTriggerController.launcher}:
991
+ * the answer changes with every hit, and render-side consumers subscribe
992
+ * instead of re-polling sources during a render.
993
+ */
994
+ headers = (0, _qilin_agent_client_store.createSnapshotStore)(/* @__PURE__ */ new Map());
995
+ /**
996
+ * Aggregated hot reference lexicon, grouped by trigger (plain-text-reference decision;
997
+ * see .agents/notes/archived/architecture/2026-07-25-web-input-machine-and-slash-pipeline.md):
998
+ * sources implementing the lexicon hook are polled with the session
999
+ * projection; undefined answers (roll not hot yet) are skipped; multiple
1000
+ * sources on one trigger concatenate in registration order. A snapshot
1001
+ * store because rolls change asynchronously (catalog settles, children
1002
+ * spawn/exit) — render-side consumers subscribe instead of re-reading a
1003
+ * mutable answer.
1004
+ */
1005
+ lexicon = (0, _qilin_agent_client_store.createSnapshotStore)(/* @__PURE__ */ new Map());
1006
+ /** The authoritative hit: single truth for span CAS material (menu snapshot never carries it alone). */
1007
+ hit = null;
1008
+ /**
1009
+ * Identity of the hit whose menu the user dismissed. A dismissal means "not
1010
+ * this one, not now": the same token with the same query keeps its menu
1011
+ * closed, so restoring the caret after a dismissal cannot reopen it. Typing
1012
+ * (a new query) or moving to another token clears it.
1013
+ */
1014
+ dismissed = null;
1015
+ /** Whether the open menu was reached by a drill pick; cleared with the menu. */
1016
+ drilled = false;
1017
+ fetch = null;
1018
+ disposed = false;
1019
+ /** Per-source lexicon unsubscribers (sources without the hook never enter). */
1020
+ lexiconOffs = /* @__PURE__ */ new Map();
1021
+ constructor(deps) {
1022
+ this.deps = deps;
1023
+ const projection = this.project();
1024
+ for (const src of deps.roster.all()) {
1025
+ src.warm?.(projection);
1026
+ this.watchLexicon(src, projection);
1027
+ }
1028
+ this.refreshLexicon();
1029
+ }
1030
+ /**
1031
+ * Feed a draft/caret change through trigger detection and drive the menu.
1032
+ * @param draft - full draft text.
1033
+ * @param caret - caret offset into `draft`.
1034
+ * @param guard - availability tier derived from the input phase.
1035
+ * @param draftRev - the input machine's current draft revision, stamped
1036
+ * into the hit span for pick-time CAS.
1037
+ */
1038
+ track(draft, caret, guard, draftRev) {
1039
+ if (this.disposed) return;
1040
+ const launched = this.launcher.getSnapshot() !== null;
1041
+ this.clearLauncher();
1042
+ const raw = detectTrigger(draft, caret, guard);
1043
+ if (raw === null) {
1044
+ if (launched) return;
1045
+ this.hit = null;
1046
+ if (guard.tier !== "frozen") this.dismissed = null;
1047
+ this.stopFetch();
1048
+ this.reduce({ type: "close" });
1049
+ return;
1050
+ }
1051
+ const hit = {
1052
+ ...raw,
1053
+ span: {
1054
+ ...raw.span,
1055
+ draftRev
1056
+ }
1057
+ };
1058
+ if (launched) this.dismissed = null;
1059
+ if (this.dismissed !== null) if (!dismissedHit(this.dismissed, hit)) this.dismissed = null;
1060
+ else {
1061
+ this.hit = hit;
1062
+ return;
1063
+ }
1064
+ const prev = this.menu.getSnapshot();
1065
+ const same = !launched && prev.open && prev.hit !== null && prev.hit.trigger === hit.trigger && prev.hit.query === hit.query && prev.hit.quoted === hit.quoted && prev.hit.span.start === hit.span.start && prev.hit.span.end === hit.span.end;
1066
+ this.hit = hit;
1067
+ if (same) return;
1068
+ const roster = this.deps.roster.sources(hit.trigger);
1069
+ if (roster.length === 0) {
1070
+ this.stopFetch();
1071
+ this.reduce({ type: "close" });
1072
+ return;
1073
+ }
1074
+ if (launched || !prev.open || prev.hit === null || prev.hit.trigger !== hit.trigger) this.menu.set(seedGroups(this.menu.getSnapshot(), roster));
1075
+ this.reduce({
1076
+ type: "hit",
1077
+ hit
1078
+ });
1079
+ this.refreshHeaders(hit, roster);
1080
+ this.fetchCandidates(hit, roster);
1081
+ }
1082
+ /**
1083
+ * Toggle a menu containing exactly one registered source. The supplied hit
1084
+ * is a synthetic selection span rather than a typed trigger token, but
1085
+ * picks deliberately reuse the ordinary source callback and scoped input
1086
+ * mutation pipeline.
1087
+ * @param source - registered source name under `hit.trigger`.
1088
+ * @param hit - synthetic hit carrying position and pick-time draft CAS.
1089
+ */
1090
+ toggleSource(source, hit) {
1091
+ if (this.disposed) return;
1092
+ if (this.launcher.getSnapshot() === source && this.menu.getSnapshot().open) {
1093
+ this.dismiss();
1094
+ return;
1095
+ }
1096
+ const match = this.deps.roster.sources(hit.trigger).find((item) => item.name === source);
1097
+ if (match === void 0) {
1098
+ this.dismiss();
1099
+ return;
1100
+ }
1101
+ this.stopFetch();
1102
+ this.hit = hit;
1103
+ this.launcher.set(source);
1104
+ this.menu.set(seedGroups(this.menu.getSnapshot(), [match]));
1105
+ this.reduce({
1106
+ type: "hit",
1107
+ hit
1108
+ });
1109
+ this.refreshHeaders(hit, [match]);
1110
+ this.fetchCandidates(hit, [match]);
1111
+ }
1112
+ /**
1113
+ * Pointer pick from MenuView: route the clicked candidate through onPick
1114
+ * and execute claim/insert outcomes via the scoped input events.
1115
+ * @param source - source (group) name.
1116
+ * @param index - candidate index within the group.
1117
+ * @param action - settling pick (default) or the candidate's drill action.
1118
+ */
1119
+ pick(source, index, action = "pick") {
1120
+ const state = this.menu.getSnapshot();
1121
+ const hit = this.hit;
1122
+ if (this.disposed || !state.open || hit === null) return;
1123
+ const group = state.groups.find((g) => g.source === source);
1124
+ const candidate = group !== void 0 && group.status === "ready" ? group.items[index] : void 0;
1125
+ if (candidate === void 0) return;
1126
+ const src = this.deps.roster.sources(hit.trigger).find((s) => s.name === source);
1127
+ if (src === void 0) return;
1128
+ this.settle(src, candidate, hit, action);
1129
+ }
1130
+ /**
1131
+ * Pointer pick on one crumb of a source's menu header: route it through the
1132
+ * same drill path a folder row takes, so returning to a step and descending
1133
+ * into one share one outcome.
1134
+ * @param source - source (group) name.
1135
+ * @param index - crumb index within that source's published header.
1136
+ */
1137
+ pickCrumb(source, index) {
1138
+ const hit = this.hit;
1139
+ if (this.disposed || !this.menu.getSnapshot().open || hit === null) return;
1140
+ const crumb = this.headers.getSnapshot().get(source)?.[index];
1141
+ if (crumb === void 0 || crumb.current === true) return;
1142
+ const src = this.deps.roster.sources(hit.trigger).find((s) => s.name === source);
1143
+ if (src === void 0) return;
1144
+ this.settle(src, {
1145
+ name: crumb.label,
1146
+ value: crumb.value
1147
+ }, hit, "drill");
1148
+ }
1149
+ /**
1150
+ * Pointer hover from MenuView: park the shared highlight on the hovered
1151
+ * candidate (keyboard `move` and pointer hover drive one highlight —
1152
+ * last input wins).
1153
+ * @param source - source (group) name.
1154
+ * @param index - candidate index within the group.
1155
+ */
1156
+ hover(source, index) {
1157
+ if (this.disposed) return;
1158
+ this.reduce({
1159
+ type: "hover",
1160
+ source,
1161
+ index
1162
+ });
1163
+ }
1164
+ /**
1165
+ * Keyboard arbitration while the menu is open.
1166
+ * @param key - intercepted key.
1167
+ * @param composing - inside IME composition: everything passes.
1168
+ * @returns `pass` when the browser keeps the key (closed menu, no
1169
+ * highlight, or a vanished candidate), `consumed` when the menu handled
1170
+ * the key without a settling pick (move, close, drill descent, or a
1171
+ * pending-refinement no-op), or `pick-highlighted` when the highlighted
1172
+ * candidate settled and the menu closed.
1173
+ */
1174
+ arbitrate(key, composing) {
1175
+ if (composing || this.disposed) return "pass";
1176
+ const state = this.menu.getSnapshot();
1177
+ if (!state.open) return "pass";
1178
+ switch (key) {
1179
+ case "up":
1180
+ this.reduce({
1181
+ type: "move",
1182
+ dir: -1
1183
+ });
1184
+ return "consumed";
1185
+ case "down":
1186
+ this.reduce({
1187
+ type: "move",
1188
+ dir: 1
1189
+ });
1190
+ return "consumed";
1191
+ case "escape":
1192
+ case "tabBack":
1193
+ this.rememberDismissed();
1194
+ this.stopFetch();
1195
+ this.reduce({ type: "close" });
1196
+ return "consumed";
1197
+ case "enter": {
1198
+ if (state.highlight === null) return "pass";
1199
+ const group = state.groups.find((g) => g.source === state.highlight?.source);
1200
+ if (group === void 0 || group.status !== "ready") return "consumed";
1201
+ this.pick(state.highlight.source, state.highlight.index);
1202
+ return "pick-highlighted";
1203
+ }
1204
+ case "tab": {
1205
+ if (state.highlight === null) return "pass";
1206
+ const group = state.groups.find((g) => g.source === state.highlight?.source);
1207
+ if (group === void 0 || group.status !== "ready") return "consumed";
1208
+ const item = group.items[state.highlight.index];
1209
+ if (item === void 0) return "pass";
1210
+ if (item.drill === true) {
1211
+ this.pick(state.highlight.source, state.highlight.index, "drill");
1212
+ return "consumed";
1213
+ }
1214
+ this.pick(state.highlight.source, state.highlight.index);
1215
+ return "pick-highlighted";
1216
+ }
1217
+ }
1218
+ }
1219
+ /**
1220
+ * Space adjudication over the just-completed leading token: polls sources'
1221
+ * matchSpace (hot state, synchronous) and dispatches the outcome itself.
1222
+ * @returns true when a claim/insert was actually applied by the input —
1223
+ * the caller preventDefaults exactly then.
1224
+ */
1225
+ onSpace() {
1226
+ const hit = this.hit;
1227
+ if (this.disposed || hit === null || hit.position !== "leading") return false;
1228
+ const token = hit.trigger + hit.query;
1229
+ const projection = this.project();
1230
+ for (const src of this.deps.roster.sources(hit.trigger)) {
1231
+ if (src.matchSpace === void 0) continue;
1232
+ const outcome = src.matchSpace(projection, token);
1233
+ if (outcome === void 0) continue;
1234
+ if (outcome === "handled") return true;
1235
+ return this.execute(outcome, hit.span);
1236
+ }
1237
+ return false;
1238
+ }
1239
+ /**
1240
+ * Serialize one reference occurrence to its model form via the owning
1241
+ * source's codec (prompt serialization: registry → explicit
1242
+ * call → await). Owner missing or codec-less rejects — the submit attempt
1243
+ * blocks instead of silently downgrading to the clipboard text.
1244
+ * @param source - owning source name.
1245
+ * @param ref - owner-scoped reference id.
1246
+ * @param signal - the submit attempt's abort signal.
1247
+ * @returns the model representation (e.g. `<skill>name</skill>`).
1248
+ */
1249
+ serializeReference(source, ref, signal) {
1250
+ const owner = this.deps.roster.all().find((s) => s.name === source);
1251
+ if (owner?.codec === void 0) return Promise.reject(/* @__PURE__ */ new Error(`slash: no serializer for reference source "${source}"`));
1252
+ return owner.codec.serialize(ref, signal);
1253
+ }
1254
+ /**
1255
+ * Route a chip to its owner or an editable token to its current lexicon owner.
1256
+ * @param source - chip source name; undefined for editable text.
1257
+ * @param reference - source-owned id and optional chip glyph.
1258
+ * @returns whether an owner accepted the preview, possibly awaiting its catalog.
1259
+ */
1260
+ openReference(source, reference) {
1261
+ if (this.disposed) return false;
1262
+ const session = this.project();
1263
+ for (const owner of this.deps.roster.all()) if ((source === void 0 ? reference.ref.startsWith(owner.trigger) && owner.lexicon?.(session)?.includes(reference.ref.slice(1)) : owner.name === source) && owner.openReference?.(session, reference)) {
1264
+ this.dismiss();
1265
+ return true;
1266
+ }
1267
+ return false;
1268
+ }
1269
+ /**
1270
+ * Enter last adjudication: polls sources' matchEnter in registration
1271
+ * order, first non-undefined wins. The outcome returns to the caller (the
1272
+ * input machine applies it inside the same submit attempt — no event).
1273
+ * @param line - trimmed draft; the leading char selects the trigger roster.
1274
+ * @param signal - attempt-scoped abort from the input machine.
1275
+ * @param envelope - non-text submission state accompanying the draft.
1276
+ * @returns the winning outcome or undefined (default sink). Rejects when a
1277
+ * polled source's warmup fails or the winning source refuses the envelope —
1278
+ * the caller must not silently downgrade.
1279
+ */
1280
+ async adjudicate(line, signal, envelope) {
1281
+ const projection = this.project();
1282
+ for (const src of this.deps.roster.all()) {
1283
+ if (signal.aborted) throw signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("slash adjudication aborted");
1284
+ if (src.matchEnter === void 0 || !line.startsWith(src.trigger)) continue;
1285
+ const outcome = await src.matchEnter(projection, line, signal, envelope);
1286
+ if (outcome !== void 0) return outcome;
1287
+ }
1288
+ }
1289
+ /**
1290
+ * Drop the menu group of a disposed source (root registry change notification).
1291
+ * @param source - the source whose registration was disposed.
1292
+ */
1293
+ sourceRemoved(source) {
1294
+ const state = this.menu.getSnapshot();
1295
+ if (state.open && state.hit !== null && state.hit.trigger === source.trigger) this.reduce({
1296
+ type: "source-failed",
1297
+ generation: state.generation,
1298
+ source: source.name
1299
+ });
1300
+ this.lexiconOffs.get(source)?.();
1301
+ this.lexiconOffs.delete(source);
1302
+ this.refreshLexicon();
1303
+ }
1304
+ /**
1305
+ * Admit a source registered after this controller's birth (root registry
1306
+ * change notification): warm it and fold its roll into the live lexicon —
1307
+ * the constructor-time prewarm covers only the roster present at scope
1308
+ * birth.
1309
+ * @param source - the newly registered source.
1310
+ */
1311
+ sourceAdded(source) {
1312
+ const projection = this.project();
1313
+ source.warm?.(projection);
1314
+ this.watchLexicon(source, projection);
1315
+ this.refreshLexicon();
1316
+ }
1317
+ /** External dismiss (e.g. pointer outside the composer area). */
1318
+ dismiss() {
1319
+ if (this.disposed) return;
1320
+ this.rememberDismissed();
1321
+ this.stopFetch();
1322
+ this.reduce({ type: "close" });
1323
+ }
1324
+ /** Re-fetch the currently open menu without changing its hit or visible rows. */
1325
+ refreshOpenMenu() {
1326
+ if (this.disposed || !this.menu.getSnapshot().open || this.hit === null) return;
1327
+ const launched = this.launcher.getSnapshot();
1328
+ const roster = this.deps.roster.sources(this.hit.trigger).filter((source) => launched === null || source.name === launched);
1329
+ if (roster.length === 0) return;
1330
+ this.fetchCandidates(this.hit, roster);
1331
+ }
1332
+ /** Scope teardown: close and abort (the service deletes the map entry). */
1333
+ dispose() {
1334
+ this.disposed = true;
1335
+ this.stopFetch();
1336
+ this.reduce({ type: "close" });
1337
+ this.hit = null;
1338
+ for (const off of this.lexiconOffs.values()) off();
1339
+ this.lexiconOffs.clear();
1340
+ }
1341
+ /** The session projection handed to sources (agent-backed identity; constant per scope). */
1342
+ project() {
1343
+ return { sessionId: this.deps.sessionId };
1344
+ }
1345
+ /** Execute a claim/insert/text outcome via the scoped input events (actx as dispatch subject); true = the input applied it. */
1346
+ execute(outcome, span) {
1347
+ const { actx } = this.deps;
1348
+ if (outcome === void 0 || outcome === "handled") return false;
1349
+ if ("claim" in outcome) return actx.bail(actx, "slash/input-begin-command", {
1350
+ claim: outcome.claim,
1351
+ span
1352
+ }) === true;
1353
+ if ("text" in outcome) return actx.bail(actx, "slash/input-insert-text", {
1354
+ text: outcome.text,
1355
+ span,
1356
+ ...outcome.continue === true ? { continue: true } : {}
1357
+ }) === true;
1358
+ return actx.bail(actx, "slash/input-insert-reference", {
1359
+ reference: outcome.insert,
1360
+ span
1361
+ }) === true;
1362
+ }
1363
+ /** Re-poll every lexicon-bearing source and publish the aggregated rolls (see the store doc). */
1364
+ refreshLexicon() {
1365
+ const projection = this.project();
1366
+ const rolls = /* @__PURE__ */ new Map();
1367
+ for (const src of this.deps.roster.all()) {
1368
+ if (src.lexicon === void 0) continue;
1369
+ let names;
1370
+ try {
1371
+ names = src.lexicon(projection);
1372
+ } catch (error) {
1373
+ console.error(`[ui-input-trigger] source "${src.name}" lexicon failed:`, error);
1374
+ continue;
1375
+ }
1376
+ if (names === void 0) continue;
1377
+ const prev = rolls.get(src.trigger);
1378
+ rolls.set(src.trigger, prev === void 0 ? names : [...prev, ...names]);
1379
+ }
1380
+ this.lexicon.set(rolls);
1381
+ }
1382
+ /** Wire one source's lexicon invalidation channel into refresh (hookless or roll-less sources never notify). */
1383
+ watchLexicon(source, projection) {
1384
+ if (source.lexicon === void 0 || source.subscribeLexicon === void 0) return;
1385
+ this.lexiconOffs.set(source, source.subscribeLexicon(projection, () => {
1386
+ this.refreshLexicon();
1387
+ const hit = this.hit;
1388
+ if (hit === null || !this.menu.getSnapshot().open || hit.trigger !== source.trigger) return;
1389
+ Promise.resolve().then(() => {
1390
+ if (this.disposed || this.hit !== hit || !this.menu.getSnapshot().open) return;
1391
+ this.fetchCandidates(hit, this.deps.roster.sources(hit.trigger));
1392
+ });
1393
+ }));
1394
+ }
1395
+ /** Launch the candidate fetch for one hit generation, superseding the previous one. */
1396
+ fetchCandidates(hit, roster) {
1397
+ this.stopFetch();
1398
+ const controller = new AbortController();
1399
+ this.fetch = controller;
1400
+ const generation = this.menu.getSnapshot().generation;
1401
+ const projection = this.project();
1402
+ for (const source of roster) source.candidates(projection, {
1403
+ query: hit.query,
1404
+ quoted: hit.quoted,
1405
+ position: hit.position,
1406
+ drilled: this.drilled,
1407
+ signal: controller.signal
1408
+ }).then((items) => {
1409
+ if (controller.signal.aborted) return;
1410
+ this.reduce({
1411
+ type: "source-settled",
1412
+ generation,
1413
+ source: source.name,
1414
+ items
1415
+ });
1416
+ }, (error) => {
1417
+ if (controller.signal.aborted) return;
1418
+ console.error(`[ui-input-trigger] source "${source.name}" candidates failed:`, error);
1419
+ this.reduce({
1420
+ type: "source-failed",
1421
+ generation,
1422
+ source: source.name
1423
+ });
1424
+ });
1425
+ }
1426
+ stopFetch() {
1427
+ this.fetch?.abort();
1428
+ this.fetch = null;
1429
+ }
1430
+ /**
1431
+ * Run one candidate (or crumb) through its source and apply the outcome.
1432
+ *
1433
+ * A drill is the one pick that leaves the menu open, so it is also the one
1434
+ * that records how the next query was reached; every other pick closes the
1435
+ * menu, which clears that record.
1436
+ * @param src - the owning source.
1437
+ * @param candidate - the picked candidate, or a crumb projected as one.
1438
+ * @param hit - the authoritative hit supplying position and span CAS.
1439
+ * @param action - settling pick or drill.
1440
+ */
1441
+ settle(src, candidate, hit, action) {
1442
+ const outcome = src.onPick({
1443
+ candidate,
1444
+ session: this.project(),
1445
+ position: hit.position,
1446
+ via: "menu",
1447
+ action,
1448
+ span: hit.span
1449
+ });
1450
+ this.stopFetch();
1451
+ if (action === "pick") this.rememberDismissed();
1452
+ this.reduce({ type: "close" });
1453
+ this.drilled = action === "drill";
1454
+ if (!this.execute(outcome, hit.span)) this.drilled = false;
1455
+ }
1456
+ /** Re-poll every header-bearing source in the hit roster and publish their crumbs. */
1457
+ refreshHeaders(hit, roster) {
1458
+ const projection = this.project();
1459
+ const crumbs = /* @__PURE__ */ new Map();
1460
+ for (const src of roster) {
1461
+ if (src.header === void 0) continue;
1462
+ let published;
1463
+ try {
1464
+ published = src.header(projection, {
1465
+ query: hit.query,
1466
+ quoted: hit.quoted,
1467
+ drilled: this.drilled
1468
+ });
1469
+ } catch (error) {
1470
+ console.error(`[ui-input-trigger] source "${src.name}" header failed:`, error);
1471
+ continue;
1472
+ }
1473
+ if (published === void 0 || published.length === 0) continue;
1474
+ crumbs.set(src.name, published);
1475
+ }
1476
+ this.setHeaders(crumbs);
1477
+ }
1478
+ setHeaders(next) {
1479
+ if (this.headers.getSnapshot().size === 0 && next.size === 0) return;
1480
+ this.headers.set(next);
1481
+ }
1482
+ /** Record the open menu's identity as dismissed, so a bare re-track cannot revive it. */
1483
+ rememberDismissed() {
1484
+ const hit = this.hit;
1485
+ this.dismissed = hit === null ? null : {
1486
+ trigger: hit.trigger,
1487
+ query: hit.query,
1488
+ quoted: hit.quoted,
1489
+ start: hit.span.start,
1490
+ end: hit.span.end
1491
+ };
1492
+ }
1493
+ clearLauncher() {
1494
+ if (this.launcher.getSnapshot() !== null) this.launcher.set(null);
1495
+ }
1496
+ reduce(ev) {
1497
+ const cur = this.menu.getSnapshot();
1498
+ const next = menuReduce(cur, ev);
1499
+ if (next !== cur) this.menu.set(next);
1500
+ if (next.open) return;
1501
+ this.clearLauncher();
1502
+ this.drilled = false;
1503
+ this.setHeaders(/* @__PURE__ */ new Map());
1504
+ }
1505
+ };
1506
+ //#endregion
1507
+ //#region lib/types/client/service.js
1508
+ /**
1509
+ * InputTriggerService (`ctx.inputTriggers`): the root half of the trigger pipeline — the
1510
+ * stateless source registry plus the per-session controller map. Every piece
1511
+ * of mutable interaction state (hit, menu, fetch) lives on the
1512
+ * {@link InputTriggerController}; the service only registers sources, resolves
1513
+ * controllers by session scope, and relays roster changes.
1514
+ */
1515
+ /** The `ctx.inputTriggers` trigger pipeline service (root registry + controller resolution). */
1516
+ var InputTriggerService = class extends _qilin_agent_kylin.Service {
1517
+ static inject = ["sessions"];
1518
+ live = {
1519
+ sources: [],
1520
+ controllers: new WeakMapWithValues()
1521
+ };
1522
+ /**
1523
+ * @param ctx - owning root context (the service registers itself as `slash`).
1524
+ */
1525
+ constructor(ctx) {
1526
+ super(ctx, "inputTriggers");
1527
+ ctx.on("locale/change", () => {
1528
+ for (const controller of this.live.controllers.values) controller.refreshOpenMenu();
1529
+ });
1530
+ }
1531
+ /**
1532
+ * Register one trigger source. Live session controllers are notified so a
1533
+ * source arriving after scope birth still warms and joins the lexicon.
1534
+ * @param src - the source; (trigger, name) must be unique — duplicates throw.
1535
+ * @returns the disposer (callers wrap registration in ctx.effect). Disposal
1536
+ * while a controller shows the source's menu group drops that group.
1537
+ */
1538
+ registerSource(src) {
1539
+ const { live } = this;
1540
+ if (live.sources.some((s) => s.trigger === src.trigger && s.name === src.name)) throw new Error(`slash source "${src.trigger}${src.name}" is already registered`);
1541
+ live.sources.push(src);
1542
+ for (const controller of live.controllers.values) try {
1543
+ controller.sourceAdded(src);
1544
+ } catch (error) {
1545
+ console.error(`[ui-input-trigger] source "${src.trigger}${src.name}" late-registration setup failed:`, error);
1546
+ }
1547
+ return () => {
1548
+ const at = live.sources.indexOf(src);
1549
+ if (at < 0) return;
1550
+ live.sources.splice(at, 1);
1551
+ for (const controller of live.controllers.values) controller.sourceRemoved(src);
1552
+ };
1553
+ }
1554
+ /**
1555
+ * Resolve the per-session controller for one session scope (lazy; the
1556
+ * scope disposer removes and disposes it). Construction warms the source
1557
+ * roster once — sessions are always agent-backed, so scope birth is the
1558
+ * single prewarm moment.
1559
+ * @param actx - session-scope ctx.
1560
+ * @returns the resident controller.
1561
+ * @throws when the Context no longer belongs to a retained Session generation.
1562
+ */
1563
+ sessionOf(actx) {
1564
+ const sessions = this.sessions();
1565
+ const session = sessions.sessionOf(actx);
1566
+ const binding = session === void 0 ? void 0 : sessions.binding(session.sessionId);
1567
+ if (binding === void 0 || binding.session !== session) throw new Error("slash.sessionOf requires a retained Session scope");
1568
+ const id = binding.sessionId;
1569
+ const { live } = this;
1570
+ const existing = live.controllers.get(binding);
1571
+ if (existing !== void 0) return existing;
1572
+ const controller = new InputTriggerController({
1573
+ actx: binding.ctx,
1574
+ sessionId: id,
1575
+ roster: {
1576
+ sources: (trigger) => live.sources.filter((s) => s.trigger === trigger).sort((a, b) => (a.order ?? 0) - (b.order ?? 0)),
1577
+ all: () => live.sources
1578
+ }
1579
+ });
1580
+ live.controllers.set(binding, controller);
1581
+ binding.ctx.effect(() => () => {
1582
+ controller.dispose();
1583
+ live.controllers.delete(binding);
1584
+ }, "slash: session controller");
1585
+ return controller;
1586
+ }
1587
+ sessions() {
1588
+ const sessions = this.ctx.get("sessions");
1589
+ if (sessions === void 0) throw new Error("ui-input-trigger: sessions service unavailable");
1590
+ return sessions;
1591
+ }
1592
+ };
1593
+ //#endregion
1594
+ //#region ../../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
1595
+ function r(e) {
1596
+ var t, f, n = "";
1597
+ if ("string" == typeof e || "number" == typeof e) n += e;
1598
+ else if ("object" == typeof e) if (Array.isArray(e)) {
1599
+ var o = e.length;
1600
+ for (t = 0; t < o; t++) e[t] && (f = r(e[t])) && (n && (n += " "), n += f);
1601
+ } else for (f in e) e[f] && (n && (n += " "), n += f);
1602
+ return n;
1603
+ }
1604
+ function clsx() {
1605
+ for (var e, t, f = 0, n = "", o = arguments.length; f < o; f++) (e = arguments[f]) && (t = r(e)) && (n && (n += " "), n += t);
1606
+ return n;
1607
+ }
1608
+ //#endregion
1609
+ //#region \0qilin-css:/home/runner/work/QiLin/QiLin/packages/client/ui-input-trigger/src/client/MenuView.module.css.mjs
1610
+ const css = ".-LmocW_menu{z-index:100;box-sizing:border-box;--qilin-scrollbar-thumb:var(--qilin-alias-scrollbar-bg-l2);--qilin-scrollbar-thumb-hover:var(--qilin-alias-scrollbar-hover-l2);--qilin-scrollbar-width:10px;--qilin-scrollbar-thumb-border:3px;--qilin-scrollbar-track-margin:12px;background:var(--qilin-specific-menu);--qilin-elevation-stroke-color:var(--qilin-alias-border-l1);max-height:400px;box-shadow:var(--qilin-elevation-prominent);border:0;border-radius:20px;flex-direction:column;padding:4px;display:flex;position:absolute;bottom:calc(100% + 4px);left:0;right:0;overflow:hidden}.-LmocW_menu[data-overflow-below]:after{content:\"\";background:linear-gradient(to bottom, transparent, var(--qilin-specific-menu));pointer-events:none;height:16px;position:absolute;bottom:4px;left:4px;right:14px}.-LmocW_viewport{flex-direction:column;min-height:0;display:flex;overflow-y:auto}.-LmocW_item{cursor:pointer;width:100%;min-height:40px;color:var(--qilin-alias-label-primary);text-align:left;background:0 0;border:none;border-radius:10px;align-items:center;gap:8px;padding:8px 10px;font-size:14px;line-height:22px;display:flex}.-LmocW_item.-LmocW_active{background:var(--qilin-alias-interactive-bg-hover)}.-LmocW_sectionTitle{min-height:26px;color:var(--qilin-alias-label-tertiary);flex:none;padding:6px 10px 2px;font-size:12px;font-weight:500;line-height:18px}.-LmocW_sectionTitle:not(:first-child){margin-top:4px}.-LmocW_itemIcon{width:16px;height:16px;color:var(--qilin-alias-label-tertiary);flex:none;justify-content:center;align-items:center;display:inline-flex}.-LmocW_itemName{text-overflow:ellipsis;white-space:nowrap;flex:none;max-width:40%;overflow:hidden}.-LmocW_itemAlias{text-overflow:ellipsis;white-space:nowrap;max-width:20%;color:var(--qilin-alias-label-tertiary);flex:none;overflow:hidden}.-LmocW_itemDescription{text-overflow:ellipsis;white-space:nowrap;text-align:right;min-width:0;color:var(--qilin-alias-label-tertiary);flex:1;overflow:hidden}.-LmocW_trailing{flex:none;align-items:center;gap:4px;margin-left:auto;display:inline-flex}.-LmocW_drillHintText{color:var(--qilin-alias-label-caption);white-space:nowrap;font-size:11px;line-height:18px;display:none}.-LmocW_drillHint{background:var(--qilin-alias-interactive-bg-hover);color:var(--qilin-alias-label-caption);border-radius:4px;padding:0 5px;font-family:inherit;font-size:11px;line-height:18px;display:none}.-LmocW_item.-LmocW_active .-LmocW_drillHintText,.-LmocW_item.-LmocW_active .-LmocW_drillHint{display:inline-flex}.-LmocW_drill{width:20px;height:20px;color:var(--qilin-alias-label-caption);border-radius:4px;flex:none;place-items:center;display:inline-grid}.-LmocW_drill:hover{background:var(--qilin-alias-interactive-bg-hover);color:var(--qilin-alias-label-primary)}.-LmocW_groupTitle{color:var(--qilin-alias-label-tertiary);padding:8px 10px;font-size:12px;line-height:16px}.-LmocW_skeletonRow{box-sizing:border-box;align-items:center;min-height:40px;padding:8px 10px;display:flex}.-LmocW_skeletonBar{background:var(--qilin-alias-bg-skeleton);border-radius:4px;height:20px;animation:2s cubic-bezier(.36,0,.64,1) infinite -LmocW_qilin-menu-skeleton}@keyframes -LmocW_qilin-menu-skeleton{0%{opacity:1}40%{opacity:.6}80%,to{opacity:1}}.-LmocW_crumbs{border-bottom:.5px solid var(--qilin-alias-border-l1);flex-wrap:wrap;flex:none;align-items:center;gap:2px;margin-bottom:2px;padding:4px 4px 6px;display:flex}.-LmocW_crumb{max-width:40%;color:var(--qilin-alias-label-tertiary);cursor:pointer;text-overflow:ellipsis;white-space:nowrap;background:0 0;border:none;border-radius:6px;flex:0 auto;padding:2px 6px;font-family:inherit;font-size:12px;line-height:18px;overflow:hidden}.-LmocW_crumb:hover{background:var(--qilin-alias-interactive-bg-hover);color:var(--qilin-alias-label-primary)}.-LmocW_crumbCurrent,.-LmocW_crumbCurrent:hover{color:var(--qilin-alias-label-primary);cursor:default;background:0 0}.-LmocW_crumbSeparator{color:var(--qilin-alias-label-caption);flex:none;display:inline-flex}";
1611
+ const tagId = "@qilin-agent/client-ui-input-trigger/MenuView.module.css";
1612
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
1613
+ const tag = document.createElement("style");
1614
+ tag.dataset.plugin = "@qilin-agent/client-ui-input-trigger";
1615
+ tag.dataset.pluginCss = tagId;
1616
+ tag.textContent = css;
1617
+ document.head.appendChild(tag);
1618
+ }
1619
+ var MenuView_module_css_default = {
1620
+ "active": "-LmocW_active",
1621
+ "crumb": "-LmocW_crumb",
1622
+ "crumbCurrent": "-LmocW_crumbCurrent",
1623
+ "crumbSeparator": "-LmocW_crumbSeparator",
1624
+ "crumbs": "-LmocW_crumbs",
1625
+ "drill": "-LmocW_drill",
1626
+ "drillHint": "-LmocW_drillHint",
1627
+ "drillHintText": "-LmocW_drillHintText",
1628
+ "groupTitle": "-LmocW_groupTitle",
1629
+ "item": "-LmocW_item",
1630
+ "itemAlias": "-LmocW_itemAlias",
1631
+ "itemDescription": "-LmocW_itemDescription",
1632
+ "itemIcon": "-LmocW_itemIcon",
1633
+ "itemName": "-LmocW_itemName",
1634
+ "menu": "-LmocW_menu",
1635
+ "qilin-menu-skeleton": "-LmocW_qilin-menu-skeleton",
1636
+ "sectionTitle": "-LmocW_sectionTitle",
1637
+ "skeletonBar": "-LmocW_skeletonBar",
1638
+ "skeletonRow": "-LmocW_skeletonRow",
1639
+ "trailing": "-LmocW_trailing",
1640
+ "viewport": "-LmocW_viewport"
1641
+ };
1642
+ //#endregion
1643
+ //#region lib/types/client/MenuView.js
1644
+ /**
1645
+ * Trigger candidate menu: renders the InputTriggerService menu store into the
1646
+ * conversation.input.overlay anchor. Closed state renders null (the overlay
1647
+ * slot stays mounted); groups render in roster order under localized title
1648
+ * rows. A pending group keeps showing the items it already had (the reducer
1649
+ * retains them across a query refinement) and falls back to two skeleton
1650
+ * rows only while it has none; pointer picks route back through
1651
+ * the service (combobox pattern — focus never leaves the textarea, so rows
1652
+ * are mousedown-handled and the highlight is exposed via
1653
+ * aria-activedescendant on the listbox). A row reads title, then the
1654
+ * command-name alias when the title is not the name in another letter case
1655
+ * (a localized title), then the description right-aligned. A source publishing crumbs gets a breadcrumb
1656
+ * header pinned above the scrolling list.
1657
+ */
1658
+ /** Height cap that fits the two headings and eight built-in command rows. */
1659
+ const MAX_HEIGHT = 400;
1660
+ /** DOM id of one option row (the aria-activedescendant target). */
1661
+ function optionId(source, index) {
1662
+ return `qilin-slash-option-${source}-${index}`;
1663
+ }
1664
+ /**
1665
+ * Render the candidate menu overlay entry.
1666
+ * @param props - injected face (the menu store and the pick route); `t` rides the standard locale seat.
1667
+ * @returns the dropdown while open; null while closed.
1668
+ */
1669
+ function MenuView({ menu, headers, onPick, onCrumb, onHover, onDismiss, t }) {
1670
+ const state = (0, react.useSyncExternalStore)((fn) => menu.subscribe(fn), () => menu.getSnapshot());
1671
+ const crumbs = (0, react.useSyncExternalStore)((fn) => headers.subscribe(fn), () => headers.getSnapshot());
1672
+ const listRef = (0, react.useRef)(null);
1673
+ const viewportRef = (0, react.useRef)(null);
1674
+ const [hasOverflowBelow, setHasOverflowBelow] = (0, react.useState)(false);
1675
+ const maxHeight = (0, _qilin_agent_client_ui_primitives.useAnchoredMaxHeight)(listRef, MAX_HEIGHT, state);
1676
+ const updateOverflowHint = (0, react.useCallback)(() => {
1677
+ const viewport = viewportRef.current;
1678
+ setHasOverflowBelow(viewport !== null && viewport.scrollTop + viewport.clientHeight < viewport.scrollHeight - 1);
1679
+ }, []);
1680
+ (0, react.useLayoutEffect)(() => {
1681
+ updateOverflowHint();
1682
+ }, [
1683
+ state,
1684
+ maxHeight,
1685
+ updateOverflowHint
1686
+ ]);
1687
+ const highlight = state.open ? state.highlight : null;
1688
+ (0, react.useEffect)(() => {
1689
+ if (highlight === null) return;
1690
+ document.getElementById(optionId(highlight.source, highlight.index))?.scrollIntoView({ block: "nearest" });
1691
+ }, [highlight]);
1692
+ (0, react.useEffect)(() => {
1693
+ if (!state.open) return;
1694
+ const onPointerDown = (ev) => {
1695
+ if (!(ev.target instanceof Node)) return;
1696
+ if (listRef.current?.contains(ev.target)) return;
1697
+ if ((listRef.current?.closest("[data-composer-card]"))?.contains(ev.target)) return;
1698
+ onDismiss();
1699
+ };
1700
+ document.addEventListener("pointerdown", onPointerDown, true);
1701
+ return () => {
1702
+ document.removeEventListener("pointerdown", onPointerDown, true);
1703
+ };
1704
+ }, [state.open, onDismiss]);
1705
+ if (!state.open) return null;
1706
+ return (0, react_jsx_runtime.jsxs)("div", {
1707
+ ref: listRef,
1708
+ className: MenuView_module_css_default.menu,
1709
+ style: { maxHeight },
1710
+ "data-trigger-menu": "",
1711
+ "data-overflow-below": hasOverflowBelow || void 0,
1712
+ children: [state.groups.map((group) => {
1713
+ const trail = crumbs.get(group.source);
1714
+ return trail === void 0 ? null : (0, react_jsx_runtime.jsx)("nav", {
1715
+ className: MenuView_module_css_default.crumbs,
1716
+ "aria-label": t("crumbs.aria"),
1717
+ children: trail.map((crumb, index) => (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [index > 0 && (0, react_jsx_runtime.jsx)("span", {
1718
+ className: MenuView_module_css_default.crumbSeparator,
1719
+ "aria-hidden": true,
1720
+ children: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconChevronRightOutline14, {})
1721
+ }), (0, react_jsx_runtime.jsx)("button", {
1722
+ type: "button",
1723
+ className: clsx(MenuView_module_css_default.crumb, crumb.current === true && MenuView_module_css_default.crumbCurrent),
1724
+ "aria-current": crumb.current === true ? "location" : void 0,
1725
+ disabled: crumb.current === true,
1726
+ onMouseDown: (ev) => {
1727
+ ev.preventDefault();
1728
+ onCrumb(group.source, index);
1729
+ },
1730
+ children: crumb.label
1731
+ })] }, `${String(index)}-${crumb.value}`))
1732
+ }, group.source);
1733
+ }), (0, react_jsx_runtime.jsx)("div", {
1734
+ ref: viewportRef,
1735
+ className: MenuView_module_css_default.viewport,
1736
+ role: "listbox",
1737
+ "aria-label": t("suggestions.aria"),
1738
+ "aria-activedescendant": highlight !== null ? optionId(highlight.source, highlight.index) : void 0,
1739
+ onScroll: updateOverflowHint,
1740
+ children: state.groups.map((group) => group.status === "ready" && group.items.length === 0 ? null : (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [group.showGroupTitle === false || group.items.some((item) => item.section !== void 0) ? null : (0, react_jsx_runtime.jsx)("div", {
1741
+ className: MenuView_module_css_default.groupTitle,
1742
+ role: "presentation",
1743
+ "data-source": group.source,
1744
+ children: t(group.source)
1745
+ }), group.status === "pending" && group.items.length === 0 ? (0, react_jsx_runtime.jsxs)("div", {
1746
+ role: "status",
1747
+ "aria-label": t("loading"),
1748
+ "data-source": group.source,
1749
+ children: [(0, react_jsx_runtime.jsx)("div", {
1750
+ className: MenuView_module_css_default.skeletonRow,
1751
+ children: (0, react_jsx_runtime.jsx)("span", {
1752
+ className: MenuView_module_css_default.skeletonBar,
1753
+ style: { width: "32%" }
1754
+ })
1755
+ }), (0, react_jsx_runtime.jsx)("div", {
1756
+ className: MenuView_module_css_default.skeletonRow,
1757
+ children: (0, react_jsx_runtime.jsx)("span", {
1758
+ className: MenuView_module_css_default.skeletonBar,
1759
+ style: { width: "48%" }
1760
+ })
1761
+ })]
1762
+ }) : group.items.map((item, index) => {
1763
+ const active = highlight !== null && highlight.source === group.source && highlight.index === index;
1764
+ return (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [item.section !== void 0 && item.section !== group.items[index - 1]?.section ? (0, react_jsx_runtime.jsx)("div", {
1765
+ className: MenuView_module_css_default.sectionTitle,
1766
+ role: "presentation",
1767
+ children: item.section
1768
+ }) : null, (0, react_jsx_runtime.jsxs)("button", {
1769
+ id: optionId(group.source, index),
1770
+ type: "button",
1771
+ role: "option",
1772
+ "aria-selected": active,
1773
+ className: clsx(MenuView_module_css_default.item, active && MenuView_module_css_default.active),
1774
+ onMouseDown: (ev) => {
1775
+ ev.preventDefault();
1776
+ onPick(group.source, index);
1777
+ },
1778
+ onMouseMove: active ? void 0 : () => {
1779
+ onHover(group.source, index);
1780
+ },
1781
+ children: [
1782
+ item.icon !== void 0 && (0, react_jsx_runtime.jsx)("span", {
1783
+ className: MenuView_module_css_default.itemIcon,
1784
+ "aria-hidden": true,
1785
+ children: typeof item.icon === "string" ? (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.ReferenceIcon, {
1786
+ kind: item.icon,
1787
+ size: 16
1788
+ }) : (0, react_jsx_runtime.jsx)(item.icon, { size: 16 })
1789
+ }),
1790
+ (0, react_jsx_runtime.jsx)("span", {
1791
+ className: MenuView_module_css_default.itemName,
1792
+ children: item.label ?? item.name
1793
+ }),
1794
+ item.label !== void 0 && item.label.toLowerCase() !== item.name.toLowerCase() && (0, react_jsx_runtime.jsx)("span", {
1795
+ className: MenuView_module_css_default.itemAlias,
1796
+ children: item.name
1797
+ }),
1798
+ item.description !== void 0 && (0, react_jsx_runtime.jsx)("span", {
1799
+ className: MenuView_module_css_default.itemDescription,
1800
+ children: item.description
1801
+ }),
1802
+ item.drill === true && (0, react_jsx_runtime.jsxs)("span", {
1803
+ className: MenuView_module_css_default.trailing,
1804
+ children: [
1805
+ (0, react_jsx_runtime.jsx)("span", {
1806
+ className: MenuView_module_css_default.drillHintText,
1807
+ "aria-hidden": true,
1808
+ children: t("drill.hint")
1809
+ }),
1810
+ (0, react_jsx_runtime.jsx)("kbd", {
1811
+ className: MenuView_module_css_default.drillHint,
1812
+ "aria-hidden": true,
1813
+ children: t("drill.key")
1814
+ }),
1815
+ (0, react_jsx_runtime.jsx)("span", {
1816
+ role: "button",
1817
+ "aria-label": t("drill.aria"),
1818
+ className: MenuView_module_css_default.drill,
1819
+ onMouseDown: (ev) => {
1820
+ ev.preventDefault();
1821
+ ev.stopPropagation();
1822
+ onPick(group.source, index, "drill");
1823
+ },
1824
+ children: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconChevronRightOutline14, {})
1825
+ })
1826
+ ]
1827
+ })
1828
+ ]
1829
+ })] }, optionId(group.source, index));
1830
+ })] }, group.source))
1831
+ })]
1832
+ });
1833
+ }
1834
+ //#endregion
1835
+ //#region lib/types/client/locales.js
1836
+ /**
1837
+ * `slash.menu` namespace dictionaries: group titles keyed by source name
1838
+ * (the lookup chain returns the key itself, so an unknown source shows its
1839
+ * raw name), the pending row, and the listbox and header aria labels.
1840
+ */
1841
+ /** Simplified Chinese dictionary (the key-set source of truth). */
1842
+ const zh = {
1843
+ "command": "指令",
1844
+ "skill": "技能",
1845
+ "subagent": "子智能体",
1846
+ "loading": "正在加载…",
1847
+ "drill.aria": "进入目录",
1848
+ "drill.hint": "进入目录",
1849
+ "drill.key": "Tab",
1850
+ "crumbs.aria": "目录导航",
1851
+ "suggestions.aria": "触发候选建议"
1852
+ };
1853
+ /** English dictionary, checked complete against the zh key set. */
1854
+ const en = {
1855
+ "command": "Commands",
1856
+ "skill": "Skills",
1857
+ "subagent": "Subagents",
1858
+ "loading": "Loading…",
1859
+ "drill.aria": "Browse folder",
1860
+ "drill.hint": "Browse folder",
1861
+ "drill.key": "Tab",
1862
+ "crumbs.aria": "Folder navigation",
1863
+ "suggestions.aria": "Trigger suggestions"
1864
+ };
1865
+ //#endregion
1866
+ //#region lib/types/client/index.js
1867
+ /** Namespace owning the candidate-menu copy. */
1868
+ const MENU_NS = "slash.menu";
1869
+ /** Required services: controller resolution reads the session scope tree; the menu copy is localized. */
1870
+ const inject = ["sessions", "locale"];
1871
+ /**
1872
+ * Client plugin body: mount the service, then register MenuView into the
1873
+ * input overlay once its declarer is up.
1874
+ * @param ctx - client root context.
1875
+ */
1876
+ function apply(ctx) {
1877
+ ctx.plugin(InputTriggerService);
1878
+ ctx.effect(() => ctx.locale.register(MENU_NS, {
1879
+ zh,
1880
+ en
1881
+ }), "ui-input-trigger: menu dictionaries");
1882
+ ctx.inject([
1883
+ "slots",
1884
+ "inputTriggers",
1885
+ "sessions"
1886
+ ], (scope) => {
1887
+ const inputTriggers = scope.inputTriggers;
1888
+ const sessions = scope.sessions;
1889
+ scope.slots.inject("conversation.input.overlay", () => scope.slots.register({
1890
+ name: "conversation.input.overlay",
1891
+ id: "slash-menu",
1892
+ order: 0,
1893
+ locale: MENU_NS,
1894
+ inject: (sessionId) => {
1895
+ const actx = sessions.scope(sessionId);
1896
+ if (actx === void 0) throw new Error(`ui-input-trigger: session "${String(sessionId)}" resolved no scope`);
1897
+ const controller = inputTriggers.sessionOf(actx);
1898
+ return {
1899
+ menu: controller.menu,
1900
+ headers: controller.headers,
1901
+ onPick: (source, index, action) => {
1902
+ controller.pick(source, index, action);
1903
+ },
1904
+ onCrumb: (source, index) => {
1905
+ controller.pickCrumb(source, index);
1906
+ },
1907
+ onHover: (source, index) => {
1908
+ controller.hover(source, index);
1909
+ },
1910
+ onDismiss: () => {
1911
+ controller.dismiss();
1912
+ }
1913
+ };
1914
+ }
1915
+ }, MenuView));
1916
+ });
1917
+ }
1918
+ //#endregion
1919
+ exports.InputTriggerController = InputTriggerController;
1920
+ exports.InputTriggerService = InputTriggerService;
1921
+ exports.apply = apply;
1922
+ exports.inject = inject;
1923
+ return module.exports;
1924
+ }
1925
+ });
1926
+
1927
+ //# sourceMappingURL=client.js.map