@runtypelabs/persona 4.24.0 → 4.26.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.
Files changed (237) hide show
  1. package/dist/animations/glyph-cycle.d.cts +1 -1
  2. package/dist/animations/glyph-cycle.d.ts +1 -1
  3. package/dist/animations/{types-Db61aI9J.d.cts → types-BwKTZhg9.d.cts} +14 -0
  4. package/dist/animations/{types-Db61aI9J.d.ts → types-BwKTZhg9.d.ts} +14 -0
  5. package/dist/animations/wipe.d.cts +1 -1
  6. package/dist/animations/wipe.d.ts +1 -1
  7. package/dist/animations-extra.d.cts +14 -0
  8. package/dist/animations-extra.d.ts +14 -0
  9. package/dist/approval-ui-QAKWNFNM.js +1 -0
  10. package/dist/approval-ui.cjs +2 -2
  11. package/dist/approval-ui.d.cts +243 -8
  12. package/dist/approval-ui.d.ts +243 -8
  13. package/dist/approval-ui.js +2 -2
  14. package/dist/{artifacts-ui-IGB5YKLO.js → artifacts-ui-NJGMM27I.js} +1 -1
  15. package/dist/artifacts-ui.cjs +4 -4
  16. package/dist/artifacts-ui.d.cts +243 -8
  17. package/dist/artifacts-ui.d.ts +243 -8
  18. package/dist/artifacts-ui.js +5 -5
  19. package/dist/chunk-2ANTRKTV.js +1 -0
  20. package/dist/chunk-4HPTPUC6.js +1 -0
  21. package/dist/chunk-6V3MNEKB.js +1 -0
  22. package/dist/chunk-HGUCGM3K.js +9 -0
  23. package/dist/chunk-IO5VVUKP.js +3 -0
  24. package/dist/chunk-J4J3Y2P2.js +1 -0
  25. package/dist/chunk-K6HKIP5K.js +1 -0
  26. package/dist/chunk-K7NA7G7J.js +1 -0
  27. package/dist/chunk-MXUTS3LB.js +1 -0
  28. package/dist/chunk-PDPVORUF.js +1 -0
  29. package/dist/chunk-QPPJUSZH.js +1 -0
  30. package/dist/chunk-QRKGQPU7.js +1 -0
  31. package/dist/chunk-S6PBWE75.js +1 -0
  32. package/dist/chunk-T7QM7YMF.js +1 -0
  33. package/dist/chunk-TR2H475E.js +1 -0
  34. package/dist/chunk-UPO4GUFC.js +1 -0
  35. package/dist/chunk-ZC66NAIR.js +1 -0
  36. package/dist/client-history-YPEG45Q4.js +1 -0
  37. package/dist/client-history.js +1 -0
  38. package/dist/client-stream-42FOI3EC.js +4 -0
  39. package/dist/client-stream.js +5 -0
  40. package/dist/codegen.cjs +1 -1
  41. package/dist/codegen.js +1 -1
  42. package/dist/context-mentions-E7HJYWO6.js +161 -0
  43. package/dist/{context-mentions-inline-I2RDEBAK.js → context-mentions-inline-QVESLW55.js} +1 -1
  44. package/dist/context-mentions.cjs +7 -7
  45. package/dist/context-mentions.d.cts +243 -8
  46. package/dist/context-mentions.d.ts +243 -8
  47. package/dist/context-mentions.js +8 -8
  48. package/dist/event-stream-view-FOIJHFQO.js +1 -0
  49. package/dist/event-stream-view.cjs +1 -1
  50. package/dist/event-stream-view.d.cts +246 -31
  51. package/dist/event-stream-view.d.ts +246 -31
  52. package/dist/event-stream-view.js +1 -1
  53. package/dist/forms-ui.d.cts +295 -667
  54. package/dist/forms-ui.d.ts +295 -667
  55. package/dist/history-shell-QDQ44PSE.js +3 -0
  56. package/dist/history-shell.js +3 -0
  57. package/dist/{history-view-BD6OXLNA.js → history-view-NCFBI5GR.js} +2 -2
  58. package/dist/history-view.cjs +2 -2
  59. package/dist/history-view.d.cts +35 -1
  60. package/dist/history-view.d.ts +35 -1
  61. package/dist/history-view.js +2 -2
  62. package/dist/index.cjs +80 -74
  63. package/dist/index.cjs.map +1 -1
  64. package/dist/index.d.cts +301 -738
  65. package/dist/index.d.ts +301 -738
  66. package/dist/index.global.js +3 -215
  67. package/dist/index.global.js.map +1 -1
  68. package/dist/index.js +81 -75
  69. package/dist/index.js.map +1 -1
  70. package/dist/install.global.js +1 -1
  71. package/dist/install.global.js.map +1 -1
  72. package/dist/launcher.global.js +3 -67
  73. package/dist/launcher.global.js.map +1 -1
  74. package/dist/markdown-parsers.js +22 -22
  75. package/dist/runtype-tts-entry-NFBVWR7N.js +1 -0
  76. package/dist/runtype-tts.js +1 -1
  77. package/dist/session-actions-ISSKSI66.js +1 -0
  78. package/dist/session-actions.js +1 -0
  79. package/dist/session-reconnect.d.cts +243 -8
  80. package/dist/session-reconnect.d.ts +243 -8
  81. package/dist/smart-dom-reader.cjs +13 -13
  82. package/dist/smart-dom-reader.d.cts +243 -8
  83. package/dist/smart-dom-reader.d.ts +243 -8
  84. package/dist/smart-dom-reader.js +11 -11
  85. package/dist/theme-editor-preview.cjs +116 -98
  86. package/dist/theme-editor-preview.d.cts +245 -8
  87. package/dist/theme-editor-preview.d.ts +245 -8
  88. package/dist/theme-editor-preview.js +49 -54
  89. package/dist/theme-editor.cjs +2 -2
  90. package/dist/theme-editor.d.cts +244 -23
  91. package/dist/theme-editor.d.ts +244 -23
  92. package/dist/theme-editor.js +2 -2
  93. package/dist/theme-reference.d.cts +14 -0
  94. package/dist/theme-reference.d.ts +14 -0
  95. package/dist/ui-extras-entry-WAYFKVNW.js +3 -0
  96. package/dist/ui-extras.js +72 -0
  97. package/dist/voice-runtime-UOQOFAFY.js +17 -0
  98. package/dist/voice-runtime.cjs +17 -1
  99. package/dist/voice-runtime.d.cts +11643 -192
  100. package/dist/voice-runtime.d.ts +11643 -192
  101. package/dist/voice-runtime.js +17 -1
  102. package/dist/voice-worklet-player.cjs +19 -4
  103. package/dist/voice-worklet-player.d.cts +9 -10
  104. package/dist/voice-worklet-player.d.ts +9 -10
  105. package/dist/voice-worklet-player.js +19 -4
  106. package/dist/webmcp-polyfill.js +1 -1
  107. package/dist/webmcp-runtime-V6ZWW3PI.js +6 -0
  108. package/dist/webmcp-runtime.cjs +2 -2
  109. package/dist/webmcp-runtime.d.cts +1 -33
  110. package/dist/webmcp-runtime.d.ts +1 -33
  111. package/dist/webmcp-runtime.js +2 -2
  112. package/dist/widget.css +1 -1
  113. package/package.json +11 -5
  114. package/src/approval-paused-tool-settle.test.ts +221 -0
  115. package/src/client-approve.test.ts +311 -0
  116. package/src/client-history-eager.ts +9 -0
  117. package/src/client-history-loader.ts +23 -0
  118. package/src/client-history.ts +579 -0
  119. package/src/client-resume.ts +177 -0
  120. package/src/client-stream-eager.ts +12 -0
  121. package/src/client-stream-loader.ts +25 -0
  122. package/src/client-stream.ts +1880 -0
  123. package/src/client.test.ts +31 -1
  124. package/src/client.ts +293 -2607
  125. package/src/components/approval-actions.test.ts +27 -0
  126. package/src/components/approval-actions.ts +27 -3
  127. package/src/components/message-bubble.test.ts +19 -0
  128. package/src/components/message-bubble.ts +2 -0
  129. package/src/components/registry.ts +6 -6
  130. package/src/components/tool-bubble.test.ts +9 -0
  131. package/src/components/tool-bubble.ts +1 -0
  132. package/src/history-shell-bundle.test.ts +54 -0
  133. package/src/history-shell-eager.ts +12 -0
  134. package/src/history-shell-loader.ts +27 -0
  135. package/src/history-shell.ts +2362 -0
  136. package/src/history-view-loader.ts +2 -0
  137. package/src/history-view.ts +3 -0
  138. package/src/index-core.ts +8 -0
  139. package/src/index-global.ts +90 -248
  140. package/src/index.ts +13 -0
  141. package/src/install.ts +6 -4
  142. package/src/internal/runtype-history-provider.test.ts +1 -0
  143. package/src/internal/runtype-history-provider.ts +28 -13
  144. package/src/plugins/registry.ts +8 -8
  145. package/src/session-actions-chunk.test.ts +55 -0
  146. package/src/session-actions-eager.ts +11 -0
  147. package/src/session-actions-loader.ts +37 -0
  148. package/src/session-actions.ts +909 -0
  149. package/src/session-history.test.ts +3 -1
  150. package/src/session.ts +875 -1530
  151. package/src/session.voice.test.ts +1241 -5
  152. package/src/styles/widget.css +58 -0
  153. package/src/theme-editor/state.ts +91 -91
  154. package/src/types.ts +269 -5
  155. package/src/ui-ask-user-sheet.ts +382 -0
  156. package/src/ui-extras-eager.ts +10 -0
  157. package/src/ui-extras-entry.ts +9 -0
  158. package/src/ui-extras-loader.ts +26 -0
  159. package/src/ui.artifact-rail-resize.test.ts +2 -1
  160. package/src/ui.ask-user-chunk.test.ts +93 -0
  161. package/src/ui.history-chunk.test.ts +2 -1
  162. package/src/ui.history-config-provider.test.ts +2 -1
  163. package/src/ui.history-confirm-fallback.test.ts +142 -0
  164. package/src/ui.history-controller.test.ts +2 -1
  165. package/src/ui.history-lifecycle.test.ts +2 -1
  166. package/src/ui.history-render-hooks.test.ts +2 -1
  167. package/src/ui.history-shell.test.ts +2 -1
  168. package/src/ui.rail-overlay.test.ts +2 -1
  169. package/src/ui.rail-resize.test.ts +2 -1
  170. package/src/ui.rail-trigger-pending.test.ts +2 -1
  171. package/src/ui.ts +399 -2770
  172. package/src/ui.voice-prewarm.test.ts +222 -0
  173. package/src/utils/attachment-manager.ts +67 -67
  174. package/src/utils/constants.ts +3 -0
  175. package/src/utils/context-mention-controller.ts +323 -323
  176. package/src/utils/context-mention-manager.ts +40 -40
  177. package/src/utils/context-mention-orchestrator.ts +28 -0
  178. package/src/utils/event-stream-buffer.ts +50 -50
  179. package/src/utils/event-stream-store.ts +43 -43
  180. package/src/utils/throughput-tracker.ts +34 -34
  181. package/src/utils/virtual-scroller.ts +67 -67
  182. package/src/utils/visitor-store.test.ts +50 -0
  183. package/src/utils/visitor-store.ts +23 -5
  184. package/src/voice/__fixtures__/voice-wire/README.md +1 -0
  185. package/src/voice/__fixtures__/voice-wire/client/cancel.json +3 -0
  186. package/src/voice/__fixtures__/voice-wire/client/context.json +4 -0
  187. package/src/voice/__fixtures__/voice-wire/client/delegation_result.json +6 -0
  188. package/src/voice/__fixtures__/voice-wire/client/delegation_update.json +6 -0
  189. package/src/voice/__fixtures__/voice-wire/client/ping.json +3 -0
  190. package/src/voice/__fixtures__/voice-wire/client/playback_progress.json +5 -0
  191. package/src/voice/__fixtures__/voice-wire/client/start.json +3 -0
  192. package/src/voice/__fixtures__/voice-wire/server/artifact.json +11 -0
  193. package/src/voice/__fixtures__/voice-wire/server/attached.json +4 -0
  194. package/src/voice/__fixtures__/voice-wire/server/audio_clear.json +4 -0
  195. package/src/voice/__fixtures__/voice-wire/server/audio_end.json +3 -0
  196. package/src/voice/__fixtures__/voice-wire/server/delegation_cancelled.json +5 -0
  197. package/src/voice/__fixtures__/voice-wire/server/delegation_completed.json +8 -0
  198. package/src/voice/__fixtures__/voice-wire/server/delegation_started.json +15 -0
  199. package/src/voice/__fixtures__/voice-wire/server/error.json +6 -0
  200. package/src/voice/__fixtures__/voice-wire/server/metrics.json +10 -0
  201. package/src/voice/__fixtures__/voice-wire/server/pong.json +3 -0
  202. package/src/voice/__fixtures__/voice-wire/server/session_config.json +16 -0
  203. package/src/voice/__fixtures__/voice-wire/server/transcript_final.json +7 -0
  204. package/src/voice/__fixtures__/voice-wire/server/transcript_interim.json +4 -0
  205. package/src/voice/__fixtures__/voice-wire/server/transcript_partial.json +6 -0
  206. package/src/voice/__fixtures__/voice-wire/server/transcript_update.json +10 -0
  207. package/src/voice/__fixtures__/voice-wire/server/warning.json +5 -0
  208. package/src/voice/audio-playback-manager.test.ts +57 -0
  209. package/src/voice/audio-playback-manager.ts +120 -98
  210. package/src/voice/browser-voice-provider.ts +37 -37
  211. package/src/voice/fallback-speech-engine.ts +22 -16
  212. package/src/voice/keyed-voice-transcript.ts +314 -0
  213. package/src/voice/mic-state-styles.ts +182 -0
  214. package/src/voice/read-aloud-controller.ts +46 -44
  215. package/src/voice/runtype-speech-engine.ts +35 -33
  216. package/src/voice/runtype-voice-provider.ts +856 -164
  217. package/src/voice/session-voice-wiring.ts +266 -0
  218. package/src/voice/voice-delegation.test.ts +71 -0
  219. package/src/voice/voice-delegation.ts +364 -0
  220. package/src/voice/voice-turn-tracker.ts +6 -6
  221. package/src/voice/voice.test.ts +1250 -7
  222. package/src/voice/worklet-playback-engine.test.ts +81 -0
  223. package/src/voice/worklet-playback-engine.ts +21 -3
  224. package/src/voice-runtime-loader.ts +4 -1
  225. package/src/voice-runtime.ts +7 -0
  226. package/src/webmcp-runtime-entry.ts +41 -39
  227. package/dist/approval-ui-3NMYNXBD.js +0 -1
  228. package/dist/chunk-5EIIHQLQ.js +0 -1
  229. package/dist/chunk-7CO4T5KB.js +0 -1
  230. package/dist/chunk-EFJBUH5F.js +0 -1
  231. package/dist/chunk-R2TMIMDI.js +0 -3
  232. package/dist/chunk-UTVLOTWB.js +0 -9
  233. package/dist/context-mentions-45MODWOY.js +0 -161
  234. package/dist/event-stream-view-GL3ADRY2.js +0 -1
  235. package/dist/runtype-tts-entry-UJAEF7NZ.js +0 -1
  236. package/dist/voice-runtime-EFR3R5ES.js +0 -1
  237. package/dist/webmcp-runtime-JI7T4QYF.js +0 -6
