@qilin-agent/client-ui-user-questions 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,1935 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@qilin-agent/client-ui-user-questions",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let _qilin_agent_client_store = require("@qilin-agent/client-store");
8
+ let react_jsx_runtime = require("react/jsx-runtime");
9
+ let react = require("react");
10
+ let _qilin_agent_client_ui_primitives = require("@qilin-agent/client-ui-primitives");
11
+ //#region ../../util/brand/src/index.ts
12
+ /**
13
+ * Apply a compile-time string brand without changing the value.
14
+ * @param value - string admitted by the domain that owns the target brand.
15
+ * @returns the same string with the requested compile-time brand.
16
+ */
17
+ function brandString(value) {
18
+ return value;
19
+ }
20
+ //#endregion
21
+ //#region lib/types/client/contract/slots.js
22
+ function settlePendingComposer(settle, failureMessage) {
23
+ try {
24
+ settle();
25
+ return Promise.resolve();
26
+ } catch (error) {
27
+ return Promise.reject(error instanceof Error ? error : new Error(failureMessage, { cause: error }));
28
+ }
29
+ }
30
+ /**
31
+ * Narrow a request to a renderable plan review, or return undefined to leave it
32
+ * to the generic question flow.
33
+ *
34
+ * The card offers approval and a return to the composer for change requests.
35
+ * It accepts one question carrying the plan as detail and the named approve
36
+ * option, with at most one alternative and no multi-select. Larger choices
37
+ * remain in the generic question flow.
38
+ *
39
+ * @param questions - the request's whole question batch.
40
+ * @returns The narrowed review, or undefined when the generic flow owns it.
41
+ */
42
+ function planReviewOf(questions) {
43
+ if (questions.length !== 1) return void 0;
44
+ const question = questions[0];
45
+ const intent = question.intent;
46
+ if (intent?.kind !== "plan-review" || question.detail === void 0) return void 0;
47
+ if (question.multiSelect === true) return void 0;
48
+ const options = question.options ?? [];
49
+ if (options.length > 2) return void 0;
50
+ const approve = options.find((option) => option.label === intent.approve);
51
+ if (approve === void 0) return void 0;
52
+ const decline = options.find((option) => option.label !== intent.approve);
53
+ return {
54
+ id: question.id,
55
+ question: question.question,
56
+ plan: question.detail,
57
+ ...intent.callId === void 0 ? {} : { callId: intent.callId },
58
+ approve,
59
+ ...decline === void 0 ? {} : { decline }
60
+ };
61
+ }
62
+ /** Reload-unique prefix so an unnamed legacy card cannot reuse a persisted draft. */
63
+ const unnamedQuestionPrefix = Array.from(globalThis.crypto.getRandomValues(new Uint8Array(16)), (byte) => byte.toString(16).padStart(2, "0")).join("");
64
+ let nextQuestionKey = 0;
65
+ const rejectionMessages = {
66
+ ASK_ABORTED: "ask_user_question was aborted before the user answered",
67
+ ASK_CANCELLED: "the user cancelled ask_user_question",
68
+ ASK_TIMED_OUT: "ask_user_question timed out before the user answered"
69
+ };
70
+ /** Create a wire-preserved user-question rejection. */
71
+ function questionError(code) {
72
+ const error = new Error(rejectionMessages[code]);
73
+ error.name = "UserQuestionError";
74
+ error.code = code;
75
+ return error;
76
+ }
77
+ /**
78
+ * Create the deferred one Remote Event listener settles through a card.
79
+ * The request signal ends the channel with `ASK_ABORTED`; the Host ignores that
80
+ * outcome for an event it already finished, so a cancel frame loses nothing.
81
+ * @param deadline - Client-clock deadline in epoch milliseconds derived from the business claim's remaining duration.
82
+ * @param signal - Delivery lifetime of the forwarded request.
83
+ * @param onSettle - Called once with the channel when it settles or aborts.
84
+ * @returns The channel to attach and the promise the listener awaits.
85
+ */
86
+ function createWaterfallRequest(deadline, signal, onSettle) {
87
+ const completion = Promise.withResolvers();
88
+ const delegated = Symbol("pending question delegated");
89
+ let settled = false;
90
+ const finish = (settle) => {
91
+ if (settled) return;
92
+ settled = true;
93
+ signal?.removeEventListener("abort", onAbort);
94
+ settle();
95
+ onSettle(channel);
96
+ };
97
+ const onAbort = () => {
98
+ finish(() => {
99
+ completion.reject(questionError("ASK_ABORTED"));
100
+ });
101
+ };
102
+ const channel = {
103
+ deadline,
104
+ resolve: (answer) => {
105
+ finish(() => {
106
+ completion.resolve(answer);
107
+ });
108
+ },
109
+ reject: (code) => {
110
+ finish(() => {
111
+ completion.reject(questionError(code));
112
+ });
113
+ },
114
+ delegate: () => {
115
+ finish(() => {
116
+ completion.reject(delegated);
117
+ });
118
+ }
119
+ };
120
+ if (signal !== void 0) {
121
+ signal.addEventListener("abort", onAbort, { once: true });
122
+ if (signal.aborted) onAbort();
123
+ }
124
+ return {
125
+ channel,
126
+ result: completion.promise,
127
+ isDelegation: (reason) => reason === delegated
128
+ };
129
+ }
130
+ /**
131
+ * One answerable Client card. A card keyed by tool call is created by whichever
132
+ * source arrives first, the forwarded waterfall or the Session projection, and
133
+ * removed only when the projection no longer lists the call.
134
+ */
135
+ var PendingQuestion = class PendingQuestion {
136
+ /** Presentation discriminator used by Session pending-interaction consumers. */
137
+ kind;
138
+ /** Render identity and request key for the Session-scoped draft store. */
139
+ key;
140
+ /** Agent/Session identity owning the request. */
141
+ sessionId;
142
+ /** The request's question list. */
143
+ questions;
144
+ /** Tool call identity; absent for a blocking request that carried no `wait`. */
145
+ callId;
146
+ /**
147
+ * Recorded answers of a call that already settled. Present only on a
148
+ * read-only review card, which the tool call row builds from its own
149
+ * transcript so a finished question can be read back in the panel that
150
+ * asked it. Such a card has no answer channel and no countdown.
151
+ */
152
+ review;
153
+ /**
154
+ * What closing the panel does. A card keyed by tool call stays reachable
155
+ * from its tool call row, so closing only withdraws the panel (`hide`) and
156
+ * persists nothing. A card the Host never named has no way back, so closing
157
+ * it ends the request (`cancel`).
158
+ */
159
+ dismissal;
160
+ #state = "open";
161
+ #waterfall;
162
+ #rpc;
163
+ #seat;
164
+ #timedWait = false;
165
+ #timer;
166
+ #deadline;
167
+ #remainingMs;
168
+ #focused = false;
169
+ #engaged = false;
170
+ #held = false;
171
+ #closed = false;
172
+ #siblings;
173
+ #listeners = /* @__PURE__ */ new Set();
174
+ #snapshot;
175
+ /**
176
+ * @param sessionId - Agent/Session identity owning the request.
177
+ * @param questions - complete question batch.
178
+ * @param callId - tool call identity when the Host named one.
179
+ * @param siblings - keys of every card currently registered for the Session, for draft pruning.
180
+ * @param review - recorded answers of a settled call, making this a read-only card.
181
+ */
182
+ constructor(sessionId, questions, callId, siblings, review) {
183
+ this.sessionId = sessionId;
184
+ this.questions = questions;
185
+ this.kind = planReviewOf(questions) === void 0 ? "question" : "plan-review";
186
+ this.callId = callId;
187
+ this.review = review;
188
+ this.dismissal = callId === void 0 ? "cancel" : "hide";
189
+ this.#siblings = siblings;
190
+ if (callId === void 0) nextQuestionKey += 1;
191
+ this.key = callId === void 0 ? `question:${unnamedQuestionPrefix}:${String(nextQuestionKey)}` : PendingQuestion.keyOf(sessionId, callId);
192
+ this.#snapshot = this.createSnapshot();
193
+ }
194
+ /**
195
+ * Card key of a tool call, shared by every source that names one. The Host
196
+ * request and the Session projection carry the branded `ToolCallId`; a
197
+ * transcript row carries the same wire value as a plain string.
198
+ * @param sessionId - owning Session.
199
+ * @param callId - tool call identity, branded or as a transcript spells it.
200
+ * @returns the render identity and draft key.
201
+ */
202
+ static keyOf(sessionId, callId) {
203
+ return `question:${String(sessionId)}:${callId}`;
204
+ }
205
+ /**
206
+ * Draft keys that are still live in this Session, this card included.
207
+ * @returns keys the draft store must keep; everything else is stale.
208
+ */
209
+ liveKeys() {
210
+ return this.#siblings?.() ?? [this.key];
211
+ }
212
+ /** Subscribe to card state changes. */
213
+ subscribe = (listener) => {
214
+ this.#listeners.add(listener);
215
+ return () => {
216
+ this.#listeners.delete(listener);
217
+ };
218
+ };
219
+ /** Read the stable current card state. */
220
+ snapshot = () => this.#snapshot;
221
+ /** Observable snapshot read by the renderer's keyed Hook. */
222
+ getSnapshot = this.snapshot;
223
+ createSnapshot() {
224
+ const channel = this.#waterfall !== void 0 ? "waterfall" : this.#state === "continued" && this.#rpc !== void 0 ? "rpc" : "none";
225
+ const waitState = this.#state === "continued" ? "continued" : this.#held ? "waiting" : this.#engaged ? "editing" : this.#focused ? "focused" : "counting";
226
+ return {
227
+ state: this.#state,
228
+ waitState,
229
+ countdown: this.#timedWait ? {
230
+ remainingMs: this.#deadline === void 0 ? this.#remainingMs ?? 0 : Math.max(0, this.#deadline - Date.now()),
231
+ running: this.#deadline !== void 0
232
+ } : void 0,
233
+ channel,
234
+ closed: this.#closed
235
+ };
236
+ }
237
+ publish() {
238
+ this.#snapshot = this.createSnapshot();
239
+ this.#reschedule();
240
+ for (const listener of this.#listeners) listener();
241
+ }
242
+ /**
243
+ * Own the countdown here rather than in a mounted component: the panel can be
244
+ * hidden and remounted while the request stands, and a timer that died with
245
+ * the component would leave the tool call waiting past its deadline.
246
+ */
247
+ #reschedule() {
248
+ const wanted = this.#deadline !== void 0 && !this.#closed;
249
+ if (wanted === (this.#timer !== void 0)) return;
250
+ if (!wanted) {
251
+ clearInterval(this.#timer);
252
+ this.#timer = void 0;
253
+ return;
254
+ }
255
+ this.#timer = setInterval(() => {
256
+ this.#tick();
257
+ }, 1e3);
258
+ }
259
+ #tick() {
260
+ const deadline = this.#deadline;
261
+ /* v8 ignore next -- #reschedule clears the interval in the same publish that drops the deadline. */
262
+ if (deadline === void 0) return;
263
+ if (Date.now() >= deadline) {
264
+ this.timeout();
265
+ return;
266
+ }
267
+ this.publish();
268
+ }
269
+ /**
270
+ * Attach the live waterfall of a forwarded request.
271
+ * @param channel - request channel created by {@link createWaterfallRequest}.
272
+ */
273
+ attachWaterfall(channel) {
274
+ this.#waterfall = channel;
275
+ this.#timedWait = channel.deadline !== void 0;
276
+ if (!this.#held && !this.#engaged) {
277
+ this.#deadline = channel.deadline;
278
+ if (this.#focused && channel.deadline !== void 0) {
279
+ this.#remainingMs = Math.max(0, channel.deadline - Date.now());
280
+ this.#deadline = void 0;
281
+ }
282
+ }
283
+ this.publish();
284
+ }
285
+ /**
286
+ * Drop a waterfall channel that settled or was cancelled; the card stays.
287
+ * @param channel - the channel that ended.
288
+ */
289
+ detachWaterfall(channel) {
290
+ if (this.#waterfall !== channel) return;
291
+ this.#waterfall = void 0;
292
+ this.#timedWait = false;
293
+ this.#deadline = void 0;
294
+ this.publish();
295
+ }
296
+ /**
297
+ * Whether a live waterfall is attached.
298
+ * @returns whether the pending Host request still accepts settlement.
299
+ */
300
+ hasWaterfall() {
301
+ return this.#waterfall !== void 0;
302
+ }
303
+ /**
304
+ * Attach the Remote answer path used once the question is continued.
305
+ * @param channel - Remote calls bound to this Session and call.
306
+ */
307
+ attachRpc(channel) {
308
+ this.#rpc = channel;
309
+ this.publish();
310
+ }
311
+ /**
312
+ * Attach the composer seat this card is published into.
313
+ * @param seat - withdrawal of the published panel, owned by the card registry.
314
+ */
315
+ attachSeat(seat) {
316
+ this.#seat = seat;
317
+ }
318
+ /**
319
+ * Copy the projection row state.
320
+ * @param state - `open` or `continued`.
321
+ */
322
+ setState(state) {
323
+ if (this.#state === state) return;
324
+ this.#state = state;
325
+ this.publish();
326
+ }
327
+ /** Stop this Client's countdown indefinitely; the waterfall then waits like a blocking question. */
328
+ takeTime() {
329
+ if (this.#held) return;
330
+ this.#held = true;
331
+ this.#focused = false;
332
+ this.#remainingMs = void 0;
333
+ this.#deadline = void 0;
334
+ this.publish();
335
+ }
336
+ /**
337
+ * Record focus even before the request arrives, freezing a pristine countdown once attached.
338
+ * @param now - current Client epoch time.
339
+ */
340
+ holdFocus(now = Date.now()) {
341
+ if (this.#held || this.#engaged || this.#focused) return;
342
+ this.#remainingMs = this.#deadline === void 0 ? void 0 : Math.max(0, this.#deadline - now);
343
+ this.#deadline = void 0;
344
+ this.#focused = true;
345
+ this.publish();
346
+ }
347
+ /**
348
+ * Resume a pristine countdown after the answer surface loses focus.
349
+ * @param now - current Client epoch time.
350
+ */
351
+ releaseFocus(now = Date.now()) {
352
+ if (!this.#focused) return;
353
+ this.#focused = false;
354
+ this.#deadline = this.#timedWait && this.#remainingMs !== void 0 ? now + this.#remainingMs : void 0;
355
+ this.#remainingMs = void 0;
356
+ this.publish();
357
+ }
358
+ /**
359
+ * Keep the first edited draft answerable without a foreground deadline.
360
+ * @param now - current Client epoch time used to preserve the remaining duration.
361
+ */
362
+ engage(now = Date.now()) {
363
+ if (this.#held || this.#engaged) return;
364
+ if (this.#remainingMs === void 0 && this.#deadline !== void 0) this.#remainingMs = Math.max(0, this.#deadline - now);
365
+ this.#engaged = true;
366
+ this.#focused = false;
367
+ this.#deadline = void 0;
368
+ this.publish();
369
+ }
370
+ /** Local countdown reached zero: settle the waterfall with `ASK_TIMED_OUT`, keep the card. */
371
+ timeout() {
372
+ if (this.#held || this.#engaged || this.#focused) return;
373
+ const channel = this.#waterfall;
374
+ if (channel === void 0) return;
375
+ this.#waterfall = void 0;
376
+ this.#timedWait = false;
377
+ this.#deadline = void 0;
378
+ channel.reject("ASK_TIMED_OUT");
379
+ this.publish();
380
+ }
381
+ /** Hand a live waterfall to the next listener when this presentation domain unloads. */
382
+ delegate() {
383
+ this.#waterfall?.delegate();
384
+ }
385
+ /** Mark the card removed from the registry; the mounted composer clears its draft. */
386
+ close() {
387
+ if (this.#closed) return;
388
+ this.#closed = true;
389
+ this.publish();
390
+ }
391
+ /**
392
+ * Submit the whole answer batch through the live waterfall, or through the
393
+ * Remote path once the question is continued.
394
+ * @param answer - complete structured answer batch.
395
+ */
396
+ answer(answer) {
397
+ const waterfall = this.#waterfall;
398
+ if (waterfall !== void 0) return settlePendingComposer(() => {
399
+ waterfall.resolve(answer);
400
+ }, "pending question settlement failed");
401
+ const rpc = this.#rpc;
402
+ if (this.#state === "continued" && rpc !== void 0) return rpc.answer(answer).then((accepted) => {
403
+ if (!accepted) throw new Error("this question is no longer answerable");
404
+ });
405
+ return Promise.reject(/* @__PURE__ */ new Error("no channel accepts an answer yet"));
406
+ }
407
+ /**
408
+ * Close the panel. A tool-call-keyed card only leaves the composer seat: the
409
+ * request stands, the countdown keeps running here, and the tool call row
410
+ * reopens it. A card the Host never named ends its request instead, because
411
+ * nothing could bring it back.
412
+ */
413
+ dismiss() {
414
+ if (this.dismissal === "hide") return settlePendingComposer(() => {
415
+ this.#seat?.hide();
416
+ }, "pending question hide failed");
417
+ const waterfall = this.#waterfall;
418
+ if (waterfall === void 0) return Promise.reject(/* @__PURE__ */ new Error("no channel accepts a cancellation yet"));
419
+ return settlePendingComposer(() => {
420
+ waterfall.reject("ASK_CANCELLED");
421
+ }, "pending question cancellation failed");
422
+ }
423
+ };
424
+ //#endregion
425
+ //#region lib/types/client/draft-store.js
426
+ /**
427
+ * Session-scoped draft state for the generic question composer. The Slot
428
+ * registry owns store instances; this module exports only the factory so a
429
+ * plugin reload cannot reuse a module-global handle.
430
+ */
431
+ /**
432
+ * Declare the question composer's Session store. Drafts persist per Session so
433
+ * leaving the Session or restarting the Client does not erase an unfinished answer.
434
+ * @returns a persisted store handle whose instance is owned by the Slot registry.
435
+ */
436
+ function createQuestionDraftStore() {
437
+ return (0, _qilin_agent_client_store.defineStore)({
438
+ init: () => ({ progressByRequest: {} }),
439
+ persist: "dsh.user-questions.drafts.v1",
440
+ actions: {
441
+ replace: (draft, requestKey, progress) => {
442
+ draft.progressByRequest[requestKey] = progress;
443
+ },
444
+ clear: (draft, requestKey) => {
445
+ draft.progressByRequest = Object.fromEntries(Object.entries(draft.progressByRequest).filter(([key]) => key !== requestKey));
446
+ },
447
+ prune: (draft, keep) => {
448
+ const live = new Set(keep);
449
+ draft.progressByRequest = Object.fromEntries(Object.entries(draft.progressByRequest).filter(([key]) => live.has(key)));
450
+ }
451
+ }
452
+ });
453
+ }
454
+ //#endregion
455
+ //#region ../../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
456
+ function r(e) {
457
+ var t, f, n = "";
458
+ if ("string" == typeof e || "number" == typeof e) n += e;
459
+ else if ("object" == typeof e) if (Array.isArray(e)) {
460
+ var o = e.length;
461
+ for (t = 0; t < o; t++) e[t] && (f = r(e[t])) && (n && (n += " "), n += f);
462
+ } else for (f in e) e[f] && (n && (n += " "), n += f);
463
+ return n;
464
+ }
465
+ function clsx() {
466
+ for (var e, t, f = 0, n = "", o = arguments.length; f < o; f++) (e = arguments[f]) && (t = r(e)) && (n && (n += " "), n += t);
467
+ return n;
468
+ }
469
+ //#endregion
470
+ //#region \0qilin-css:/home/runner/work/QiLin/QiLin/packages/client/ui-user-questions/src/client/PlanReviewPanel.module.css.mjs
471
+ const css$2 = ".CE6Vxa_frame{padding:6px calc(var(--qilin-composer-side-clearance) + 16px) 10px;justify-content:center;display:flex}.CE6Vxa_card{width:100%;max-width:var(--qilin-chat-content-width);--qilin-elevation-stroke-color:var(--qilin-alias-border-l2);background:var(--qilin-specific-input-major);box-shadow:var(--qilin-elevation-panel);color:var(--qilin-alias-label-primary);border:0;border-radius:20px;flex-direction:column;display:flex;overflow:hidden}.CE6Vxa_card,.CE6Vxa_card *{box-sizing:border-box}.CE6Vxa_strip{background:var(--qilin-alias-state-warn-tertiary);color:var(--qilin-alias-state-warn-primary);flex-shrink:0;align-items:center;gap:8px;padding:12px 16px;font-size:14px;line-height:20px;display:flex}.CE6Vxa_dot{corner-shape:round;background:var(--qilin-alias-state-warn-primary);border-radius:50%;width:8px;height:8px}.CE6Vxa_footer{flex-shrink:0;justify-content:space-between;align-items:center;gap:12px;padding:8px 16px 12px;display:flex}.CE6Vxa_feedback{min-height:16px;color:var(--qilin-alias-state-error-primary);font-size:11px;line-height:16px}.CE6Vxa_actions{flex-shrink:0;align-items:center;gap:8px;display:flex}.CE6Vxa_summary{min-width:0;padding:14px 16px 12px}.CE6Vxa_title{text-overflow:ellipsis;white-space:nowrap;margin:0;font-size:15px;font-weight:500;line-height:22px;overflow:hidden}.CE6Vxa_description{-webkit-line-clamp:2;color:var(--qilin-alias-label-secondary);-webkit-box-orient:vertical;margin:8px 0 0;font-size:14px;line-height:24px;display:-webkit-box;overflow:hidden}.CE6Vxa_discuss{gap:6px}@media (width<=720px){.CE6Vxa_card{border-radius:16px}.CE6Vxa_footer{align-items:flex-end;padding:8px 12px 10px}}.CE6Vxa_previewActions{align-items:center;margin-left:auto;display:flex}";
472
+ const tagId$2 = "@qilin-agent/client-ui-user-questions/PlanReviewPanel.module.css";
473
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
474
+ const tag = document.createElement("style");
475
+ tag.dataset.plugin = "@qilin-agent/client-ui-user-questions";
476
+ tag.dataset.pluginCss = tagId$2;
477
+ tag.textContent = css$2;
478
+ document.head.appendChild(tag);
479
+ }
480
+ var PlanReviewPanel_module_css_default = {
481
+ "actions": "CE6Vxa_actions",
482
+ "card": "CE6Vxa_card",
483
+ "description": "CE6Vxa_description",
484
+ "discuss": "CE6Vxa_discuss",
485
+ "dot": "CE6Vxa_dot",
486
+ "feedback": "CE6Vxa_feedback",
487
+ "footer": "CE6Vxa_footer",
488
+ "frame": "CE6Vxa_frame",
489
+ "previewActions": "CE6Vxa_previewActions",
490
+ "strip": "CE6Vxa_strip",
491
+ "summary": "CE6Vxa_summary",
492
+ "title": "CE6Vxa_title"
493
+ };
494
+ //#endregion
495
+ //#region lib/types/client/PlanReviewPanel.js
496
+ /**
497
+ * Optional-prop spread for a decision button's tooltip: `title` is optional on
498
+ * the DOM props, and exactOptionalPropertyTypes rejects an explicit undefined.
499
+ *
500
+ * @param description - the asker's option description, when it carries one.
501
+ * @returns The `title` prop to spread, or nothing.
502
+ */
503
+ function tooltip(description) {
504
+ return description === void 0 ? {} : { title: description };
505
+ }
506
+ /**
507
+ * Render plan review controls; the submitted document opens in the sidebar.
508
+ *
509
+ * @param props - the question domain face, the narrowed plan review, and `t`.
510
+ * @returns The plan-review takeover for this request.
511
+ */
512
+ function PlanReviewPanel({ pending, review, t, renderSlot }) {
513
+ const [busy, setBusy] = (0, react.useState)(false);
514
+ const [error, setError] = (0, react.useState)(null);
515
+ const settle = (send, remote = false) => {
516
+ setBusy(true);
517
+ setError(null);
518
+ send().then(() => {
519
+ if (!remote) return;
520
+ setBusy(false);
521
+ pending.dismiss().catch(() => {
522
+ setError(t("status.sent"));
523
+ });
524
+ }).catch((cause) => {
525
+ setBusy(false);
526
+ setError(cause instanceof Error ? cause.message : String(cause));
527
+ });
528
+ };
529
+ const decide = (label) => {
530
+ settle(() => pending.answer({ answers: [{
531
+ id: review.id,
532
+ selected: [label]
533
+ }] }), pending.snapshot().channel === "rpc");
534
+ };
535
+ const summary = (0, react.useMemo)(() => {
536
+ const title = (0, _qilin_agent_client_ui_primitives.extractMarkdownPlainText)(review.plan, { mode: "first-line" });
537
+ const description = (0, _qilin_agent_client_ui_primitives.extractMarkdownPlainText)(review.plan, { mode: "first-paragraph" });
538
+ return {
539
+ title,
540
+ description: description === title ? "" : description
541
+ };
542
+ }, [review.plan]);
543
+ return (0, react_jsx_runtime.jsx)("div", {
544
+ className: PlanReviewPanel_module_css_default.frame,
545
+ "data-plan-review-key": pending.key,
546
+ children: (0, react_jsx_runtime.jsxs)("section", {
547
+ className: PlanReviewPanel_module_css_default.card,
548
+ "aria-label": review.question,
549
+ "aria-busy": busy,
550
+ children: [
551
+ (0, react_jsx_runtime.jsxs)("div", {
552
+ className: PlanReviewPanel_module_css_default.strip,
553
+ children: [
554
+ (0, react_jsx_runtime.jsx)("span", { className: PlanReviewPanel_module_css_default.dot }),
555
+ t("plan.header"),
556
+ (0, react_jsx_runtime.jsx)("div", {
557
+ className: PlanReviewPanel_module_css_default.previewActions,
558
+ children: renderSlot("conversation.plan-review.actions", {
559
+ review,
560
+ requestKey: pending.key
561
+ })
562
+ })
563
+ ]
564
+ }),
565
+ (0, react_jsx_runtime.jsxs)("div", {
566
+ className: PlanReviewPanel_module_css_default.summary,
567
+ children: [(0, react_jsx_runtime.jsx)("h3", {
568
+ className: PlanReviewPanel_module_css_default.title,
569
+ children: summary.title
570
+ }), summary.description !== "" && (0, react_jsx_runtime.jsx)("p", {
571
+ className: PlanReviewPanel_module_css_default.description,
572
+ children: summary.description
573
+ })]
574
+ }),
575
+ (0, react_jsx_runtime.jsxs)("div", {
576
+ className: PlanReviewPanel_module_css_default.footer,
577
+ children: [(0, react_jsx_runtime.jsx)("div", {
578
+ className: PlanReviewPanel_module_css_default.feedback,
579
+ role: "status",
580
+ children: error
581
+ }), (0, react_jsx_runtime.jsxs)("div", {
582
+ className: PlanReviewPanel_module_css_default.actions,
583
+ children: [(0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Button, {
584
+ variant: "outline",
585
+ className: PlanReviewPanel_module_css_default.discuss,
586
+ icon: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconEditOutline16, { size: 14 }),
587
+ disabled: busy,
588
+ onClick: () => {
589
+ settle(() => pending.dismiss());
590
+ },
591
+ children: t("plan.discuss")
592
+ }), (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Button, {
593
+ variant: "primary",
594
+ ...tooltip(review.approve.description),
595
+ disabled: busy,
596
+ onClick: () => {
597
+ decide(review.approve.label);
598
+ },
599
+ children: t("plan.approve")
600
+ })]
601
+ })]
602
+ })
603
+ ]
604
+ })
605
+ });
606
+ }
607
+ //#endregion
608
+ //#region \0qilin-css:/home/runner/work/QiLin/QiLin/packages/client/ui-user-questions/src/client/QuestionComposer.module.css.mjs
609
+ const css$1 = ".vluglG_frame{padding:6px calc(var(--qilin-composer-side-clearance) + 16px) 10px;justify-content:center;display:flex}.vluglG_card{width:100%;max-width:var(--qilin-chat-content-width);--qilin-elevation-stroke-color:var(--qilin-alias-border-l2-darkmode-thin);background:var(--qilin-specific-input-major);max-height:min(60vh,520px);box-shadow:var(--qilin-elevation-panel);color:var(--qilin-alias-label-primary);--qilin-scrollbar-thumb:var(--qilin-alias-scrollbar-bg-l2);--qilin-scrollbar-thumb-hover:var(--qilin-alias-scrollbar-hover-l2);border:0;border-radius:20px;flex-direction:column;padding:0 0 10px;display:flex;overflow:hidden}.vluglG_card,.vluglG_card *{box-sizing:border-box}.vluglG_cardMinimized{max-height:none}.vluglG_cardMinimized .vluglG_header{padding-bottom:14px}.vluglG_headerActions{flex-shrink:0;align-items:center;gap:4px;display:flex}.vluglG_header{flex-shrink:0;justify-content:space-between;align-items:flex-start;gap:16px;padding:20px 16px 0 24px;display:flex}.vluglG_headingBlock{min-width:0}.vluglG_eyebrow{color:var(--qilin-alias-label-tertiary);margin-bottom:5px;font-size:11px;line-height:16px}.vluglG_title{margin:0;font-size:16px;font-weight:500;line-height:22px}.vluglG_detail{margin:0 2px 8px}.vluglG_footerActions{flex-shrink:0;align-items:center;gap:12px;display:flex}.vluglG_pager{flex-shrink:0;align-items:center;gap:6px;display:flex}.vluglG_progress{color:var(--qilin-alias-label-secondary);white-space:nowrap;word-spacing:-2px;padding:0 4px;font-size:14px;font-weight:500;line-height:24px}.vluglG_iconButton{corner-shape:round;width:24px;height:24px;color:var(--qilin-alias-label-tertiary);cursor:pointer;background:0 0;border:none;border-radius:999px;place-items:center;padding:0;display:grid}.vluglG_iconButton:hover:not(:disabled){background:var(--qilin-alias-interactive-bg-hover);color:var(--qilin-alias-label-primary)}.vluglG_iconButton:disabled{color:var(--qilin-alias-label-dimmed);cursor:default}.vluglG_body{overscroll-behavior:contain;flex-direction:column;flex:auto;min-height:0;display:flex;overflow-y:auto}.vluglG_options{flex-direction:column;gap:1px;margin:8px 0 0;padding:4px 12px;display:flex}.vluglG_option{width:100%;min-height:40px;color:inherit;text-align:left;cursor:pointer;background:0 0;border:1px solid #0000;border-radius:12px;flex-shrink:0;align-items:flex-start;gap:8px;padding:8px 12px 8px 8px;transition:background-color .12s,border-color .12s;display:flex}.vluglG_option:hover:not(:disabled),.vluglG_optionSelected{background:var(--qilin-alias-interactive-bg-hover)}.vluglG_optionSelected{border-color:var(--qilin-alias-border-l2)}.vluglG_option:disabled{cursor:default}.vluglG_number{background:var(--qilin-alias-bg-overlay);width:20px;height:20px;color:var(--qilin-alias-label-secondary);border-radius:6px;flex:0 0 20px;place-items:center;margin-top:2px;font-size:12px;font-weight:500;line-height:18px;display:grid}.vluglG_checkbox{flex:0 0 20px;place-items:center;width:20px;height:20px;margin-top:2px;display:grid}.vluglG_checkbox:before{content:\"\";border:.5px solid var(--qilin-alias-border-l4);border-radius:4px;grid-area:1/1;width:14px;height:14px;transition:background-color .12s,border-color .12s}.vluglG_checkbox>svg{grid-area:1/1}.vluglG_checkboxChecked{color:var(--qilin-alias-label-primary-foreground)}.vluglG_checkboxChecked:before{border-color:var(--qilin-alias-label-primary);background:var(--qilin-alias-label-primary)}.vluglG_optionCopy{flex:1;min-width:0}.vluglG_optionLine{flex-wrap:wrap;align-items:baseline;gap:2px 6px;display:flex}.vluglG_optionLabel{font-size:14px;font-weight:500;line-height:24px}.vluglG_badge{background:var(--qilin-specific-sidebar-nav-item-active-accent);color:var(--qilin-alias-button-info-fill);border-radius:6px;padding:0 4px;font-size:11px;font-weight:600;line-height:18px}.vluglG_description{color:var(--qilin-alias-label-tertiary);font-size:14px;font-weight:400;line-height:24px}.vluglG_customRow{border:1px solid #0000;border-radius:12px;flex-shrink:0;align-items:flex-start;gap:8px;width:100%;min-height:40px;padding:8px 12px 8px 8px;transition:background-color .12s,border-color .12s;display:flex}.vluglG_customRow:hover,.vluglG_customRow:focus-within,.vluglG_customRowActive{background:var(--qilin-alias-interactive-bg-hover)}.vluglG_customRow:focus-within,.vluglG_customRowActive{border-color:var(--qilin-alias-border-l2)}.vluglG_field{--qilin-answer-field-padding:0;min-width:0;display:grid}.vluglG_field>*{min-width:0;padding:var(--qilin-answer-field-padding);font:inherit;white-space:pre-wrap;word-break:break-word;overflow-wrap:anywhere;grid-area:1/1;font-size:14px;line-height:24px}.vluglG_fieldMirror{box-sizing:content-box;visibility:hidden;max-height:144px;overflow:hidden}.vluglG_fieldInput{resize:none;color:var(--qilin-alias-label-primary);caret-color:var(--qilin-alias-state-business-primary);background:0 0;border:none;outline:none;overflow-y:auto}.vluglG_fieldInput::placeholder{color:var(--qilin-alias-label-caption)}.vluglG_customInline{flex:1}.vluglG_customBlock{border:.5px solid var(--qilin-alias-border-l4);background:var(--qilin-alias-bg-module-platform);--qilin-answer-field-padding:8px 12px;border-radius:10px;flex-shrink:0;min-height:64px;margin:0 12px}.vluglG_customBlock:focus-within{border-color:var(--qilin-alias-state-business-primary)}.vluglG_footer{flex-shrink:0;justify-content:space-between;align-items:center;gap:12px;margin-top:12px;padding:0 10px 0 18px;display:flex}.vluglG_feedback{min-height:16px;color:var(--qilin-alias-state-error-primary);text-align:right;flex:1;font-size:11px;line-height:16px}@media (width<=720px){.vluglG_card{border-radius:16px}.vluglG_header{padding:10px 12px 0 18px}.vluglG_options{padding:4px 8px}.vluglG_title{font-size:15px;line-height:21px}.vluglG_option,.vluglG_customRow{padding:8px 6px}.vluglG_footer{align-items:flex-end;padding:0 10px}.vluglG_footerActions{flex-shrink:0}}@media (prefers-reduced-motion:reduce){.vluglG_option,.vluglG_customRow{transition:none}}";
610
+ const tagId$1 = "@qilin-agent/client-ui-user-questions/QuestionComposer.module.css";
611
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
612
+ const tag = document.createElement("style");
613
+ tag.dataset.plugin = "@qilin-agent/client-ui-user-questions";
614
+ tag.dataset.pluginCss = tagId$1;
615
+ tag.textContent = css$1;
616
+ document.head.appendChild(tag);
617
+ }
618
+ var QuestionComposer_module_css_default = {
619
+ "badge": "vluglG_badge",
620
+ "body": "vluglG_body",
621
+ "card": "vluglG_card",
622
+ "cardMinimized": "vluglG_cardMinimized",
623
+ "checkbox": "vluglG_checkbox",
624
+ "checkboxChecked": "vluglG_checkboxChecked",
625
+ "customBlock": "vluglG_customBlock",
626
+ "customInline": "vluglG_customInline",
627
+ "customRow": "vluglG_customRow",
628
+ "customRowActive": "vluglG_customRowActive",
629
+ "description": "vluglG_description",
630
+ "detail": "vluglG_detail",
631
+ "eyebrow": "vluglG_eyebrow",
632
+ "feedback": "vluglG_feedback",
633
+ "field": "vluglG_field",
634
+ "fieldInput": "vluglG_fieldInput",
635
+ "fieldMirror": "vluglG_fieldMirror",
636
+ "footer": "vluglG_footer",
637
+ "footerActions": "vluglG_footerActions",
638
+ "frame": "vluglG_frame",
639
+ "header": "vluglG_header",
640
+ "headerActions": "vluglG_headerActions",
641
+ "headingBlock": "vluglG_headingBlock",
642
+ "iconButton": "vluglG_iconButton",
643
+ "number": "vluglG_number",
644
+ "option": "vluglG_option",
645
+ "optionCopy": "vluglG_optionCopy",
646
+ "optionLabel": "vluglG_optionLabel",
647
+ "optionLine": "vluglG_optionLine",
648
+ "optionSelected": "vluglG_optionSelected",
649
+ "options": "vluglG_options",
650
+ "pager": "vluglG_pager",
651
+ "progress": "vluglG_progress",
652
+ "title": "vluglG_title"
653
+ };
654
+ //#endregion
655
+ //#region lib/types/client/QuestionComposer.js
656
+ /** A removed card can remain mounted until the composer seat updates. */
657
+ const REMOVED_CARD = {
658
+ state: "open",
659
+ waitState: "counting",
660
+ countdown: void 0,
661
+ channel: "none",
662
+ closed: true
663
+ };
664
+ /**
665
+ * Split the conventional recommendation suffix without changing the answer value.
666
+ * @param label - Original option label returned if selected.
667
+ * @returns Display label plus recommendation state.
668
+ */
669
+ function parseRecommendedLabel(label) {
670
+ const suffix = /\s*(?:\((?:recommended|推荐)\)|((?:recommended|推荐)))\s*$/i;
671
+ return suffix.test(label) ? {
672
+ label: label.replace(suffix, ""),
673
+ recommended: true
674
+ } : {
675
+ label,
676
+ recommended: false
677
+ };
678
+ }
679
+ /** Only a marked first choice is an implicit draft; the user still submits it. */
680
+ function recommendedFirstOption(question) {
681
+ const label = question.options?.[0]?.label;
682
+ return label !== void 0 && parseRecommendedLabel(label).recommended ? label : void 0;
683
+ }
684
+ /** Accept persisted progress only when it still describes this question batch. */
685
+ function isQuestionDraftProgress(value, questionCount) {
686
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
687
+ const progress = value;
688
+ if (typeof progress.index !== "number" || !Number.isInteger(progress.index) || progress.index < 0 || progress.index >= questionCount || !Array.isArray(progress.drafts) || progress.drafts.length !== questionCount || progress.wait !== void 0 && progress.wait !== "editing" && progress.wait !== "waiting") return false;
689
+ return progress.drafts.every((item) => {
690
+ if (typeof item !== "object" || item === null || Array.isArray(item)) return false;
691
+ const draft = item;
692
+ return Array.isArray(draft.selected) && draft.selected.every((label) => typeof label === "string") && typeof draft.custom === "string" && typeof draft.skipped === "boolean";
693
+ });
694
+ }
695
+ /** Return whether a text-field key event belongs to an active IME composition. */
696
+ function isComposing(event) {
697
+ return event.nativeEvent.isComposing || Reflect.get(event.nativeEvent, "keyCode") === 229;
698
+ }
699
+ /**
700
+ * Auto-growing free-text answer: a textarea, so a long answer soft-wraps and
701
+ * Shift+Enter breaks a line, over a hidden mirror that owns the height.
702
+ *
703
+ * The mirror renders the draft plus a trailing newline in normal flow and so
704
+ * sizes the grid row (counting rows by '\n' cannot see soft wraps); the
705
+ * textarea shares that one cell and stretches to it, and `rows={1}` keeps the
706
+ * control's own intrinsic height out of the row sizing so the mirror alone
707
+ * decides. Past the mirror's cap the textarea scrolls itself — it is the only
708
+ * scrollport in the stack, there being no second glyph layer to keep aligned.
709
+ * Mirror and textarea MUST share font, line-height, padding and wrapping rules
710
+ * or the two heights diverge.
711
+ *
712
+ * @param props - visual variant, draft text, and the field's event handlers.
713
+ * @returns The mirrored auto-growing field.
714
+ */
715
+ function AnswerField(props) {
716
+ return (0, react_jsx_runtime.jsxs)("div", {
717
+ className: clsx(QuestionComposer_module_css_default.field, props.variant === "inline" ? QuestionComposer_module_css_default.customInline : QuestionComposer_module_css_default.customBlock),
718
+ children: [(0, react_jsx_runtime.jsx)("div", {
719
+ "aria-hidden": true,
720
+ className: QuestionComposer_module_css_default.fieldMirror,
721
+ children: `${props.value}\n`
722
+ }), (0, react_jsx_runtime.jsx)("textarea", {
723
+ autoFocus: props.autoFocus,
724
+ className: QuestionComposer_module_css_default.fieldInput,
725
+ value: props.value,
726
+ disabled: props.disabled,
727
+ rows: 1,
728
+ placeholder: props.placeholder,
729
+ onFocus: props.onFocus,
730
+ onChange: props.onChange,
731
+ onKeyDown: props.onKeyDown
732
+ })]
733
+ });
734
+ }
735
+ /**
736
+ * Composer takeover router. Generic-question drafts live in this entry's
737
+ * Session-scoped Slot store, keyed by the pending carrier, so a strict Session
738
+ * entry remount restores the same request without exposing it to another one.
739
+ *
740
+ * One takeover, two presentations: a request that declares a presentation intent this
741
+ * package renders uses that presentation (a plan review is one decision over one
742
+ * plan, not a question set), and every other request takes the generic flow.
743
+ * The routing lives here, at the one entry that owns the composer seat, so
744
+ * neither presentation can claim a request the other is already rendering.
745
+ *
746
+ * @param props - the selector-matched pending question carrier plus the framework standard kit.
747
+ * @returns The question flow, or the intent's own surface, for this request.
748
+ */
749
+ function QuestionComposer(props) {
750
+ const question = props.matched;
751
+ const review = (0, react.useMemo)(() => planReviewOf(question.questions), [question]);
752
+ return review === void 0 ? (0, react_jsx_runtime.jsx)(QuestionFlow, {
753
+ pending: question,
754
+ t: props.t,
755
+ useStore: props.useStore,
756
+ useQuestionCard: props.useQuestionCard,
757
+ actions: props.actions
758
+ }, question.key) : (0, react_jsx_runtime.jsx)(PlanReviewPanel, {
759
+ pending: question,
760
+ review,
761
+ t: props.t,
762
+ renderSlot: props.renderSlot
763
+ }, question.key);
764
+ }
765
+ function QuestionFlow({ pending, t, useStore, useQuestionCard, actions }) {
766
+ const questions = pending.questions;
767
+ const review = pending.review;
768
+ const markdownLabels = (0, react.useMemo)(() => ({
769
+ code: {
770
+ copyLabel: t("copy"),
771
+ copiedLabel: t("copied")
772
+ },
773
+ mermaid: {
774
+ diagramLabel: t("markdown.diagram"),
775
+ enlargedLabel: t("markdown.enlarged")
776
+ },
777
+ footnotes: t("markdown.footnotes")
778
+ }), [t]);
779
+ const initialDrafts = (0, react.useMemo)(() => {
780
+ const recorded = new Map((review ?? []).map((answer) => [answer.id, answer]));
781
+ return questions.map((item) => {
782
+ const answer = recorded.get(item.id);
783
+ const custom = answer?.custom ?? "";
784
+ const recommended = review === void 0 ? recommendedFirstOption(item) : void 0;
785
+ return {
786
+ selected: answer === void 0 && recommended !== void 0 ? [recommended] : [...answer?.selected ?? []],
787
+ custom,
788
+ skipped: answer !== void 0 && answer.selected.length === 0 && custom === ""
789
+ };
790
+ });
791
+ }, [questions, review]);
792
+ const stored = useStore((state) => state.progressByRequest[pending.key]);
793
+ const validStored = isQuestionDraftProgress(stored, questions.length) ? stored : void 0;
794
+ const storedProgress = review === void 0 ? validStored : void 0;
795
+ const index = validStored?.index ?? 0;
796
+ const drafts = storedProgress?.drafts ?? initialDrafts;
797
+ const restoredWait = storedProgress?.wait ?? (storedProgress?.drafts.some((item, itemIndex) => item.selected.length !== initialDrafts[itemIndex]?.selected.length || item.selected.some((label, labelIndex) => label !== initialDrafts[itemIndex]?.selected[labelIndex]) || item.custom !== "" || item.skipped) === true ? "editing" : void 0);
798
+ const [busy, setBusy] = (0, react.useState)(null);
799
+ const [error, setError] = (0, react.useState)(null);
800
+ const card = useQuestionCard(pending.key, (snapshot) => snapshot ?? REMOVED_CARD);
801
+ const canSubmit = card.channel !== "none";
802
+ const locked = busy !== null || review !== void 0;
803
+ const sentVia = (0, react.useRef)(null);
804
+ (0, react.useEffect)(() => {
805
+ if (sentVia.current !== "waterfall" || card.state !== "continued") return;
806
+ sentVia.current = null;
807
+ setBusy(null);
808
+ setError({ key: "error.resubmit" });
809
+ }, [card.state]);
810
+ (0, react.useEffect)(() => {
811
+ if (card.closed) actions.clear(pending.key);
812
+ }, [
813
+ actions,
814
+ card.closed,
815
+ pending.key
816
+ ]);
817
+ (0, react.useEffect)(() => {
818
+ actions.prune(pending.liveKeys());
819
+ }, [actions, pending]);
820
+ const waitDisposition = (0, react.useRef)(restoredWait);
821
+ (0, react.useEffect)(() => {
822
+ if (waitDisposition.current === "waiting") pending.takeTime();
823
+ if (waitDisposition.current === "editing") pending.engage();
824
+ }, [pending]);
825
+ const countdown = card.countdown;
826
+ const [minimized, setMinimized] = (0, react.useState)(false);
827
+ const focusedQuestions = (0, react.useRef)(/* @__PURE__ */ new Set());
828
+ const answerSurface = (0, react.useRef)(null);
829
+ const question = questions[index];
830
+ const draft = drafts[index];
831
+ const hasOptions = (question.options?.length ?? 0) > 0;
832
+ const replaceProgress = (nextIndex, nextDrafts) => {
833
+ actions.replace(pending.key, {
834
+ index: nextIndex,
835
+ drafts: nextDrafts,
836
+ ...waitDisposition.current === void 0 ? {} : { wait: waitDisposition.current }
837
+ });
838
+ };
839
+ const takeTime = () => {
840
+ waitDisposition.current = "waiting";
841
+ pending.takeTime();
842
+ replaceProgress(index, drafts);
843
+ };
844
+ const engage = () => {
845
+ if (waitDisposition.current !== void 0) return;
846
+ waitDisposition.current = "editing";
847
+ pending.engage();
848
+ };
849
+ const focusAnswerSurface = (event) => {
850
+ if (!event.currentTarget.contains(event.relatedTarget)) pending.holdFocus();
851
+ };
852
+ const blurAnswerSurface = (event) => {
853
+ if (!event.currentTarget.contains(event.relatedTarget)) pending.releaseFocus();
854
+ };
855
+ (0, react.useEffect)(() => {
856
+ const release = () => {
857
+ pending.releaseFocus();
858
+ };
859
+ const refocus = () => {
860
+ if (waitDisposition.current === "editing") {
861
+ pending.engage();
862
+ return;
863
+ }
864
+ if (answerSurface.current?.contains(document.activeElement) === true) pending.holdFocus();
865
+ };
866
+ const visibilityChanged = () => {
867
+ if (document.hidden) release();
868
+ else refocus();
869
+ };
870
+ window.addEventListener("blur", release);
871
+ window.addEventListener("focus", refocus);
872
+ document.addEventListener("visibilitychange", visibilityChanged);
873
+ return () => {
874
+ release();
875
+ window.removeEventListener("blur", release);
876
+ window.removeEventListener("focus", refocus);
877
+ document.removeEventListener("visibilitychange", visibilityChanged);
878
+ };
879
+ }, [pending]);
880
+ const dismissFlow = () => {
881
+ if (pending.dismissal === "hide") {
882
+ pending.dismiss();
883
+ return;
884
+ }
885
+ if (!canSubmit) {
886
+ setError({ key: "error.unavailable" });
887
+ return;
888
+ }
889
+ setBusy("cancel");
890
+ setError(null);
891
+ sentVia.current = "waterfall";
892
+ pending.dismiss().catch((cause) => {
893
+ sentVia.current = null;
894
+ setBusy(null);
895
+ setError({ text: cause instanceof Error ? cause.message : String(cause) });
896
+ });
897
+ };
898
+ const updateDraft = (update, nextIndex = index) => {
899
+ engage();
900
+ replaceProgress(nextIndex, drafts.map((item, itemIndex) => itemIndex === index ? update(item) : item));
901
+ setError(null);
902
+ };
903
+ const choose = (label) => {
904
+ updateDraft((current) => {
905
+ if (question.multiSelect === true) {
906
+ const selected = current.selected.includes(label) ? current.selected.filter((item) => item !== label) : [...current.selected, label];
907
+ return {
908
+ ...current,
909
+ selected,
910
+ skipped: false
911
+ };
912
+ }
913
+ return {
914
+ selected: [label],
915
+ custom: "",
916
+ skipped: false
917
+ };
918
+ }, question.multiSelect !== true && index < questions.length - 1 ? index + 1 : index);
919
+ };
920
+ const answered = (item) => item.selected.length > 0 || item.custom.trim() !== "";
921
+ const completed = (item) => answered(item) || item.skipped;
922
+ const submitDrafts = (values) => {
923
+ const missing = values.findIndex((item) => !completed(item));
924
+ if (missing >= 0) {
925
+ replaceProgress(missing, values);
926
+ setError({ key: "error.incomplete" });
927
+ return;
928
+ }
929
+ if (!canSubmit) {
930
+ setError({ key: "error.unavailable" });
931
+ return;
932
+ }
933
+ const answer = { answers: questions.map((item, itemIndex) => {
934
+ const value = values[itemIndex];
935
+ if (value.skipped) return {
936
+ id: item.id,
937
+ selected: []
938
+ };
939
+ const custom = value.custom.trim();
940
+ return {
941
+ id: item.id,
942
+ selected: custom === "" || item.multiSelect === true ? value.selected : [],
943
+ ...custom === "" ? {} : { custom }
944
+ };
945
+ }) };
946
+ setBusy("answer");
947
+ setError(null);
948
+ const channel = pending.snapshot().channel;
949
+ sentVia.current = channel === "waterfall" ? "waterfall" : null;
950
+ pending.answer(answer).then(() => {
951
+ if (channel !== "rpc") return;
952
+ sentVia.current = null;
953
+ setBusy(null);
954
+ setError(null);
955
+ pending.dismiss().catch(() => {
956
+ setError({ key: "status.sent" });
957
+ });
958
+ }).catch((cause) => {
959
+ sentVia.current = null;
960
+ setBusy(null);
961
+ setError({ text: cause instanceof Error ? cause.message : String(cause) });
962
+ });
963
+ };
964
+ const continueFlow = () => {
965
+ if (!answered(draft)) {
966
+ setError({ key: "error.unanswered" });
967
+ return;
968
+ }
969
+ if (index < questions.length - 1) {
970
+ replaceProgress(index + 1, drafts);
971
+ setError(null);
972
+ return;
973
+ }
974
+ submitDrafts(drafts);
975
+ };
976
+ const draftCustom = (event) => {
977
+ const value = event.target.value;
978
+ updateDraft((current) => ({
979
+ ...current,
980
+ selected: question.multiSelect === true ? current.selected : [],
981
+ custom: value,
982
+ skipped: false
983
+ }));
984
+ };
985
+ const continueFromCustom = (event) => {
986
+ if (event.key !== "Enter" || event.shiftKey || isComposing(event)) return;
987
+ event.preventDefault();
988
+ continueFlow();
989
+ };
990
+ const skipQuestion = () => {
991
+ engage();
992
+ const nextDrafts = drafts.map((item, itemIndex) => itemIndex === index ? {
993
+ selected: [],
994
+ custom: "",
995
+ skipped: true
996
+ } : item);
997
+ replaceProgress(index < questions.length - 1 ? index + 1 : index, nextDrafts);
998
+ setError(null);
999
+ if (index < questions.length - 1) return;
1000
+ submitDrafts(nextDrafts);
1001
+ };
1002
+ return (0, react_jsx_runtime.jsx)("div", {
1003
+ className: QuestionComposer_module_css_default.frame,
1004
+ "data-question-key": pending.key,
1005
+ children: (0, react_jsx_runtime.jsxs)("section", {
1006
+ className: clsx(QuestionComposer_module_css_default.card, minimized && QuestionComposer_module_css_default.cardMinimized),
1007
+ "aria-labelledby": `question-${pending.key}-${String(index)}`,
1008
+ children: [(0, react_jsx_runtime.jsxs)("header", {
1009
+ className: QuestionComposer_module_css_default.header,
1010
+ children: [(0, react_jsx_runtime.jsxs)("div", {
1011
+ className: QuestionComposer_module_css_default.headingBlock,
1012
+ children: [question.header !== void 0 && (0, react_jsx_runtime.jsx)("div", {
1013
+ className: QuestionComposer_module_css_default.eyebrow,
1014
+ children: question.header
1015
+ }), (0, react_jsx_runtime.jsx)("h2", {
1016
+ className: QuestionComposer_module_css_default.title,
1017
+ id: `question-${pending.key}-${String(index)}`,
1018
+ children: question.question
1019
+ })]
1020
+ }), (0, react_jsx_runtime.jsxs)("div", {
1021
+ className: QuestionComposer_module_css_default.headerActions,
1022
+ children: [
1023
+ countdown !== void 0 && card.waitState !== "waiting" && card.waitState !== "editing" && card.waitState !== "continued" && (0, react_jsx_runtime.jsx)("span", {
1024
+ className: QuestionComposer_module_css_default.waitStatus,
1025
+ children: t(countdown.running ? "wait.countdown" : "wait.paused", { seconds: Math.ceil(countdown.remainingMs / 1e3) })
1026
+ }),
1027
+ countdown !== void 0 && card.waitState !== "waiting" && card.waitState !== "editing" && card.waitState !== "continued" && (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Button, {
1028
+ variant: "outline",
1029
+ className: QuestionComposer_module_css_default.waitButton,
1030
+ onClick: takeTime,
1031
+ children: t("wait.takeTime")
1032
+ }),
1033
+ card.state === "continued" && (0, react_jsx_runtime.jsx)("span", {
1034
+ className: QuestionComposer_module_css_default.waitStatus,
1035
+ children: t("wait.continued")
1036
+ }),
1037
+ countdown !== void 0 && (card.waitState === "waiting" || card.waitState === "editing") && (0, react_jsx_runtime.jsx)("span", {
1038
+ className: QuestionComposer_module_css_default.waitStatus,
1039
+ children: t("wait.held")
1040
+ }),
1041
+ review !== void 0 && (0, react_jsx_runtime.jsx)("span", {
1042
+ className: QuestionComposer_module_css_default.waitStatus,
1043
+ children: t("review.status")
1044
+ }),
1045
+ (0, react_jsx_runtime.jsx)("button", {
1046
+ type: "button",
1047
+ className: QuestionComposer_module_css_default.iconButton,
1048
+ "aria-label": t(minimized ? "nav.maximize" : "nav.minimize"),
1049
+ title: t(minimized ? "nav.maximize" : "nav.minimize"),
1050
+ "aria-expanded": !minimized,
1051
+ disabled: busy !== null,
1052
+ onClick: () => {
1053
+ setMinimized((current) => !current);
1054
+ },
1055
+ children: minimized ? (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconChevronUpOutline14, {}) : (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconChevronDownOutline14, {})
1056
+ }),
1057
+ (0, react_jsx_runtime.jsx)("button", {
1058
+ type: "button",
1059
+ className: QuestionComposer_module_css_default.iconButton,
1060
+ "aria-label": t(pending.dismissal === "hide" ? "nav.close" : "nav.cancel"),
1061
+ title: t(pending.dismissal === "hide" ? "nav.close" : "nav.cancel"),
1062
+ disabled: busy !== null,
1063
+ onClick: dismissFlow,
1064
+ children: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconCloseOutline16, {})
1065
+ })
1066
+ ]
1067
+ })]
1068
+ }), !minimized && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsxs)("div", {
1069
+ ref: answerSurface,
1070
+ className: QuestionComposer_module_css_default.body,
1071
+ "data-question-scroll": true,
1072
+ onFocusCapture: focusAnswerSurface,
1073
+ onBlurCapture: blurAnswerSurface,
1074
+ children: [question.detail !== void 0 && (0, react_jsx_runtime.jsx)("div", {
1075
+ className: QuestionComposer_module_css_default.detail,
1076
+ children: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.MarkdownText, {
1077
+ text: question.detail,
1078
+ labels: markdownLabels
1079
+ })
1080
+ }), (0, react_jsx_runtime.jsxs)("div", {
1081
+ className: QuestionComposer_module_css_default.options,
1082
+ role: question.multiSelect === true ? "group" : "radiogroup",
1083
+ children: [
1084
+ (question.options ?? []).map((option, optionIndex) => {
1085
+ const selected = draft.selected.includes(option.label);
1086
+ const display = parseRecommendedLabel(option.label);
1087
+ return (0, react_jsx_runtime.jsxs)("button", {
1088
+ type: "button",
1089
+ className: clsx(QuestionComposer_module_css_default.option, selected && question.multiSelect !== true && QuestionComposer_module_css_default.optionSelected),
1090
+ role: question.multiSelect === true ? "checkbox" : "radio",
1091
+ "aria-checked": selected,
1092
+ "aria-label": display.label,
1093
+ disabled: locked,
1094
+ onClick: () => {
1095
+ choose(option.label);
1096
+ },
1097
+ onKeyDown: (event) => {
1098
+ if (event.key !== "Enter") return;
1099
+ event.preventDefault();
1100
+ submitDrafts(drafts);
1101
+ },
1102
+ children: [question.multiSelect === true ? (0, react_jsx_runtime.jsx)("span", {
1103
+ className: clsx(QuestionComposer_module_css_default.checkbox, selected && QuestionComposer_module_css_default.checkboxChecked),
1104
+ "aria-hidden": "true",
1105
+ children: selected && (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconCheckOutline14, { size: 12 })
1106
+ }) : (0, react_jsx_runtime.jsx)("span", {
1107
+ className: QuestionComposer_module_css_default.number,
1108
+ children: optionIndex + 1
1109
+ }), (0, react_jsx_runtime.jsx)("span", {
1110
+ className: QuestionComposer_module_css_default.optionCopy,
1111
+ children: (0, react_jsx_runtime.jsxs)("span", {
1112
+ className: QuestionComposer_module_css_default.optionLine,
1113
+ children: [
1114
+ (0, react_jsx_runtime.jsx)("span", {
1115
+ className: QuestionComposer_module_css_default.optionLabel,
1116
+ children: display.label
1117
+ }),
1118
+ display.recommended && (0, react_jsx_runtime.jsx)("span", {
1119
+ className: QuestionComposer_module_css_default.badge,
1120
+ children: t("option.recommended")
1121
+ }),
1122
+ option.description !== void 0 && (0, react_jsx_runtime.jsx)("span", {
1123
+ className: QuestionComposer_module_css_default.description,
1124
+ children: option.description
1125
+ })
1126
+ ]
1127
+ })
1128
+ })]
1129
+ }, `${option.label}-${String(optionIndex)}`);
1130
+ }),
1131
+ review !== void 0 && draft.skipped && (0, react_jsx_runtime.jsx)("p", {
1132
+ className: QuestionComposer_module_css_default.reviewNote,
1133
+ children: t("review.skipped")
1134
+ }),
1135
+ (review === void 0 || draft.custom !== "") && (hasOptions ? (0, react_jsx_runtime.jsxs)("div", {
1136
+ className: clsx(QuestionComposer_module_css_default.customRow, draft.custom !== "" && QuestionComposer_module_css_default.customRowActive),
1137
+ children: [question.multiSelect === true ? (0, react_jsx_runtime.jsx)("span", {
1138
+ className: clsx(QuestionComposer_module_css_default.checkbox, draft.custom !== "" && QuestionComposer_module_css_default.checkboxChecked),
1139
+ "aria-hidden": "true",
1140
+ children: draft.custom !== "" && (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconCheckOutline14, { size: 12 })
1141
+ }) : (0, react_jsx_runtime.jsx)("span", {
1142
+ className: QuestionComposer_module_css_default.number,
1143
+ "aria-hidden": "true",
1144
+ children: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconEditOutline16, { size: 12 })
1145
+ }), (0, react_jsx_runtime.jsx)(AnswerField, {
1146
+ variant: "inline",
1147
+ value: draft.custom,
1148
+ disabled: locked,
1149
+ placeholder: t("custom.placeholder"),
1150
+ onChange: draftCustom,
1151
+ onKeyDown: continueFromCustom
1152
+ })]
1153
+ }) : (0, react_jsx_runtime.jsx)(AnswerField, {
1154
+ autoFocus: canSubmit && countdown === void 0 && !locked && !focusedQuestions.current.has(index),
1155
+ variant: "block",
1156
+ value: draft.custom,
1157
+ disabled: locked,
1158
+ placeholder: t("custom.placeholder"),
1159
+ onFocus: () => {
1160
+ focusedQuestions.current.add(index);
1161
+ },
1162
+ onChange: draftCustom,
1163
+ onKeyDown: continueFromCustom
1164
+ }))
1165
+ ]
1166
+ })]
1167
+ }), (0, react_jsx_runtime.jsxs)("footer", {
1168
+ className: QuestionComposer_module_css_default.footer,
1169
+ children: [
1170
+ (0, react_jsx_runtime.jsxs)("div", {
1171
+ className: QuestionComposer_module_css_default.pager,
1172
+ children: [
1173
+ (0, react_jsx_runtime.jsx)("button", {
1174
+ type: "button",
1175
+ className: QuestionComposer_module_css_default.iconButton,
1176
+ "aria-label": t("nav.prev"),
1177
+ disabled: index === 0 || busy !== null,
1178
+ onClick: () => {
1179
+ replaceProgress(index - 1, drafts);
1180
+ setError(null);
1181
+ },
1182
+ children: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconChevronLeftOutline14, {})
1183
+ }),
1184
+ (0, react_jsx_runtime.jsxs)("span", {
1185
+ className: QuestionComposer_module_css_default.progress,
1186
+ children: [
1187
+ index + 1,
1188
+ " / ",
1189
+ questions.length
1190
+ ]
1191
+ }),
1192
+ (0, react_jsx_runtime.jsx)("button", {
1193
+ type: "button",
1194
+ className: QuestionComposer_module_css_default.iconButton,
1195
+ "aria-label": t("nav.next"),
1196
+ disabled: index === questions.length - 1 || busy !== null,
1197
+ onClick: () => {
1198
+ replaceProgress(index + 1, drafts);
1199
+ setError(null);
1200
+ },
1201
+ children: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconChevronRightOutline14, {})
1202
+ })
1203
+ ]
1204
+ }),
1205
+ (0, react_jsx_runtime.jsx)("div", {
1206
+ className: QuestionComposer_module_css_default.feedback,
1207
+ role: "status",
1208
+ children: error === null ? null : "key" in error ? t(error.key) : error.text
1209
+ }),
1210
+ review === void 0 && (0, react_jsx_runtime.jsxs)("div", {
1211
+ className: QuestionComposer_module_css_default.footerActions,
1212
+ children: [(0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Button, {
1213
+ variant: "outline",
1214
+ disabled: busy !== null,
1215
+ onClick: skipQuestion,
1216
+ children: t("action.skip")
1217
+ }), (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Button, {
1218
+ variant: "primary",
1219
+ disabled: busy !== null || !answered(draft) || index === questions.length - 1 && !canSubmit,
1220
+ onClick: continueFlow,
1221
+ children: busy === "answer" ? t("submitting") : index === questions.length - 1 ? t("submit") : t("action.next")
1222
+ })]
1223
+ })
1224
+ ]
1225
+ })] })]
1226
+ })
1227
+ });
1228
+ }
1229
+ //#endregion
1230
+ //#region ../../core/session/src/surface.ts
1231
+ /** Runtime counterpart of the message-producing event union. */
1232
+ const SURFACE_EVENT_TYPES = new Set([
1233
+ "system/message",
1234
+ "developer/message",
1235
+ "user/message",
1236
+ "assistant/message",
1237
+ "tool/result"
1238
+ ]);
1239
+ /**
1240
+ * Narrow an event to a surface-eligible event carrying its required marker.
1241
+ * @param event - event to test.
1242
+ * @returns true when both the type and marker identify a surface event.
1243
+ */
1244
+ function isSurfaceEvent(event) {
1245
+ if (!SURFACE_EVENT_TYPES.has(event.type)) return false;
1246
+ return event.surfaceOp !== void 0;
1247
+ }
1248
+ /**
1249
+ * Narrow an event to an append-origin surface event: one that entered the
1250
+ * surface at its own log position and was never itself a replacement copy.
1251
+ *
1252
+ * The model-visible surface deliberately shadows replaced ranges, so it is the
1253
+ * wrong source for a human transcript — a landed replacement would erase
1254
+ * conversation the user already saw. Append-origin events are that transcript's
1255
+ * durable source material; replacement copies stay model-only.
1256
+ * @param event - event to test.
1257
+ * @returns true when the event appended to the surface tail.
1258
+ */
1259
+ function isAppendSurfaceEvent(event) {
1260
+ return isSurfaceEvent(event) && event.surfaceOp === "append";
1261
+ }
1262
+ //#endregion
1263
+ //#region lib/types/client/question-reply.js
1264
+ function isRecord(value) {
1265
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1266
+ }
1267
+ /**
1268
+ * Read the question and answer pairs out of the reply text at the conversation boundary.
1269
+ * @param text - Model-facing JSON payload of the steered message.
1270
+ * @returns The pairs, or empty lists when the payload is unreadable.
1271
+ */
1272
+ function replyPairsOf(text) {
1273
+ let value;
1274
+ try {
1275
+ value = JSON.parse(text);
1276
+ } catch (error) {
1277
+ return {
1278
+ questions: [],
1279
+ answers: []
1280
+ };
1281
+ }
1282
+ if (!isRecord(value) || !Array.isArray(value.questions)) return {
1283
+ questions: [],
1284
+ answers: []
1285
+ };
1286
+ const questions = [];
1287
+ for (const question of value.questions) {
1288
+ if (!isRecord(question) || typeof question.id !== "string" || typeof question.question !== "string") continue;
1289
+ questions.push({
1290
+ id: question.id,
1291
+ question: question.question,
1292
+ ...typeof question.detail === "string" ? { detail: question.detail } : {},
1293
+ ...typeof question.header === "string" ? { header: question.header } : {},
1294
+ ...Array.isArray(question.options) ? { options: question.options.flatMap((option) => {
1295
+ if (!isRecord(option) || typeof option.label !== "string") return [];
1296
+ return [{
1297
+ label: option.label,
1298
+ ...typeof option.description === "string" ? { description: option.description } : {}
1299
+ }];
1300
+ }) } : {},
1301
+ ...typeof question.multiSelect === "boolean" ? { multiSelect: question.multiSelect } : {}
1302
+ });
1303
+ }
1304
+ const answers = [];
1305
+ for (const answer of Array.isArray(value.answers) ? value.answers : []) {
1306
+ if (!isRecord(answer) || typeof answer.id !== "string" || !Array.isArray(answer.selected)) continue;
1307
+ answers.push({
1308
+ id: answer.id,
1309
+ selected: answer.selected.filter((item) => typeof item === "string"),
1310
+ ...typeof answer.custom === "string" ? { custom: answer.custom } : {}
1311
+ });
1312
+ }
1313
+ return {
1314
+ questions,
1315
+ answers
1316
+ };
1317
+ }
1318
+ /**
1319
+ * Answer values of one question in display order: the selected option labels
1320
+ * followed by a non-blank custom answer.
1321
+ * @param data - Projected reply holding the recorded answers.
1322
+ * @param id - Question id to read.
1323
+ * @returns The values, empty when the user skipped that question.
1324
+ */
1325
+ function replyAnswerValues(data, id) {
1326
+ const answer = data.answers.find((item) => item.id === id);
1327
+ const custom = answer?.custom?.trim() ?? "";
1328
+ return [...answer?.selected ?? [], ...custom === "" ? [] : [custom]];
1329
+ }
1330
+ /**
1331
+ * Clipboard text of one late reply: every question with the answer the user
1332
+ * gave, in the layout the open bubble shows, without the options nobody chose.
1333
+ * The text does not depend on whether the bubble is open.
1334
+ * @param data - Projected reply to copy.
1335
+ * @param t - Bound `question` namespace translator owning the answer labels.
1336
+ * @returns One block per question, or the model-facing text when the payload was unreadable.
1337
+ */
1338
+ function replyClipboardText(data, t) {
1339
+ if (data.questions.length === 0) return data.text;
1340
+ return data.questions.map((question) => {
1341
+ const values = replyAnswerValues(data, question.id);
1342
+ return `${question.header && question.header !== question.question ? `${question.header} — ${question.question}` : question.question}\n${values.length === 0 ? t("reply.skipped") : `${t("reply.answerLabel")}${values.join(", ")}`}`;
1343
+ }).join("\n\n");
1344
+ }
1345
+ /** Validate a late reply source at the persisted conversation boundary. */
1346
+ function replySource(event) {
1347
+ const source = event.data.source;
1348
+ return isRecord(source) && source.kind === "user-question-reply" && typeof source.callId === "string" && source.outcome === "answered" ? {
1349
+ callId: source.callId,
1350
+ outcome: "answered"
1351
+ } : null;
1352
+ }
1353
+ /** Late-reply projection owned by the questions UI. */
1354
+ const questionReplyDefinition = {
1355
+ kind: "user-question-reply",
1356
+ target: "chat",
1357
+ match: (event) => {
1358
+ if (event.type !== "user/message" || !isAppendSurfaceEvent(event)) return null;
1359
+ return replySource(event) === null ? null : {
1360
+ id: String(event.data.id),
1361
+ role: "start"
1362
+ };
1363
+ },
1364
+ start: (_context, match) => {
1365
+ if (match.event.type !== "user/message") throw new Error("user-question-reply start requires user/message");
1366
+ const event = match.event;
1367
+ const text = event.data.content.find((item) => item.type === "text")?.text ?? "";
1368
+ const source = replySource(event);
1369
+ if (source === null) throw new Error("user-question-reply start requires a reply source");
1370
+ return {
1371
+ seq: event.seq,
1372
+ time: event.time,
1373
+ callId: source.callId,
1374
+ outcome: source.outcome,
1375
+ text,
1376
+ ...replyPairsOf(text)
1377
+ };
1378
+ },
1379
+ update: (context) => context.state,
1380
+ buildViewNode: (context) => {
1381
+ if (context.state === void 0) return null;
1382
+ const { seq, ...data } = context.state;
1383
+ return {
1384
+ key: context.key,
1385
+ kind: "question-reply",
1386
+ id: context.id,
1387
+ target: "chat",
1388
+ anchorSeq: seq,
1389
+ location: context.start?.location ?? { kind: "unresolved" },
1390
+ visibility: "visible",
1391
+ data
1392
+ };
1393
+ }
1394
+ };
1395
+ //#endregion
1396
+ //#region \0qilin-css:/home/runner/work/QiLin/QiLin/packages/client/ui-user-questions/src/client/QuestionReplyView.module.css.mjs
1397
+ const css = ".wgh9Da_row{flex-direction:column;justify-content:flex-end;align-items:flex-end;gap:6px;padding:4px 0;display:flex}.wgh9Da_bubble{border-radius:var(--qilin-radius-xl);background:var(--qilin-specific-bubble);max-width:72%;color:var(--qilin-alias-label-primary);cursor:pointer;font-size:var(--dsh-content-font-size,14px);line-height:calc(22px + var(--dsh-content-font-delta,0px));text-align:left;overflow-wrap:anywhere;white-space:pre-wrap;padding:10px 16px}.wgh9Da_toggle{width:100%;color:inherit;cursor:pointer;font:inherit;text-align:left;background:0 0;border:0;padding:0}.wgh9Da_toggle:focus-visible{outline:2px solid var(--qilin-focus-ring-color,var(--qilin-alias-state-business-primary));outline-offset:2px}.wgh9Da_bubble:has(.wgh9Da_toggle:focus-visible){box-shadow:0 0 0 2px var(--qilin-focus-ring-color,var(--qilin-alias-state-business-primary))}.wgh9Da_labelRow{align-items:center;gap:3px;display:flex}.wgh9Da_caret{color:var(--qilin-alias-label-secondary);display:inline-flex}.wgh9Da_caret svg{width:12px;height:12px}.wgh9Da_label{color:var(--qilin-alias-label-secondary);font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(18px + var(--dsh-content-font-delta-secondary,0px));display:block}.wgh9Da_summary{color:var(--qilin-alias-label-primary);font-size:var(--dsh-content-font-size,14px);margin-top:3px;display:block}.wgh9Da_text{margin:8px 0 0}.wgh9Da_details{border-top:1px solid color-mix(in srgb, var(--qilin-alias-label-secondary) 24%, transparent);margin:8px 0 0;padding-top:8px}.wgh9Da_details>div+div{margin-top:12px}.wgh9Da_details dt{color:var(--qilin-alias-label-secondary);font-size:12px}.wgh9Da_question{gap:3px;display:grid}.wgh9Da_header,.wgh9Da_detail,.wgh9Da_options{color:var(--qilin-alias-label-secondary);font-size:11px}.wgh9Da_questionText{color:var(--qilin-alias-label-primary)}.wgh9Da_options{margin:0;padding-left:16px}.wgh9Da_details dd{margin:4px 0 0}.wgh9Da_answer{font-size:14px}.wgh9Da_answerLabel{color:var(--qilin-alias-label-secondary);margin-right:4px;font-size:12px}.wgh9Da_actions{height:28px;color:var(--qilin-alias-label-tertiary);align-items:center;gap:8px;padding-right:4px;display:flex}.wgh9Da_time{white-space:nowrap;padding-right:4px;font-size:13px;line-height:24px}.wgh9Da_action{border-radius:var(--qilin-radius-sm);width:28px;height:28px;color:inherit;cursor:pointer;background:0 0;border:none;justify-content:center;align-items:center;padding:6px;display:inline-flex}.wgh9Da_action svg{width:15px;height:15px}.wgh9Da_action:hover{background:var(--qilin-alias-interactive-bg-hover);color:var(--qilin-alias-label-secondary)}";
1398
+ const tagId = "@qilin-agent/client-ui-user-questions/QuestionReplyView.module.css";
1399
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
1400
+ const tag = document.createElement("style");
1401
+ tag.dataset.plugin = "@qilin-agent/client-ui-user-questions";
1402
+ tag.dataset.pluginCss = tagId;
1403
+ tag.textContent = css;
1404
+ document.head.appendChild(tag);
1405
+ }
1406
+ var QuestionReplyView_module_css_default = {
1407
+ "action": "wgh9Da_action",
1408
+ "actions": "wgh9Da_actions",
1409
+ "answer": "wgh9Da_answer",
1410
+ "answerLabel": "wgh9Da_answerLabel",
1411
+ "bubble": "wgh9Da_bubble",
1412
+ "caret": "wgh9Da_caret",
1413
+ "detail": "wgh9Da_detail",
1414
+ "details": "wgh9Da_details",
1415
+ "header": "wgh9Da_header",
1416
+ "label": "wgh9Da_label",
1417
+ "labelRow": "wgh9Da_labelRow",
1418
+ "options": "wgh9Da_options",
1419
+ "question": "wgh9Da_question",
1420
+ "questionText": "wgh9Da_questionText",
1421
+ "row": "wgh9Da_row",
1422
+ "summary": "wgh9Da_summary",
1423
+ "text": "wgh9Da_text",
1424
+ "time": "wgh9Da_time",
1425
+ "toggle": "wgh9Da_toggle"
1426
+ };
1427
+ //#endregion
1428
+ //#region lib/types/client/QuestionReplyView.js
1429
+ /**
1430
+ * Right-aligned late-reply bubble: a label naming the earlier pending
1431
+ * questions, then one question and answer pair per question. A payload the
1432
+ * Client cannot read falls back to the model-facing text.
1433
+ */
1434
+ const QuestionReplyView = (0, react.memo)(function QuestionReplyView({ node, t }) {
1435
+ return (0, react_jsx_runtime.jsx)(QuestionReplyBubble, {
1436
+ data: node.data,
1437
+ t
1438
+ });
1439
+ });
1440
+ /**
1441
+ * Render one settled question reply as a compact transcript bubble.
1442
+ * @param props - Reply data and the question locale translator.
1443
+ * @returns The expandable reply bubble.
1444
+ */
1445
+ function QuestionReplyBubble({ data, t }) {
1446
+ const label = t("reply.label");
1447
+ const [open, setOpen] = (0, react.useState)(false);
1448
+ const toggleLabel = t(open ? "reply.close" : "reply.open");
1449
+ const summary = data.questions.map((question) => replyAnswerValues(data, question.id)).flat().join(", ");
1450
+ const copyText = replyClipboardText(data, t);
1451
+ const [copied, setCopied] = (0, react.useState)(false);
1452
+ const onCopy = (0, react.useCallback)(() => {
1453
+ if (copied) return;
1454
+ (0, _qilin_agent_client_ui_primitives.writeClipboard)(copyText).then((ok) => {
1455
+ if (!ok) return;
1456
+ setCopied(true);
1457
+ window.setTimeout(() => {
1458
+ setCopied(false);
1459
+ }, 1e3);
1460
+ });
1461
+ }, [copied, copyText]);
1462
+ return (0, react_jsx_runtime.jsxs)("div", {
1463
+ className: QuestionReplyView_module_css_default.row,
1464
+ "data-question-reply": data.callId,
1465
+ "data-reply-outcome": data.outcome,
1466
+ role: "group",
1467
+ "aria-label": label,
1468
+ children: [(0, react_jsx_runtime.jsxs)("div", {
1469
+ className: QuestionReplyView_module_css_default.bubble,
1470
+ children: [(0, react_jsx_runtime.jsxs)("button", {
1471
+ type: "button",
1472
+ className: QuestionReplyView_module_css_default.toggle,
1473
+ "aria-expanded": open,
1474
+ "aria-label": `${label} · ${toggleLabel}`,
1475
+ onClick: () => {
1476
+ setOpen((value) => !value);
1477
+ },
1478
+ children: [(0, react_jsx_runtime.jsxs)("span", {
1479
+ className: QuestionReplyView_module_css_default.labelRow,
1480
+ children: [(0, react_jsx_runtime.jsx)("span", {
1481
+ className: QuestionReplyView_module_css_default.caret,
1482
+ "aria-hidden": "true",
1483
+ children: open ? (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconChevronDownOutline14, {}) : (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconChevronRightOutline14, {})
1484
+ }), (0, react_jsx_runtime.jsx)("span", {
1485
+ className: QuestionReplyView_module_css_default.label,
1486
+ children: label
1487
+ })]
1488
+ }), !open && (0, react_jsx_runtime.jsx)("span", {
1489
+ className: QuestionReplyView_module_css_default.summary,
1490
+ children: summary === "" ? t("reply.skipped") : summary
1491
+ })]
1492
+ }), open && (data.questions.length === 0 ? (0, react_jsx_runtime.jsx)("p", {
1493
+ className: QuestionReplyView_module_css_default.text,
1494
+ children: data.text
1495
+ }) : (0, react_jsx_runtime.jsx)("dl", {
1496
+ className: QuestionReplyView_module_css_default.details,
1497
+ children: data.questions.map((question) => {
1498
+ const values = replyAnswerValues(data, question.id);
1499
+ return (0, react_jsx_runtime.jsxs)("div", { children: [(0, react_jsx_runtime.jsxs)("dt", {
1500
+ className: QuestionReplyView_module_css_default.question,
1501
+ children: [
1502
+ question.header && question.header !== question.question && (0, react_jsx_runtime.jsx)("span", {
1503
+ className: QuestionReplyView_module_css_default.header,
1504
+ children: question.header
1505
+ }),
1506
+ (0, react_jsx_runtime.jsx)("span", {
1507
+ className: QuestionReplyView_module_css_default.questionText,
1508
+ children: question.question
1509
+ }),
1510
+ question.detail && (0, react_jsx_runtime.jsx)("span", {
1511
+ className: QuestionReplyView_module_css_default.detail,
1512
+ children: question.detail
1513
+ }),
1514
+ question.options && question.options.length > 0 && (0, react_jsx_runtime.jsx)("ul", {
1515
+ className: QuestionReplyView_module_css_default.options,
1516
+ children: question.options.map((option) => (0, react_jsx_runtime.jsxs)("li", { children: [option.label, option.description && ` — ${option.description}`] }, option.label))
1517
+ })
1518
+ ]
1519
+ }), (0, react_jsx_runtime.jsxs)("dd", {
1520
+ className: QuestionReplyView_module_css_default.answer,
1521
+ children: [(0, react_jsx_runtime.jsx)("span", {
1522
+ className: QuestionReplyView_module_css_default.answerLabel,
1523
+ children: t("reply.answerLabel")
1524
+ }), values.length === 0 ? t("reply.skipped") : values.join(", ")]
1525
+ })] }, question.id);
1526
+ })
1527
+ }))]
1528
+ }), (0, react_jsx_runtime.jsxs)("div", {
1529
+ className: QuestionReplyView_module_css_default.actions,
1530
+ children: [(0, react_jsx_runtime.jsx)("span", {
1531
+ className: QuestionReplyView_module_css_default.time,
1532
+ children: new Intl.DateTimeFormat(void 0, {
1533
+ hour: "2-digit",
1534
+ minute: "2-digit",
1535
+ hour12: false
1536
+ }).format(data.time)
1537
+ }), (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Tooltip, {
1538
+ label: copied ? t("copied") : t("copy"),
1539
+ side: "bottom",
1540
+ children: (0, react_jsx_runtime.jsx)("button", {
1541
+ type: "button",
1542
+ className: QuestionReplyView_module_css_default.action,
1543
+ "aria-label": copied ? t("copied") : t("copy"),
1544
+ onClick: onCopy,
1545
+ children: copied ? (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconCheckOutline14, {}) : (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconCopyOutline16, {})
1546
+ })
1547
+ })]
1548
+ })]
1549
+ });
1550
+ }
1551
+ //#endregion
1552
+ //#region lib/types/client/locales.js
1553
+ /** `question` namespace dictionaries. */
1554
+ /** Simplified Chinese dictionary (the key-set source of truth). */
1555
+ const zh = {
1556
+ "error.incomplete": "请先完成这道问题。",
1557
+ "error.unanswered": "请选择一个选项或填写自定义答案。",
1558
+ "error.unavailable": "当前无法提交,请稍候再试。",
1559
+ "error.resubmit": "回答未送达,工作已继续,请再提交一次。",
1560
+ "status.sent": "回答已发送;面板未能关闭。",
1561
+ "wait.takeTime": "慢慢回答",
1562
+ "wait.countdown": "{seconds} 秒后继续工作",
1563
+ "wait.paused": "已暂停 · 剩余 {seconds} 秒",
1564
+ "wait.held": "会一直等你回答",
1565
+ "wait.continued": "已继续工作,仍可回答",
1566
+ "review.status": "已回答",
1567
+ "review.skipped": "这道问题当时被跳过。",
1568
+ "reply.label": "回答先前等待中的问题",
1569
+ "reply.open": "展开问题详情",
1570
+ "reply.close": "收起问题详情",
1571
+ "reply.answerLabel": "回答:",
1572
+ "reply.skipped": "已跳过",
1573
+ "nav.prev": "上一题",
1574
+ "nav.next": "下一题",
1575
+ "nav.minimize": "收起问题卡片",
1576
+ "nav.maximize": "展开问题卡片",
1577
+ "nav.cancel": "放弃整组问题",
1578
+ "nav.close": "收起问题面板,可从工具调用重新打开",
1579
+ "option.recommended": "推荐",
1580
+ "custom.placeholder": "输入你的答案",
1581
+ "action.skip": "跳过",
1582
+ "action.next": "下一题",
1583
+ "plan.header": "计划待审",
1584
+ "plan.approve": "同意执行",
1585
+ "plan.decline": "拒绝",
1586
+ "plan.discuss": "要求修改"
1587
+ };
1588
+ /** English dictionary, checked complete against the zh key set. */
1589
+ const en = {
1590
+ "error.incomplete": "Please complete this question first.",
1591
+ "error.unanswered": "Please select an option or enter a custom answer.",
1592
+ "error.unavailable": "Cannot submit right now; try again in a moment.",
1593
+ "error.resubmit": "The answer did not arrive before work continued; submit it again.",
1594
+ "status.sent": "Reply sent; the panel could not close.",
1595
+ "wait.takeTime": "Take time",
1596
+ "wait.countdown": "Continuing in {seconds}s",
1597
+ "wait.paused": "Paused · {seconds}s remaining",
1598
+ "wait.held": "Waiting until you answer",
1599
+ "wait.continued": "Work continued — you can still answer",
1600
+ "review.status": "Answered",
1601
+ "review.skipped": "This question was skipped.",
1602
+ "reply.label": "Reply to earlier pending questions",
1603
+ "reply.open": "Open question details",
1604
+ "reply.close": "Close question details",
1605
+ "reply.answerLabel": "Answer: ",
1606
+ "reply.skipped": "Skipped",
1607
+ "nav.prev": "Previous question",
1608
+ "nav.next": "Next question",
1609
+ "nav.minimize": "Collapse the question card",
1610
+ "nav.maximize": "Expand the question card",
1611
+ "nav.cancel": "Dismiss all questions",
1612
+ "nav.close": "Close the panel — reopen it from the tool call",
1613
+ "option.recommended": "Recommended",
1614
+ "custom.placeholder": "Type your answer",
1615
+ "action.skip": "Skip",
1616
+ "action.next": "Next",
1617
+ "plan.header": "Plan review",
1618
+ "plan.approve": "Approve",
1619
+ "plan.decline": "Refuse",
1620
+ "plan.discuss": "Request changes"
1621
+ };
1622
+ //#endregion
1623
+ //#region lib/types/client/index.js
1624
+ /** Dictionary namespace owned by this plugin. */
1625
+ const NS = "question";
1626
+ /** Required services: Agent scopes, Remote Events, Session UI, Slot registry, conversation nodes, and copy. */
1627
+ const inject = [
1628
+ "sessions",
1629
+ "remote",
1630
+ "remote.userQuestions",
1631
+ "uiSession",
1632
+ "slots",
1633
+ "locale",
1634
+ "uiConversation"
1635
+ ];
1636
+ /**
1637
+ * Cards published to the Session pending-interaction registry, keyed by
1638
+ * `PendingQuestion.key`. A tool-call-keyed card is shared by the forwarded
1639
+ * waterfall and the Session projection.
1640
+ *
1641
+ * A card outlives its seat. Closing the panel only unpublishes it, which keeps
1642
+ * the request answerable from its tool call row, so this registry — not the
1643
+ * pending-interaction registry — decides when a request is over.
1644
+ */
1645
+ var QuestionCards = class {
1646
+ publish;
1647
+ #cards = /* @__PURE__ */ new Map();
1648
+ constructor(publish) {
1649
+ this.publish = publish;
1650
+ }
1651
+ byCallId(sessionId, callId) {
1652
+ return this.#cards.get(PendingQuestion.keyOf(sessionId, callId));
1653
+ }
1654
+ /** Observable state for one card while the registry owns it. */
1655
+ source(key) {
1656
+ return this.#cards.get(key)?.pending;
1657
+ }
1658
+ values() {
1659
+ return [...this.#cards.values()];
1660
+ }
1661
+ /** Keys of every card registered for one Session. */
1662
+ keysFor(sessionId) {
1663
+ return this.values().filter((card) => card.pending.sessionId === sessionId).map((card) => card.pending.key);
1664
+ }
1665
+ /**
1666
+ * Return the card of a tool call, creating and publishing it when absent.
1667
+ * A request without a call id always gets a fresh card.
1668
+ */
1669
+ ensure(sessionId, questions, callId) {
1670
+ if (callId !== void 0) {
1671
+ const existing = this.byCallId(sessionId, callId);
1672
+ if (existing !== void 0) return existing;
1673
+ }
1674
+ return this.#create(new PendingQuestion(sessionId, questions, callId, () => this.keysFor(sessionId)));
1675
+ }
1676
+ /** Publish one carrier into the composer seat and register the card that owns it. */
1677
+ #create(pending) {
1678
+ const requests = /* @__PURE__ */ new Set();
1679
+ const delegate = async () => {
1680
+ pending.delegate();
1681
+ await Promise.all(requests);
1682
+ };
1683
+ let unpublish = this.publish(pending, delegate);
1684
+ const card = {
1685
+ pending,
1686
+ hide: () => {
1687
+ unpublish?.();
1688
+ unpublish = void 0;
1689
+ },
1690
+ reveal: () => {
1691
+ unpublish?.();
1692
+ unpublish = this.publish(pending, delegate);
1693
+ },
1694
+ remove: () => {
1695
+ /* v8 ignore next -- a card leaves the registry once; no caller holds a card the registry already replaced. */
1696
+ if (this.#cards.get(pending.key) !== card) return;
1697
+ this.#cards.delete(pending.key);
1698
+ pending.close();
1699
+ unpublish?.();
1700
+ unpublish = void 0;
1701
+ },
1702
+ trackRequest: (completion) => {
1703
+ requests.add(completion);
1704
+ return () => {
1705
+ requests.delete(completion);
1706
+ };
1707
+ },
1708
+ hasRequest: () => requests.size > 0
1709
+ };
1710
+ pending.attachSeat({ hide: pending.review === void 0 ? card.hide : card.remove });
1711
+ this.#cards.set(pending.key, card);
1712
+ return card;
1713
+ }
1714
+ /**
1715
+ * Show the panel of one answerable tool call.
1716
+ * @param sessionId - Session the tool call belongs to.
1717
+ * @param callId - `ask_user_question` call whose panel to show, as its
1718
+ * transcript row spells it.
1719
+ * @returns whether a card for that call is still answerable.
1720
+ */
1721
+ reveal(sessionId, callId) {
1722
+ const card = this.byCallId(sessionId, callId);
1723
+ if (card === void 0) return false;
1724
+ card.reveal();
1725
+ return true;
1726
+ }
1727
+ /**
1728
+ * Show one settled tool call's recorded answers as a read-only card. A call
1729
+ * that somehow still holds a card is shown as it stands, so a live request is
1730
+ * never replaced by a stale copy of itself.
1731
+ * @param sessionId - Session the tool call belongs to.
1732
+ * @param callId - `ask_user_question` call whose record to show, as its
1733
+ * transcript row spells it.
1734
+ * @param record - the call's questions and recorded answers.
1735
+ * @returns true; a record always produces a card.
1736
+ */
1737
+ review(sessionId, callId, record) {
1738
+ (this.byCallId(sessionId, callId) ?? this.#create(new PendingQuestion(sessionId, record.questions, brandString(callId), () => this.keysFor(sessionId), record.answers))).reveal();
1739
+ return true;
1740
+ }
1741
+ /**
1742
+ * Hand every live waterfall back at plugin teardown. The pending-interaction
1743
+ * registry only drains what it currently holds, and a hidden card is not in
1744
+ * it, so its Host request would wait for its own abort instead.
1745
+ */
1746
+ dispose() {
1747
+ for (const card of this.values()) {
1748
+ card.pending.delegate();
1749
+ card.remove();
1750
+ }
1751
+ }
1752
+ };
1753
+ /** Present one forwarded request through its card until the waterfall settles. */
1754
+ async function answerQuestion(ctx, owner, request, next, cards) {
1755
+ const sessionId = ctx.sessions.scopeOf(owner);
1756
+ if (sessionId === void 0) return next();
1757
+ const callId = request.wait?.callId;
1758
+ const card = cards.ensure(sessionId, request.questions, callId);
1759
+ const claimLifetime = new AbortController();
1760
+ const claimSignal = request.signal === void 0 ? claimLifetime.signal : AbortSignal.any([claimLifetime.signal, request.signal]);
1761
+ let claim;
1762
+ let claimEnded;
1763
+ let delegateRequest;
1764
+ const releaseClaim = request.wait?.timed === true && callId !== void 0 ? ctx.effect(() => {
1765
+ claim = ctx.remote.userQuestions.attachWait(sessionId, callId, claimSignal);
1766
+ return async () => {
1767
+ delegateRequest?.();
1768
+ claimLifetime.abort();
1769
+ claim?.dispose();
1770
+ if (claimEnded !== void 0) await Promise.allSettled([claimEnded]);
1771
+ };
1772
+ }, "ui-user-questions: foreground claim") : void 0;
1773
+ const completed = Promise.withResolvers();
1774
+ const finishRequest = card.trackRequest(completed.promise);
1775
+ try {
1776
+ const iterator = claim?.[Symbol.asyncIterator]();
1777
+ const opening = iterator === void 0 ? void 0 : await iterator.next();
1778
+ if (opening?.done === true) return await next();
1779
+ const waterfall = createWaterfallRequest(opening === void 0 ? void 0 : Date.now() + opening.value.remainingMs, claimSignal, (channel) => {
1780
+ card.pending.detachWaterfall(channel);
1781
+ });
1782
+ if (claimSignal.aborted) return await waterfall.result;
1783
+ delegateRequest = () => {
1784
+ waterfall.channel.delegate();
1785
+ };
1786
+ card.pending.attachWaterfall(waterfall.channel);
1787
+ if (iterator !== void 0) claimEnded = (async () => {
1788
+ await iterator.next();
1789
+ throw new Error("the foreground question wait ended");
1790
+ })();
1791
+ try {
1792
+ return await (claimEnded === void 0 ? waterfall.result : Promise.race([waterfall.result, claimEnded]));
1793
+ } catch (error) {
1794
+ if (waterfall.isDelegation(error)) {
1795
+ await releaseClaim?.();
1796
+ return await next();
1797
+ }
1798
+ throw error;
1799
+ }
1800
+ } finally {
1801
+ if (releaseClaim === void 0) claimLifetime.abort();
1802
+ else if (claimEnded === void 0) await releaseClaim();
1803
+ else Promise.allSettled([claimEnded]).then(() => {
1804
+ releaseClaim();
1805
+ });
1806
+ if (callId === void 0) card.remove();
1807
+ completed.resolve();
1808
+ finishRequest();
1809
+ }
1810
+ }
1811
+ /**
1812
+ * Mirror answerable calls onto cards. A submitted reply in the durable Inbox
1813
+ * removes the editable card until the reply is admitted or discarded.
1814
+ */
1815
+ function publishContinuedQuestions(ctx, cards) {
1816
+ const sessions = ctx.sessions;
1817
+ const stopProjections = /* @__PURE__ */ new Map();
1818
+ const unwrap = (result) => {
1819
+ if (!result.ok) throw new Error(result.error.message);
1820
+ return result.value;
1821
+ };
1822
+ const rpcFor = (sessionId, callId) => ({ answer: async (answer) => unwrap(await ctx.remote.userQuestions.answer(sessionId, callId, answer)) });
1823
+ const reconcile = () => {
1824
+ const snapshot = sessions.list.getSnapshot();
1825
+ const bound = new Map(Object.values(snapshot.byId).flatMap((summary) => {
1826
+ const binding = sessions.binding(summary.id);
1827
+ return binding === void 0 ? [] : [[summary.id, binding]];
1828
+ }));
1829
+ for (const [sessionId, stop] of stopProjections) {
1830
+ if (bound.has(sessionId)) continue;
1831
+ stop();
1832
+ stopProjections.delete(sessionId);
1833
+ }
1834
+ for (const [sessionId, binding] of bound) {
1835
+ if (stopProjections.has(sessionId)) continue;
1836
+ const stopQuestions = binding.session.projections.faceOf("userQuestions").subscribe(reconcile);
1837
+ const stopInbox = binding.session.projections.faceOf("inbox").subscribe(reconcile);
1838
+ stopProjections.set(sessionId, () => {
1839
+ stopQuestions();
1840
+ stopInbox();
1841
+ });
1842
+ }
1843
+ const rows = /* @__PURE__ */ new Map();
1844
+ for (const [sessionId, binding] of bound) {
1845
+ const projected = binding.session.projections.faceOf("userQuestions").getSnapshot();
1846
+ const inbox = binding.session.projections.faceOf("inbox").getSnapshot();
1847
+ const queued = /* @__PURE__ */ new Set();
1848
+ for (const message of [...inbox?.["next-step"] ?? [], ...inbox?.["next-turn"] ?? []]) {
1849
+ if (typeof message !== "object" || message === null || Array.isArray(message)) continue;
1850
+ const source = message.source;
1851
+ if (typeof source !== "object" || source === null || Array.isArray(source)) continue;
1852
+ if (source.kind === "user-question-reply" && typeof source.callId === "string") queued.add(source.callId);
1853
+ }
1854
+ for (const row of projected?.active ?? []) {
1855
+ if (row.state === "continued" && queued.has(row.callId)) continue;
1856
+ rows.set(PendingQuestion.keyOf(sessionId, row.callId), {
1857
+ sessionId,
1858
+ row
1859
+ });
1860
+ }
1861
+ }
1862
+ for (const { sessionId, row } of rows.values()) {
1863
+ if (row.state === "continued") {
1864
+ const card = cards.ensure(sessionId, row.questions, row.callId);
1865
+ card.pending.attachRpc(rpcFor(sessionId, row.callId));
1866
+ card.pending.setState("continued");
1867
+ continue;
1868
+ }
1869
+ cards.byCallId(sessionId, row.callId)?.pending.setState("open");
1870
+ }
1871
+ for (const card of cards.values()) {
1872
+ if (card.pending.callId === void 0 || card.pending.review !== void 0 || rows.has(card.pending.key) || card.hasRequest() || card.pending.hasWaterfall()) continue;
1873
+ card.remove();
1874
+ }
1875
+ };
1876
+ reconcile();
1877
+ const stopList = sessions.list.subscribe(reconcile);
1878
+ return () => {
1879
+ stopList();
1880
+ for (const stop of stopProjections.values()) stop();
1881
+ stopProjections.clear();
1882
+ };
1883
+ }
1884
+ /**
1885
+ * Client plugin body: register the `question` dictionaries, the question
1886
+ * composer into the composer chain, and the late-reply conversation node.
1887
+ * Zero business face — data and verbs live on the matched carrier; t rides
1888
+ * the standard locale seat.
1889
+ * @param ctx - client root context.
1890
+ */
1891
+ function apply(ctx) {
1892
+ ctx.effect(() => ctx.locale.register(NS, {
1893
+ zh,
1894
+ en
1895
+ }), "ui-user-questions: dictionaries");
1896
+ const questionDraftStore = createQuestionDraftStore();
1897
+ const cards = new QuestionCards(ctx.uiSession.registerPendingInteraction((pending) => pending.kind === "plan-review" ? 2 : 1));
1898
+ ctx.effect(() => () => {
1899
+ cards.dispose();
1900
+ }, "ui-user-questions: cards");
1901
+ const disposePanels = ctx.reflect.provide("userQuestionPanels", {
1902
+ reveal: (sessionId, callId) => cards.reveal(sessionId, callId),
1903
+ review: (sessionId, callId, record) => cards.review(sessionId, callId, record)
1904
+ });
1905
+ ctx.effect(() => disposePanels, "ui-user-questions: answer panels");
1906
+ ctx.effect(() => publishContinuedQuestions(ctx, cards), "ui-user-questions: continued questions");
1907
+ ctx.slots.inject("conversation.composer", () => ctx.slots.register({
1908
+ name: "conversation.composer",
1909
+ select: ({ pendingInteraction }) => pendingInteraction instanceof PendingQuestion ? pendingInteraction : null,
1910
+ locale: NS,
1911
+ store: questionDraftStore,
1912
+ inject: () => ({ keyedHooks: { questionCard: (key) => cards.source(key) } }),
1913
+ children: { "conversation.plan-review.actions": {
1914
+ kind: "list",
1915
+ scope: "session"
1916
+ } }
1917
+ }, QuestionComposer));
1918
+ ctx.uiConversation.events.register(questionReplyDefinition);
1919
+ ctx.slots.inject("conversation.chat.node", () => ctx.slots.register({
1920
+ name: "conversation.chat.node",
1921
+ key: "question-reply",
1922
+ locale: NS
1923
+ }, QuestionReplyView));
1924
+ ctx.remote.$on("user-questions/request", function(request, next) {
1925
+ return answerQuestion(ctx, this, request, next, cards);
1926
+ });
1927
+ }
1928
+ //#endregion
1929
+ exports.apply = apply;
1930
+ exports.inject = inject;
1931
+ return module.exports;
1932
+ }
1933
+ });
1934
+
1935
+ //# sourceMappingURL=client.js.map