@runtypelabs/persona 4.14.0 → 4.15.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 (81) hide show
  1. package/README.md +4 -4
  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-DveIaNx6.d.cts → types-BpWGm7_6.d.cts} +17 -0
  5. package/dist/animations/{types-DveIaNx6.d.ts → types-BpWGm7_6.d.ts} +17 -0
  6. package/dist/animations/wipe.d.cts +1 -1
  7. package/dist/animations/wipe.d.ts +1 -1
  8. package/dist/chunk-QJ4LV2KW.js +1 -0
  9. package/dist/codegen.cjs +12 -11
  10. package/dist/codegen.js +14 -13
  11. package/dist/{context-mentions-ONG7A7M6.js → context-mentions-BTSTIBFC.js} +1 -1
  12. package/dist/context-mentions.cjs +11 -8
  13. package/dist/context-mentions.d.cts +241 -16
  14. package/dist/context-mentions.d.ts +241 -16
  15. package/dist/context-mentions.js +11 -8
  16. package/dist/index.cjs +67 -66
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +301 -41
  19. package/dist/index.d.ts +301 -41
  20. package/dist/index.global.js +59 -56
  21. package/dist/index.global.js.map +1 -1
  22. package/dist/index.js +67 -66
  23. package/dist/index.js.map +1 -1
  24. package/dist/launcher.global.js +5 -2
  25. package/dist/launcher.global.js.map +1 -1
  26. package/dist/smart-dom-reader.d.cts +241 -16
  27. package/dist/smart-dom-reader.d.ts +241 -16
  28. package/dist/theme-editor-preview.cjs +66 -66
  29. package/dist/theme-editor-preview.d.cts +255 -17
  30. package/dist/theme-editor-preview.d.ts +255 -17
  31. package/dist/theme-editor-preview.js +56 -56
  32. package/dist/theme-editor.cjs +5 -5
  33. package/dist/theme-editor.d.cts +242 -17
  34. package/dist/theme-editor.d.ts +242 -17
  35. package/dist/theme-editor.js +7 -7
  36. package/dist/theme-reference.cjs +1 -1
  37. package/dist/theme-reference.d.cts +38 -0
  38. package/dist/theme-reference.d.ts +38 -0
  39. package/dist/theme-reference.js +1 -1
  40. package/dist/widget.css +1 -1
  41. package/package.json +1 -1
  42. package/src/ask-user-question-tool.ts +10 -7
  43. package/src/components/artifact-preview.ts +1 -1
  44. package/src/components/composer-parts.test.ts +103 -4
  45. package/src/components/composer-parts.ts +53 -15
  46. package/src/components/messages.ts +5 -2
  47. package/src/components/panel.ts +31 -5
  48. package/src/components/pill-composer-builder.test.ts +18 -0
  49. package/src/components/suggestions.test.ts +233 -0
  50. package/src/components/suggestions.ts +463 -81
  51. package/src/components/widget-view.ts +4 -0
  52. package/src/defaults.ts +5 -3
  53. package/src/generated/runtype-openapi-contract.ts +17 -0
  54. package/src/index-core.ts +18 -2
  55. package/src/launcher-global.ts +1 -1
  56. package/src/plugins/types.ts +71 -3
  57. package/src/presets.ts +3 -3
  58. package/src/session.ts +10 -8
  59. package/src/styles/widget-styles.test.ts +162 -0
  60. package/src/styles/widget.css +430 -0
  61. package/src/suggest-replies-tool.test.ts +233 -2
  62. package/src/suggest-replies-tool.ts +115 -39
  63. package/src/theme-editor/preview-utils.test.ts +34 -0
  64. package/src/theme-editor/preview-utils.ts +77 -2
  65. package/src/theme-editor/sections.test.ts +33 -0
  66. package/src/theme-editor/sections.ts +53 -5
  67. package/src/theme-reference.ts +9 -2
  68. package/src/types/theme.ts +32 -0
  69. package/src/types.ts +152 -16
  70. package/src/ui.detached-panel.test.ts +1 -1
  71. package/src/ui.suggest-replies.test.ts +1066 -1
  72. package/src/ui.ts +268 -20
  73. package/src/utils/artifact-file.test.ts +1 -1
  74. package/src/utils/code-generators.test.ts +40 -0
  75. package/src/utils/code-generators.ts +22 -0
  76. package/src/utils/config-merge.test.ts +24 -0
  77. package/src/utils/icons.test.ts +26 -0
  78. package/src/utils/icons.ts +8 -0
  79. package/src/utils/theme.test.ts +79 -0
  80. package/src/utils/tokens.ts +64 -0
  81. package/dist/chunk-MMUPR2JW.js +0 -1
