@agimon-ai/po-user-feedback 0.0.0-alpha.0

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.
@@ -0,0 +1,619 @@
1
+ import { defineToolRenderer, defineWebPlugin } from "@agimon-ai/po-core/web";
2
+ import { Badge, Button, Kbd, Markdown, MessageItem, MessageItemBody, MessageItemHeader, MessageItemStatus, OptionLabel, OptionRow, SectionLabel, Separator, Textarea, toolTone } from "@agimon-ai/po-web-components";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ import { useEffect, useMemo, useRef, useState } from "react";
5
+ //#region src/extensions/workspaces/sessions/(frontend)/tool/_lib/askUserText.ts
6
+ function isRecord$1(value) {
7
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8
+ }
9
+ function asString$1(value) {
10
+ return typeof value === "string" ? value : "";
11
+ }
12
+ /** The call header: how many questions, and their headers in parentheses. */
13
+ function askCallSummary(args) {
14
+ const questions = Array.isArray(args.questions) ? args.questions.filter(isRecord$1) : [];
15
+ const headers = questions.map((question) => asString$1(question.header)).filter(Boolean);
16
+ return {
17
+ count: `${questions.length} question${questions.length === 1 ? "" : "s"}`,
18
+ headers: headers.join(", ")
19
+ };
20
+ }
21
+ /**
22
+ * The result body the TUI's renderResult chooses: a cancelled glyph, the
23
+ * voice prompt when the questionnaire was handed to voice, the answered
24
+ * list, or the raw text when the tool attached no details (an error path).
25
+ */
26
+ function askResultView(details, output) {
27
+ if (!isRecord$1(details) || !Array.isArray(details.answers)) return {
28
+ kind: "text",
29
+ text: output
30
+ };
31
+ if (details.cancelled === true) return { kind: "cancelled" };
32
+ if (details.delivery === "voice" && typeof details.voicePrompt === "string") return {
33
+ kind: "voice",
34
+ prompt: details.voicePrompt
35
+ };
36
+ return {
37
+ kind: "answers",
38
+ answers: details.answers.filter(isRecord$1).map((answer) => ({
39
+ question: asString$1(answer.question),
40
+ answer: answer.kind === "multi" ? (Array.isArray(answer.selected) ? answer.selected : []).filter((v) => typeof v === "string").join(", ") : asString$1(answer.answer)
41
+ }))
42
+ };
43
+ }
44
+ //#endregion
45
+ //#region src/extensions/workspaces/sessions/(frontend)/tool/_components/AskUserQuestionToolMessage.tsx
46
+ /** The answered list, or the outcome that stood in for it. */
47
+ function AskUserQuestionBody({ result, output, running, isError }) {
48
+ if (running) return /* @__PURE__ */ jsx(MessageItemBody, {
49
+ "data-testid": "tool-result-ask_user_question",
50
+ children: /* @__PURE__ */ jsx(MessageItemStatus, {
51
+ tone: "running",
52
+ children: "answer below to continue"
53
+ })
54
+ });
55
+ const view = askResultView(result?.details, output);
56
+ if (view.kind === "cancelled") return /* @__PURE__ */ jsx(MessageItemBody, {
57
+ "data-testid": "tool-result-ask_user_question",
58
+ children: /* @__PURE__ */ jsx(MessageItemStatus, {
59
+ tone: "running",
60
+ children: "cancelled"
61
+ })
62
+ });
63
+ if (view.kind === "voice") return /* @__PURE__ */ jsx(MessageItemBody, {
64
+ "data-testid": "tool-result-ask_user_question",
65
+ children: /* @__PURE__ */ jsx("pre", {
66
+ className: "whitespace-pre-wrap break-words text-doom-dim",
67
+ children: view.prompt
68
+ })
69
+ });
70
+ if (view.kind === "text") return /* @__PURE__ */ jsx(MessageItemBody, {
71
+ "data-testid": "tool-result-ask_user_question",
72
+ children: view.text ? /* @__PURE__ */ jsx("pre", {
73
+ className: `whitespace-pre-wrap break-words ${isError ? "text-doom-red" : "text-doom-dim"}`,
74
+ children: view.text
75
+ }) : /* @__PURE__ */ jsx(MessageItemStatus, {
76
+ tone: isError ? "error" : "ok",
77
+ children: isError ? "failed" : "submitted"
78
+ })
79
+ });
80
+ return /* @__PURE__ */ jsx(MessageItemBody, {
81
+ "data-testid": "tool-result-ask_user_question",
82
+ children: view.answers.length === 0 ? /* @__PURE__ */ jsx(MessageItemStatus, {
83
+ tone: "ok",
84
+ children: "submitted"
85
+ }) : /* @__PURE__ */ jsx("ul", {
86
+ className: "flex flex-col gap-1",
87
+ children: view.answers.map((answer, index) => /* @__PURE__ */ jsxs("li", {
88
+ className: "break-words",
89
+ children: [
90
+ /* @__PURE__ */ jsx("span", {
91
+ className: "text-doom-green",
92
+ children: "✓"
93
+ }),
94
+ " ",
95
+ /* @__PURE__ */ jsx("span", {
96
+ className: "text-doom-cyan",
97
+ children: answer.question
98
+ }),
99
+ ":",
100
+ " ",
101
+ /* @__PURE__ */ jsx("span", {
102
+ className: "text-doom-dim",
103
+ children: answer.answer
104
+ })
105
+ ]
106
+ }, `${String(index)}-${answer.question}`))
107
+ })
108
+ });
109
+ }
110
+ /**
111
+ * The ask_user_question tool's timeline item: "N question(s) (headers)" in
112
+ * the header, the web half of the TUI renderCall, and the answered list or
113
+ * the outcome that stood in for it in the body. The live questionnaire
114
+ * arrives as an extension-UI dialog; this is the transcript record.
115
+ */
116
+ function AskUserQuestionToolMessage(props) {
117
+ const summary = askCallSummary(props.args);
118
+ return /* @__PURE__ */ jsxs(MessageItem, {
119
+ tone: toolTone({
120
+ running: props.running,
121
+ isError: props.isError
122
+ }),
123
+ children: [/* @__PURE__ */ jsx(MessageItemHeader, {
124
+ title: "ask_user_question",
125
+ children: /* @__PURE__ */ jsxs("span", {
126
+ "data-testid": "tool-call-ask_user_question",
127
+ className: "flex min-w-0 flex-1 items-center gap-2",
128
+ children: [/* @__PURE__ */ jsx("span", {
129
+ className: "text-doom-hi",
130
+ children: summary.count
131
+ }), summary.headers ? /* @__PURE__ */ jsxs("span", {
132
+ className: "min-w-0 truncate text-doom-dim",
133
+ children: [
134
+ "(",
135
+ summary.headers,
136
+ ")"
137
+ ]
138
+ }) : null]
139
+ })
140
+ }), /* @__PURE__ */ jsx(AskUserQuestionBody, { ...props })]
141
+ });
142
+ }
143
+ //#endregion
144
+ //#region src/types/askUserWire.ts
145
+ /**
146
+ * How a rich client answers the whole questionnaire in one reply.
147
+ *
148
+ * Pi's select request carries a title and a list of labels and nothing else,
149
+ * so over RPC the questionnaire degrades into one request per question, asked
150
+ * forwards only. A client that can already see the whole call (the cockpit
151
+ * reads it from the tool's arguments) has no use for that: it renders every
152
+ * question at once, lets the reader move between them, and replies once.
153
+ *
154
+ * The reply rides on the select response, because that is the only channel a
155
+ * blocked extension has. An ordinary label is still an ordinary label, so a
156
+ * client that knows nothing about this keeps the per-question flow exactly as
157
+ * it was; only a response carrying the prefix is read as the whole set.
158
+ *
159
+ * It lives under src/types because that is the one server root a browser
160
+ * bundle may read, so neither half can drift from the other.
161
+ */
162
+ const ANSWER_ENVELOPE_PREFIX = "doom-ask-user/v1:";
163
+ function encodeAnswerEnvelope(answers) {
164
+ return `${ANSWER_ENVELOPE_PREFIX}${JSON.stringify({ answers })}`;
165
+ }
166
+ //#endregion
167
+ //#region src/extensions/workspaces/sessions/(frontend)/tool/_lib/questionnaireDraft.ts
168
+ /** The row every question gets, so a reader always has a way past a bad set of options. */
169
+ const CUSTOM_LABEL = "Type something.";
170
+ const EMPTY_ENTRY = {
171
+ selected: [],
172
+ custom: null,
173
+ notes: ""
174
+ };
175
+ function isRecord(value) {
176
+ return typeof value === "object" && value !== null && !Array.isArray(value);
177
+ }
178
+ function asString(value) {
179
+ return typeof value === "string" ? value : "";
180
+ }
181
+ function readOption(value) {
182
+ if (!isRecord(value)) return null;
183
+ const label = asString(value.label);
184
+ if (label === "") return null;
185
+ const preview = asString(value.preview);
186
+ return {
187
+ label,
188
+ description: asString(value.description),
189
+ ...preview === "" ? {} : { preview }
190
+ };
191
+ }
192
+ /** The questions as the tool was called with them; an unreadable call yields none. */
193
+ function readPromptQuestions(args) {
194
+ if (!Array.isArray(args.questions)) return [];
195
+ return args.questions.flatMap((entry) => {
196
+ if (!isRecord(entry)) return [];
197
+ const question = asString(entry.question);
198
+ if (question === "") return [];
199
+ const options = (Array.isArray(entry.options) ? entry.options : []).flatMap((option) => {
200
+ const read = readOption(option);
201
+ return read === null ? [] : [read];
202
+ });
203
+ return [{
204
+ question,
205
+ header: asString(entry.header),
206
+ multiSelect: entry.multiSelect === true,
207
+ options
208
+ }];
209
+ });
210
+ }
211
+ function emptyDraft(count) {
212
+ return Array.from({ length: count }, () => EMPTY_ENTRY);
213
+ }
214
+ function draftEntry(draft, index) {
215
+ return draft[index] ?? EMPTY_ENTRY;
216
+ }
217
+ function withEntry(draft, index, patch) {
218
+ if (index < 0 || index >= draft.length) return draft;
219
+ return draft.map((entry, position) => position === index ? {
220
+ ...entry,
221
+ ...patch
222
+ } : entry);
223
+ }
224
+ /**
225
+ * Picks an option. A single-select question replaces its choice; a multiSelect
226
+ * one toggles. Either way the typed answer is dropped, because an option and
227
+ * free text are two answers to the same question and only one can be sent.
228
+ */
229
+ function chooseOption(draft, index, label, multiSelect) {
230
+ const current = draftEntry(draft, index).selected;
231
+ return withEntry(draft, index, {
232
+ selected: multiSelect ? current.includes(label) ? current.filter((entry) => entry !== label) : [...current, label] : [label],
233
+ custom: null
234
+ });
235
+ }
236
+ /** Types an answer instead of choosing one; null returns to the options. */
237
+ function setCustom(draft, index, custom) {
238
+ return withEntry(draft, index, {
239
+ custom,
240
+ selected: []
241
+ });
242
+ }
243
+ function setNotes(draft, index, notes) {
244
+ return withEntry(draft, index, { notes });
245
+ }
246
+ function isAnswered(entry) {
247
+ return entry.selected.length > 0 || entry.custom !== null && entry.custom.trim() !== "";
248
+ }
249
+ /** The next question still needing an answer, searching after `from` first; -1 when none is left. */
250
+ function nextUnanswered(draft, from) {
251
+ const after = draft.findIndex((entry, index) => index > from && !isAnswered(entry));
252
+ return after >= 0 ? after : draft.findIndex((entry) => !isAnswered(entry));
253
+ }
254
+ function isComplete(draft) {
255
+ return draft.length > 0 && draft.every(isAnswered);
256
+ }
257
+ /**
258
+ * The answers as the tool reads them back. `preview` is left off: the session
259
+ * half looks it up from the option it declared, so sending one here would only
260
+ * be a claim it has to ignore.
261
+ */
262
+ function draftAnswers(questions, draft) {
263
+ return questions.flatMap((question, index) => {
264
+ const entry = draftEntry(draft, index);
265
+ if (!isAnswered(entry)) return [];
266
+ const notes = entry.notes.trim();
267
+ const base = {
268
+ questionIndex: index,
269
+ question: question.question,
270
+ ...notes === "" ? {} : { notes }
271
+ };
272
+ if (entry.custom !== null && entry.custom.trim() !== "") return [{
273
+ ...base,
274
+ kind: "custom",
275
+ answer: entry.custom
276
+ }];
277
+ if (question.multiSelect) return [{
278
+ ...base,
279
+ kind: "multi",
280
+ answer: null,
281
+ selected: [...entry.selected]
282
+ }];
283
+ return [{
284
+ ...base,
285
+ kind: "option",
286
+ answer: entry.selected[0] ?? null
287
+ }];
288
+ });
289
+ }
290
+ //#endregion
291
+ //#region src/extensions/workspaces/sessions/(frontend)/tool/_components/QuestionnairePrompt.tsx
292
+ /** The step bar: which questions are answered, which one is open, and a click back to any of them. */
293
+ function StepBar({ questions, draft, current, onPick }) {
294
+ return /* @__PURE__ */ jsx("div", {
295
+ "data-testid": "questionnaire-steps",
296
+ className: "flex min-w-0 flex-wrap items-center gap-1.5",
297
+ children: questions.map((question, index) => {
298
+ const answered = isAnswered(draftEntry(draft, index));
299
+ const active = index === current;
300
+ return /* @__PURE__ */ jsxs(Button, {
301
+ variant: "ghost",
302
+ size: "xs",
303
+ "data-testid": `questionnaire-step-${String(index)}`,
304
+ "data-step-state": active ? "current" : answered ? "answered" : "pending",
305
+ onClick: () => onPick(index),
306
+ className: `gap-1.5 rounded-full px-2.5 py-0.5 text-base font-medium ${active ? "bg-doom-selected font-semibold text-doom-on-selected" : answered ? "text-doom-green hover:bg-doom-panel" : "text-doom-faint hover:bg-doom-panel"}`,
307
+ children: [/* @__PURE__ */ jsx("span", {
308
+ "aria-hidden": true,
309
+ children: answered ? "✓" : "○"
310
+ }), question.header || `question ${String(index + 1)}`]
311
+ }, question.question);
312
+ })
313
+ });
314
+ }
315
+ /**
316
+ * The frame's shortcuts stop at a text field: while the caret is in one the
317
+ * arrows and Enter belong to the field, so they are kept from bubbling. Enter
318
+ * commits the field the way the terminal editor does, Shift+Enter writes a
319
+ * newline, and Escape and Cmd/Ctrl+Enter still reach the frame.
320
+ */
321
+ function fieldKeys(commit) {
322
+ return (event) => {
323
+ if (event.key === "Escape") return;
324
+ if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) return;
325
+ event.stopPropagation();
326
+ if (event.key !== "Enter" || event.shiftKey) return;
327
+ event.preventDefault();
328
+ commit();
329
+ };
330
+ }
331
+ /** One question's options, its typed-answer row, and the preview of whatever is focused. */
332
+ function QuestionBody({ question, index, draft, cursor, onCursor, onDraft, onCommit }) {
333
+ const entry = draftEntry(draft, index);
334
+ const typing = entry.custom !== null;
335
+ const preview = question.options[cursor]?.preview;
336
+ return /* @__PURE__ */ jsxs("div", {
337
+ className: "flex min-w-0 flex-col gap-2",
338
+ children: [
339
+ /* @__PURE__ */ jsx("p", {
340
+ "data-testid": "questionnaire-question",
341
+ className: "break-words text-base leading-relaxed text-doom-hi",
342
+ children: question.question
343
+ }),
344
+ /* @__PURE__ */ jsxs("div", {
345
+ role: "listbox",
346
+ "aria-label": "options",
347
+ className: "flex min-w-0 flex-col",
348
+ children: [question.options.map((option, optionIndex) => {
349
+ const chosen = entry.selected.includes(option.label);
350
+ return /* @__PURE__ */ jsxs(OptionRow, {
351
+ active: optionIndex === cursor && !typing,
352
+ "data-testid": `questionnaire-option-${String(optionIndex)}`,
353
+ "data-chosen": chosen,
354
+ onMouseEnter: () => onCursor(optionIndex),
355
+ onClick: () => onDraft(chooseOption(draft, index, option.label, question.multiSelect), !question.multiSelect),
356
+ className: "min-w-0 items-start gap-2.5 rounded-md px-2.5 py-1.5 whitespace-normal",
357
+ children: [/* @__PURE__ */ jsx("span", {
358
+ className: `mt-[2px] shrink-0 text-sm ${chosen ? "text-doom-green" : "text-doom-faint"}`,
359
+ children: question.multiSelect ? chosen ? "[x]" : "[ ]" : chosen ? "●" : "○"
360
+ }), /* @__PURE__ */ jsxs(OptionLabel, {
361
+ className: "min-w-0 flex flex-col gap-0.5 text-left",
362
+ children: [/* @__PURE__ */ jsx("span", {
363
+ className: `break-words text-base font-medium ${chosen ? "font-semibold text-doom-hi" : "text-doom-text"}`,
364
+ children: option.label
365
+ }), option.description ? /* @__PURE__ */ jsx("span", {
366
+ className: "break-words text-sm leading-relaxed whitespace-normal text-doom-dim",
367
+ children: option.description
368
+ }) : null]
369
+ })]
370
+ }, option.label);
371
+ }), /* @__PURE__ */ jsxs(OptionRow, {
372
+ active: typing,
373
+ "data-testid": "questionnaire-option-custom",
374
+ onClick: () => onDraft(setCustom(draft, index, entry.custom ?? ""), false),
375
+ className: "min-w-0 items-center gap-2.5 rounded-md px-2.5 py-1.5 whitespace-normal",
376
+ children: [/* @__PURE__ */ jsx("span", {
377
+ className: "shrink-0 text-sm text-doom-faint",
378
+ children: "✎"
379
+ }), /* @__PURE__ */ jsx(OptionLabel, {
380
+ className: "min-w-0 whitespace-normal text-doom-text",
381
+ children: CUSTOM_LABEL
382
+ })]
383
+ })]
384
+ }),
385
+ typing ? /* @__PURE__ */ jsxs("div", {
386
+ className: "flex flex-col gap-1",
387
+ children: [/* @__PURE__ */ jsx(SectionLabel, { children: "your answer" }), /* @__PURE__ */ jsx(Textarea, {
388
+ "data-testid": "questionnaire-custom",
389
+ autoFocus: true,
390
+ rows: 2,
391
+ value: entry.custom ?? "",
392
+ placeholder: "type your answer…",
393
+ onChange: (event) => onDraft(setCustom(draft, index, event.target.value), false),
394
+ onKeyDown: fieldKeys(() => onCommit(true))
395
+ })]
396
+ }) : null,
397
+ preview ? /* @__PURE__ */ jsxs("div", {
398
+ "data-testid": "questionnaire-preview",
399
+ className: "flex flex-col gap-1",
400
+ children: [/* @__PURE__ */ jsx(SectionLabel, { children: "preview" }), /* @__PURE__ */ jsx("div", {
401
+ className: "max-h-40 overflow-y-auto border-l-2 border-doom-edge-magenta pl-3 text-base text-doom-dim",
402
+ children: /* @__PURE__ */ jsx(Markdown, { text: preview })
403
+ })]
404
+ }) : null,
405
+ /* @__PURE__ */ jsxs("div", {
406
+ className: "flex flex-col gap-1",
407
+ children: [/* @__PURE__ */ jsx(SectionLabel, { children: "note (optional)" }), /* @__PURE__ */ jsx(Textarea, {
408
+ "data-testid": "questionnaire-notes",
409
+ rows: 1,
410
+ value: entry.notes,
411
+ placeholder: "anything the options do not cover",
412
+ onChange: (event) => onDraft(setNotes(draft, index, event.target.value), false),
413
+ onKeyDown: fieldKeys(() => onCommit(false))
414
+ })]
415
+ })
416
+ ]
417
+ });
418
+ }
419
+ /**
420
+ * The questionnaire, standing in for the composer input while the agent waits.
421
+ *
422
+ * Every question is here at once, because the cockpit can see the whole call
423
+ * even though the request it answers can only carry one question's labels.
424
+ * That is what makes the step bar honest: nothing is sent until the reader
425
+ * submits, so going back to a step is just moving a cursor, and the agent is
426
+ * asked exactly once.
427
+ */
428
+ function QuestionnairePrompt({ args, dialog, answer, cancel }) {
429
+ const questions = useMemo(() => readPromptQuestions(args), [args]);
430
+ const [draft, setDraft] = useState(() => emptyDraft(questions.length));
431
+ const [current, setCurrent] = useState(0);
432
+ const [cursor, setCursor] = useState(0);
433
+ const frame = useRef(null);
434
+ useEffect(() => {
435
+ setDraft(emptyDraft(questions.length));
436
+ setCurrent(0);
437
+ setCursor(0);
438
+ }, [dialog.id, questions.length]);
439
+ useEffect(() => frame.current?.focus(), [dialog.id]);
440
+ const question = questions[current];
441
+ const complete = isComplete(draft);
442
+ const move = (next, answeredIndex, settles) => {
443
+ setDraft(next);
444
+ if (!settles) return;
445
+ const pending = nextUnanswered(next, answeredIndex);
446
+ if (pending >= 0 && pending !== answeredIndex) {
447
+ setCurrent(pending);
448
+ setCursor(0);
449
+ }
450
+ };
451
+ const show = (index) => {
452
+ setCurrent(index);
453
+ setCursor(0);
454
+ };
455
+ const submit = () => {
456
+ if (!complete) return;
457
+ answer(encodeAnswerEnvelope(draftAnswers(questions, draft)));
458
+ };
459
+ if (question === void 0) return /* @__PURE__ */ jsxs("div", {
460
+ className: "flex items-center justify-between gap-3 px-3.5 py-3",
461
+ children: [/* @__PURE__ */ jsx("span", {
462
+ "data-testid": "questionnaire-empty",
463
+ className: "text-sm text-doom-dim",
464
+ children: "the agent asked something this cockpit could not read"
465
+ }), /* @__PURE__ */ jsx(Button, {
466
+ variant: "outline",
467
+ size: "sm",
468
+ "data-testid": "questionnaire-cancel",
469
+ onClick: cancel,
470
+ children: "cancel"
471
+ })]
472
+ });
473
+ const typing = draftEntry(draft, current).custom !== null;
474
+ return /* @__PURE__ */ jsxs("div", {
475
+ ref: frame,
476
+ tabIndex: -1,
477
+ "data-testid": "questionnaire",
478
+ className: "flex min-w-0 flex-col overflow-hidden outline-none",
479
+ onKeyDown: (event) => {
480
+ if (event.key === "Escape") {
481
+ event.preventDefault();
482
+ cancel();
483
+ return;
484
+ }
485
+ if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
486
+ event.preventDefault();
487
+ submit();
488
+ return;
489
+ }
490
+ if (typing) return;
491
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
492
+ event.preventDefault();
493
+ const count = question.options.length;
494
+ setCursor((event.key === "ArrowDown" ? cursor + 1 : cursor - 1 + count) % count);
495
+ return;
496
+ }
497
+ if (event.key === "ArrowRight" || event.key === "ArrowLeft") {
498
+ event.preventDefault();
499
+ const count = questions.length;
500
+ show((event.key === "ArrowRight" ? current + 1 : current - 1 + count) % count);
501
+ return;
502
+ }
503
+ if (event.key === "Enter") {
504
+ event.preventDefault();
505
+ const option = question.options[cursor];
506
+ if (option === void 0) {
507
+ submit();
508
+ return;
509
+ }
510
+ move(chooseOption(draft, current, option.label, question.multiSelect), current, !question.multiSelect);
511
+ }
512
+ },
513
+ children: [
514
+ /* @__PURE__ */ jsxs("div", {
515
+ className: "flex min-w-0 flex-wrap items-center gap-2 px-3 pt-2.5 pb-2 sm:gap-3 sm:px-3.5",
516
+ children: [
517
+ /* @__PURE__ */ jsx(StepBar, {
518
+ questions,
519
+ draft,
520
+ current,
521
+ onPick: show
522
+ }),
523
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1" }),
524
+ /* @__PURE__ */ jsxs(Badge, {
525
+ size: "xs",
526
+ className: "shrink-0 border-transparent bg-doom-panel py-0.5 text-doom-dim",
527
+ children: [
528
+ "question ",
529
+ current + 1,
530
+ " / ",
531
+ questions.length
532
+ ]
533
+ })
534
+ ]
535
+ }),
536
+ /* @__PURE__ */ jsx(Separator, {}),
537
+ /* @__PURE__ */ jsx("div", {
538
+ className: "max-h-[46vh] min-w-0 overflow-x-hidden overflow-y-auto px-3 py-3 sm:px-3.5",
539
+ children: /* @__PURE__ */ jsx(QuestionBody, {
540
+ question,
541
+ index: current,
542
+ draft,
543
+ cursor,
544
+ onCursor: setCursor,
545
+ onDraft: (next, settles) => move(next, current, settles),
546
+ onCommit: (settles) => {
547
+ frame.current?.focus();
548
+ move(draft, current, settles);
549
+ }
550
+ })
551
+ }),
552
+ /* @__PURE__ */ jsx(Separator, {}),
553
+ /* @__PURE__ */ jsxs("div", {
554
+ className: "flex min-w-0 items-center gap-2 px-3 py-2 sm:px-3.5",
555
+ children: [
556
+ /* @__PURE__ */ jsxs("span", {
557
+ "data-testid": "questionnaire-hint",
558
+ className: "min-w-0 truncate text-sm text-doom-dim max-sm:hidden",
559
+ children: [
560
+ /* @__PURE__ */ jsx(Kbd, { children: "←→" }),
561
+ " questions · ",
562
+ /* @__PURE__ */ jsx(Kbd, { children: "↑↓" }),
563
+ " options · ",
564
+ /* @__PURE__ */ jsx(Kbd, { children: "enter" }),
565
+ " ",
566
+ question.multiSelect ? "toggles" : "selects",
567
+ " · ",
568
+ /* @__PURE__ */ jsx(Kbd, { children: "esc" }),
569
+ " cancels"
570
+ ]
571
+ }),
572
+ /* @__PURE__ */ jsx("span", { className: "flex-1" }),
573
+ /* @__PURE__ */ jsx(Button, {
574
+ variant: "outline",
575
+ size: "md",
576
+ "data-testid": "questionnaire-cancel",
577
+ onClick: cancel,
578
+ children: "cancel"
579
+ }),
580
+ /* @__PURE__ */ jsx(Button, {
581
+ variant: "primary",
582
+ size: "md",
583
+ "data-testid": "questionnaire-submit",
584
+ onClick: submit,
585
+ disabled: !complete,
586
+ title: complete ? "send every answer to the agent" : "answer every question first",
587
+ className: "shrink-0 px-3 max-sm:max-w-32 max-sm:truncate sm:px-3.5",
588
+ children: "submit answers"
589
+ })
590
+ ]
591
+ })
592
+ ]
593
+ });
594
+ }
595
+ //#endregion
596
+ //#region src/extensions/workspaces/sessions/(frontend)/tool/askUserQuestion.web.tsx
597
+ var askUserQuestion_web_default = defineToolRenderer({
598
+ message: AskUserQuestionToolMessage,
599
+ prompt: {
600
+ claims: (dialog, args) => dialog.method === "select" && readPromptQuestions(args).some((question) => dialog.title.startsWith(question.question)),
601
+ component: QuestionnairePrompt
602
+ }
603
+ });
604
+ //#endregion
605
+ //#region generated/web.ts
606
+ const via = (identity, value) => value === void 0 ? void 0 : {
607
+ ...identity,
608
+ ...value
609
+ };
610
+ const webPlugin = defineWebPlugin({
611
+ id: "ask-user",
612
+ session: { get toolRenderers() {
613
+ return [via({ tools: ["ask_user_question"] }, askUserQuestion_web_default)];
614
+ } }
615
+ });
616
+ //#endregion
617
+ export { webPlugin };
618
+
619
+ //# sourceMappingURL=web.mjs.map