@@ -0,0 +1,382 @@
1
+ /**
2
+ * Event handlers for the `ask_user_question` answer sheet that mounts in the
3
+ * composer overlay: pill pick (single), multi-select toggle + submit, free-text
4
+ * expansion + submit, grouped-question paging, skip, and dismissal. A selection
5
+ * becomes a regular user message (or a LOCAL-tool resume) so the agent
6
+ * continues on the next turn.
7
+ *
8
+ * Lives in the lazy `ui-extras` chunk: `ui.ts` warms it when a sheet mounts
9
+ * and routes overlay events here once loaded.
10
+ */
11
+ import type { AgentWidgetConfig } from "./types";
12
+ import type { AgentWidgetSession } from "./session";
13
+ import {
14
+ buildStructuredAnswers,
15
+ getCurrentIndex,
16
+ getQuestionCount,
17
+ getSelectedLabels,
18
+ isGroupedSheet,
19
+ navigateToPage,
20
+ readAnswersFromSheet,
21
+ removeAskUserQuestionSheet,
22
+ setCurrentAnswer,
23
+ } from "./components/ask-user-question-bubble";
24
+
25
+ export type AskUserSheetContext = {
26
+ mount: HTMLElement;
27
+ overlay: HTMLElement;
28
+ sessionRef: { current: AgentWidgetSession | null };
29
+ /** Read live: `controller.update()` replaces the config. */
30
+ config: () => AgentWidgetConfig;
31
+ };
32
+
33
+ export type AskUserSheetHandlers = {
34
+ click: (event: MouseEvent) => void;
35
+ keydown: (event: KeyboardEvent) => void;
36
+ };
37
+
38
+ export const createAskUserSheetHandlers = (ctx: AskUserSheetContext): AskUserSheetHandlers => {
39
+ const { mount, overlay: askUserOverlay, sessionRef } = ctx;
40
+
41
+ const submitAskUserAnswer = (
42
+ sheet: HTMLElement,
43
+ text: string,
44
+ meta: {
45
+ source: "pick" | "multi" | "free-text" | "submit-all";
46
+ values?: string[];
47
+ structured?: Record<string, string | string[]>;
48
+ }
49
+ ): void => {
50
+ const trimmed = text.trim();
51
+ if (!trimmed || !sessionRef.current) return;
52
+ const toolCallId = sheet.getAttribute("data-tool-call-id") ?? "";
53
+ const isFreeText = meta.source === "free-text";
54
+
55
+ // Dispatch before removing the sheet so listeners can still query DOM state.
56
+ mount.dispatchEvent(
57
+ new CustomEvent("persona:askUserQuestion:answered", {
58
+ detail: {
59
+ toolUseId: toolCallId,
60
+ answer: trimmed,
61
+ answers: meta.structured,
62
+ values: meta.values ?? (meta.source === "multi" ? trimmed.split(", ") : [trimmed]),
63
+ isFreeText,
64
+ source: meta.source,
65
+ },
66
+ bubbles: true,
67
+ composed: true,
68
+ })
69
+ );
70
+
71
+ removeAskUserQuestionSheet(askUserOverlay, toolCallId);
72
+
73
+ // Branch: LOCAL-tool pause (await) resumes via /resume with structured
74
+ // toolOutputs; legacy path sends as a plain user message.
75
+ const sourceMessage = sessionRef.current
76
+ .getMessages()
77
+ .find((m) => m.toolCall?.id === toolCallId);
78
+ if (sourceMessage?.agentMetadata?.awaitingLocalTool) {
79
+ sessionRef.current.resolveAskUserQuestion(sourceMessage, meta.structured ?? trimmed);
80
+ } else {
81
+ sessionRef.current.sendMessage(trimmed);
82
+ }
83
+ };
84
+
85
+ /**
86
+ * Persist in-progress grouped-question answers + page index back to the
87
+ * source message so a refresh restores the user's spot.
88
+ */
89
+ const persistGroupedProgress = (sheet: HTMLElement): void => {
90
+ const session = sessionRef.current;
91
+ if (!session) return;
92
+ const toolCallId = sheet.getAttribute("data-tool-call-id") ?? "";
93
+ const sourceMessage = session.getMessages().find((m) => m.toolCall?.id === toolCallId);
94
+ if (!sourceMessage) return;
95
+ session.persistAskUserQuestionProgress(sourceMessage, {
96
+ answers: buildStructuredAnswers(sheet, sourceMessage),
97
+ currentIndex: getCurrentIndex(sheet),
98
+ });
99
+ };
100
+
101
+ /**
102
+ * Build a one-line summary string for the legacy `answer` field on the
103
+ * answered event when submit-all fires from a grouped sheet.
104
+ */
105
+ const stringifyStructured = (answers: Record<string, string | string[]>): string => {
106
+ return Object.entries(answers)
107
+ .map(([q, v]) => `${q}: ${Array.isArray(v) ? v.join(", ") : v}`)
108
+ .join(" | ");
109
+ };
110
+
111
+ /**
112
+ * If `groupedAutoAdvance` is enabled (default) and we're not on the final
113
+ * page, advance one step. The final page never auto-submits: users always
114
+ * confirm with an explicit Submit-all click so they can review.
115
+ */
116
+ const maybeAutoAdvance = (sheet: HTMLElement): void => {
117
+ if (ctx.config().features?.askUserQuestion?.groupedAutoAdvance === false) return;
118
+ const idx = getCurrentIndex(sheet);
119
+ const count = getQuestionCount(sheet);
120
+ if (idx >= count - 1) return;
121
+ const sourceMessage = sessionRef.current
122
+ ?.getMessages()
123
+ .find((m) => m.toolCall?.id === sheet.getAttribute("data-tool-call-id"));
124
+ if (!sourceMessage) return;
125
+ navigateToPage(sheet, sourceMessage, ctx.config(), idx + 1);
126
+ persistGroupedProgress(sheet);
127
+ };
128
+
129
+ const click = (event: MouseEvent): void => {
130
+ const target = event.target as HTMLElement;
131
+ const trigger = target.closest<HTMLElement>("[data-ask-user-action]");
132
+ if (!trigger) return;
133
+ const sheet = trigger.closest<HTMLElement>("[data-persona-ask-sheet-for]");
134
+ if (!sheet) return;
135
+
136
+ const action = trigger.getAttribute("data-ask-user-action");
137
+ event.preventDefault();
138
+ event.stopPropagation();
139
+
140
+ if (action === "dismiss") {
141
+ const toolCallId = sheet.getAttribute("data-tool-call-id") ?? "";
142
+ mount.dispatchEvent(
143
+ new CustomEvent("persona:askUserQuestion:dismissed", {
144
+ detail: { toolUseId: toolCallId },
145
+ bubbles: true,
146
+ composed: true,
147
+ })
148
+ );
149
+ removeAskUserQuestionSheet(askUserOverlay, toolCallId);
150
+
151
+ // Best-effort: if this sheet corresponds to a LOCAL-awaiting tool,
152
+ // unblock the paused execution with a sentinel answer so the server
153
+ // doesn't sit in waiting_for_local forever. Fire-and-forget: errors
154
+ // are surfaced to the onError callback. Flip the answered flag first
155
+ // so a racing render pass doesn't re-mount the sheet mid-dismissal.
156
+ const sourceMessage = sessionRef.current
157
+ ?.getMessages()
158
+ .find((m) => m.toolCall?.id === toolCallId);
159
+ if (sourceMessage?.agentMetadata?.awaitingLocalTool) {
160
+ sessionRef.current?.markAskUserQuestionResolved(sourceMessage);
161
+ sessionRef.current?.resolveAskUserQuestion(sourceMessage, "(dismissed)");
162
+ }
163
+ return;
164
+ }
165
+
166
+ if (action === "pick") {
167
+ const label = trigger.getAttribute("data-option-label");
168
+ if (!label) return;
169
+ const multiSelect = sheet.getAttribute("data-multi-select") === "true";
170
+ const grouped = isGroupedSheet(sheet);
171
+
172
+ if (grouped && multiSelect) {
173
+ const stored = readAnswersFromSheet(sheet)[getCurrentIndex(sheet)];
174
+ const set = new Set<string>(Array.isArray(stored) ? stored : []);
175
+ if (set.has(label)) set.delete(label);
176
+ else set.add(label);
177
+ setCurrentAnswer(sheet, Array.from(set));
178
+ persistGroupedProgress(sheet);
179
+ return;
180
+ }
181
+
182
+ if (grouped) {
183
+ setCurrentAnswer(sheet, label);
184
+ persistGroupedProgress(sheet);
185
+ maybeAutoAdvance(sheet);
186
+ return;
187
+ }
188
+
189
+ // 1-question modes: preserve original UX.
190
+ if (multiSelect) {
191
+ const pressed = trigger.getAttribute("aria-pressed") === "true";
192
+ trigger.setAttribute("aria-pressed", pressed ? "false" : "true");
193
+ trigger.classList.toggle("persona-ask-pill-selected", !pressed);
194
+ const submitBtn = sheet.querySelector<HTMLButtonElement>(
195
+ '[data-ask-user-action="submit-multi"]'
196
+ );
197
+ if (submitBtn) {
198
+ submitBtn.disabled = getSelectedLabels(sheet).length === 0;
199
+ }
200
+ return;
201
+ }
202
+ submitAskUserAnswer(sheet, label, { source: "pick", values: [label] });
203
+ return;
204
+ }
205
+
206
+ if (action === "submit-multi") {
207
+ const labels = getSelectedLabels(sheet);
208
+ if (labels.length === 0) return;
209
+ submitAskUserAnswer(sheet, labels.join(", "), {
210
+ source: "multi",
211
+ values: labels,
212
+ });
213
+ return;
214
+ }
215
+
216
+ if (action === "open-free-text") {
217
+ const row = sheet.querySelector<HTMLElement>('[data-ask-free-text-row="true"]');
218
+ if (row) {
219
+ row.classList.remove("persona-hidden");
220
+ const input = row.querySelector<HTMLInputElement>('[data-ask-free-text-input="true"]');
221
+ input?.focus();
222
+ }
223
+ return;
224
+ }
225
+
226
+ if (action === "focus-free-text") {
227
+ // Rows-layout Other row: input lives inside the row container itself.
228
+ // Native click on the input already focuses it; this branch handles
229
+ // clicks on the badge or row chrome AND digit-shortcut activations.
230
+ const input = sheet.querySelector<HTMLInputElement>('[data-ask-free-text-input="true"]');
231
+ input?.focus();
232
+ return;
233
+ }
234
+
235
+ if (action === "submit-free-text") {
236
+ const input = sheet.querySelector<HTMLInputElement>('[data-ask-free-text-input="true"]');
237
+ const text = input?.value ?? "";
238
+ if (!text.trim()) return;
239
+ if (isGroupedSheet(sheet)) {
240
+ setCurrentAnswer(sheet, text.trim());
241
+ persistGroupedProgress(sheet);
242
+ maybeAutoAdvance(sheet);
243
+ return;
244
+ }
245
+ submitAskUserAnswer(sheet, text, { source: "free-text" });
246
+ return;
247
+ }
248
+
249
+ if (action === "next" || action === "back") {
250
+ if (!sessionRef.current) return;
251
+ const toolCallId = sheet.getAttribute("data-tool-call-id") ?? "";
252
+ const sourceMessage = sessionRef.current
253
+ .getMessages()
254
+ .find((m) => m.toolCall?.id === toolCallId);
255
+ if (!sourceMessage) return;
256
+ // Flush any unsubmitted free-text input as the current answer.
257
+ const freeInput = sheet.querySelector<HTMLInputElement>('[data-ask-free-text-input="true"]');
258
+ const pending = freeInput?.value?.trim() ?? "";
259
+ if (pending) {
260
+ const stored = readAnswersFromSheet(sheet)[getCurrentIndex(sheet)];
261
+ if (typeof stored !== "string" || stored !== pending) {
262
+ setCurrentAnswer(sheet, pending);
263
+ }
264
+ }
265
+ const direction = action === "next" ? 1 : -1;
266
+ const nextIdx = getCurrentIndex(sheet) + direction;
267
+ navigateToPage(sheet, sourceMessage, ctx.config(), nextIdx);
268
+ persistGroupedProgress(sheet);
269
+ return;
270
+ }
271
+
272
+ if (action === "submit-all") {
273
+ if (!sessionRef.current) return;
274
+ const toolCallId = sheet.getAttribute("data-tool-call-id") ?? "";
275
+ const sourceMessage = sessionRef.current
276
+ .getMessages()
277
+ .find((m) => m.toolCall?.id === toolCallId);
278
+ if (!sourceMessage) return;
279
+ // Flush any pending free-text on the final page first.
280
+ const freeInput = sheet.querySelector<HTMLInputElement>('[data-ask-free-text-input="true"]');
281
+ const pending = freeInput?.value?.trim() ?? "";
282
+ if (pending) setCurrentAnswer(sheet, pending);
283
+
284
+ const structured = buildStructuredAnswers(sheet, sourceMessage);
285
+ // Persist final answers to message metadata BEFORE resolving so the
286
+ // answered-state review card (which reads `agentMetadata
287
+ // .askUserQuestionAnswers`) shows the user's actual picks instead of
288
+ // "(skipped)" placeholders. Without this, any answer set only via the
289
+ // pending-flush above (or via paths that bypassed the per-pick persist
290
+ // hook) would be missing from the transcript review even though it
291
+ // landed in the structured payload sent to the agent.
292
+ sessionRef.current.persistAskUserQuestionProgress(sourceMessage, {
293
+ answers: structured,
294
+ currentIndex: getCurrentIndex(sheet),
295
+ });
296
+ const summary = stringifyStructured(structured);
297
+ submitAskUserAnswer(sheet, summary || "(submitted)", {
298
+ source: "submit-all",
299
+ structured,
300
+ });
301
+ return;
302
+ }
303
+
304
+ if (action === "skip") {
305
+ if (!sessionRef.current) return;
306
+ const toolCallId = sheet.getAttribute("data-tool-call-id") ?? "";
307
+ const sourceMessage = sessionRef.current
308
+ .getMessages()
309
+ .find((m) => m.toolCall?.id === toolCallId);
310
+ if (!sourceMessage) return;
311
+
312
+ const grouped = isGroupedSheet(sheet);
313
+ const idx = getCurrentIndex(sheet);
314
+ const count = getQuestionCount(sheet);
315
+ const isFinal = idx >= count - 1;
316
+
317
+ // Single-question payloads behave like dismiss.
318
+ if (!grouped) {
319
+ mount.dispatchEvent(
320
+ new CustomEvent("persona:askUserQuestion:dismissed", {
321
+ detail: { toolUseId: toolCallId },
322
+ bubbles: true,
323
+ composed: true,
324
+ })
325
+ );
326
+ removeAskUserQuestionSheet(askUserOverlay, toolCallId);
327
+ if (sourceMessage.agentMetadata?.awaitingLocalTool) {
328
+ sessionRef.current.markAskUserQuestionResolved(sourceMessage);
329
+ sessionRef.current.resolveAskUserQuestion(sourceMessage, "(dismissed)");
330
+ }
331
+ return;
332
+ }
333
+
334
+ // Drop the current question's answer (if any) so it's absent from the
335
+ // resolved Record. setCurrentAnswer with an empty string deletes the
336
+ // index from the in-memory map.
337
+ setCurrentAnswer(sheet, "");
338
+ // Also clear any unsubmitted free-text on this page.
339
+ const freeInput = sheet.querySelector<HTMLInputElement>('[data-ask-free-text-input="true"]');
340
+ if (freeInput) freeInput.value = "";
341
+
342
+ if (isFinal) {
343
+ // Submit with whatever has been recorded so far.
344
+ const structured = buildStructuredAnswers(sheet, sourceMessage);
345
+ const summary = stringifyStructured(structured);
346
+ submitAskUserAnswer(sheet, summary || "(skipped)", {
347
+ source: "submit-all",
348
+ structured,
349
+ });
350
+ return;
351
+ }
352
+
353
+ // Intermediate page: advance one step without recording.
354
+ navigateToPage(sheet, sourceMessage, ctx.config(), idx + 1);
355
+ persistGroupedProgress(sheet);
356
+ return;
357
+ }
358
+ };
359
+
360
+ // Enter on the free-text input → submit. Stays on the overlay because the
361
+ // event target IS the input, which lives inside the overlay subtree.
362
+ const keydown = (event: KeyboardEvent): void => {
363
+ if (event.key !== "Enter") return;
364
+ const target = event.target as HTMLElement;
365
+ const input = target as HTMLInputElement;
366
+ if (!input.matches?.('[data-ask-free-text-input="true"]')) return;
367
+ const sheet = input.closest<HTMLElement>("[data-persona-ask-sheet-for]");
368
+ if (!sheet) return;
369
+ event.preventDefault();
370
+ const text = input.value;
371
+ if (!text.trim()) return;
372
+ if (isGroupedSheet(sheet)) {
373
+ setCurrentAnswer(sheet, text.trim());
374
+ persistGroupedProgress(sheet);
375
+ maybeAutoAdvance(sheet);
376
+ return;
377
+ }
378
+ submitAskUserAnswer(sheet, text, { source: "free-text" });
379
+ };
380
+
381
+ return { click, keydown };
382
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Eager `ui-extras` registration for the bundled (ESM / CJS) builds and tests,
3
+ * so the features behind it respond synchronously. Like
4
+ * `markdown-parsers-eager.ts`, this module must NOT be reachable from
5
+ * `index-global.ts`: the IIFE/CDN build fetches the `ui-extras.js` chunk.
6
+ */
7
+ import * as uiExtras from "./ui-extras-entry";
8
+ import { provideUiExtras } from "./ui-extras-loader";
9
+
10
+ provideUiExtras(uiExtras);
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Entry for the lazy `ui-extras` chunk: `ui.ts` glue for opt-in or
3
+ * interaction-only features, kept out of the IIFE/CDN core bundle. See
4
+ * `ui-extras-loader.ts`.
5
+ */
6
+ export { createAskUserSheetHandlers } from "./ui-ask-user-sheet";
7
+ // Context-mention orchestrator (opt-in `contextMentions.enabled`). Callers pass
8
+ // core's loaders / icon registry via `deps` so this chunk's copies stay unused.
9
+ export { createContextMentionOrchestrator } from "./utils/context-mention-orchestrator";
@@ -0,0 +1,26 @@
1
+ import { createChunkLoader } from "./utils/chunk-loader";
2
+
3
+ /**
4
+ * Loader indirection for the `ui-extras` chunk (`ui-extras-entry.ts`): UI glue
5
+ * for opt-in or interaction-only features. The ESM/CJS fallback imports
6
+ * `./ui-extras-entry` directly (inlined by `--splitting false`); the IIFE/CDN
7
+ * build marks it external and registers a loader for the sibling
8
+ * `ui-extras.js` chunk (see `index-global.ts`).
9
+ */
10
+ export type UiExtrasModule = typeof import("./ui-extras-entry");
11
+
12
+ const { setLoader, load, provide, getSync } = createChunkLoader<UiExtrasModule>({
13
+ fallbackImport: () => import("./ui-extras-entry"),
14
+ });
15
+
16
+ /** Override how the chunk is fetched (the IIFE build registers a sibling-URL loader). */
17
+ export const setUiExtrasLoader = setLoader;
18
+
19
+ /** Load the chunk. Memoized; retries after rejection. */
20
+ export const loadUiExtras = load;
21
+
22
+ /** Eagerly supply the module (`ui-extras-eager.ts`: npm builds and tests). */
23
+ export const provideUiExtras = provide;
24
+
25
+ /** Synchronous access once loaded/provided; null before that. */
26
+ export const getUiExtrasSync = getSync;
@@ -11,6 +11,7 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vite
11
11
 
12
12
  import { createAgentExperience } from "./ui";
13
13
  import { createHistoryView } from "./components/history-view";
14
+ import { showHistoryConfirm } from "./components/history-confirm-dialog";
14
15
  import { setHistoryViewLoader } from "./history-view-loader";
15
16
  import { setHistoryProviderFactory } from "./internal/history-provider-registry";
16
17
  import {
@@ -162,7 +163,7 @@ const dragArtifact = (mount: HTMLElement, dx: number) => {
162
163
  describe("history rail beside a resizable artifact split", () => {
163
164
  beforeEach(() => {
164
165
  window.scrollTo = vi.fn();
165
- setHistoryViewLoader(async () => ({ createHistoryView }));
166
+ setHistoryViewLoader(async () => ({ createHistoryView, showHistoryConfirm }));
166
167
  });
167
168
 
168
169
  afterEach(() => {
@@ -0,0 +1,93 @@
1
+ // @vitest-environment jsdom
2
+ import { describe, it, expect, vi } from "vitest";
3
+
4
+ // Simulate the IIFE/CDN path: the ui-extras chunk is not provided up front and
5
+ // each test controls when its load resolves.
6
+ const chunk = vi.hoisted(() => {
7
+ const click = vi.fn();
8
+ const keydown = vi.fn();
9
+ let release: () => void = () => {};
10
+ let promise: Promise<unknown> = Promise.resolve();
11
+ const reset = () => {
12
+ click.mockClear();
13
+ keydown.mockClear();
14
+ promise = new Promise((resolve) => {
15
+ release = () =>
16
+ resolve({
17
+ createAskUserSheetHandlers: () => ({ click, keydown }),
18
+ createContextMentionOrchestrator: () => null,
19
+ });
20
+ });
21
+ };
22
+ return { click, keydown, reset, release: () => release(), load: () => promise };
23
+ });
24
+
25
+ vi.mock("./ui-extras-loader", () => ({
26
+ getUiExtrasSync: () => null,
27
+ loadUiExtras: () => chunk.load(),
28
+ setUiExtrasLoader: () => {},
29
+ provideUiExtras: () => {},
30
+ }));
31
+
32
+ import { createAgentExperience } from "./ui";
33
+
34
+ const mountWithSheet = () => {
35
+ const mount = document.createElement("div");
36
+ document.body.appendChild(mount);
37
+ const controller = createAgentExperience(mount, {
38
+ apiUrl: "https://api.example.com/chat",
39
+ launcher: { enabled: false },
40
+ });
41
+ const overlay = mount.querySelector<HTMLElement>("[data-persona-composer-overlay]")!;
42
+ const sheet = document.createElement("div");
43
+ sheet.setAttribute("data-persona-ask-sheet-for", "tool-1");
44
+ const pill = document.createElement("button");
45
+ pill.setAttribute("data-ask-user-action", "pick");
46
+ sheet.appendChild(pill);
47
+ overlay.appendChild(sheet);
48
+ return { controller, sheet, pill };
49
+ };
50
+
51
+ const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
52
+
53
+ describe("ask-user sheet events waiting on the lazy chunk", () => {
54
+ it("handles only the first of repeated early clicks", async () => {
55
+ chunk.reset();
56
+ const { controller, pill } = mountWithSheet();
57
+ pill.click();
58
+ pill.click();
59
+ chunk.release();
60
+ await flush();
61
+ expect(chunk.click).toHaveBeenCalledTimes(1);
62
+ controller.destroy();
63
+ });
64
+
65
+ it("typing in the free-text input does not block the Enter that submits", async () => {
66
+ chunk.reset();
67
+ const { controller, sheet } = mountWithSheet();
68
+ const input = document.createElement("input");
69
+ input.setAttribute("data-ask-free-text-input", "true");
70
+ sheet.appendChild(input);
71
+ const key = (k: string) =>
72
+ input.dispatchEvent(new KeyboardEvent("keydown", { key: k, bubbles: true }));
73
+ key("h");
74
+ key("i");
75
+ key("Enter");
76
+ chunk.release();
77
+ await flush();
78
+ expect(chunk.keydown).toHaveBeenCalledTimes(1);
79
+ expect((chunk.keydown.mock.calls[0][0] as KeyboardEvent).key).toBe("Enter");
80
+ controller.destroy();
81
+ });
82
+
83
+ it("drops a queued click whose sheet was removed before the chunk loaded", async () => {
84
+ chunk.reset();
85
+ const { controller, sheet, pill } = mountWithSheet();
86
+ pill.click();
87
+ sheet.remove();
88
+ chunk.release();
89
+ await flush();
90
+ expect(chunk.click).not.toHaveBeenCalled();
91
+ controller.destroy();
92
+ });
93
+ });
@@ -10,6 +10,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
10
10
 
11
11
  import { createAgentExperience } from "./ui";
12
12
  import { createHistoryView } from "./components/history-view";
13
+ import { showHistoryConfirm } from "./components/history-confirm-dialog";
13
14
  import { setHistoryViewLoader } from "./history-view-loader";
14
15
  import { setHistoryProviderFactory } from "./internal/history-provider-registry";
15
16
  import { createDemoHistoryProvider } from "./internal/demo-history-provider";
@@ -63,7 +64,7 @@ describe("history lazy chunk", () => {
63
64
  expect(body.hasAttribute("inert")).toBe(false);
64
65
 
65
66
  // A dropped fetch must not disable the feature for the session.
66
- setHistoryViewLoader(async () => ({ createHistoryView }));
67
+ setHistoryViewLoader(async () => ({ createHistoryView, showHistoryConfirm }));
67
68
  mount.querySelector<HTMLButtonElement>("[data-persona-history-toggle]")!.click();
68
69
  await flush();
69
70
  expect(mount.querySelector(".persona-history-view")).not.toBeNull();
@@ -11,6 +11,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
11
11
 
12
12
  import { createAgentExperience } from "./ui";
13
13
  import { createHistoryView } from "./components/history-view";
14
+ import { showHistoryConfirm } from "./components/history-confirm-dialog";
14
15
  import { setHistoryViewLoader } from "./history-view-loader";
15
16
  import { setHistoryProviderFactory } from "./internal/history-provider-registry";
16
17
  import {
@@ -118,7 +119,7 @@ describe("features.history.provider", () => {
118
119
  window.scrollTo = vi.fn();
119
120
  // No registry override: this suite exercises the public config path only.
120
121
  setHistoryProviderFactory(null);
121
- setHistoryViewLoader(async () => ({ createHistoryView }));
122
+ setHistoryViewLoader(async () => ({ createHistoryView, showHistoryConfirm }));
122
123
  });
123
124
 
124
125
  afterEach(() => {