package/src/ui.ts CHANGED
@@ -34,7 +34,9 @@ import {
34
34
  PersonaArtifactFileMeta,
35
35
  PersonaArtifactActionContext,
36
36
  AgentWidgetContentSegment,
37
- AgentWidgetContextMentionRef
37
+ AgentWidgetContextMentionRef,
38
+ AgentWidgetSuggestion,
39
+ AgentWidgetSuggestionSource
38
40
  } from "./types";
39
41
  import { AttachmentManager } from "./utils/attachment-manager";
40
42
  import {
@@ -111,6 +113,7 @@ import {
111
113
  import {
112
114
  isSuggestRepliesMessage,
113
115
  latestAgentSuggestions,
116
+ resolveFollowUpsFeature,
114
117
  } from "./suggest-replies-tool";
115
118
  import { formatElapsedMs } from "./utils/formatting";
116
119
  import { approvalDetailsExpansionState, createApprovalBubble, updateApprovalDetailsUI } from "./components/approval-bubble";
@@ -359,6 +362,19 @@ type Controller = {
359
362
  * @deprecated Use injectMessage() instead.
360
363
  */
361
364
  injectTestMessage: (event: AgentWidgetEvent) => void;
365
+ /**
366
+ * Show follow-up suggestions from host code. An ephemeral UI overlay: the
367
+ * items never enter the transcript, the wire payload, or persisted state, so
368
+ * they do not survive a refresh. Cleared automatically when the next user
369
+ * message appends. Latest writer wins against the agent's `suggest_replies`
370
+ * payloads: this call overrides chips shown now, and a payload arriving
371
+ * afterwards overrides these. Renders through the `suggestions.followUps`
372
+ * presentation config and the full plugin and `persona:suggestReplies:*`
373
+ * event pipeline, with source `"host"`. An empty array clears.
374
+ */
375
+ setFollowUpSuggestions: (items: AgentWidgetSuggestion[]) => void;
376
+ /** Remove host-set follow-up suggestions. Agent-set chips are untouched. */
377
+ clearFollowUpSuggestions: () => void;
362
378
  getMessages: () => AgentWidgetMessage[];
363
379
  getStatus: () => AgentWidgetSessionStatus;
364
380
  getPersistentMetadata: () => Record<string, unknown>;
@@ -872,6 +888,8 @@ export const createAgentExperience = (
872
888
  container,
873
889
  body,
874
890
  messagesWrapper,
891
+ starterSuggestions,
892
+ transcriptSuggestions,
875
893
  suggestions,
876
894
  textarea,
877
895
  sendButton,
@@ -1326,6 +1344,11 @@ export const createAgentExperience = (
1326
1344
  messagesWrapper.style.marginLeft = "auto";
1327
1345
  messagesWrapper.style.marginRight = "auto";
1328
1346
  messagesWrapper.style.width = "100%";
1347
+ starterSuggestions.style.maxWidth = contentMaxWidth;
1348
+ transcriptSuggestions.style.maxWidth = contentMaxWidth;
1349
+ transcriptSuggestions.style.marginLeft = "auto";
1350
+ transcriptSuggestions.style.marginRight = "auto";
1351
+ transcriptSuggestions.style.width = "100%";
1329
1352
  }
1330
1353
  // The pill IS the composer in composer-bar mode and should match the
1331
1354
  // wrapper's responsive width (50vw / 70vw / 90vw), not be capped by
@@ -3446,7 +3469,17 @@ export const createAgentExperience = (
3446
3469
  }
3447
3470
  }
3448
3471
 
3449
- const suggestionsManager = createSuggestions(suggestions);
3472
+ const composerSuggestionsManager = createSuggestions(suggestions);
3473
+ const starterSuggestionsManager = createSuggestions(starterSuggestions);
3474
+ const transcriptSuggestionsManager = createSuggestions(transcriptSuggestions);
3475
+ const suggestionManagers = [
3476
+ composerSuggestionsManager,
3477
+ starterSuggestionsManager,
3478
+ transcriptSuggestionsManager,
3479
+ ];
3480
+ destroyCallbacks.push(() => {
3481
+ suggestionManagers.forEach((manager) => manager.destroy());
3482
+ });
3450
3483
  let closeHandler: (() => void) | null = null;
3451
3484
  let session: AgentWidgetSession;
3452
3485
 
@@ -3457,35 +3490,217 @@ export const createAgentExperience = (
3457
3490
  // keep their before-first-user-message behavior. Config updates MUST route
3458
3491
  // through here too: re-rendering with only `config.suggestionChips` would
3459
3492
  // drop a live agent chip row until the next message change.
3493
+ // Warn once per widget instance: renderSuggestions runs on every change.
3494
+ // Host-set follow-ups (`setFollowUpSuggestions`) are an ephemeral UI overlay:
3495
+ // never appended to the session, never sent on the wire, never persisted.
3496
+ let hostFollowUps: AgentWidgetSuggestion[] | null = null;
3497
+ // Latches: a host that drives the surface itself is a legitimate no-tool
3498
+ // integration, so the never-called hint must stay off even after the overlay
3499
+ // expires.
3500
+ let hostFollowUpsEverSet = false;
3501
+ // Latest-writer-wins bookkeeping captured at set time: the overlay is dropped
3502
+ // once another user message appends or a newer suggest_replies message lands.
3503
+ let hostFollowUpsUserCount = 0;
3504
+ let hostFollowUpsAgentId: string | null = null;
3505
+
3506
+ const countUserMessages = (messages: AgentWidgetMessage[]): number =>
3507
+ messages.reduce(
3508
+ (total, message) => (message.role === "user" ? total + 1 : total),
3509
+ 0
3510
+ );
3511
+
3512
+ /** Identity of the newest suggest_replies message, regardless of position. */
3513
+ const latestSuggestRepliesId = (
3514
+ messages: AgentWidgetMessage[]
3515
+ ): string | null => {
3516
+ for (let i = messages.length - 1; i >= 0; i--) {
3517
+ if (isSuggestRepliesMessage(messages[i])) return messages[i].id ?? null;
3518
+ }
3519
+ return null;
3520
+ };
3521
+
3522
+ let warnedPinnedStarterPlacement = false;
3523
+ const warnPinnedStarterPlacement = () => {
3524
+ if (warnedPinnedStarterPlacement || config.debug !== true) return;
3525
+ warnedPinnedStarterPlacement = true;
3526
+ console.warn(
3527
+ '[persona] suggestions.starters.placement is pinned to "welcome" but copy.showWelcomeCard is false, so no starters render. Use "auto" or "composer".',
3528
+ );
3529
+ };
3530
+ // Debug-only setup hint: a stream this session ended without the agent ever
3531
+ // calling suggest_replies, so the model never got the tool. Gated on a
3532
+ // COMPLETED stream, not on transcript shape: a restored transcript
3533
+ // (initialMessages/onStateLoaded) proves nothing about this session's wiring.
3534
+ let hintedFollowUpsNeverCalled = false;
3535
+ let completedStreamThisSession = false;
3536
+ const hintFollowUpsNeverCalled = (
3537
+ messages: AgentWidgetMessage[],
3538
+ feature: ReturnType<typeof resolveFollowUpsFeature>
3539
+ ) => {
3540
+ if (hintedFollowUpsNeverCalled || config.debug !== true) return;
3541
+ if (!feature.enabled || feature.expose) return;
3542
+ if (hostFollowUpsEverSet) return;
3543
+ if (!completedStreamThisSession) return;
3544
+ if (session.isStreaming()) return;
3545
+ if (messages.some(isSuggestRepliesMessage)) return;
3546
+ hintedFollowUpsNeverCalled = true;
3547
+ console.info(
3548
+ "[persona] Follow-up suggestions are enabled but the agent has not called suggest_replies. The model needs the tool declared (runtimeTools server-side, suggestions.followUps.expose: true from the widget, or your own backend's tool definition) and benefits from instructions telling it when to offer follow-ups. See the integration paths table in UI-COMPONENTS.md."
3549
+ );
3550
+ };
3551
+
3460
3552
  const renderSuggestions = (messages?: AgentWidgetMessage[]) => {
3461
3553
  if (!session) return;
3462
3554
  const current = messages ?? session.getMessages();
3463
- const agentChips =
3464
- config.features?.suggestReplies?.enabled !== false
3465
- ? latestAgentSuggestions(current)
3466
- : null;
3467
- if (agentChips) {
3468
- suggestionsManager.render(
3469
- agentChips,
3555
+ const clearManager = (manager: ReturnType<typeof createSuggestions>) => {
3556
+ manager.render([], session, textarea, current);
3557
+ };
3558
+ const clearOthers = (active: ReturnType<typeof createSuggestions>) => {
3559
+ suggestionManagers.forEach((manager) => {
3560
+ if (manager !== active) clearManager(manager);
3561
+ });
3562
+ };
3563
+ // Same lifecycle as agent follow-ups: the overlay expires on the next user
3564
+ // message, and a suggest_replies message newer than the set call wins.
3565
+ if (
3566
+ hostFollowUps &&
3567
+ (countUserMessages(current) > hostFollowUpsUserCount ||
3568
+ latestSuggestRepliesId(current) !== hostFollowUpsAgentId)
3569
+ ) {
3570
+ hostFollowUps = null;
3571
+ }
3572
+
3573
+ // `enabled: false` disables the tool, not the surface: host-set follow-ups
3574
+ // still render.
3575
+ const followUpsFeature = resolveFollowUpsFeature(config);
3576
+ hintFollowUpsNeverCalled(current, followUpsFeature);
3577
+ const agentSuggestions = followUpsFeature.enabled
3578
+ ? latestAgentSuggestions(current)
3579
+ : null;
3580
+ const followUpSuggestions = hostFollowUps ?? agentSuggestions;
3581
+ const followUpSource: AgentWidgetSuggestionSource = hostFollowUps
3582
+ ? "host"
3583
+ : "agent";
3584
+
3585
+ if (followUpSuggestions) {
3586
+ const followUps = config.suggestions?.followUps;
3587
+ const requestedPlacement =
3588
+ followUps?.placement ?? (followUps ? "auto" : "composer");
3589
+ const placement =
3590
+ requestedPlacement === "auto"
3591
+ ? isComposerBar()
3592
+ ? "composer"
3593
+ : "after-message"
3594
+ : requestedPlacement;
3595
+ const manager =
3596
+ placement === "after-message"
3597
+ ? transcriptSuggestionsManager
3598
+ : composerSuggestionsManager;
3599
+ clearOthers(manager);
3600
+ manager.render(
3601
+ followUpSuggestions,
3470
3602
  session,
3471
3603
  textarea,
3472
3604
  current,
3473
3605
  config.suggestionChipsConfig,
3474
- { agentPushed: true }
3606
+ {
3607
+ source: followUpSource,
3608
+ surface: "followUp",
3609
+ variant: followUps?.variant ?? "chip",
3610
+ behavior: followUps?.behavior ?? "send",
3611
+ overflow: followUps?.overflow ?? "scroll",
3612
+ maxItems: followUps?.maxItems ?? 4,
3613
+ config,
3614
+ plugins,
3615
+ }
3475
3616
  );
3476
- } else if (current.some((msg) => msg.role === "user")) {
3477
- // Hide suggestions once a user message exists.
3478
- suggestionsManager.render([], session, textarea, current);
3479
- } else {
3480
- suggestionsManager.render(
3481
- config.suggestionChips,
3617
+ return;
3618
+ }
3619
+
3620
+ if (current.some((message) => message.role === "user")) {
3621
+ suggestionManagers.forEach(clearManager);
3622
+ return;
3623
+ }
3624
+
3625
+ const starters = config.suggestions?.starters;
3626
+ if (starters) {
3627
+ // The starter host lives inside the welcome card, so a pinned "welcome"
3628
+ // placement has nowhere to render when that card is hidden.
3629
+ const welcomeCardVisible = config.copy?.showWelcomeCard !== false;
3630
+ const requestedPlacement = starters.placement ?? "auto";
3631
+ const placement =
3632
+ requestedPlacement === "auto"
3633
+ ? welcomeCardVisible
3634
+ ? "welcome"
3635
+ : "composer"
3636
+ : requestedPlacement;
3637
+ if (placement === "welcome" && !welcomeCardVisible) {
3638
+ warnPinnedStarterPlacement();
3639
+ suggestionManagers.forEach(clearManager);
3640
+ return;
3641
+ }
3642
+ const manager =
3643
+ placement === "welcome"
3644
+ ? starterSuggestionsManager
3645
+ : composerSuggestionsManager;
3646
+ clearOthers(manager);
3647
+ manager.render(
3648
+ starters.items ?? config.suggestionChips,
3482
3649
  session,
3483
3650
  textarea,
3484
3651
  current,
3485
- config.suggestionChipsConfig
3652
+ config.suggestionChipsConfig,
3653
+ {
3654
+ surface: "starter",
3655
+ variant: starters.variant ?? "card",
3656
+ behavior: starters.behavior ?? "send",
3657
+ overflow: starters.overflow ?? "wrap",
3658
+ maxItems: starters.maxItems ?? 4,
3659
+ config,
3660
+ plugins,
3661
+ }
3486
3662
  );
3663
+ return;
3664
+ }
3665
+
3666
+ clearOthers(composerSuggestionsManager);
3667
+ composerSuggestionsManager.render(
3668
+ config.suggestionChips,
3669
+ session,
3670
+ textarea,
3671
+ current,
3672
+ config.suggestionChipsConfig,
3673
+ {
3674
+ surface: "starter",
3675
+ variant: "chip",
3676
+ behavior: "send",
3677
+ overflow: "wrap",
3678
+ config,
3679
+ plugins,
3680
+ }
3681
+ );
3682
+ };
3683
+
3684
+ const clearHostFollowUps = () => {
3685
+ if (!hostFollowUps) return;
3686
+ hostFollowUps = null;
3687
+ renderSuggestions();
3688
+ };
3689
+
3690
+ const setHostFollowUps = (items: AgentWidgetSuggestion[]) => {
3691
+ const list = Array.isArray(items) ? [...items] : [];
3692
+ if (!list.length) {
3693
+ clearHostFollowUps();
3694
+ return;
3487
3695
  }
3696
+ const current = session ? session.getMessages() : [];
3697
+ hostFollowUps = list;
3698
+ hostFollowUpsEverSet = true;
3699
+ hostFollowUpsUserCount = countUserMessages(current);
3700
+ hostFollowUpsAgentId = latestSuggestRepliesId(current);
3701
+ renderSuggestions(current);
3488
3702
  };
3703
+
3489
3704
  let isStreaming = false;
3490
3705
  const messageCache = createMessageCache();
3491
3706
  // Tracks the last fingerprint we rendered a plugin-rendered ask_user_question
@@ -4511,7 +4726,7 @@ export const createAgentExperience = (
4511
4726
  // visible instead of silently swallowed.
4512
4727
  if (
4513
4728
  isSuggestRepliesMessage(message) &&
4514
- config.features?.suggestReplies?.enabled !== false
4729
+ resolveFollowUpsFeature(config).enabled
4515
4730
  ) {
4516
4731
  return;
4517
4732
  }
@@ -5884,8 +6099,16 @@ export const createAgentExperience = (
5884
6099
  if (micButton) {
5885
6100
  micButton.disabled = disabled;
5886
6101
  }
5887
- suggestionsManager.buttons.forEach((btn) => {
5888
- btn.disabled = disabled;
6102
+ suggestionManagers.forEach((manager) => {
6103
+ manager.elements.forEach((element) => {
6104
+ element.toggleAttribute("data-disabled", disabled);
6105
+ if (!(element instanceof HTMLButtonElement)) {
6106
+ element.setAttribute("aria-disabled", disabled ? "true" : "false");
6107
+ }
6108
+ });
6109
+ manager.buttons.forEach((button) => {
6110
+ button.disabled = disabled;
6111
+ });
5889
6112
  });
5890
6113
  footer.dataset.personaComposerStreaming = disabled ? "true" : "false";
5891
6114
  footer.querySelectorAll<HTMLElement>("[data-persona-composer-disable-when-streaming]").forEach((el) => {
@@ -6196,6 +6419,16 @@ export const createAgentExperience = (
6196
6419
  }
6197
6420
  if (!streaming) {
6198
6421
  scheduleAutoScroll(true);
6422
+ // setStreaming only fires on a change, so this is always a real stream
6423
+ // ending. Evaluate the never-called hint here: no later render is
6424
+ // guaranteed on a settled transcript.
6425
+ completedStreamThisSession = true;
6426
+ if (session) {
6427
+ hintFollowUpsNeverCalled(
6428
+ session.getMessages(),
6429
+ resolveFollowUpsFeature(config)
6430
+ );
6431
+ }
6199
6432
  }
6200
6433
  // Keep the "streaming below" hint and its announcement in sync with the
6201
6434
  // streaming lifecycle (Principles 8 + 15).
@@ -9358,6 +9591,21 @@ export const createAgentExperience = (
9358
9591
  }
9359
9592
  return session.resolveApproval(approvalMessage.approval, decision, options);
9360
9593
  },
9594
+ /**
9595
+ * Show follow-up suggestions from host code. Ephemeral UI state only: the
9596
+ * items never enter the transcript, the wire payload, or persistence, so
9597
+ * they do not survive a refresh. Cleared when the next user message
9598
+ * appends. Latest writer wins against agent `suggest_replies` payloads.
9599
+ * Renders through the `suggestions.followUps` presentation config and the
9600
+ * full plugin and DOM-event pipeline with source `"host"`.
9601
+ */
9602
+ setFollowUpSuggestions(items: AgentWidgetSuggestion[]): void {
9603
+ setHostFollowUps(items);
9604
+ },
9605
+ /** Remove host-set follow-ups. Agent-set chips are untouched. */
9606
+ clearFollowUpSuggestions(): void {
9607
+ clearHostFollowUps();
9608
+ },
9361
9609
  getMessages() {
9362
9610
  return session.getMessages();
9363
9611
  },
@@ -10,7 +10,7 @@ import {
10
10
  // Canonical cross-repo fixture. The zero-width space is written as a
11
11
  // backslash-u200b escape sequence (not a literal invisible char) so the bytes
12
12
  // are unambiguous in source.
13
- // Mirrors core/packages/shared/src/file-artifact.ts escapeFenceTerminators fixture — keep byte-identical.
13
+ // Mirrors core/packages/shared/src/file-artifact.ts escapeFenceTerminators fixture — keep in sync.
14
14
  const ZWSP = "\u200b";
15
15
  const FIXTURE_RAW = "# Title\n\n```js\nconsole.log('hi')\n```\n";
16
16
  const FIXTURE_ESCAPED =
@@ -563,6 +563,46 @@ describe("Full Config with Hooks Integration", () => {
563
563
  // Backward Compatibility Tests
564
564
  // =============================================================================
565
565
 
566
+ describe("Suggestions Serialization", () => {
567
+ it("preserves structured suggestion configuration in every format", () => {
568
+ const config = {
569
+ ...minimalConfig,
570
+ suggestions: {
571
+ starters: {
572
+ variant: "card",
573
+ behavior: "fill",
574
+ items: [
575
+ {
576
+ id: "compare",
577
+ label: "Compare plans",
578
+ prompt: "Compare plans for my team",
579
+ },
580
+ ],
581
+ },
582
+ followUps: {
583
+ placement: "after-message",
584
+ overflow: "scroll",
585
+ },
586
+ },
587
+ };
588
+ const formats: CodeFormat[] = [
589
+ "esm",
590
+ "react-component",
591
+ "react-advanced",
592
+ "script-installer",
593
+ "script-manual",
594
+ "script-advanced",
595
+ ];
596
+
597
+ for (const format of formats) {
598
+ const code = generateCodeSnippet(config, format);
599
+ expect(code).toContain("suggestions");
600
+ expect(code).toContain("Compare plans");
601
+ expect(code).toContain("after-message");
602
+ }
603
+ });
604
+ });
605
+
566
606
  describe("Backward Compatibility", () => {
567
607
  it("should work without options parameter", () => {
568
608
  const code = generateCodeSnippet(minimalConfig, "esm");
@@ -430,6 +430,19 @@ function generateMarkdownConfig(config: any, indent: string): string[] {
430
430
  return lines;
431
431
  }
432
432
 
433
+ // Suggestions may contain nested rich items. JSON object syntax is also valid
434
+ // JavaScript/TypeScript, so preserve the complete public shape instead of
435
+ // duplicating every leaf in each output-format generator.
436
+ function generateSuggestionsConfig(config: any, indent: string): string[] {
437
+ if (!config.suggestions) return [];
438
+ const serialized = JSON.stringify(config.suggestions, null, 2).split("\n");
439
+ return serialized.map((line, index) => {
440
+ const prefix = index === 0 ? `${indent}suggestions: ` : indent;
441
+ const suffix = index === serialized.length - 1 ? "," : "";
442
+ return `${prefix}${line}${suffix}`;
443
+ });
444
+ }
445
+
433
446
  // Helper to generate layout config (excluding render functions and slots)
434
447
  function generateLayoutConfig(config: any, indent: string): string[] {
435
448
  const lines: string[] = [];
@@ -697,6 +710,8 @@ function generateESMCode(config: any, options?: CodeGeneratorOptions): string {
697
710
  lines.push(" },");
698
711
  }
699
712
 
713
+ lines.push(...generateSuggestionsConfig(config, " "));
714
+
700
715
  if (config.suggestionChips && config.suggestionChips.length > 0) {
701
716
  lines.push(" suggestionChips: [");
702
717
  config.suggestionChips.forEach((chip: string) => {
@@ -845,6 +860,8 @@ function generateReactComponentCode(config: any, options?: CodeGeneratorOptions)
845
860
  lines.push(" },");
846
861
  }
847
862
 
863
+ lines.push(...generateSuggestionsConfig(config, " "));
864
+
848
865
  if (config.suggestionChips && config.suggestionChips.length > 0) {
849
866
  lines.push(" suggestionChips: [");
850
867
  config.suggestionChips.forEach((chip: string) => {
@@ -1110,6 +1127,8 @@ function generateReactAdvancedCode(config: any, options?: CodeGeneratorOptions):
1110
1127
  lines.push(" },");
1111
1128
  }
1112
1129
 
1130
+ lines.push(...generateSuggestionsConfig(config, " "));
1131
+
1113
1132
  if (config.suggestionChips && config.suggestionChips.length > 0) {
1114
1133
  lines.push(" suggestionChips: [");
1115
1134
  config.suggestionChips.forEach((chip: string) => {
@@ -1312,6 +1331,7 @@ function buildSerializableConfig(config: any): Record<string, any> {
1312
1331
  if (config.voiceRecognition) serializableConfig.voiceRecognition = config.voiceRecognition;
1313
1332
  if (config.statusIndicator) serializableConfig.statusIndicator = config.statusIndicator;
1314
1333
  if (config.features) serializableConfig.features = config.features;
1334
+ if (config.suggestions) serializableConfig.suggestions = config.suggestions;
1315
1335
  if (config.suggestionChips?.length > 0) serializableConfig.suggestionChips = config.suggestionChips;
1316
1336
  if (config.suggestionChipsConfig) serializableConfig.suggestionChipsConfig = config.suggestionChipsConfig;
1317
1337
  if (config.debug) serializableConfig.debug = config.debug;
@@ -1520,6 +1540,8 @@ function generateScriptManualCode(config: any, options?: CodeGeneratorOptions):
1520
1540
  lines.push(" },");
1521
1541
  }
1522
1542
 
1543
+ lines.push(...generateSuggestionsConfig(config, " "));
1544
+
1523
1545
  if (config.suggestionChips && config.suggestionChips.length > 0) {
1524
1546
  lines.push(" suggestionChips: [");
1525
1547
  config.suggestionChips.forEach((chip: string) => {
@@ -32,6 +32,30 @@ describe("mergeConfigUpdate", () => {
32
32
  expect(next.suggestionChips).toEqual(["x"]);
33
33
  });
34
34
 
35
+ it("patches suggestion surfaces without dropping sibling behavior", () => {
36
+ const prev = base({
37
+ suggestions: {
38
+ starters: { variant: "card", behavior: "fill" },
39
+ followUps: { variant: "chip", placement: "after-message" },
40
+ },
41
+ });
42
+ const next = mergeConfigUpdate(prev, {
43
+ suggestions: {
44
+ followUps: { overflow: "wrap" },
45
+ },
46
+ });
47
+
48
+ expect(next.suggestions?.starters).toEqual({
49
+ variant: "card",
50
+ behavior: "fill",
51
+ });
52
+ expect(next.suggestions?.followUps).toEqual({
53
+ variant: "chip",
54
+ placement: "after-message",
55
+ overflow: "wrap",
56
+ });
57
+ });
58
+
35
59
  it("replaces callbacks wholesale", () => {
36
60
  const first = vi.fn();
37
61
  const second = vi.fn();
@@ -0,0 +1,26 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { describe, expect, it, vi } from "vitest";
4
+
5
+ import { renderLucideIcon } from "./icons";
6
+
7
+ describe("renderLucideIcon", () => {
8
+ it.each(["lightbulb", "chart-column", "image", "pen-line"] as const)(
9
+ "renders %s as an inline svg",
10
+ (name) => {
11
+ const svg = renderLucideIcon(name);
12
+ expect(svg).not.toBeNull();
13
+ expect(svg?.tagName.toLowerCase()).toBe("svg");
14
+ expect(svg?.getAttribute("viewBox")).toBe("0 0 24 24");
15
+ // Geometry, not an empty shell.
16
+ expect(svg!.childNodes.length).toBeGreaterThan(0);
17
+ }
18
+ );
19
+
20
+ it("returns null and warns for a name outside the registry", () => {
21
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
22
+ expect(renderLucideIcon("definitely-not-an-icon")).toBeNull();
23
+ expect(warn).toHaveBeenCalled();
24
+ warn.mockRestore();
25
+ });
26
+ });
@@ -70,6 +70,7 @@ import {
70
70
  Plus,
71
71
  Minus,
72
72
  Pencil,
73
+ PenLine,
73
74
  Trash,
74
75
  Trash2,
75
76
  Save,
@@ -117,12 +118,15 @@ import {
117
118
  Folder,
118
119
  FolderOpen,
119
120
  Files,
121
+ // ---------- Data ----------
122
+ ChartColumn,
120
123
  // ---------- Decorative ----------
121
124
  Sparkles,
122
125
  Zap,
123
126
  Sun,
124
127
  Moon,
125
128
  Flag,
129
+ Lightbulb,
126
130
  // ---------- Devices ----------
127
131
  Monitor,
128
132
  Smartphone,
@@ -220,6 +224,7 @@ const LUCIDE_ICONS = {
220
224
  "plus": Plus,
221
225
  "minus": Minus,
222
226
  "pencil": Pencil,
227
+ "pen-line": PenLine,
223
228
  "trash": Trash,
224
229
  "trash-2": Trash2,
225
230
  "save": Save,
@@ -267,12 +272,15 @@ const LUCIDE_ICONS = {
267
272
  "folder": Folder,
268
273
  "folder-open": FolderOpen,
269
274
  "files": Files,
275
+ // Data
276
+ "chart-column": ChartColumn,
270
277
  // Decorative
271
278
  "sparkles": Sparkles,
272
279
  "zap": Zap,
273
280
  "sun": Sun,
274
281
  "moon": Moon,
275
282
  "flag": Flag,
283
+ "lightbulb": Lightbulb,
276
284
  // Devices
277
285
  "monitor": Monitor,
278
286
  "smartphone": Smartphone,