@runtypelabs/persona 4.13.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 +339 -22
- package/dist/context-mentions.d.ts +339 -22
- package/dist/context-mentions.js +11 -8
- package/dist/index.cjs +68 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +483 -48
- package/dist/index.d.ts +483 -48
- package/dist/index.global.js +61 -58
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +70 -69
- 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 +339 -22
- package/dist/smart-dom-reader.d.ts +339 -22
- package/dist/theme-editor-preview.cjs +67 -67
- package/dist/theme-editor-preview.d.cts +355 -23
- package/dist/theme-editor-preview.d.ts +355 -23
- package/dist/theme-editor-preview.js +75 -75
- package/dist/theme-editor.cjs +10 -10
- package/dist/theme-editor.d.cts +376 -24
- package/dist/theme-editor.d.ts +376 -24
- package/dist/theme-editor.js +10 -10
- 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/artifacts-session.test.ts +106 -0
- package/src/ask-user-question-tool.ts +10 -7
- package/src/client.test.ts +2 -2
- package/src/client.ts +4 -1
- package/src/components/artifact-card.ts +13 -2
- package/src/components/artifact-inline.ts +11 -2
- package/src/components/artifact-preview.ts +1 -1
- package/src/components/composer-builder.test.ts +2 -0
- package/src/components/composer-builder.ts +1 -1
- package/src/components/composer-parts.test.ts +105 -4
- package/src/components/composer-parts.ts +54 -16
- package/src/components/message-bubble.test.ts +7 -1
- package/src/components/message-bubble.ts +11 -8
- 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 +52 -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 +55 -12
- package/src/styles/widget-styles.test.ts +162 -0
- package/src/styles/widget.css +440 -1
- package/src/suggest-replies-tool.test.ts +233 -2
- package/src/suggest-replies-tool.ts +115 -39
- package/src/theme-editor/index.ts +5 -0
- package/src/theme-editor/presets.test.ts +43 -0
- package/src/theme-editor/presets.ts +101 -87
- 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 +72 -7
- package/src/theme-editor/sections.ts +113 -13
- package/src/theme-editor/state-preferences.test.ts +15 -0
- package/src/theme-editor/state.ts +46 -0
- package/src/theme-editor/types.ts +15 -1
- package/src/theme-editor/webmcp/tools.test.ts +13 -0
- package/src/theme-editor/webmcp/tools.ts +5 -0
- package/src/theme-reference.ts +9 -2
- package/src/types/theme.ts +32 -0
- package/src/types.ts +291 -25
- package/src/ui.artifact-pane-gating.test.ts +5 -5
- package/src/ui.config-preferences.test.ts +166 -0
- package/src/ui.detached-panel.test.ts +1 -1
- package/src/ui.suggest-replies.test.ts +1066 -1
- package/src/ui.ts +311 -45
- package/src/ui.typing-indicator.test.ts +135 -0
- package/src/utils/artifact-display.test.ts +180 -16
- package/src/utils/artifact-display.ts +155 -14
- 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 +84 -0
- package/src/utils/config-merge.ts +6 -1
- package/src/utils/feature-preferences.test.ts +326 -0
- package/src/utils/feature-preferences.ts +498 -0
- package/src/utils/icons.test.ts +26 -0
- package/src/utils/icons.ts +8 -0
- package/src/utils/theme.test.ts +171 -0
- package/src/utils/tokens.ts +71 -7
- 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 {
|
|
@@ -91,7 +93,7 @@ import { buildHeaderWithLayout } from "./components/header-layouts";
|
|
|
91
93
|
import { positionMap } from "./utils/positioning";
|
|
92
94
|
import type { HeaderElements as _HeaderElements, ComposerElements as _ComposerElements } from "./components/panel";
|
|
93
95
|
import { MessageTransform, MessageActionCallbacks, LoadingIndicatorRenderer } from "./components/message-bubble";
|
|
94
|
-
import { createStandardBubble, createTypingIndicator } from "./components/message-bubble";
|
|
96
|
+
import { createStandardBubble, createTypingIndicator, getBubbleClasses } from "./components/message-bubble";
|
|
95
97
|
import { createReasoningBubble, reasoningExpansionState, updateReasoningBubbleUI } from "./components/reasoning-bubble";
|
|
96
98
|
import { createToolBubble, toolExpansionState, updateToolBubbleUI } from "./components/tool-bubble";
|
|
97
99
|
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";
|
|
@@ -133,6 +136,7 @@ import {
|
|
|
133
136
|
shouldExpandLauncherForArtifacts
|
|
134
137
|
} from "./utils/artifact-gate";
|
|
135
138
|
import { resolveArtifactDisplayMode } from "./utils/artifact-display";
|
|
139
|
+
import { resolveConfigPreferences } from "./utils/feature-preferences";
|
|
136
140
|
import { readFlexGapPx, resolveArtifactPaneWidthPx } from "./utils/artifact-resize";
|
|
137
141
|
import { enhanceWithForms } from "./components/forms";
|
|
138
142
|
import { pluginRegistry } from "./plugins/registry";
|
|
@@ -358,6 +362,19 @@ type Controller = {
|
|
|
358
362
|
* @deprecated Use injectMessage() instead.
|
|
359
363
|
*/
|
|
360
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;
|
|
361
378
|
getMessages: () => AgentWidgetMessage[];
|
|
362
379
|
getStatus: () => AgentWidgetSessionStatus;
|
|
363
380
|
getPersistentMetadata: () => Record<string, unknown>;
|
|
@@ -596,6 +613,11 @@ export const createAgentExperience = (
|
|
|
596
613
|
}
|
|
597
614
|
|
|
598
615
|
let config = mergeWithDefaults(initialConfig) as AgentWidgetConfig;
|
|
616
|
+
// `config.features` holds the preference-resolved view every read site uses;
|
|
617
|
+
// `baseFeatures` keeps the pre-preference base so update() re-resolves from
|
|
618
|
+
// it instead of stacking new preferences onto already-overlaid values.
|
|
619
|
+
let baseFeatures = config.features;
|
|
620
|
+
config = resolveConfigPreferences(config);
|
|
599
621
|
// Note: applyThemeVariables is called after applyFullHeightStyles() below
|
|
600
622
|
// because applyFullHeightStyles resets mount.style.cssText
|
|
601
623
|
|
|
@@ -866,6 +888,8 @@ export const createAgentExperience = (
|
|
|
866
888
|
container,
|
|
867
889
|
body,
|
|
868
890
|
messagesWrapper,
|
|
891
|
+
starterSuggestions,
|
|
892
|
+
transcriptSuggestions,
|
|
869
893
|
suggestions,
|
|
870
894
|
textarea,
|
|
871
895
|
sendButton,
|
|
@@ -1320,6 +1344,11 @@ export const createAgentExperience = (
|
|
|
1320
1344
|
messagesWrapper.style.marginLeft = "auto";
|
|
1321
1345
|
messagesWrapper.style.marginRight = "auto";
|
|
1322
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%";
|
|
1323
1352
|
}
|
|
1324
1353
|
// The pill IS the composer in composer-bar mode and should match the
|
|
1325
1354
|
// wrapper's responsive width (50vw / 70vw / 90vw), not be capped by
|
|
@@ -1800,7 +1829,7 @@ export const createAgentExperience = (
|
|
|
1800
1829
|
let artifactPaneExpandedPinned = false;
|
|
1801
1830
|
// Whether the user explicitly opened the pane (card click, inline Expand,
|
|
1802
1831
|
// showArtifacts(), programmatic upsert). Auto-open is otherwise reserved for
|
|
1803
|
-
// artifacts whose resolved display mode is "panel": "
|
|
1832
|
+
// artifacts whose resolved display mode is "panel": "collapsed" keeps the card as
|
|
1804
1833
|
// the only affordance and "inline" renders in the transcript. "inline" no
|
|
1805
1834
|
// longer *auto*-opens the pane, but its Expand control is a deliberate,
|
|
1806
1835
|
// user-driven open (setting artifactsPaneUserOpened, same as a card click);
|
|
@@ -1811,7 +1840,7 @@ export const createAgentExperience = (
|
|
|
1811
1840
|
artifactsPaneUserOpened ||
|
|
1812
1841
|
lastArtifactsState.artifacts.some(
|
|
1813
1842
|
(a) =>
|
|
1814
|
-
resolveArtifactDisplayMode(config.features?.artifacts, a
|
|
1843
|
+
resolveArtifactDisplayMode(config.features?.artifacts, a) === "panel"
|
|
1815
1844
|
);
|
|
1816
1845
|
const artifactPaneVisible = () =>
|
|
1817
1846
|
lastArtifactsState.artifacts.length > 0 &&
|
|
@@ -2010,7 +2039,7 @@ export const createAgentExperience = (
|
|
|
2010
2039
|
event.preventDefault();
|
|
2011
2040
|
event.stopPropagation();
|
|
2012
2041
|
artifactsPaneUserHidden = false;
|
|
2013
|
-
// Card click is an explicit open: it overrides the "
|
|
2042
|
+
// Card click is an explicit open: it overrides the "collapsed"/"inline"
|
|
2014
2043
|
// auto-open suppression for as long as artifacts exist.
|
|
2015
2044
|
artifactsPaneUserOpened = true;
|
|
2016
2045
|
session.selectArtifact(artifactId);
|
|
@@ -2518,7 +2547,7 @@ export const createAgentExperience = (
|
|
|
2518
2547
|
} else if (lastArtifactsState.artifacts.length > 0 && artifactPaneCanShow()) {
|
|
2519
2548
|
// User chose “show” again (e.g. programmatic showArtifacts): clear dismiss chrome
|
|
2520
2549
|
// and force drawer open so narrow-host / mobile slide-out is not stuck off-screen.
|
|
2521
|
-
// Artifacts whose display mode is "
|
|
2550
|
+
// Artifacts whose display mode is "collapsed" or "inline" don't auto-open the
|
|
2522
2551
|
// pane (artifactPaneCanShow); it stays hidden until an explicit open or
|
|
2523
2552
|
// until a "panel"-mode artifact arrives.
|
|
2524
2553
|
artifactPaneApi.element.classList.remove("persona-hidden");
|
|
@@ -3440,7 +3469,17 @@ export const createAgentExperience = (
|
|
|
3440
3469
|
}
|
|
3441
3470
|
}
|
|
3442
3471
|
|
|
3443
|
-
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
|
+
});
|
|
3444
3483
|
let closeHandler: (() => void) | null = null;
|
|
3445
3484
|
let session: AgentWidgetSession;
|
|
3446
3485
|
|
|
@@ -3451,35 +3490,217 @@ export const createAgentExperience = (
|
|
|
3451
3490
|
// keep their before-first-user-message behavior. Config updates MUST route
|
|
3452
3491
|
// through here too: re-rendering with only `config.suggestionChips` would
|
|
3453
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
|
+
|
|
3454
3552
|
const renderSuggestions = (messages?: AgentWidgetMessage[]) => {
|
|
3455
3553
|
if (!session) return;
|
|
3456
3554
|
const current = messages ?? session.getMessages();
|
|
3457
|
-
const
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
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,
|
|
3464
3602
|
session,
|
|
3465
3603
|
textarea,
|
|
3466
3604
|
current,
|
|
3467
3605
|
config.suggestionChipsConfig,
|
|
3468
|
-
{
|
|
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
|
+
}
|
|
3469
3616
|
);
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
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,
|
|
3476
3649
|
session,
|
|
3477
3650
|
textarea,
|
|
3478
3651
|
current,
|
|
3479
|
-
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
|
+
}
|
|
3480
3662
|
);
|
|
3663
|
+
return;
|
|
3481
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
|
+
);
|
|
3482
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;
|
|
3695
|
+
}
|
|
3696
|
+
const current = session ? session.getMessages() : [];
|
|
3697
|
+
hostFollowUps = list;
|
|
3698
|
+
hostFollowUpsEverSet = true;
|
|
3699
|
+
hostFollowUpsUserCount = countUserMessages(current);
|
|
3700
|
+
hostFollowUpsAgentId = latestSuggestRepliesId(current);
|
|
3701
|
+
renderSuggestions(current);
|
|
3702
|
+
};
|
|
3703
|
+
|
|
3483
3704
|
let isStreaming = false;
|
|
3484
3705
|
const messageCache = createMessageCache();
|
|
3485
3706
|
// Tracks the last fingerprint we rendered a plugin-rendered ask_user_question
|
|
@@ -4505,7 +4726,7 @@ export const createAgentExperience = (
|
|
|
4505
4726
|
// visible instead of silently swallowed.
|
|
4506
4727
|
if (
|
|
4507
4728
|
isSuggestRepliesMessage(message) &&
|
|
4508
|
-
config.
|
|
4729
|
+
resolveFollowUpsFeature(config).enabled
|
|
4509
4730
|
) {
|
|
4510
4731
|
return;
|
|
4511
4732
|
}
|
|
@@ -5045,28 +5266,30 @@ export const createAgentExperience = (
|
|
|
5045
5266
|
|
|
5046
5267
|
// Only render if we have an indicator (allows hiding via returning null)
|
|
5047
5268
|
if (typingIndicator) {
|
|
5048
|
-
//
|
|
5269
|
+
// Bubble wrapper for the typing indicator. It borrows the assistant
|
|
5270
|
+
// bubble's classes and background so the "thinking" bubble matches the
|
|
5271
|
+
// message bubbles that replace it under every layout preset.
|
|
5049
5272
|
const typingBubble = document.createElement("div");
|
|
5050
5273
|
const showBubble = config.loadingIndicator?.showBubble !== false; // default true
|
|
5274
|
+
const messageLayout = config.layout?.messages?.layout ?? "bubble";
|
|
5051
5275
|
typingBubble.className = showBubble
|
|
5052
|
-
?
|
|
5053
|
-
"persona-rounded-2xl",
|
|
5054
|
-
"persona-text-sm",
|
|
5055
|
-
"persona-leading-relaxed",
|
|
5056
|
-
"persona-shadow-sm",
|
|
5057
|
-
"persona-bg-persona-surface",
|
|
5058
|
-
"persona-border",
|
|
5059
|
-
"persona-text-persona-primary",
|
|
5060
|
-
"persona-px-5",
|
|
5061
|
-
"persona-py-3"
|
|
5062
|
-
].join(" ")
|
|
5276
|
+
? getBubbleClasses("assistant", messageLayout).join(" ")
|
|
5063
5277
|
: [
|
|
5064
5278
|
"persona-text-sm",
|
|
5065
5279
|
"persona-leading-relaxed",
|
|
5066
|
-
"persona-text-persona-
|
|
5280
|
+
"persona-text-persona-text"
|
|
5067
5281
|
].join(" ");
|
|
5068
5282
|
typingBubble.setAttribute("data-typing-indicator", "true");
|
|
5069
|
-
|
|
5283
|
+
if (showBubble) {
|
|
5284
|
+
typingBubble.style.color =
|
|
5285
|
+
"var(--persona-message-assistant-text, var(--persona-text))";
|
|
5286
|
+
if (messageLayout !== "flat") {
|
|
5287
|
+
typingBubble.style.backgroundColor =
|
|
5288
|
+
"var(--persona-message-assistant-bg, var(--persona-container))";
|
|
5289
|
+
typingBubble.style.borderColor =
|
|
5290
|
+
"var(--persona-message-assistant-border, var(--persona-border, #e5e7eb))";
|
|
5291
|
+
}
|
|
5292
|
+
}
|
|
5070
5293
|
|
|
5071
5294
|
typingBubble.appendChild(typingIndicator);
|
|
5072
5295
|
|
|
@@ -5876,8 +6099,16 @@ export const createAgentExperience = (
|
|
|
5876
6099
|
if (micButton) {
|
|
5877
6100
|
micButton.disabled = disabled;
|
|
5878
6101
|
}
|
|
5879
|
-
|
|
5880
|
-
|
|
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
|
+
});
|
|
5881
6112
|
});
|
|
5882
6113
|
footer.dataset.personaComposerStreaming = disabled ? "true" : "false";
|
|
5883
6114
|
footer.querySelectorAll<HTMLElement>("[data-persona-composer-disable-when-streaming]").forEach((el) => {
|
|
@@ -6188,6 +6419,16 @@ export const createAgentExperience = (
|
|
|
6188
6419
|
}
|
|
6189
6420
|
if (!streaming) {
|
|
6190
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
|
+
}
|
|
6191
6432
|
}
|
|
6192
6433
|
// Keep the "streaming below" hint and its announcement in sync with the
|
|
6193
6434
|
// streaming lifecycle (Principles 8 + 15).
|
|
@@ -7846,8 +8087,11 @@ export const createAgentExperience = (
|
|
|
7846
8087
|
const previousStreamAnimationType = config.features?.streamAnimation?.type;
|
|
7847
8088
|
// One consistent recursive patch policy across the live controller and the
|
|
7848
8089
|
// init handle. See utils/config-merge.ts for the replace-leaf list and
|
|
7849
|
-
// explicit-undefined reset semantics.
|
|
7850
|
-
|
|
8090
|
+
// explicit-undefined reset semantics. The patch merges over the
|
|
8091
|
+
// pre-preference base features, then preferences re-resolve on top.
|
|
8092
|
+
config = mergeConfigUpdate({ ...config, features: baseFeatures }, nextConfig);
|
|
8093
|
+
baseFeatures = config.features;
|
|
8094
|
+
config = resolveConfigPreferences(config);
|
|
7851
8095
|
// applyFullHeightStyles resets mount.style.cssText, so call it before applyThemeVariables
|
|
7852
8096
|
applyFullHeightStyles();
|
|
7853
8097
|
applyThemeVariables(mount, config);
|
|
@@ -9282,13 +9526,20 @@ export const createAgentExperience = (
|
|
|
9282
9526
|
if (!artifactsSidebarEnabled(config)) return null;
|
|
9283
9527
|
// Programmatic upserts match the streamed UX: only "panel"-mode
|
|
9284
9528
|
// artifacts auto-open the pane (overriding a previous Close), while
|
|
9285
|
-
// "
|
|
9529
|
+
// "collapsed"/"inline" stay calm — the injected transcript block is the
|
|
9286
9530
|
// affordance. Independent of `transcript: false`: pane-only callers
|
|
9287
|
-
// (e.g. the theme editor preview) rely on the
|
|
9288
|
-
// callers that want the pane in
|
|
9531
|
+
// (e.g. the theme editor preview) rely on the auto-open default
|
|
9532
|
+
// surfacing; callers that want the pane in another mode call
|
|
9533
|
+
// showArtifacts().
|
|
9289
9534
|
const mode = resolveArtifactDisplayMode(
|
|
9290
9535
|
config.features?.artifacts,
|
|
9291
|
-
|
|
9536
|
+
{
|
|
9537
|
+
artifactType: manual.artifactType,
|
|
9538
|
+
...(manual.artifactType === "markdown" && manual.file
|
|
9539
|
+
? { file: manual.file }
|
|
9540
|
+
: {}),
|
|
9541
|
+
...(manual.presentation ? { presentation: manual.presentation } : {}),
|
|
9542
|
+
}
|
|
9292
9543
|
);
|
|
9293
9544
|
if (mode === "panel") {
|
|
9294
9545
|
artifactsPaneUserHidden = false;
|
|
@@ -9340,6 +9591,21 @@ export const createAgentExperience = (
|
|
|
9340
9591
|
}
|
|
9341
9592
|
return session.resolveApproval(approvalMessage.approval, decision, options);
|
|
9342
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
|
+
},
|
|
9343
9609
|
getMessages() {
|
|
9344
9610
|
return session.getMessages();
|
|
9345
9611
|
},
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { createAgentExperience } from "./ui";
|
|
6
|
+
|
|
7
|
+
const createMount = () => {
|
|
8
|
+
const mount = document.createElement("div");
|
|
9
|
+
document.body.appendChild(mount);
|
|
10
|
+
return mount;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Mount a widget with a never-resolving fetch, send a message, and return the
|
|
15
|
+
* standalone typing indicator bubble rendered during the silent gap.
|
|
16
|
+
*/
|
|
17
|
+
const renderTypingBubble = async (
|
|
18
|
+
config: Record<string, unknown>
|
|
19
|
+
): Promise<{ bubble: HTMLElement; destroy: () => void }> => {
|
|
20
|
+
global.fetch = vi.fn().mockImplementation(
|
|
21
|
+
() => new Promise(() => {})
|
|
22
|
+
) as unknown as typeof fetch;
|
|
23
|
+
|
|
24
|
+
const mount = createMount();
|
|
25
|
+
const controller = createAgentExperience(mount, {
|
|
26
|
+
apiUrl: "https://api.example.com/chat",
|
|
27
|
+
launcher: { enabled: false },
|
|
28
|
+
...config,
|
|
29
|
+
} as unknown as Parameters<typeof createAgentExperience>[1]);
|
|
30
|
+
|
|
31
|
+
controller.submitMessage("hello");
|
|
32
|
+
await Promise.resolve();
|
|
33
|
+
await Promise.resolve();
|
|
34
|
+
|
|
35
|
+
const bubble = mount.querySelector<HTMLElement>('[data-typing-indicator="true"]')!;
|
|
36
|
+
return { bubble, destroy: () => controller.destroy() };
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
describe("standalone typing indicator bubble", () => {
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
document.body.innerHTML = "";
|
|
42
|
+
if (typeof localStorage !== "undefined") localStorage.clear();
|
|
43
|
+
vi.restoreAllMocks();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("uses the assistant bubble classes under the default bubble layout", async () => {
|
|
47
|
+
const { bubble, destroy } = await renderTypingBubble({});
|
|
48
|
+
|
|
49
|
+
expect(bubble).not.toBeNull();
|
|
50
|
+
expect(bubble.classList.contains("persona-message-assistant-bubble")).toBe(true);
|
|
51
|
+
expect(bubble.classList.contains("persona-shadow-sm")).toBe(true);
|
|
52
|
+
expect(bubble.classList.contains("persona-border")).toBe(true);
|
|
53
|
+
expect(bubble.classList.contains("persona-px-5")).toBe(true);
|
|
54
|
+
|
|
55
|
+
destroy();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("drops the shadow and border under the minimal layout", async () => {
|
|
59
|
+
const { bubble, destroy } = await renderTypingBubble({
|
|
60
|
+
layout: { messages: { layout: "minimal" } },
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(bubble.classList.contains("persona-message-assistant-bubble")).toBe(true);
|
|
64
|
+
expect(bubble.classList.contains("persona-shadow-sm")).toBe(false);
|
|
65
|
+
expect(bubble.classList.contains("persona-border")).toBe(false);
|
|
66
|
+
expect(bubble.classList.contains("persona-px-3")).toBe(true);
|
|
67
|
+
// Minimal keeps the surface background, matching its message bubbles.
|
|
68
|
+
expect(bubble.style.backgroundColor).toContain("--persona-message-assistant-bg");
|
|
69
|
+
|
|
70
|
+
destroy();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("drops the background entirely under the flat layout", async () => {
|
|
74
|
+
const { bubble, destroy } = await renderTypingBubble({
|
|
75
|
+
layout: { messages: { layout: "flat" } },
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(bubble.classList.contains("persona-shadow-sm")).toBe(false);
|
|
79
|
+
expect(bubble.classList.contains("persona-border")).toBe(false);
|
|
80
|
+
expect(bubble.style.backgroundColor).toBe("");
|
|
81
|
+
expect(bubble.style.color).toBe(
|
|
82
|
+
"var(--persona-message-assistant-text, var(--persona-text))"
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
destroy();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("renders bare text classes when loadingIndicator.showBubble is false", async () => {
|
|
89
|
+
const { bubble, destroy } = await renderTypingBubble({
|
|
90
|
+
loadingIndicator: { showBubble: false },
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(bubble.classList.contains("persona-message-assistant-bubble")).toBe(false);
|
|
94
|
+
expect(bubble.classList.contains("persona-shadow-sm")).toBe(false);
|
|
95
|
+
expect(bubble.classList.contains("persona-border")).toBe(false);
|
|
96
|
+
expect(bubble.classList.contains("persona-text-persona-text")).toBe(true);
|
|
97
|
+
expect(bubble.classList.contains("persona-text-persona-primary")).toBe(false);
|
|
98
|
+
expect(bubble.style.backgroundColor).toBe("");
|
|
99
|
+
|
|
100
|
+
destroy();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("honors the assistant bubble theme background token", async () => {
|
|
104
|
+
const { bubble, destroy } = await renderTypingBubble({
|
|
105
|
+
layout: { messages: { layout: "minimal" } },
|
|
106
|
+
theme: {
|
|
107
|
+
semantic: {
|
|
108
|
+
colors: {
|
|
109
|
+
container: "#112233",
|
|
110
|
+
text: "#ddeeff",
|
|
111
|
+
border: "#445566",
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// The bubble reads the same variable the assistant message bubbles read,
|
|
118
|
+
// so `theme.components.message.assistant.background` covers both.
|
|
119
|
+
expect(bubble.style.backgroundColor).toBe(
|
|
120
|
+
"var(--persona-message-assistant-bg, var(--persona-container))"
|
|
121
|
+
);
|
|
122
|
+
const root = bubble.closest<HTMLElement>("[data-persona-root]");
|
|
123
|
+
expect(root?.style.getPropertyValue("--persona-message-assistant-bg")).toBe(
|
|
124
|
+
"#112233"
|
|
125
|
+
);
|
|
126
|
+
expect(root?.style.getPropertyValue("--persona-message-assistant-text")).toBe(
|
|
127
|
+
"#ddeeff"
|
|
128
|
+
);
|
|
129
|
+
expect(root?.style.getPropertyValue("--persona-message-assistant-border")).toBe(
|
|
130
|
+
"#445566"
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
destroy();
|
|
134
|
+
});
|
|
135
|
+
});
|