@runtypelabs/persona 4.7.0 → 4.9.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 (185) hide show
  1. package/README.md +1 -0
  2. package/dist/animations/glyph-cycle.cjs +2 -2
  3. package/dist/animations/glyph-cycle.d.cts +1 -1
  4. package/dist/animations/glyph-cycle.d.ts +1 -1
  5. package/dist/animations/glyph-cycle.js +2 -2
  6. package/dist/animations/{types-CSmiKRVa.d.cts → types-4ROVJ1gA.d.cts} +85 -3
  7. package/dist/animations/{types-CSmiKRVa.d.ts → types-4ROVJ1gA.d.ts} +85 -3
  8. package/dist/animations/wipe.cjs +2 -2
  9. package/dist/animations/wipe.d.cts +1 -1
  10. package/dist/animations/wipe.d.ts +1 -1
  11. package/dist/chunk-5EIIHQLQ.js +1 -0
  12. package/dist/chunk-IO5VVUKP.js +3 -0
  13. package/dist/chunk-IPVK3KOM.js +1 -0
  14. package/dist/chunk-UPO4GUFC.js +1 -0
  15. package/dist/codegen.cjs +13 -13
  16. package/dist/codegen.js +14 -14
  17. package/dist/context-mentions-7S5KVUTG.js +169 -0
  18. package/dist/context-mentions-inline-TTCN7ZM2.js +4 -0
  19. package/dist/context-mentions-inline.cjs +4 -0
  20. package/dist/context-mentions-inline.d.cts +203 -0
  21. package/dist/context-mentions-inline.d.ts +203 -0
  22. package/dist/context-mentions-inline.js +4 -0
  23. package/dist/context-mentions.cjs +295 -0
  24. package/dist/context-mentions.d.cts +7025 -0
  25. package/dist/context-mentions.d.ts +7025 -0
  26. package/dist/context-mentions.js +295 -0
  27. package/dist/index.cjs +99 -68
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +1507 -182
  30. package/dist/index.d.ts +1507 -182
  31. package/dist/index.global.js +88 -56
  32. package/dist/index.global.js.map +1 -1
  33. package/dist/index.js +99 -68
  34. package/dist/index.js.map +1 -1
  35. package/dist/install.global.js +1 -1
  36. package/dist/install.global.js.map +1 -1
  37. package/dist/launcher.global.js +3 -2
  38. package/dist/launcher.global.js.map +1 -1
  39. package/dist/markdown-parsers.js +24 -24
  40. package/dist/plugin-kit.cjs +1 -1
  41. package/dist/plugin-kit.d.cts +17 -0
  42. package/dist/plugin-kit.d.ts +17 -0
  43. package/dist/plugin-kit.js +1 -1
  44. package/dist/runtype-tts-entry-UJAEF7NZ.js +1 -0
  45. package/dist/runtype-tts.js +1 -1
  46. package/dist/session-reconnect-JKIJBHS5.js +1 -0
  47. package/dist/smart-dom-reader.cjs +18 -17
  48. package/dist/smart-dom-reader.d.cts +1260 -16
  49. package/dist/smart-dom-reader.d.ts +1260 -16
  50. package/dist/smart-dom-reader.js +18 -17
  51. package/dist/testing.cjs +3 -3
  52. package/dist/testing.js +3 -3
  53. package/dist/theme-editor-preview.cjs +260 -58
  54. package/dist/theme-editor-preview.d.cts +1234 -15
  55. package/dist/theme-editor-preview.d.ts +1234 -15
  56. package/dist/theme-editor-preview.js +87 -58
  57. package/dist/theme-editor.cjs +10 -10
  58. package/dist/theme-editor.d.cts +1226 -15
  59. package/dist/theme-editor.d.ts +1226 -15
  60. package/dist/theme-editor.js +10 -10
  61. package/dist/theme-reference.cjs +1 -1
  62. package/dist/theme-reference.d.cts +76 -0
  63. package/dist/theme-reference.d.ts +76 -0
  64. package/dist/theme-reference.js +1 -1
  65. package/dist/voice-worklet-player.cjs +2 -2
  66. package/dist/voice-worklet-player.js +2 -2
  67. package/dist/webmcp-polyfill.js +2 -2
  68. package/dist/widget.css +1 -1
  69. package/package.json +16 -5
  70. package/src/artifacts-session.test.ts +178 -0
  71. package/src/client.test.ts +255 -1
  72. package/src/client.ts +157 -60
  73. package/src/components/artifact-card.test.ts +333 -0
  74. package/src/components/artifact-card.ts +75 -28
  75. package/src/components/artifact-inline.test.ts +1328 -0
  76. package/src/components/artifact-inline.ts +920 -0
  77. package/src/components/artifact-pane.test.ts +1042 -0
  78. package/src/components/artifact-pane.ts +440 -131
  79. package/src/components/artifact-preview.test.ts +1155 -0
  80. package/src/components/artifact-preview.ts +994 -0
  81. package/src/components/composer-parts.ts +3 -12
  82. package/src/components/context-mention-button.test.ts +70 -0
  83. package/src/components/context-mention-button.ts +82 -0
  84. package/src/components/context-mention-chip.ts +134 -0
  85. package/src/components/context-mention-menu.test.ts +508 -0
  86. package/src/components/context-mention-menu.ts +0 -0
  87. package/src/components/message-bubble.test.ts +175 -0
  88. package/src/components/message-bubble.ts +177 -19
  89. package/src/components/panel.ts +7 -10
  90. package/src/components/pill-composer-builder.test.ts +6 -2
  91. package/src/components/pill-composer-builder.ts +6 -6
  92. package/src/components/reasoning-bubble.ts +1 -13
  93. package/src/components/registry.ts +38 -3
  94. package/src/components/tool-bubble.ts +1 -13
  95. package/src/context-mentions-bundle.test.ts +163 -0
  96. package/src/context-mentions-entry.ts +185 -0
  97. package/src/context-mentions-inline-entry.test.ts +136 -0
  98. package/src/context-mentions-inline-entry.ts +226 -0
  99. package/src/context-mentions-inline-loader.test.ts +30 -0
  100. package/src/context-mentions-inline-loader.ts +36 -0
  101. package/src/context-mentions-inline.ts +15 -0
  102. package/src/context-mentions-loader.ts +32 -0
  103. package/src/context-mentions.ts +16 -0
  104. package/src/defaults.ts +1 -0
  105. package/src/generated/runtype-openapi-contract.ts +55 -3
  106. package/src/index-core.ts +46 -1
  107. package/src/index-global.ts +51 -0
  108. package/src/index.ts +8 -0
  109. package/src/markdown-parsers-loader.test.ts +158 -0
  110. package/src/markdown-parsers-loader.ts +83 -27
  111. package/src/plugin-kit.test.ts +40 -0
  112. package/src/plugin-kit.ts +39 -5
  113. package/src/runtime/host-layout.test.ts +163 -0
  114. package/src/runtime/host-layout.ts +110 -7
  115. package/src/runtime/init.ts +18 -61
  116. package/src/session.mentions.test.ts +175 -0
  117. package/src/session.test.ts +52 -4
  118. package/src/session.ts +197 -27
  119. package/src/smart-dom-reader.test.ts +129 -2
  120. package/src/smart-dom-reader.ts +127 -1
  121. package/src/styles/context-mention-menu-css.ts +176 -0
  122. package/src/styles/widget.css +1002 -152
  123. package/src/theme-editor/preview.ts +2 -0
  124. package/src/theme-editor/sections.test.ts +26 -1
  125. package/src/theme-editor/sections.ts +13 -5
  126. package/src/theme-reference.ts +2 -2
  127. package/src/tool-call-display-defaults.test.ts +1 -0
  128. package/src/types/theme.ts +79 -0
  129. package/src/types.ts +1041 -17
  130. package/src/ui.artifact-pane-gating.test.ts +636 -0
  131. package/src/ui.component-directive.test.ts +104 -0
  132. package/src/ui.composer-bar.test.ts +60 -2
  133. package/src/ui.detached-panel.test.ts +1049 -0
  134. package/src/ui.mention-submit.test.ts +235 -0
  135. package/src/ui.tool-display.test.ts +51 -0
  136. package/src/ui.ts +998 -159
  137. package/src/utils/artifact-custom-actions.ts +128 -0
  138. package/src/utils/artifact-display.test.ts +42 -0
  139. package/src/utils/artifact-display.ts +84 -0
  140. package/src/utils/artifact-file.test.ts +116 -0
  141. package/src/utils/artifact-file.ts +117 -0
  142. package/src/utils/artifact-gate.test.ts +112 -5
  143. package/src/utils/artifact-gate.ts +39 -14
  144. package/src/utils/artifact-loading-status.ts +55 -0
  145. package/src/utils/artifact-status-label.ts +190 -0
  146. package/src/utils/buttons.ts +7 -1
  147. package/src/utils/chunk-loader.test.ts +97 -0
  148. package/src/utils/chunk-loader.ts +88 -0
  149. package/src/utils/code-highlight.test.ts +186 -0
  150. package/src/utils/code-highlight.ts +400 -0
  151. package/src/utils/composer-contenteditable.test.ts +507 -0
  152. package/src/utils/composer-contenteditable.ts +626 -0
  153. package/src/utils/composer-document.test.ts +280 -0
  154. package/src/utils/composer-document.ts +293 -0
  155. package/src/utils/composer-history.test.ts +35 -7
  156. package/src/utils/composer-history.ts +30 -20
  157. package/src/utils/composer-input.ts +159 -0
  158. package/src/utils/context-mention-controller.test.ts +1215 -0
  159. package/src/utils/context-mention-controller.ts +1186 -0
  160. package/src/utils/context-mention-manager.test.ts +422 -0
  161. package/src/utils/context-mention-manager.ts +410 -0
  162. package/src/utils/context-mention-orchestrator.test.ts +538 -0
  163. package/src/utils/context-mention-orchestrator.ts +348 -0
  164. package/src/utils/icons.ts +2 -0
  165. package/src/utils/live-region.test.ts +108 -0
  166. package/src/utils/live-region.ts +94 -0
  167. package/src/utils/mention-channels.ts +63 -0
  168. package/src/utils/mention-llm-format.test.ts +91 -0
  169. package/src/utils/mention-llm-format.ts +79 -0
  170. package/src/utils/mention-matcher.test.ts +86 -0
  171. package/src/utils/mention-matcher.ts +221 -0
  172. package/src/utils/mention-token.ts +72 -0
  173. package/src/utils/mention-trigger.test.ts +155 -0
  174. package/src/utils/mention-trigger.ts +156 -0
  175. package/src/utils/roving-tablist.test.ts +152 -0
  176. package/src/utils/roving-tablist.ts +111 -0
  177. package/src/utils/spinner.ts +45 -0
  178. package/src/utils/theme.test.ts +76 -4
  179. package/src/utils/theme.ts +7 -0
  180. package/src/utils/tokens.ts +118 -11
  181. package/src/utils/tool-loading-animation.test.ts +32 -0
  182. package/src/utils/tool-loading-animation.ts +24 -0
  183. package/dist/chunk-DFBSCFYN.js +0 -1
  184. package/dist/runtype-tts-entry-HFUV2UF7.js +0 -1
  185. package/dist/session-reconnect-U77QFUR7.js +0 -1
