@runtypelabs/persona 4.8.0 → 4.10.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 (130) hide show
  1. package/README.md +14 -1
  2. package/dist/animations/glyph-cycle.d.cts +1 -1
  3. package/dist/animations/glyph-cycle.d.ts +1 -1
  4. package/dist/animations/{types-BsZtXPKK.d.cts → types-4ROVJ1gA.d.cts} +42 -0
  5. package/dist/animations/{types-BsZtXPKK.d.ts → types-4ROVJ1gA.d.ts} +42 -0
  6. package/dist/animations/wipe.d.cts +1 -1
  7. package/dist/animations/wipe.d.ts +1 -1
  8. package/dist/chunk-IO5VVUKP.js +3 -0
  9. package/dist/chunk-IPVK3KOM.js +1 -0
  10. package/dist/chunk-UPO4GUFC.js +1 -0
  11. package/dist/codegen.cjs +6 -6
  12. package/dist/codegen.js +8 -8
  13. package/dist/context-mentions-7S5KVUTG.js +169 -0
  14. package/dist/context-mentions-inline-TTCN7ZM2.js +4 -0
  15. package/dist/context-mentions-inline.cjs +4 -0
  16. package/dist/context-mentions-inline.d.cts +203 -0
  17. package/dist/context-mentions-inline.d.ts +203 -0
  18. package/dist/context-mentions-inline.js +4 -0
  19. package/dist/context-mentions.cjs +295 -0
  20. package/dist/context-mentions.d.cts +7025 -0
  21. package/dist/context-mentions.d.ts +7025 -0
  22. package/dist/context-mentions.js +295 -0
  23. package/dist/index.cjs +72 -64
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.cts +634 -3
  26. package/dist/index.d.ts +634 -3
  27. package/dist/index.global.js +59 -51
  28. package/dist/index.global.js.map +1 -1
  29. package/dist/index.js +64 -56
  30. package/dist/index.js.map +1 -1
  31. package/dist/launcher.global.js +2 -2
  32. package/dist/launcher.global.js.map +1 -1
  33. package/dist/plugin-kit.cjs +1 -1
  34. package/dist/plugin-kit.d.cts +17 -0
  35. package/dist/plugin-kit.d.ts +17 -0
  36. package/dist/plugin-kit.js +1 -1
  37. package/dist/smart-dom-reader.cjs +17 -16
  38. package/dist/smart-dom-reader.d.cts +507 -1
  39. package/dist/smart-dom-reader.d.ts +507 -1
  40. package/dist/smart-dom-reader.js +17 -16
  41. package/dist/theme-editor-preview.cjs +236 -57
  42. package/dist/theme-editor-preview.d.cts +485 -1
  43. package/dist/theme-editor-preview.d.ts +485 -1
  44. package/dist/theme-editor-preview.js +53 -47
  45. package/dist/theme-editor.cjs +7 -7
  46. package/dist/theme-editor.d.cts +473 -0
  47. package/dist/theme-editor.d.ts +473 -0
  48. package/dist/theme-editor.js +5 -5
  49. package/dist/theme-reference.cjs +1 -1
  50. package/dist/theme-reference.d.cts +2 -0
  51. package/dist/theme-reference.d.ts +2 -0
  52. package/dist/widget.css +1 -1
  53. package/package.json +15 -3
  54. package/src/client.test.ts +69 -0
  55. package/src/client.ts +65 -51
  56. package/src/components/artifact-pane.test.ts +47 -0
  57. package/src/components/artifact-pane.ts +25 -2
  58. package/src/components/composer-parts.ts +3 -12
  59. package/src/components/context-mention-button.test.ts +70 -0
  60. package/src/components/context-mention-button.ts +82 -0
  61. package/src/components/context-mention-chip.ts +134 -0
  62. package/src/components/context-mention-menu.test.ts +508 -0
  63. package/src/components/context-mention-menu.ts +0 -0
  64. package/src/components/message-bubble.test.ts +175 -0
  65. package/src/components/message-bubble.ts +177 -19
  66. package/src/components/panel.ts +7 -10
  67. package/src/context-mentions-bundle.test.ts +163 -0
  68. package/src/context-mentions-entry.ts +185 -0
  69. package/src/context-mentions-inline-entry.test.ts +136 -0
  70. package/src/context-mentions-inline-entry.ts +226 -0
  71. package/src/context-mentions-inline-loader.test.ts +30 -0
  72. package/src/context-mentions-inline-loader.ts +36 -0
  73. package/src/context-mentions-inline.ts +15 -0
  74. package/src/context-mentions-loader.ts +32 -0
  75. package/src/context-mentions.ts +16 -0
  76. package/src/index-core.ts +27 -0
  77. package/src/index-global.ts +51 -0
  78. package/src/markdown-parsers-loader.ts +35 -44
  79. package/src/plugin-kit.test.ts +40 -0
  80. package/src/plugin-kit.ts +39 -5
  81. package/src/runtime/init-update-reset.test.ts +81 -0
  82. package/src/runtime/init.test.ts +62 -0
  83. package/src/runtime/init.ts +7 -14
  84. package/src/session.mentions.test.ts +175 -0
  85. package/src/session.test.ts +52 -4
  86. package/src/session.ts +121 -5
  87. package/src/smart-dom-reader.test.ts +129 -2
  88. package/src/smart-dom-reader.ts +127 -1
  89. package/src/styles/context-mention-menu-css.ts +176 -0
  90. package/src/styles/widget.css +243 -126
  91. package/src/theme-editor/sections.ts +3 -3
  92. package/src/types/theme.ts +2 -0
  93. package/src/types.ts +542 -0
  94. package/src/ui.artifact-pane-gating.test.ts +11 -1
  95. package/src/ui.attachments-drop.test.ts +90 -0
  96. package/src/ui.header-update-stability.test.ts +149 -0
  97. package/src/ui.launcher-update-merge.test.ts +83 -0
  98. package/src/ui.mention-submit.test.ts +235 -0
  99. package/src/ui.send-button-stream-update.test.ts +69 -0
  100. package/src/ui.ts +379 -84
  101. package/src/utils/chunk-loader.test.ts +97 -0
  102. package/src/utils/chunk-loader.ts +88 -0
  103. package/src/utils/composer-contenteditable.test.ts +507 -0
  104. package/src/utils/composer-contenteditable.ts +626 -0
  105. package/src/utils/composer-document.test.ts +280 -0
  106. package/src/utils/composer-document.ts +293 -0
  107. package/src/utils/composer-history.test.ts +35 -7
  108. package/src/utils/composer-history.ts +30 -20
  109. package/src/utils/composer-input.ts +159 -0
  110. package/src/utils/config-merge.test.ts +131 -0
  111. package/src/utils/config-merge.ts +61 -0
  112. package/src/utils/context-mention-controller.test.ts +1215 -0
  113. package/src/utils/context-mention-controller.ts +1186 -0
  114. package/src/utils/context-mention-manager.test.ts +422 -0
  115. package/src/utils/context-mention-manager.ts +410 -0
  116. package/src/utils/context-mention-orchestrator.test.ts +538 -0
  117. package/src/utils/context-mention-orchestrator.ts +348 -0
  118. package/src/utils/live-region.test.ts +108 -0
  119. package/src/utils/live-region.ts +94 -0
  120. package/src/utils/mention-channels.ts +63 -0
  121. package/src/utils/mention-llm-format.test.ts +91 -0
  122. package/src/utils/mention-llm-format.ts +79 -0
  123. package/src/utils/mention-matcher.test.ts +86 -0
  124. package/src/utils/mention-matcher.ts +221 -0
  125. package/src/utils/mention-token.ts +72 -0
  126. package/src/utils/mention-trigger.test.ts +155 -0
  127. package/src/utils/mention-trigger.ts +156 -0
  128. package/src/utils/theme.test.ts +54 -4
  129. package/src/utils/theme.ts +6 -3
  130. package/src/utils/tokens.ts +27 -11
