@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.
- package/README.md +4 -4
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-DveIaNx6.d.cts → types-BpWGm7_6.d.cts} +17 -0
- package/dist/animations/{types-DveIaNx6.d.ts → types-BpWGm7_6.d.ts} +17 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/chunk-QJ4LV2KW.js +1 -0
- package/dist/codegen.cjs +12 -11
- package/dist/codegen.js +14 -13
- package/dist/{context-mentions-ONG7A7M6.js → context-mentions-BTSTIBFC.js} +1 -1
- package/dist/context-mentions.cjs +11 -8
- package/dist/context-mentions.d.cts +241 -16
- package/dist/context-mentions.d.ts +241 -16
- package/dist/context-mentions.js +11 -8
- package/dist/index.cjs +67 -66
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +301 -41
- package/dist/index.d.ts +301 -41
- package/dist/index.global.js +59 -56
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +67 -66
- package/dist/index.js.map +1 -1
- package/dist/launcher.global.js +5 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/smart-dom-reader.d.cts +241 -16
- package/dist/smart-dom-reader.d.ts +241 -16
- package/dist/theme-editor-preview.cjs +66 -66
- package/dist/theme-editor-preview.d.cts +255 -17
- package/dist/theme-editor-preview.d.ts +255 -17
- package/dist/theme-editor-preview.js +56 -56
- package/dist/theme-editor.cjs +5 -5
- package/dist/theme-editor.d.cts +242 -17
- package/dist/theme-editor.d.ts +242 -17
- package/dist/theme-editor.js +7 -7
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +38 -0
- package/dist/theme-reference.d.ts +38 -0
- package/dist/theme-reference.js +1 -1
- package/dist/widget.css +1 -1
- package/package.json +1 -1
- package/src/ask-user-question-tool.ts +10 -7
- package/src/components/artifact-preview.ts +1 -1
- package/src/components/composer-parts.test.ts +103 -4
- package/src/components/composer-parts.ts +53 -15
- package/src/components/messages.ts +5 -2
- package/src/components/panel.ts +31 -5
- package/src/components/pill-composer-builder.test.ts +18 -0
- package/src/components/suggestions.test.ts +233 -0
- package/src/components/suggestions.ts +463 -81
- package/src/components/widget-view.ts +4 -0
- package/src/defaults.ts +5 -3
- package/src/generated/runtype-openapi-contract.ts +17 -0
- package/src/index-core.ts +18 -2
- package/src/launcher-global.ts +1 -1
- package/src/plugins/types.ts +71 -3
- package/src/presets.ts +3 -3
- package/src/session.ts +10 -8
- package/src/styles/widget-styles.test.ts +162 -0
- package/src/styles/widget.css +430 -0
- package/src/suggest-replies-tool.test.ts +233 -2
- package/src/suggest-replies-tool.ts +115 -39
- package/src/theme-editor/preview-utils.test.ts +34 -0
- package/src/theme-editor/preview-utils.ts +77 -2
- package/src/theme-editor/sections.test.ts +33 -0
- package/src/theme-editor/sections.ts +53 -5
- package/src/theme-reference.ts +9 -2
- package/src/types/theme.ts +32 -0
- package/src/types.ts +152 -16
- package/src/ui.detached-panel.test.ts +1 -1
- package/src/ui.suggest-replies.test.ts +1066 -1
- package/src/ui.ts +268 -20
- package/src/utils/artifact-file.test.ts +1 -1
- package/src/utils/code-generators.test.ts +40 -0
- package/src/utils/code-generators.ts +22 -0
- package/src/utils/config-merge.test.ts +24 -0
- package/src/utils/icons.test.ts +26 -0
- package/src/utils/icons.ts +8 -0
- package/src/utils/theme.test.ts +79 -0
- package/src/utils/tokens.ts +64 -0
- 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
|
|
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
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
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.
|
|
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
|
-
|
|
5888
|
-
|
|
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
|
|
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
|
+
});
|
package/src/utils/icons.ts
CHANGED
|
@@ -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,
|