@@ -0,0 +1,410 @@
1
+ import type {
2
+ AgentWidgetConfig,
3
+ AgentWidgetContextMentionConfig,
4
+ AgentWidgetContextMentionItem,
5
+ AgentWidgetContextMentionRef,
6
+ AgentWidgetContextMentionSource,
7
+ AgentWidgetContextMentionPayload,
8
+ AgentWidgetMessage,
9
+ ContentPart,
10
+ } from "../types";
11
+ import { createMentionChip, type MentionChipParts } from "../components/context-mention-chip";
12
+ import { formatMentionBlock } from "./mention-llm-format";
13
+
14
+ interface PendingMention {
15
+ key: string;
16
+ source: AgentWidgetContextMentionSource;
17
+ item: AgentWidgetContextMentionItem;
18
+ ref: AgentWidgetContextMentionRef;
19
+ status: "resolving" | "ready" | "error";
20
+ payload?: AgentWidgetContextMentionPayload;
21
+ abort?: AbortController;
22
+ /** The in-flight select-time resolve (absent for `resolveOn:"submit"`). Awaited
23
+ * at submit so `finalize()` reuses it instead of firing a duplicate fetch. */
24
+ resolvePromise?: Promise<void>;
25
+ /** Chip DOM — chip mode only. Inline mode tracks by `ComposerMentionId` key with
26
+ * no chip (the token lives in the composer prose). */
27
+ chip?: MentionChipParts;
28
+ /** INLINE mode only. Reflect this mention's resolve status onto its live token
29
+ * element (the composer capability's `setMentionStatus`, closed over its id).
30
+ * Chip mode leaves this undefined — the chip carries the status instead. */
31
+ reportStatus?: (status: "pending" | "resolved" | "error") => void;
32
+ /** Command args captured at add time (empty for ordinary mentions). */
33
+ args: string;
34
+ }
35
+
36
+ /**
37
+ * The stored ref for a selected item — identical shape at every call site
38
+ * (`add`, `track`, and the controller's inline commit), so it lives here once.
39
+ */
40
+ export function refFromItem(
41
+ source: AgentWidgetContextMentionSource,
42
+ item: AgentWidgetContextMentionItem
43
+ ): AgentWidgetContextMentionRef {
44
+ return {
45
+ sourceId: source.id,
46
+ itemId: item.id,
47
+ label: item.label,
48
+ iconName: item.iconName,
49
+ color: item.color,
50
+ };
51
+ }
52
+
53
+ /** The resolved bundle gathered at submit, merged into the user message. */
54
+ export interface MentionSubmitBundle {
55
+ /**
56
+ * Ready-to-join, per-mention LLM blocks (mentions-first). The mention layer
57
+ * has already applied `contextMentions.llmFormat` (fenced/document/custom), so
58
+ * the session just joins these with a blank line and appends the typed prose.
59
+ */
60
+ blocks: string[];
61
+ contentParts: ContentPart[];
62
+ /** Namespaced `{ [sourceId]: { [itemId]: context } }` for the opt-in path. */
63
+ context: Record<string, Record<string, unknown>>;
64
+ }
65
+
66
+ export interface ContextMentionManagerOptions {
67
+ mentionConfig: AgentWidgetContextMentionConfig;
68
+ /** The composer context row chips render into (created by the core orchestrator). */
69
+ contextRow: HTMLElement;
70
+ getMessages: () => AgentWidgetMessage[];
71
+ getConfig: () => AgentWidgetConfig;
72
+ /** Plain-text composer value, captured at resolve/submit time. */
73
+ getComposerText: () => string;
74
+ /** Polite live-region announcer. */
75
+ announce: (message: string) => void;
76
+ /**
77
+ * Assertive live-region announcer for failures (resolve errors). Falls back to
78
+ * the polite `announce` when the host wires only one region.
79
+ */
80
+ announceError?: (message: string) => void;
81
+ /** Emit a `persona:mention:<event>` analytics DOM event. */
82
+ emit?: (event: string, detail: unknown) => void;
83
+ }
84
+
85
+ const mentionKey = (sourceId: string, itemId: string) => `${sourceId}\u0000${itemId}`;
86
+
87
+ /**
88
+ * Owns the set of pending mentions: pill chip DOM, eager resolve-on-select with
89
+ * caching + abort, duplicate/limit checks, and the submit-time gather (resolving
90
+ * any `resolveOn:"submit"` sources, dropping failures). No menu/search logic —
91
+ * that's the controller's job.
92
+ */
93
+ export class ContextMentionManager {
94
+ private readonly opts: ContextMentionManagerOptions;
95
+ private readonly mentions: PendingMention[] = [];
96
+
97
+ constructor(opts: ContextMentionManagerOptions) {
98
+ this.opts = opts;
99
+ this.updateRowVisibility();
100
+ }
101
+
102
+ private get maxMentions(): number {
103
+ return this.opts.mentionConfig.maxMentions ?? 8;
104
+ }
105
+
106
+ hasMentions(): boolean {
107
+ return this.mentions.length > 0;
108
+ }
109
+
110
+ /**
111
+ * Add a selected item. Returns false (and fires `onMentionRejected`) on
112
+ * duplicate/limit. `args` (for server commands) is captured now and threaded
113
+ * into `resolve()`'s context, since the typed token is gone by submit time.
114
+ */
115
+ add(
116
+ source: AgentWidgetContextMentionSource,
117
+ item: AgentWidgetContextMentionItem,
118
+ args = ""
119
+ ): boolean {
120
+ const key = mentionKey(source.id, item.id);
121
+ if (this.mentions.some((m) => m.key === key)) {
122
+ return this.reject(source, item, "duplicate");
123
+ }
124
+ if (this.atLimit()) return this.reject(source, item, "limit");
125
+
126
+ const ref = refFromItem(source, item);
127
+ const pending: PendingMention = {
128
+ key,
129
+ source,
130
+ item,
131
+ ref,
132
+ status: "resolving",
133
+ args,
134
+ };
135
+ pending.chip = createMentionChip({
136
+ ref,
137
+ config: this.opts.mentionConfig,
138
+ onRemove: () => this.remove(key),
139
+ });
140
+ this.opts.contextRow.appendChild(pending.chip.el);
141
+ // startPending pushes into this.mentions, which updateRowVisibility reads —
142
+ // reversed order left the row hidden until the second chip.
143
+ this.startPending(pending);
144
+ this.updateRowVisibility();
145
+ return true;
146
+ }
147
+
148
+ /** True when the mention limit is already reached (inline pre-insert gate). */
149
+ atLimit(): boolean {
150
+ return this.mentions.length >= this.maxMentions;
151
+ }
152
+
153
+ /**
154
+ * INLINE pre-insert gate, run BEFORE the controller inserts an atomic token so a
155
+ * rejected pick never leaves a stray token. Only the mention LIMIT is gated here:
156
+ * inline mode lets the same item appear as several prose tokens (Slack, Cursor,
157
+ * and Claude all let you @-mention a thing twice in one sentence), unlike chip
158
+ * mode's `add()`, whose chip row is an attachment list where a duplicate chip is
159
+ * meaningless. Each token still counts toward `maxMentions`, and `finalize()`
160
+ * dedupes the resolved payload by (source, item) so a repeated mention's context
161
+ * reaches the LLM once even though every token renders. Returns true when the
162
+ * mention may be inserted.
163
+ */
164
+ admit(
165
+ source: AgentWidgetContextMentionSource,
166
+ item: AgentWidgetContextMentionItem
167
+ ): boolean {
168
+ if (this.atLimit()) return this.reject(source, item, "limit");
169
+ return true;
170
+ }
171
+
172
+ /**
173
+ * INLINE mode: track a mention whose atomic token was already inserted into the
174
+ * composer by the contenteditable adapter, keyed by its `ComposerMentionId`. No
175
+ * chip; duplicate/limit were gated by `admit()` before insertion. `reportStatus`
176
+ * reflects resolve state onto the live token element. Starts resolve-on-select
177
+ * just like `add()`.
178
+ */
179
+ track(
180
+ id: string,
181
+ source: AgentWidgetContextMentionSource,
182
+ item: AgentWidgetContextMentionItem,
183
+ args = "",
184
+ reportStatus?: (status: "pending" | "resolved" | "error") => void
185
+ ): void {
186
+ const pending: PendingMention = {
187
+ key: id,
188
+ source,
189
+ item,
190
+ ref: refFromItem(source, item),
191
+ status: "resolving",
192
+ args,
193
+ reportStatus,
194
+ };
195
+ this.startPending(pending);
196
+ }
197
+
198
+ /** Fire the rejection hooks for a blocked mention and return false (add's sentinel). */
199
+ private reject(
200
+ source: AgentWidgetContextMentionSource,
201
+ item: AgentWidgetContextMentionItem,
202
+ reason: "duplicate" | "limit"
203
+ ): false {
204
+ this.opts.mentionConfig.onMentionRejected?.(item, reason);
205
+ this.opts.emit?.("rejected", { sourceId: source.id, itemId: item.id, reason });
206
+ return false;
207
+ }
208
+
209
+ /**
210
+ * Push a fully-built pending mention and kick off its resolve (shared by
211
+ * `add()` and `track()`): `resolveOn:"submit"` flips straight to ready; anything
212
+ * else starts the select-time resolve, keeping the promise so `finalize()`
213
+ * awaits this exact resolve rather than firing a second one.
214
+ */
215
+ private startPending(pending: PendingMention): void {
216
+ this.mentions.push(pending);
217
+ if (pending.source.resolveOn === "submit") {
218
+ pending.status = "ready";
219
+ pending.chip?.setStatus("ready");
220
+ pending.reportStatus?.("resolved");
221
+ } else {
222
+ pending.resolvePromise = this.resolvePending(pending);
223
+ }
224
+ this.opts.announce(`Added ${pending.ref.label} to context`);
225
+ }
226
+
227
+ private buildResolveContext(
228
+ signal: AbortSignal,
229
+ args: string,
230
+ composerText = this.opts.getComposerText()
231
+ ) {
232
+ return {
233
+ messages: this.opts.getMessages(),
234
+ config: this.opts.getConfig(),
235
+ composerText,
236
+ args,
237
+ signal,
238
+ };
239
+ }
240
+
241
+ private async resolvePending(pending: PendingMention): Promise<void> {
242
+ const abort = new AbortController();
243
+ pending.abort = abort;
244
+ try {
245
+ const payload = await pending.source.resolve(
246
+ pending.item,
247
+ this.buildResolveContext(abort.signal, pending.args)
248
+ );
249
+ if (abort.signal.aborted) return;
250
+ pending.payload = payload;
251
+ pending.status = "ready";
252
+ pending.chip?.setStatus("ready", payload);
253
+ pending.reportStatus?.("resolved");
254
+ } catch (error) {
255
+ if (abort.signal.aborted) return;
256
+ pending.status = "error";
257
+ pending.chip?.setStatus("error");
258
+ // Inline tokens carry no chip: surface the failure on the token element so a
259
+ // dropped context is visible (finalize() silently skips failed payloads).
260
+ pending.reportStatus?.("error");
261
+ // Speak the failure through the ASSERTIVE region — the visual error state
262
+ // (chip/token color) is otherwise silent for screen-reader users.
263
+ (this.opts.announceError ?? this.opts.announce)(
264
+ `Couldn't attach ${pending.ref.label} to context`
265
+ );
266
+ this.opts.mentionConfig.onMentionResolveError?.(pending.item, error);
267
+ this.opts.emit?.("resolve-error", {
268
+ sourceId: pending.source.id,
269
+ itemId: pending.item.id,
270
+ });
271
+ }
272
+ }
273
+
274
+ remove(key: string): void {
275
+ const index = this.mentions.findIndex((m) => m.key === key);
276
+ if (index === -1) return;
277
+ const [pending] = this.mentions.splice(index, 1);
278
+ pending.abort?.abort();
279
+ pending.chip?.el.remove();
280
+ this.updateRowVisibility();
281
+ this.opts.announce(`Removed ${pending.ref.label} from context`);
282
+ }
283
+
284
+ /** Remove the most recently added chip (Backspace on an empty composer). */
285
+ removeLast(): boolean {
286
+ const last = this.mentions[this.mentions.length - 1];
287
+ if (!last) return false;
288
+ this.remove(last.key);
289
+ return true;
290
+ }
291
+
292
+ clear(): void {
293
+ for (const m of this.mentions) {
294
+ m.abort?.abort();
295
+ m.chip?.el.remove();
296
+ }
297
+ this.mentions.length = 0;
298
+ this.updateRowVisibility();
299
+ }
300
+
301
+ /**
302
+ * Gather everything needed at submit. Returns refs synchronously (for the
303
+ * echoed bubble) plus an async `finalize()` that resolves the mentions and
304
+ * returns the merged bundle, dropping failures.
305
+ *
306
+ * Ownership transfer: the pending mentions are DETACHED from the manager here
307
+ * (removed from the list, chip DOM cleared) WITHOUT aborting their in-flight
308
+ * select-time resolves. So the post-submit `clear()` the UI calls is a no-op
309
+ * that can't abort them, and `finalize()` awaits each existing resolve instead
310
+ * of firing a duplicate fetch. `composerText` is captured now for submit-time
311
+ * sources, since the composer is cleared right after.
312
+ */
313
+ collectForSubmit(): { refs: AgentWidgetContextMentionRef[]; finalize: () => Promise<MentionSubmitBundle> } {
314
+ const snapshot = [...this.mentions];
315
+ const refs = snapshot.map((m) => m.ref);
316
+ const composerText = this.opts.getComposerText();
317
+
318
+ // Detach: empty the composer chip row without aborting resolves.
319
+ for (const m of snapshot) m.chip?.el.remove();
320
+ this.mentions.length = 0;
321
+ this.updateRowVisibility();
322
+
323
+ const finalize = async (): Promise<MentionSubmitBundle> => {
324
+ // Resolve every mention concurrently: submit-deferred sources resolve now;
325
+ // select-time sources reuse their in-flight promise (never re-fetched).
326
+ const payloads = await Promise.all(
327
+ snapshot.map(async (m): Promise<AgentWidgetContextMentionPayload | null> => {
328
+ try {
329
+ if (m.source.resolveOn === "submit") {
330
+ return await m.source.resolve(
331
+ m.item,
332
+ this.buildResolveContext(new AbortController().signal, m.args, composerText)
333
+ );
334
+ }
335
+ if (m.resolvePromise) await m.resolvePromise;
336
+ return m.payload ?? null; // null → select-resolve already failed/dropped
337
+ } catch (error) {
338
+ // A throwing host `onMentionResolveError` must not reject the whole
339
+ // bundle — guard the callback so one bad item just drops.
340
+ try {
341
+ this.opts.mentionConfig.onMentionResolveError?.(m.item, error);
342
+ } catch (cbError) {
343
+ if (typeof console !== "undefined") {
344
+ console.warn("[Persona] onMentionResolveError callback threw", cbError);
345
+ }
346
+ }
347
+ this.opts.emit?.("resolve-error", {
348
+ sourceId: m.source.id,
349
+ itemId: m.item.id,
350
+ });
351
+ return null; // drop and still send
352
+ }
353
+ })
354
+ );
355
+
356
+ const blocks: string[] = [];
357
+ const contentParts: ContentPart[] = [];
358
+ const context: Record<string, Record<string, unknown>> = {};
359
+ // Assemble in original selection order (mentions-first block), deduping the
360
+ // resolved payload by (source, item): inline mode allows the same item to
361
+ // appear as multiple prose tokens (Slack/Cursor/Claude), but its content
362
+ // must reach the LLM once. `refs` (returned above) still lists every token
363
+ // so the sent bubble renders them all — only the bundle is deduped. The
364
+ // dedupe claim is taken AFTER the null-payload skip, so if one duplicate's
365
+ // resolve failed but another succeeded, the successful one still wins.
366
+ const contributed = new Set<string>();
367
+ for (let i = 0; i < snapshot.length; i++) {
368
+ const m = snapshot[i];
369
+ const payload = payloads[i];
370
+ if (!payload) continue;
371
+ const dedupeKey = mentionKey(m.source.id, m.item.id);
372
+ if (contributed.has(dedupeKey)) continue;
373
+ contributed.add(dedupeKey);
374
+ if (payload.llmAppend && payload.llmAppend.trim()) {
375
+ // Format each block here (the mention layer owns `mentionConfig`); the
376
+ // block index is its position among contributed blocks so `"document"`
377
+ // numbering and the function form stay 0-based and gap-free. A throwing
378
+ // host `llmFormat` must drop only this block, not reject the bundle.
379
+ try {
380
+ blocks.push(
381
+ formatMentionBlock(
382
+ { label: m.ref.label, text: payload.llmAppend, ref: m.ref, item: m.item },
383
+ blocks.length,
384
+ this.opts.mentionConfig.llmFormat
385
+ )
386
+ );
387
+ } catch (error) {
388
+ if (typeof console !== "undefined") {
389
+ console.warn("[Persona] context-mention llmFormat threw", error);
390
+ }
391
+ }
392
+ }
393
+ if (payload.contentParts?.length) {
394
+ contentParts.push(...payload.contentParts);
395
+ }
396
+ if (payload.context) {
397
+ (context[m.source.id] ??= {})[m.item.id] = payload.context;
398
+ }
399
+ }
400
+
401
+ return { blocks, contentParts, context };
402
+ };
403
+
404
+ return { refs, finalize };
405
+ }
406
+
407
+ private updateRowVisibility(): void {
408
+ this.opts.contextRow.style.display = this.mentions.length > 0 ? "flex" : "none";
409
+ }
410
+ }