package/src/types.ts CHANGED
@@ -95,6 +95,500 @@ export type AgentWidgetContextProvider = (
95
95
  | void
96
96
  | Promise<Record<string, unknown> | void>;
97
97
 
98
+ // ============================================================================
99
+ // Context Mentions (composer @-mentions / context attachment)
100
+ // ============================================================================
101
+ //
102
+ // Two entry points (a visible affordance button and the `@` trigger) over one
103
+ // shared menu. Selecting a mention strips the typed `@query` and adds a compact
104
+ // removable pill chip to the composer context row; the resolved content reaches
105
+ // the model via `llmAppend`/`contentParts` (default, no backend changes) or the
106
+ // opt-in structured `context` channel. See `docs/context-mentions-plan.md`.
107
+
108
+ /** A single selectable item in the mention menu (host-provided). */
109
+ export type AgentWidgetContextMentionItem = {
110
+ /** Stable key within its source. */
111
+ id: string;
112
+ /** Shown in the menu and on the chip (e.g. "App.tsx"). */
113
+ label: string;
114
+ /** Subtitle line in the menu. */
115
+ description?: string;
116
+ /** Lucide icon name. */
117
+ iconName?: string;
118
+ /**
119
+ * Accent color for this item's inline token (`display: "inline"`), any CSS
120
+ * color. Sets `--persona-mention-token-accent` on the token, tinting its pill
121
+ * background + icon/label — the per-item/per-type color knob (Slack/Linear
122
+ * style). Prefer setting the same color across a source's items for a "color
123
+ * per type" look, or theme by `[data-mention-source]` in CSS instead. Ignored
124
+ * in chip mode.
125
+ */
126
+ color?: string;
127
+ /** Group header override; defaults to the source's `label`. */
128
+ group?: string;
129
+ /** Optional recency/popularity hint the default matcher boosts on. */
130
+ recencyScore?: number;
131
+ /**
132
+ * SKILLS / SLASH-COMMANDS. When set, this item is a COMMAND (a verb) rather
133
+ * than a context mention (a noun), so selecting it dispatches a behavior
134
+ * instead of adding a context chip. Absent for ordinary mentions.
135
+ *
136
+ * - `"prompt"`: a prompt macro. `resolve()` returns `insertText` (or
137
+ * `llmAppend`) which is written into the composer (see `insertMode`);
138
+ * `submitOnSelect` optionally sends it immediately. No chip.
139
+ * - `"action"`: a client action. `action()` runs in the browser; no resolve,
140
+ * no chip, no message sent (e.g. `/clear`, `/theme dark`).
141
+ * - `"server"`: a server-invoked skill. Routed like a `resolveOn:"submit"`
142
+ * mention whose `resolve().context` reaches the backend via request
143
+ * `context.mentions.<sourceId>.<itemId>` (a flow/agent reads it).
144
+ */
145
+ command?: "prompt" | "action" | "server";
146
+ /**
147
+ * `command:"action"` handler. Receives the parsed `args` (text after the
148
+ * command name), plus a small `composer` capability + `config`/`messages`.
149
+ * Short-circuits the mention flow (no chip, no send). Ignored otherwise.
150
+ */
151
+ action?: (ctx: AgentWidgetContextMentionCommandContext) => void | Promise<void>;
152
+ /**
153
+ * `command:"prompt"` insertion mode. `"replace"` (default) swaps the whole
154
+ * composer value for the resolved text; `"insert-at-caret"` replaces only the
155
+ * `/token` span, keeping surrounding prose. Ignored otherwise.
156
+ */
157
+ insertMode?: "replace" | "insert-at-caret";
158
+ /**
159
+ * When true, a `command:"prompt"` submits the composer immediately after
160
+ * inserting its text (a one-shot macro). Ignored for other kinds.
161
+ */
162
+ submitOnSelect?: boolean;
163
+ /**
164
+ * COMMANDS only. Ghost hint for a free-text argument (e.g. "order id" →
165
+ * `/lookup ‹order id›` in the menu). Its presence switches the command to
166
+ * INLINE COMPLETION (Slack-style): selecting it fills `/name ` into the
167
+ * composer for inline arg entry instead of dispatching on select, and the
168
+ * command is executed at SUBMIT with the typed args. `command:"server"` items
169
+ * always use inline completion (their chip had no way to add an argument);
170
+ * for `"prompt"`/`"action"` items this is opt-in. Ignored for non-commands.
171
+ */
172
+ commandArgsPlaceholder?: string;
173
+ };
174
+
175
+ /**
176
+ * Capability object handed to a `command:"action"` handler. The widget exposes
177
+ * only composer-local operations; broader actions (clear the transcript, change
178
+ * theme) are wired by the host via closures over its own controller.
179
+ */
180
+ export type AgentWidgetContextMentionComposerCapability = {
181
+ /** Current composer text. */
182
+ getValue: () => string;
183
+ /** Replace composer text (fires `input`, moves caret to end, refocuses). */
184
+ setValue: (value: string) => void;
185
+ /** Submit the composer (same path as pressing send). */
186
+ submit: () => void;
187
+ };
188
+
189
+ /** Context passed to a `command:"action"` handler. */
190
+ export type AgentWidgetContextMentionCommandContext = {
191
+ /** Text typed after the command name (e.g. `staging` for `/deploy staging`). */
192
+ args: string;
193
+ config: AgentWidgetConfig;
194
+ messages: AgentWidgetMessage[];
195
+ composer: AgentWidgetContextMentionComposerCapability;
196
+ };
197
+
198
+ /** Reference stored on a sent message for transcript fidelity + chip rendering. */
199
+ export type AgentWidgetContextMentionRef = {
200
+ sourceId: string;
201
+ itemId: string;
202
+ label: string;
203
+ iconName?: string;
204
+ /** Accent color for the inline token (see `AgentWidgetContextMentionItem.color`). */
205
+ color?: string;
206
+ };
207
+
208
+ /**
209
+ * One ordered segment of a sent message's prose, for inline mention rendering
210
+ * (`contextMentions.display: "inline"`). Text runs and atomic mention tokens
211
+ * interleave in document order so the sent bubble re-renders `@tokens` in place
212
+ * and composer history can round-trip them. Display/transcript concern only —
213
+ * the model still sees resolved bodies via `llmContent`/`contentParts`.
214
+ */
215
+ export type AgentWidgetContentSegment =
216
+ | { kind: "text"; text: string }
217
+ | { kind: "mention"; ref: AgentWidgetContextMentionRef };
218
+
219
+ /**
220
+ * The payload a source returns from `resolve()` for one selected item.
221
+ * Lead with `llmAppend` — it is the only channel guaranteed to reach the model
222
+ * with no backend changes.
223
+ */
224
+ export type AgentWidgetContextMentionPayload = {
225
+ /**
226
+ * PRIMARY, model-visible channel. Appended to this user message's LLM content
227
+ * so the model actually sees the context with no backend changes.
228
+ */
229
+ llmAppend?: string;
230
+ /**
231
+ * `command:"prompt"` macros only. Text written into the COMPOSER on select
232
+ * (see `AgentWidgetContextMentionItem.insertMode`) — distinct from the hidden
233
+ * `llmAppend` context channel. Falls back to `llmAppend` when omitted.
234
+ */
235
+ insertText?: string;
236
+ /** Extra content parts (e.g. file text / image) via the multi-modal path. */
237
+ contentParts?: ContentPart[];
238
+ /**
239
+ * OPT-IN structured channel. Merged into request `context` under
240
+ * `mentions.<sourceId>.<itemId>` for flows/agents that read it. NOTE: ambient
241
+ * `context` is NOT guaranteed to reach the model — use `llmAppend` if you need
242
+ * the model to see it.
243
+ */
244
+ context?: Record<string, unknown>;
245
+ };
246
+
247
+ /**
248
+ * One resolved mention, handed to `contextMentions.llmFormat` to build the
249
+ * model-visible block. `text` is the source's `llmAppend`; `ref`/`item` carry
250
+ * the selection identity/metadata for a custom formatter (e.g. a source-specific
251
+ * wrapper or a path from `item`).
252
+ */
253
+ export type AgentWidgetMentionLlmEntry = {
254
+ /** The mention's display label (from `ref.label`). */
255
+ label: string;
256
+ /** The resolved model-visible body (the source's `llmAppend`). */
257
+ text: string;
258
+ ref: AgentWidgetContextMentionRef;
259
+ item: AgentWidgetContextMentionItem;
260
+ };
261
+
262
+ export type AgentWidgetContextMentionSearchContext = {
263
+ messages: AgentWidgetMessage[];
264
+ config: AgentWidgetConfig;
265
+ /** Aborts when the next keystroke supersedes this in-flight search. */
266
+ signal: AbortSignal;
267
+ };
268
+
269
+ export type AgentWidgetContextMentionResolveContext =
270
+ AgentWidgetContextMentionSearchContext & {
271
+ /** Plain-text composer value at resolve time. */
272
+ composerText: string;
273
+ /**
274
+ * For `command` items: the text typed after the command name (e.g.
275
+ * `"123"` for `/lookup 123`). Empty string for ordinary mentions.
276
+ */
277
+ args: string;
278
+ };
279
+
280
+ export type AgentWidgetContextMentionSource = {
281
+ id: string;
282
+ /** Group header shown in the menu. */
283
+ label: string;
284
+ /**
285
+ * Filter items for the current `@query` (empty query → recent/popular).
286
+ * Implement server-side search for large/remote sets, or return all items and
287
+ * let the widget rank them with `defaultMentionFilter`
288
+ * (see `createStaticMentionSource` for the common case).
289
+ */
290
+ search: (
291
+ query: string,
292
+ ctx: AgentWidgetContextMentionSearchContext
293
+ ) =>
294
+ | AgentWidgetContextMentionItem[]
295
+ | Promise<AgentWidgetContextMentionItem[]>;
296
+ /**
297
+ * Fetch the payload for dispatch. Called once when the user SELECTS the item
298
+ * (eagerly, cached on the chip, abortable on removal) unless `resolveOn` is
299
+ * `"submit"`. A throw/abort drops the mention via `onMentionResolveError`.
300
+ */
301
+ resolve: (
302
+ item: AgentWidgetContextMentionItem,
303
+ ctx: AgentWidgetContextMentionResolveContext
304
+ ) =>
305
+ | AgentWidgetContextMentionPayload
306
+ | Promise<AgentWidgetContextMentionPayload>;
307
+ /**
308
+ * When to call `resolve()`:
309
+ * - `"select"` (default): eager on pick; parallelizes the fetch with the user
310
+ * finishing their sentence so submit stays instant.
311
+ * - `"submit"`: defer to send time, for time-sensitive sources (e.g. current
312
+ * page state that may change between select and send).
313
+ * @default "select"
314
+ */
315
+ resolveOn?: "select" | "submit";
316
+ /**
317
+ * COMMAND sources only. Resolve a `/name` (the first token of a command line)
318
+ * to its menu item, for submit-time inline-command dispatch and the menu's
319
+ * "typing args now" suppression. `createSlashCommandsSource` sets this; plain
320
+ * `@`-mention sources leave it undefined.
321
+ */
322
+ matchCommand?: (name: string) => AgentWidgetContextMentionItem | undefined;
323
+ };
324
+
325
+ /** Context passed to the optional `renderMentionMenu` override. */
326
+ export type AgentWidgetContextMentionMenuRenderContext = {
327
+ /** Current trigger query ("" when freshly opened). */
328
+ query: string;
329
+ /** Grouped, ranked, capped results ready to paint. */
330
+ groups: {
331
+ source: AgentWidgetContextMentionSource;
332
+ items: AgentWidgetContextMentionItem[];
333
+ }[];
334
+ /** Per-source async state for loading/empty/error rendering, keyed by source id. */
335
+ status: Record<string, "loading" | "ready" | "empty" | "error">;
336
+ /** Index into the flat keyboard-traversal order. */
337
+ activeIndex: number;
338
+ /** Commit a selection (strips `@query`, adds chip, resolves on select). */
339
+ select: (item: AgentWidgetContextMentionItem) => void;
340
+ /** Close the menu, keeping a literal `@`. */
341
+ close: () => void;
342
+ };
343
+
344
+ /** Context passed to the optional `renderMentionItem` per-row override. */
345
+ export type AgentWidgetContextMentionItemRenderContext = {
346
+ /** The item to render. */
347
+ item: AgentWidgetContextMentionItem;
348
+ /** The source this item came from (its group). */
349
+ source: AgentWidgetContextMentionSource;
350
+ /** Current trigger query, for highlighting matches. */
351
+ query: string;
352
+ /** True when this row is the keyboard-highlighted one at render time. */
353
+ active: boolean;
354
+ /** Position in the flat keyboard-traversal order. */
355
+ index: number;
356
+ };
357
+
358
+ /** Context passed to the optional `renderMentionChip` override. */
359
+ export type AgentWidgetContextMentionChipRenderContext = {
360
+ ref: AgentWidgetContextMentionRef;
361
+ /** Resolve-on-select lifecycle for spinner→ready→error UI. */
362
+ status: "resolving" | "ready" | "error";
363
+ /**
364
+ * The resolved payload, once available (select-resolved sources only). Use it
365
+ * to preview already-fetched content on hover. Undefined while `status` is
366
+ * `"resolving"`, on `"error"`, and for `resolveOn:"submit"` sources (which
367
+ * resolve at send time, not while the chip sits in the composer — for those,
368
+ * `ref.itemId` carries the source key you can re-read on demand, e.g. a CSS
369
+ * selector for the smart-dom source).
370
+ */
371
+ payload?: AgentWidgetContextMentionPayload;
372
+ /** Remove the chip; aborts any in-flight resolve. */
373
+ remove: () => void;
374
+ };
375
+
376
+ /**
377
+ * Where a trigger char is allowed to open the menu:
378
+ * - `"anywhere"` (default): after any whitespace or start (the `@` rule).
379
+ * - `"line-start"`: only when the char before the trigger is a newline or the
380
+ * start of input — the natural rule for `/` slash-commands.
381
+ * - `"input-start"`: only at index 0 of the composer.
382
+ */
383
+ export type AgentWidgetMentionTriggerPosition =
384
+ | "anywhere"
385
+ | "line-start"
386
+ | "input-start";
387
+
388
+ /**
389
+ * A SECOND (or Nth) trigger channel sharing the same menu engine — e.g. `/` for
390
+ * slash-commands alongside `@` for context. The legacy `trigger`/`sources`
391
+ * fields form the implicit first channel; `triggers[]` adds more. At any caret
392
+ * at most one channel is active, so all channels drive one menu/controller.
393
+ */
394
+ export type AgentWidgetMentionTriggerChannel = {
395
+ /** Trigger character (e.g. "/"). */
396
+ trigger: string;
397
+ /** Sources for this channel (searched only when this trigger is active). */
398
+ sources: AgentWidgetContextMentionSource[];
399
+ /** Where the trigger may open. @default "anywhere" */
400
+ triggerPosition?: AgentWidgetMentionTriggerPosition;
401
+ /**
402
+ * Let the query span spaces so a command can take ARGS (`/deploy staging`).
403
+ * A newline still ends it. Pair with `triggerPosition: "line-start"` for
404
+ * slash-commands. @default false
405
+ */
406
+ allowSpaces?: boolean;
407
+ /** Show a composer affordance button for this channel. @default false */
408
+ showButton?: boolean;
409
+ /** Icon for this channel's affordance button. */
410
+ buttonIconName?: string;
411
+ /** Tooltip / aria-label for this channel's affordance button. */
412
+ buttonTooltipText?: string;
413
+ /** Placeholder for this channel's picker search field. */
414
+ searchPlaceholder?: string;
415
+ };
416
+
417
+ export type AgentWidgetContextMentionConfig = {
418
+ /** @default false */
419
+ enabled?: boolean;
420
+ /**
421
+ * Show the visible composer affordance button (the discoverable entry point).
422
+ * Strongly recommended; the bare `@` trigger alone is hard to discover for
423
+ * non-technical users. @default true
424
+ */
425
+ showButton?: boolean;
426
+ /**
427
+ * Icon for the affordance button (any registered Lucide name). Defaults to a
428
+ * "+" signifier — the consumer-recognized "add context" affordance — rather
429
+ * than an "@" glyph, which reads as power-user. Use `"at-sign"` on a
430
+ * developer/power-user surface. @default "plus"
431
+ */
432
+ buttonIconName?: string;
433
+ /** Tooltip / aria-label for the affordance button. @default "Add context" */
434
+ buttonTooltipText?: string;
435
+ /**
436
+ * Placeholder for the picker's search field, shown when the menu is opened
437
+ * from the affordance button (a Cursor/Copilot-style picker that inserts no
438
+ * trigger character). The typed-trigger path keeps its query in the textarea
439
+ * and never shows this field. @default "Search context…"
440
+ */
441
+ searchPlaceholder?: string;
442
+ /** Trigger character. @default "@" */
443
+ trigger?: string;
444
+ /**
445
+ * Where the primary `trigger` may open the menu. @default "anywhere"
446
+ */
447
+ triggerPosition?: AgentWidgetMentionTriggerPosition;
448
+ /**
449
+ * ADDITIONAL trigger channels sharing this engine — e.g. a `/` slash-command
450
+ * channel next to `@` context. Each channel has its own trigger char,
451
+ * sources, and position rule. See `AgentWidgetMentionTriggerChannel`.
452
+ */
453
+ triggers?: AgentWidgetMentionTriggerChannel[];
454
+ /** Max mentions per message. @default 8 */
455
+ maxMentions?: number;
456
+ /** Max items shown per source group before "keep typing to narrow". @default 6 */
457
+ maxItemsPerGroup?: number;
458
+ /**
459
+ * Debounce for ASYNC source search only (ms). Synchronous sources and the
460
+ * menu's first paint are never debounced. @default 150
461
+ */
462
+ searchDebounceMs?: number;
463
+ /** Registered mention sources (search + resolve). */
464
+ sources: AgentWidgetContextMentionSource[];
465
+ /** Chip icon fallback when a source/item omits one. @default "at-sign" */
466
+ chipIconName?: string;
467
+ /**
468
+ * How each resolved mention's `llmAppend` text is wrapped into a delimited
469
+ * block before it is prepended to the model-visible message content. Blocks
470
+ * are joined with a blank line and the typed prose follows last.
471
+ *
472
+ * - `"fenced"` (default): a fenced code block with the label in the info
473
+ * string, e.g. selecting `App.tsx` emits:
474
+ * ```text
475
+ * ```App.tsx
476
+ * <file body>
477
+ * ```
478
+ * ```
479
+ * The fence auto-escalates (four+ backticks) when the body itself contains a
480
+ * fence, so a body can never terminate its own wrapper.
481
+ * - `"document"`: Anthropic's long-context document shape, e.g.:
482
+ * ```text
483
+ * <document index="1">
484
+ * <source>App.tsx</source>
485
+ * <document_content>
486
+ * <file body>
487
+ * </document_content>
488
+ * </document>
489
+ * ```
490
+ * Prefer this for prose/document-shaped sources per Anthropic's long-context
491
+ * guidance (indexed `<document>` blocks improve grounding and citation). A
492
+ * body containing the literal `</document_content>` closing tag falls back to
493
+ * the fenced block for that entry to avoid a broken XML boundary.
494
+ * - function form: fully owns the block for one mention — its return string is
495
+ * used verbatim (no extra wrapping), joined with the other blocks. `index` is
496
+ * the 0-based position among the message's mention blocks.
497
+ *
498
+ * @default "fenced"
499
+ */
500
+ llmFormat?:
501
+ | "fenced"
502
+ | "document"
503
+ | ((entry: AgentWidgetMentionLlmEntry, index: number) => string);
504
+ /**
505
+ * How `@` selections appear in the composer and sent user bubble.
506
+ *
507
+ * - `"chip"` (default): strip `@query` on select; show a compact pill in the
508
+ * composer context row; `content` is prose-only.
509
+ * - `"inline"`: insert an atomic styled token in the sentence (Slack/Linear/
510
+ * Cursor style), backed by a contenteditable composer. Slash-command channels
511
+ * (`triggers[]` with `/`) stay inline-text regardless — they already use the
512
+ * `/name args` model, not chips. `renderMentionChip` is ignored for `@`
513
+ * mentions in this mode.
514
+ *
515
+ * The resolved-context channel (`llmContent`/`contentParts`) is unchanged
516
+ * across both modes — inline tokens are a display concern.
517
+ *
518
+ * @default "chip"
519
+ */
520
+ display?: "chip" | "inline";
521
+ /**
522
+ * Fired when a pick is rejected: it duplicates an existing chip in chip display
523
+ * (`"duplicate"` — inline display allows the same item as multiple tokens, so it
524
+ * never rejects for this reason), hits `maxMentions` (`"limit"`), or (inline
525
+ * display) the composer text changed between parse and commit so the token could
526
+ * not be inserted (`"stale"`).
527
+ */
528
+ onMentionRejected?: (
529
+ item: AgentWidgetContextMentionItem,
530
+ reason: "duplicate" | "limit" | "stale"
531
+ ) => void;
532
+ /**
533
+ * A `resolve()` that throws/aborts: the mention is dropped and the message
534
+ * still sends. Surface a non-blocking notice here if desired.
535
+ */
536
+ onMentionResolveError?: (
537
+ item: AgentWidgetContextMentionItem,
538
+ error: unknown
539
+ ) => void;
540
+ /**
541
+ * MID-LEVEL render override for the autocomplete menu. The widget still owns
542
+ * trigger detection, search, debounce/abort, keyboard nav, and positioning;
543
+ * you only supply the menu markup and drive selection through `ctx`. Omit to
544
+ * use the built-in grouped menu. @default built-in
545
+ */
546
+ renderMentionMenu?: (
547
+ ctx: AgentWidgetContextMentionMenuRenderContext
548
+ ) => HTMLElement;
549
+ /**
550
+ * NARROW render override for a single result row's INNER content, keeping the
551
+ * built-in menu chrome (group headers, loading/empty/error, keyboard nav, the
552
+ * "keep typing" hint). The widget keeps the `role="option"` wrapper and its
553
+ * click/hover wiring; you return only the row's contents. For full menu
554
+ * control use `renderMentionMenu` instead. Highlight is CSS-driven via the
555
+ * wrapper's `[data-active]` attribute, so this is called once per render, not
556
+ * on every arrow key. Ignored when `renderMentionMenu` is set. @default built-in
557
+ */
558
+ renderMentionItem?: (
559
+ ctx: AgentWidgetContextMentionItemRenderContext
560
+ ) => HTMLElement;
561
+ /**
562
+ * MID-LEVEL render override for a single context chip. Return your own pill
563
+ * (must include an accessible remove control wired to `ctx.remove`); reflect
564
+ * `ctx.status` for resolve-on-select. Omit to use the built-in compact pill.
565
+ * @default built-in
566
+ */
567
+ renderMentionChip?: (
568
+ ctx: AgentWidgetContextMentionChipRenderContext
569
+ ) => HTMLElement;
570
+ /**
571
+ * INLINE display only (`display: "inline"`). Render override for a single inline
572
+ * mention TOKEN — the atomic styled node that sits in the sentence, in both the
573
+ * contenteditable composer and the read-only sent bubble. Return your own
574
+ * element (e.g. a fully custom colored pill); the composer marks it
575
+ * `contenteditable="false"` and stamps `data-mention-id` so it stays atomic.
576
+ * Omit to use the built-in pill (icon + `@label`, tinted by
577
+ * `--persona-mention-token-accent` / `ref.color`, themeable per source via the
578
+ * `[data-mention-source]` attribute). Ignored in chip mode.
579
+ * @default built-in
580
+ */
581
+ renderMentionToken?: (
582
+ ctx: AgentWidgetContextMentionTokenRenderContext
583
+ ) => HTMLElement;
584
+ };
585
+
586
+ /** Render context for `renderMentionToken`. `readonly` is true in the sent bubble. */
587
+ export type AgentWidgetContextMentionTokenRenderContext = {
588
+ ref: AgentWidgetContextMentionRef;
589
+ readonly: boolean;
590
+ };
591
+
98
592
  export type AgentWidgetRequestPayloadMessage = {
99
593
  role: AgentWidgetMessageRole;
100
594
  content: MessageContent;
@@ -4473,6 +4967,18 @@ export type AgentWidgetConfig = {
4473
4967
  }) => string;
4474
4968
  plugins?: AgentWidgetPlugin[];
4475
4969
  contextProviders?: AgentWidgetContextProvider[];
4970
+ /**
4971
+ * Context mentions: let users explicitly pull external context into a turn by
4972
+ * typing `@` or clicking a visible composer button, both opening one shared
4973
+ * searchable menu of host-provided sources. Selecting a mention adds a
4974
+ * removable pill chip; resolved content reaches the model via `llmAppend`
4975
+ * (default) or the opt-in structured `context` channel.
4976
+ *
4977
+ * Disabled by default. When enabled, the mention runtime is lazy-loaded from a
4978
+ * sibling chunk on first use, so sites that leave it off pay no bundle cost.
4979
+ * See `docs/context-mentions-plan.md`.
4980
+ */
4981
+ contextMentions?: AgentWidgetContextMentionConfig;
4476
4982
  requestMiddleware?: AgentWidgetRequestMiddleware;
4477
4983
  actionParsers?: AgentWidgetActionParser[];
4478
4984
  actionHandlers?: AgentWidgetActionHandler[];
@@ -4925,6 +5431,23 @@ export type AgentWidgetConfig = {
4925
5431
  loadingIndicator?: AgentWidgetLoadingIndicatorConfig;
4926
5432
  };
4927
5433
 
5434
+ /**
5435
+ * Patch-aware deep-partial of {@link AgentWidgetConfig} accepted by `update()`.
5436
+ * Function types and arrays are preserved whole (not mapped over); plain-object
5437
+ * leaves recurse so a partial patch merges into the live config. See the merge
5438
+ * policy in utils/config-merge.ts (some plain-object fields still replace
5439
+ * wholesale at runtime; that is not encoded at the type level).
5440
+ */
5441
+ export type AgentWidgetConfigPatch = ConfigPatch<AgentWidgetConfig>;
5442
+
5443
+ type ConfigPatch<T> = T extends (...args: never[]) => unknown
5444
+ ? T
5445
+ : T extends readonly unknown[]
5446
+ ? T
5447
+ : T extends object
5448
+ ? { [K in keyof T]?: ConfigPatch<T[K]> }
5449
+ : T;
5450
+
4928
5451
  export type AgentWidgetMessageRole = "user" | "assistant" | "system";
4929
5452
 
4930
5453
  export type AgentWidgetReasoning = {
@@ -5024,6 +5547,25 @@ export type AgentWidgetMessage = {
5024
5547
  * The `content` field contains the text-only representation for display.
5025
5548
  */
5026
5549
  contentParts?: ContentPart[];
5550
+ /**
5551
+ * Context mentions attached to this (user) message, for transcript fidelity +
5552
+ * chip rendering in the sent bubble. Refs only — the resolved payloads are
5553
+ * merged into `llmContent`/`contentParts` (model-visible) at send time.
5554
+ */
5555
+ contextMentions?: AgentWidgetContextMentionRef[];
5556
+ /**
5557
+ * Ordered prose + mention blocks for inline-mode rendering and composer history
5558
+ * recall (`contextMentions.display: "inline"`). Present only on inline-mode user
5559
+ * messages; when omitted the sent bubble falls back to the chip row derived from
5560
+ * `contextMentions`. Display/transcript concern only — not the model channel.
5561
+ */
5562
+ contentSegments?: AgentWidgetContentSegment[];
5563
+ /**
5564
+ * Resolved opt-in structured mention context for this (user) message, merged
5565
+ * into the request `context` under `mentions.<sourceId>.<itemId>` by the
5566
+ * client. Internal: set at send time from each source's `resolve().context`.
5567
+ */
5568
+ mentionContext?: Record<string, unknown>;
5027
5569
  streaming?: boolean;
5028
5570
  variant?: AgentWidgetMessageVariant;
5029
5571
  sequence?: number;
@@ -402,7 +402,17 @@ describe("artifact pane expand toggle (full widget)", () => {
402
402
  expect(mountEl.classList.contains("persona-artifact-expanded")).toBe(true);
403
403
 
404
404
  // Turning the toggle back off hides the button and collapses the pane.
405
- controller.update(base);
405
+ // update() patches recursively now, so omission preserves the prior
406
+ // showExpandToggle: the flag must be set false explicitly to disable it.
407
+ controller.update({
408
+ features: {
409
+ artifacts: {
410
+ enabled: true,
411
+ allowedTypes: ["markdown", "component"],
412
+ layout: { showExpandToggle: false },
413
+ },
414
+ },
415
+ });
406
416
  expect(mountEl.classList.contains("persona-artifact-expanded")).toBe(false);
407
417
  expect(expandBtn(mountEl).classList.contains("persona-hidden")).toBe(true);
408
418
  controller.destroy();
@@ -186,3 +186,93 @@ describe("createAgentExperience attachment file drop", () => {
186
186
  controller.destroy();
187
187
  });
188
188
  });
189
+
190
+ describe("drop overlay live config updates", () => {
191
+ beforeEach(() => {
192
+ window.scrollTo = vi.fn();
193
+ });
194
+
195
+ afterEach(() => {
196
+ document.body.innerHTML = "";
197
+ vi.restoreAllMocks();
198
+ });
199
+
200
+ it("rebuilds the overlay with new dropOverlay values on update() (regression: built once, live updates ignored)", () => {
201
+ const mount = createMount();
202
+ const controller = createAgentExperience(mount, {
203
+ apiUrl: "https://api.example.com/chat",
204
+ launcher: { enabled: false },
205
+ attachments: { enabled: true, dropOverlay: { label: "Drop it", background: "#111111" } },
206
+ });
207
+
208
+ const overlay = () => mount.querySelector<HTMLElement>(".persona-attachment-drop-overlay")!;
209
+ expect(overlay()).not.toBeNull();
210
+ expect(overlay().querySelector(".persona-drop-overlay-label")!.textContent).toBe("Drop it");
211
+ expect(overlay().style.getPropertyValue("--persona-drop-overlay-bg")).toBe("#111111");
212
+
213
+ controller.update({
214
+ attachments: { dropOverlay: { label: "New label", background: "#222222" } },
215
+ });
216
+
217
+ expect(overlay().querySelector(".persona-drop-overlay-label")!.textContent).toBe("New label");
218
+ expect(overlay().style.getPropertyValue("--persona-drop-overlay-bg")).toBe("#222222");
219
+ // Only one overlay after the rebuild.
220
+ expect(mount.querySelectorAll(".persona-attachment-drop-overlay").length).toBe(1);
221
+
222
+ controller.destroy();
223
+ });
224
+
225
+ it("clears overlay styling when dropOverlay is reset via explicit undefined", () => {
226
+ const mount = createMount();
227
+ const controller = createAgentExperience(mount, {
228
+ apiUrl: "https://api.example.com/chat",
229
+ launcher: { enabled: false },
230
+ attachments: { enabled: true, dropOverlay: { label: "Drop it", background: "#111111" } },
231
+ });
232
+
233
+ controller.update({ attachments: { dropOverlay: undefined } });
234
+
235
+ const overlay = mount.querySelector<HTMLElement>(".persona-attachment-drop-overlay")!;
236
+ expect(overlay).not.toBeNull();
237
+ expect(overlay.querySelector(".persona-drop-overlay-label")).toBeNull();
238
+ expect(overlay.style.getPropertyValue("--persona-drop-overlay-bg")).toBe("");
239
+
240
+ controller.destroy();
241
+ });
242
+ });
243
+
244
+ describe("attachment button live config updates", () => {
245
+ beforeEach(() => {
246
+ window.scrollTo = vi.fn();
247
+ });
248
+
249
+ afterEach(() => {
250
+ document.body.innerHTML = "";
251
+ vi.restoreAllMocks();
252
+ });
253
+
254
+ it("re-renders the button icon and tooltip on update() (regression: set once at creation)", () => {
255
+ const mount = createMount();
256
+ const controller = createAgentExperience(mount, {
257
+ apiUrl: "https://api.example.com/chat",
258
+ launcher: { enabled: false },
259
+ attachments: { enabled: true, buttonIconName: "paperclip", buttonTooltipText: "Attach file" },
260
+ });
261
+
262
+ const button = () => mount.querySelector<HTMLButtonElement>(".persona-attachment-button")!;
263
+ expect(button()).not.toBeNull();
264
+ const initialSvg = button().querySelector("svg")!.outerHTML;
265
+
266
+ controller.update({
267
+ attachments: { buttonIconName: "camera", buttonTooltipText: "Add a photo" },
268
+ });
269
+
270
+ expect(button().getAttribute("aria-label")).toBe("Add a photo");
271
+ const updatedSvg = button().querySelector("svg")!.outerHTML;
272
+ expect(updatedSvg).not.toBe(initialSvg);
273
+ const tooltip = button().parentElement!.querySelector(".persona-send-button-tooltip");
274
+ expect(tooltip?.textContent).toBe("Add a photo");
275
+
276
+ controller.destroy();
277
+ });
278
+ });