@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.
- package/LICENSE +21 -0
- package/README.md +76 -0
- package/dist/apiContracts.cjs +17 -0
- package/dist/apiContracts.cjs.map +1 -0
- package/dist/apiContracts.d.mts +53 -0
- package/dist/apiContracts.d.mts.map +1 -0
- package/dist/apiContracts.mjs +13 -0
- package/dist/apiContracts.mjs.map +1 -0
- package/dist/extensions/server.cjs +627 -0
- package/dist/extensions/server.cjs.map +1 -0
- package/dist/extensions/server.d.mts +5 -0
- package/dist/extensions/server.d.mts.map +1 -0
- package/dist/extensions/server.mjs +622 -0
- package/dist/extensions/server.mjs.map +1 -0
- package/dist/extensions/web.mjs +619 -0
- package/dist/extensions/web.mjs.map +1 -0
- package/dist/index.cjs +0 -0
- package/dist/index.d.mts +49 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1 -0
- package/llms.txt +4 -0
- package/package.json +124 -0
- package/src/prompts/po-use-user-feedback/SKILL.md +14 -0
|
@@ -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
|