@runtypelabs/persona 4.23.2 → 4.25.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/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-CI7OckWI.d.cts → types-Db61aI9J.d.cts} +2 -0
- package/dist/animations/{types-CI7OckWI.d.ts → types-Db61aI9J.d.ts} +2 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/animations-extra.d.cts +2 -0
- package/dist/animations-extra.d.ts +2 -0
- package/dist/approval-ui-3NMYNXBD.js +1 -0
- package/dist/approval-ui.cjs +2 -2
- package/dist/approval-ui.d.cts +278 -37
- package/dist/approval-ui.d.ts +278 -37
- package/dist/approval-ui.js +2 -2
- package/dist/{artifacts-ui-F4N6VMKF.js → artifacts-ui-IGB5YKLO.js} +4 -4
- package/dist/artifacts-ui.d.cts +273 -25
- package/dist/artifacts-ui.d.ts +273 -25
- package/dist/{chunk-NIKYKSNZ.js → chunk-7CO4T5KB.js} +1 -1
- package/dist/codegen.cjs +11 -11
- package/dist/codegen.js +13 -13
- package/dist/context-mentions.d.cts +273 -25
- package/dist/context-mentions.d.ts +273 -25
- package/dist/event-stream-view.d.cts +273 -25
- package/dist/event-stream-view.d.ts +273 -25
- package/dist/forms-ui.d.cts +319 -27
- package/dist/forms-ui.d.ts +319 -27
- package/dist/history-view.d.cts +2 -0
- package/dist/history-view.d.ts +2 -0
- package/dist/index.cjs +70 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +344 -39
- package/dist/index.d.ts +344 -39
- package/dist/index.global.js +57 -54
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +70 -69
- package/dist/index.js.map +1 -1
- package/dist/install.global.js +1 -1
- package/dist/install.global.js.map +1 -1
- package/dist/launcher.global.js +2 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/session-reconnect.d.cts +273 -25
- package/dist/session-reconnect.d.ts +273 -25
- package/dist/smart-dom-reader.d.cts +273 -25
- package/dist/smart-dom-reader.d.ts +273 -25
- package/dist/theme-editor-preview.cjs +74 -73
- package/dist/theme-editor-preview.d.cts +280 -25
- package/dist/theme-editor-preview.d.ts +280 -25
- package/dist/theme-editor-preview.js +51 -50
- package/dist/theme-editor.cjs +10 -10
- package/dist/theme-editor.d.cts +293 -27
- package/dist/theme-editor.d.ts +293 -27
- package/dist/theme-editor.js +10 -10
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +1522 -11
- package/dist/theme-reference.d.ts +1522 -11
- package/dist/theme-reference.js +1 -1
- package/dist/voice-runtime-7DA7MJH4.js +1 -0
- package/dist/voice-runtime.cjs +1 -1
- package/dist/voice-runtime.d.cts +33 -0
- package/dist/voice-runtime.d.ts +33 -0
- package/dist/voice-runtime.js +1 -1
- package/dist/widget.css +1 -1
- package/package.json +3 -2
- package/src/__fixtures__/defaults-parity.v4.json +776 -0
- package/src/approval-ui-entry.ts +0 -1
- package/src/approval-ui-loader.ts +5 -4
- package/src/approval-ui.ts +0 -1
- package/src/client-identity-proof.test.ts +193 -0
- package/src/client-session-warm.test.ts +216 -0
- package/src/client-visitor-history.test.ts +6 -0
- package/src/client.ts +122 -30
- package/src/codegen.test.ts +24 -0
- package/src/components/activity-row.test.ts +100 -0
- package/src/components/activity-row.ts +136 -0
- package/src/components/approval-actions.test.ts +8 -5
- package/src/components/approval-actions.ts +19 -6
- package/src/components/approval-bubble.test.ts +56 -9
- package/src/components/approval-bubble.ts +36 -41
- package/src/components/composer-builder.ts +5 -2
- package/src/components/composer-parts.ts +10 -2
- package/src/components/header-builder.test.ts +32 -0
- package/src/components/header-builder.ts +7 -4
- package/src/components/header-layouts.test.ts +12 -0
- package/src/components/header-layouts.ts +3 -2
- package/src/components/launcher.ts +11 -2
- package/src/components/message-bubble.test.ts +22 -0
- package/src/components/message-bubble.ts +7 -3
- package/src/components/panel.test.ts +52 -1
- package/src/components/panel.ts +47 -8
- package/src/components/pill-composer-builder.test.ts +2 -0
- package/src/components/pill-composer-builder.ts +0 -1
- package/src/components/reasoning-bubble.ts +29 -17
- package/src/components/suggestions.test.ts +1 -0
- package/src/components/suggestions.ts +0 -16
- package/src/components/tool-bubble.ts +131 -114
- package/src/components/tool-details.test.ts +87 -0
- package/src/components/tool-details.ts +102 -0
- package/src/components/welcome.test.ts +8 -0
- package/src/components/welcome.ts +4 -2
- package/src/defaults-parity.test.ts +158 -0
- package/src/defaults.test.ts +69 -0
- package/src/defaults.ts +128 -49
- package/src/generated/runtype-openapi-contract.ts +2 -2
- package/src/index-core.ts +4 -0
- package/src/install.test.ts +24 -0
- package/src/install.ts +3 -1
- package/src/launcher-global.ts +2 -1
- package/src/launcher-variants.test.ts +52 -0
- package/src/runtime/init-update-reset.test.ts +21 -0
- package/src/session.ts +59 -7
- package/src/session.voice.test.ts +72 -1
- package/src/styles/widget-styles.test.ts +19 -0
- package/src/styles/widget.css +295 -23
- package/src/theme-editor/index.ts +2 -0
- package/src/theme-editor/preview-utils.test.ts +8 -0
- package/src/theme-editor/preview-utils.ts +7 -3
- package/src/theme-editor/sections.test.ts +31 -0
- package/src/theme-editor/sections.ts +55 -1
- package/src/theme-editor/state-preferences.test.ts +49 -0
- package/src/theme-editor/state.ts +73 -15
- package/src/theme-reference.test.ts +20 -0
- package/src/theme-reference.ts +11 -5
- package/src/tool-call-display-defaults.test.ts +33 -1
- package/src/types/theme.ts +80 -3
- package/src/types.ts +207 -22
- package/src/ui.activity-row.test.ts +91 -0
- package/src/ui.approval-plugin.test.ts +63 -0
- package/src/ui.composer-compact.test.ts +24 -2
- package/src/ui.composer-placement.test.ts +23 -0
- package/src/ui.detached-panel.test.ts +33 -6
- package/src/ui.docked.test.ts +19 -0
- package/src/ui.header-update-stability.test.ts +25 -0
- package/src/ui.identity-proof.test.ts +58 -0
- package/src/ui.launcher-teaser.test.ts +25 -0
- package/src/ui.message-width.test.ts +17 -2
- package/src/ui.reasoning-display.test.ts +92 -0
- package/src/ui.rebuild-composer.test.ts +20 -3
- package/src/ui.scroll.test.ts +32 -0
- package/src/ui.session-init.test.ts +312 -0
- package/src/ui.status-indicator.test.ts +82 -0
- package/src/ui.suggest-replies.test.ts +11 -0
- package/src/ui.ts +422 -74
- package/src/ui.v5-defaults.test.ts +144 -0
- package/src/ui.voice-prewarm.test.ts +222 -0
- package/src/ui.welcome.test.ts +92 -3
- package/src/utils/__snapshots__/tokens-parity.test.ts.snap +811 -140
- package/src/utils/code-generators.ts +14 -0
- package/src/utils/composer-compact.test.ts +0 -2
- package/src/utils/composer-compact.ts +2 -4
- package/src/utils/composer-placement.ts +2 -3
- package/src/utils/config-merge.test.ts +81 -5
- package/src/utils/config-merge.ts +13 -3
- package/src/utils/defaults-provenance.ts +67 -0
- package/src/utils/dock.ts +7 -1
- package/src/utils/launcher-variant.ts +16 -0
- package/src/utils/panel-config.ts +97 -0
- package/src/utils/status-indicator.ts +39 -0
- package/src/utils/theme.test.ts +182 -0
- package/src/utils/theme.ts +131 -18
- package/src/utils/tokens.test.ts +122 -1
- package/src/utils/tokens.ts +177 -17
- package/src/voice/runtype-voice-provider.ts +238 -27
- package/src/voice/voice.test.ts +272 -2
- package/src/welcome.test.ts +23 -0
- package/src/welcome.ts +16 -9
- package/dist/approval-ui-K7R5Q5OC.js +0 -1
- package/dist/voice-runtime-EFR3R5ES.js +0 -1
package/src/ui.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { activityVariant, activityDuration, createActivityLifecycle, createActivityGroup } from "./components/activity-row";
|
|
2
|
+
import { applyStatusIndicatorState, placeStatusIndicator } from "./utils/status-indicator";
|
|
1
3
|
import { usesSessionVoice } from "./utils/voice-support";
|
|
2
4
|
import { escapeHtml, createMarkdownProcessorFromConfig } from "./postprocessors";
|
|
3
5
|
import { resolveSanitizer } from "./utils/sanitize";
|
|
@@ -13,6 +15,8 @@ import {
|
|
|
13
15
|
import {
|
|
14
16
|
AgentWidgetConfig,
|
|
15
17
|
AgentWidgetConfigPatch,
|
|
18
|
+
AgentWidgetSessionInitHook,
|
|
19
|
+
AgentWidgetVoicePrewarmHook,
|
|
16
20
|
AgentWidgetApprovalDecisionOptions,
|
|
17
21
|
AgentWidgetMessage,
|
|
18
22
|
AgentWidgetEvent,
|
|
@@ -147,8 +151,9 @@ import {
|
|
|
147
151
|
} from "./utils/context-mention-orchestrator";
|
|
148
152
|
import type { MentionSubmitBundle } from "./utils/context-mention-manager";
|
|
149
153
|
import { createTextPart, ALL_SUPPORTED_MIME_TYPES } from "./utils/content";
|
|
150
|
-
import { applyThemeVariables, createThemeObserver, getActiveTheme } from "./utils/theme";
|
|
154
|
+
import { applyThemeVariables, createThemeObserver, getActiveTheme, getColorScheme } from "./utils/theme";
|
|
151
155
|
import { resolveTokenValue } from "./utils/tokens";
|
|
156
|
+
import { deepMerge } from "./utils/deep-merge";
|
|
152
157
|
import { Activity, Check, Copy } from "lucide";
|
|
153
158
|
import { renderLucideIcon, onExtraIconsReady } from "./utils/icons";
|
|
154
159
|
import { renderIconNode } from "./utils/icon-node";
|
|
@@ -222,6 +227,7 @@ import {
|
|
|
222
227
|
attachHeaderToContainer,
|
|
223
228
|
COMPOSER_BAR_CLEAR_CHAT_ICON_SIZE,
|
|
224
229
|
COMPOSER_BAR_CLOSE_ICON_SIZE,
|
|
230
|
+
resolvePanelGeometry,
|
|
225
231
|
} from "./components/panel";
|
|
226
232
|
import { buildPillComposer } from "./components/pill-composer-builder";
|
|
227
233
|
import {
|
|
@@ -263,8 +269,9 @@ import {
|
|
|
263
269
|
getBubbleClasses,
|
|
264
270
|
CUSTOM_MESSAGE_ACTION_PREFIX,
|
|
265
271
|
} from "./components/message-bubble";
|
|
266
|
-
import { createReasoningBubble,
|
|
267
|
-
import {
|
|
272
|
+
import { createReasoningBubble, updateReasoningBubbleUI } from "./components/reasoning-bubble";
|
|
273
|
+
import { copyToolDetail } from "./components/tool-details";
|
|
274
|
+
import { createToolBubble, updateToolBubbleUI } from "./components/tool-bubble";
|
|
268
275
|
import {
|
|
269
276
|
buildStructuredAnswers,
|
|
270
277
|
ensureAskUserQuestionSheet,
|
|
@@ -324,8 +331,9 @@ import {
|
|
|
324
331
|
} from "./utils/artifact-resize";
|
|
325
332
|
import { loadFormsUi, getFormsUiSync } from "./forms-ui-loader";
|
|
326
333
|
import { pluginRegistry } from "./plugins/registry";
|
|
327
|
-
import {
|
|
334
|
+
import { DEFAULT_HEADER_ICON_SIZE, mergeWithDefaults } from "./defaults";
|
|
328
335
|
import { mergeConfigUpdate } from "./utils/config-merge";
|
|
336
|
+
import { getPanelAliasProvenance } from "./utils/panel-config";
|
|
329
337
|
import { createEventBus } from "./utils/events";
|
|
330
338
|
import {
|
|
331
339
|
createActionManager,
|
|
@@ -701,6 +709,13 @@ type Controller = {
|
|
|
701
709
|
) => Promise<void>;
|
|
702
710
|
/** Clean local state plus a new owned server conversation. `focus` as on `openConversation`. */
|
|
703
711
|
startNewConversation: (opts?: { focus?: boolean }) => Promise<void>;
|
|
712
|
+
/**
|
|
713
|
+
* Client token mode: start `/v1/client/init` now, ahead of the first send
|
|
714
|
+
* (e.g. when the visitor hovers your own "Chat with us" button).
|
|
715
|
+
* Fire-and-forget, at most once per session lifetime, errors swallowed; the
|
|
716
|
+
* send reuses the in-flight init. No-op in other modes. See `sessionInit`.
|
|
717
|
+
*/
|
|
718
|
+
warmSession: () => void;
|
|
704
719
|
/** Permanently delete one server record; the active one is replaced. */
|
|
705
720
|
deleteConversation: (conversationId: string) => Promise<void>;
|
|
706
721
|
/**
|
|
@@ -930,6 +945,18 @@ export const createAgentExperience = (
|
|
|
930
945
|
}
|
|
931
946
|
|
|
932
947
|
let config = mergeWithDefaults(initialConfig) as AgentWidgetConfig;
|
|
948
|
+
// Message ids are only unique within a widget session. Keep disclosure state
|
|
949
|
+
// beside the widget that renders it, not in component module scope.
|
|
950
|
+
const toolExpansionState = new Set<string>();
|
|
951
|
+
const reasoningExpansionState = new Set<string>();
|
|
952
|
+
const activityLifecycle = createActivityLifecycle((id, kind) => {
|
|
953
|
+
messageCache.delete(id);
|
|
954
|
+
const bubble = Array.from(messagesWrapper.querySelectorAll<HTMLElement>("[data-message-id]")).find(node => node.dataset.messageId === id);
|
|
955
|
+
if (!bubble) return;
|
|
956
|
+
if (kind === "tool") updateToolBubbleUI(id, bubble, toolExpansionState, config);
|
|
957
|
+
else updateReasoningBubbleUI(id, bubble, reasoningExpansionState);
|
|
958
|
+
});
|
|
959
|
+
const approvalDetailsExpansionState = new Map<string, boolean>();
|
|
933
960
|
const applyTooltipTiming = (): void => {
|
|
934
961
|
configureTooltipTiming({
|
|
935
962
|
delayMs: config.tooltip?.delayMs ?? DEFAULT_TOOLTIP_DELAY_MS,
|
|
@@ -983,7 +1010,7 @@ export const createAgentExperience = (
|
|
|
983
1010
|
mod.initApprovalUi({
|
|
984
1011
|
webMcpToolTitle: getWebMcpToolDisplayTitle,
|
|
985
1012
|
});
|
|
986
|
-
const built = mod.createBuiltInApprovalPlugin();
|
|
1013
|
+
const built = mod.createBuiltInApprovalPlugin(approvalDetailsExpansionState);
|
|
987
1014
|
builtInApprovalPlugin = built.plugin;
|
|
988
1015
|
teardownBuiltInApprovals = built.teardown;
|
|
989
1016
|
};
|
|
@@ -1361,7 +1388,7 @@ export const createAgentExperience = (
|
|
|
1361
1388
|
|
|
1362
1389
|
// Get status indicator config
|
|
1363
1390
|
const statusConfig = config.statusIndicator ?? {};
|
|
1364
|
-
const _getStatusText = (status: AgentWidgetSessionStatus): string => {
|
|
1391
|
+
const _getStatusText = (status: AgentWidgetSessionStatus, statusConfig = config.statusIndicator ?? {}): string => {
|
|
1365
1392
|
if (status === "idle") return statusConfig.idleText ?? statusCopy.idle;
|
|
1366
1393
|
if (status === "connecting") return statusConfig.connectingText ?? statusCopy.connecting;
|
|
1367
1394
|
if (status === "connected") return statusConfig.connectedText ?? statusCopy.connected;
|
|
@@ -1373,6 +1400,10 @@ export const createAgentExperience = (
|
|
|
1373
1400
|
|
|
1374
1401
|
/** Update statusText element, rendering a link for idle status when idleLink is configured. */
|
|
1375
1402
|
function applyStatusToElement(el: HTMLElement, text: string, statusCfg: typeof statusConfig, status: string): void {
|
|
1403
|
+
if (!isComposerBar() && (statusCfg.mode === "transient" || el.classList.contains("persona-mb-2"))) {
|
|
1404
|
+
placeStatusIndicator(el, composerForm, statusCfg);
|
|
1405
|
+
applyStatusIndicatorState(el, statusCfg, status);
|
|
1406
|
+
}
|
|
1376
1407
|
// A composer lock reason owns the status region until the lock clears.
|
|
1377
1408
|
if (el.hasAttribute(COMPOSER_REASON_ATTR)) return;
|
|
1378
1409
|
if (status === "idle" && statusCfg.idleLink) {
|
|
@@ -1544,6 +1575,16 @@ export const createAgentExperience = (
|
|
|
1544
1575
|
}, 400);
|
|
1545
1576
|
};
|
|
1546
1577
|
|
|
1578
|
+
const isWelcomeFullscreen = () => {
|
|
1579
|
+
const win = mount.ownerDocument.defaultView ?? window;
|
|
1580
|
+
const mobile = (config.launcher?.mobileFullscreen ?? true) &&
|
|
1581
|
+
win.innerWidth <= (config.launcher?.mobileBreakpoint ?? 640) &&
|
|
1582
|
+
(launcherEnabled || isDockedMountMode(config));
|
|
1583
|
+
return mobile || (config.launcher?.fullHeight === true &&
|
|
1584
|
+
!config.launcher.sidebarMode && !isDockedMountMode(config));
|
|
1585
|
+
};
|
|
1586
|
+
const getWelcomeConfig = () => resolveWelcomeConfig(config, isWelcomeFullscreen());
|
|
1587
|
+
|
|
1547
1588
|
// Derived from the session's user messages on every welcome render; the
|
|
1548
1589
|
// public `data-persona-conversation-state` contract mirrors it.
|
|
1549
1590
|
let conversationState: "empty" | "active" = "empty";
|
|
@@ -1578,7 +1619,11 @@ export const createAgentExperience = (
|
|
|
1578
1619
|
// updateScrollToBottomButtonOffset reads them, never writes.
|
|
1579
1620
|
const syncComposerOverlayMetrics = () => {
|
|
1580
1621
|
const placement = resolveComposerPlacement(config, isComposerBar());
|
|
1581
|
-
const resolved =
|
|
1622
|
+
const resolved = getWelcomeConfig();
|
|
1623
|
+
mount.setAttribute("data-persona-welcome-fullscreen", String(isWelcomeFullscreen()));
|
|
1624
|
+
mount.setAttribute("data-persona-welcome-layout", resolved.layout ?? "top");
|
|
1625
|
+
mount.setAttribute("data-persona-welcome-layout-explicit", config.welcome?.layout ?? "");
|
|
1626
|
+
mount.setAttribute("data-persona-welcome-anchor", resolved.anchor ?? "bottom");
|
|
1582
1627
|
const footerHidden = footer.style.display === "none";
|
|
1583
1628
|
const footerHeight = footerHidden ? 0 : footer.offsetHeight;
|
|
1584
1629
|
|
|
@@ -1587,10 +1632,25 @@ export const createAgentExperience = (
|
|
|
1587
1632
|
? parseAnchorFraction(resolved.anchorComposerTop) ??
|
|
1588
1633
|
parseAnchorFraction(DEFAULT_ANCHOR_COMPOSER_TOP)!
|
|
1589
1634
|
: null;
|
|
1635
|
+
// The implicit fullscreen layout centers the complete greeting/composer
|
|
1636
|
+
// group. Explicit anchoring retains its percentage-based geometry.
|
|
1637
|
+
const centerGroup = fraction !== null && resolved.layout === "centered" &&
|
|
1638
|
+
config.welcome?.anchor === undefined &&
|
|
1639
|
+
config.welcome?.anchorComposerTop === undefined;
|
|
1640
|
+
const measuredBodyStyle = centerGroup ? getComputedStyle(body) : null;
|
|
1641
|
+
const previousOverlayHeight = parseFloat(mount.style.getPropertyValue("--persona-composer-overlay-height")) || 0;
|
|
1642
|
+
const previousOverlayLift = parseFloat(mount.style.getPropertyValue("--persona-composer-lift")) || 0;
|
|
1643
|
+
const groupGap = Math.max(0, (parseFloat(measuredBodyStyle?.paddingBottom ?? "") || 0) -
|
|
1644
|
+
(placement === "overlay" ? previousOverlayHeight + previousOverlayLift : 0));
|
|
1645
|
+
const columnTop = centerGroup ? body.getBoundingClientRect().top - container.getBoundingClientRect().top : 0;
|
|
1646
|
+
const greetingHeight = !centerGroup || welcomeHost.hidden ? 0 :
|
|
1647
|
+
(welcomeHost.querySelector<HTMLElement>("[data-persona-welcome-plugin]") ?? welcomeHost).offsetHeight;
|
|
1590
1648
|
const lift =
|
|
1591
1649
|
fraction === null || isComposerBar()
|
|
1592
1650
|
? 0
|
|
1593
|
-
:
|
|
1651
|
+
: centerGroup
|
|
1652
|
+
? Math.max(0, Math.round((container.clientHeight - columnTop - greetingHeight - groupGap - footerHeight) / 2))
|
|
1653
|
+
: computeComposerLift({
|
|
1594
1654
|
columnHeight: container.clientHeight,
|
|
1595
1655
|
footerHeight,
|
|
1596
1656
|
fraction,
|
|
@@ -2506,9 +2566,6 @@ export const createAgentExperience = (
|
|
|
2506
2566
|
pruneComposerModes(state.activeModeIds, composerChipModes()).length > 0
|
|
2507
2567
|
);
|
|
2508
2568
|
};
|
|
2509
|
-
// Bound once `voiceState` exists (further down this closure); the compact
|
|
2510
|
-
// sweep can run before then only via a microtask, which is later still.
|
|
2511
|
-
let isDictationActive: () => boolean = () => false;
|
|
2512
2569
|
function syncComposerCompact(): void {
|
|
2513
2570
|
const state = composerStore.getState();
|
|
2514
2571
|
const wrapped = composerCompactLatch.observe(
|
|
@@ -2524,7 +2581,6 @@ export const createAgentExperience = (
|
|
|
2524
2581
|
hasChips: hasComposerChips(),
|
|
2525
2582
|
hasQuote: state.quote !== undefined,
|
|
2526
2583
|
hasPendingSubmission: state.pendingSubmission !== undefined,
|
|
2527
|
-
dictationActive: isDictationActive(),
|
|
2528
2584
|
})
|
|
2529
2585
|
);
|
|
2530
2586
|
}
|
|
@@ -2667,6 +2723,16 @@ export const createAgentExperience = (
|
|
|
2667
2723
|
// Render custom slots
|
|
2668
2724
|
renderSlots();
|
|
2669
2725
|
|
|
2726
|
+
messagesWrapper.addEventListener("click", (event) => {
|
|
2727
|
+
const target = event.target;
|
|
2728
|
+
if (!(target instanceof Element)) return;
|
|
2729
|
+
const button = target.closest<HTMLButtonElement>("button[data-persona-copy-tool-detail]");
|
|
2730
|
+
if (!button) return;
|
|
2731
|
+
event.preventDefault();
|
|
2732
|
+
event.stopPropagation();
|
|
2733
|
+
void copyToolDetail(button);
|
|
2734
|
+
});
|
|
2735
|
+
|
|
2670
2736
|
// Add event delegation for reasoning and tool bubble expansion
|
|
2671
2737
|
// This handles clicks even after idiomorph morphs the DOM
|
|
2672
2738
|
const handleBubbleExpansion = (event: Event) => {
|
|
@@ -2684,7 +2750,17 @@ export const createAgentExperience = (
|
|
|
2684
2750
|
const messageId = bubble.getAttribute('data-message-id');
|
|
2685
2751
|
if (!messageId) return;
|
|
2686
2752
|
|
|
2753
|
+
// Disclosure is reading intent, not new streamed content. Pause before the
|
|
2754
|
+
// DOM grows so the resize observer cannot pull the opened details away.
|
|
2755
|
+
markReaderEngaged();
|
|
2756
|
+
pauseAutoScroll();
|
|
2687
2757
|
const bubbleType = headerButton.getAttribute('data-bubble-type');
|
|
2758
|
+
if (bubble.classList.contains("persona-activity-row")) {
|
|
2759
|
+
activityLifecycle.manual(messageId);
|
|
2760
|
+
const group = bubble.parentElement?.closest<HTMLElement>("[data-persona-tool-group]");
|
|
2761
|
+
if (group?.dataset.messageId) activityLifecycle.manual(group.dataset.messageId);
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2688
2764
|
|
|
2689
2765
|
// Toggle expansion state
|
|
2690
2766
|
if (bubbleType === 'reasoning') {
|
|
@@ -2693,22 +2769,22 @@ export const createAgentExperience = (
|
|
|
2693
2769
|
} else {
|
|
2694
2770
|
reasoningExpansionState.add(messageId);
|
|
2695
2771
|
}
|
|
2696
|
-
updateReasoningBubbleUI(messageId, bubble);
|
|
2772
|
+
updateReasoningBubbleUI(messageId, bubble, reasoningExpansionState);
|
|
2697
2773
|
} else if (bubbleType === 'tool') {
|
|
2698
2774
|
if (toolExpansionState.has(messageId)) {
|
|
2699
2775
|
toolExpansionState.delete(messageId);
|
|
2700
2776
|
} else {
|
|
2701
2777
|
toolExpansionState.add(messageId);
|
|
2702
2778
|
}
|
|
2703
|
-
updateToolBubbleUI(messageId, bubble, config);
|
|
2779
|
+
updateToolBubbleUI(messageId, bubble, toolExpansionState, config);
|
|
2704
2780
|
} else if (bubbleType === 'approval' && approvalUi) {
|
|
2705
2781
|
// approvalUi is always set here: approval bubbles only exist after the
|
|
2706
2782
|
// chunk was adopted. The guard keeps a stray click on a stub harmless.
|
|
2707
2783
|
const approvalConfig = config.approval !== false ? config.approval : undefined;
|
|
2708
2784
|
const defaultExpanded = (approvalConfig?.detailsDisplay ?? 'collapsed') === 'expanded';
|
|
2709
|
-
const expanded =
|
|
2710
|
-
|
|
2711
|
-
approvalUi.updateApprovalDetailsUI(messageId, bubble, config);
|
|
2785
|
+
const expanded = approvalDetailsExpansionState.get(messageId) ?? defaultExpanded;
|
|
2786
|
+
approvalDetailsExpansionState.set(messageId, !expanded);
|
|
2787
|
+
approvalUi.updateApprovalDetailsUI(messageId, bubble, config, approvalDetailsExpansionState);
|
|
2712
2788
|
}
|
|
2713
2789
|
// Invalidate cached wrapper so next render rebuilds with current expansion state
|
|
2714
2790
|
messageCache.delete(messageId);
|
|
@@ -2723,6 +2799,11 @@ export const createAgentExperience = (
|
|
|
2723
2799
|
}
|
|
2724
2800
|
});
|
|
2725
2801
|
|
|
2802
|
+
messagesWrapper.addEventListener('click', (event) => {
|
|
2803
|
+
// Assistive technologies activate buttons with a click rather than a pointer.
|
|
2804
|
+
if (event.detail === 0 && (event.target as HTMLElement).closest('.persona-activity-row')) handleBubbleExpansion(event);
|
|
2805
|
+
});
|
|
2806
|
+
|
|
2726
2807
|
messagesWrapper.addEventListener('keydown', (event) => {
|
|
2727
2808
|
const target = event.target as HTMLElement;
|
|
2728
2809
|
if ((event.key === 'Enter' || event.key === ' ') && target.closest('button[data-expand-header="true"]')) {
|
|
@@ -3984,7 +4065,7 @@ export const createAgentExperience = (
|
|
|
3984
4065
|
if (mobileFullscreen && ownerWindow.innerWidth <= mobileBreakpoint) return;
|
|
3985
4066
|
if (!shouldExpandLauncherForArtifacts(config, launcherEnabled)) return;
|
|
3986
4067
|
|
|
3987
|
-
const base = config.
|
|
4068
|
+
const base = resolvePanelGeometry(config).width;
|
|
3988
4069
|
const expanded =
|
|
3989
4070
|
config.features?.artifacts?.layout?.expandedPanelWidth ??
|
|
3990
4071
|
"min(720px, calc(100vw - 24px))";
|
|
@@ -4035,6 +4116,13 @@ export const createAgentExperience = (
|
|
|
4035
4116
|
// Apply full-height and sidebar styles if enabled
|
|
4036
4117
|
// This ensures the widget fills its container height with proper flex layout
|
|
4037
4118
|
const applyFullHeightStyles = () => {
|
|
4119
|
+
const fullScreenTranscript = isMobileFullscreenActive() ||
|
|
4120
|
+
(isComposerBar() && config.launcher?.composerBar?.expandedSize === "fullscreen") ||
|
|
4121
|
+
(config.launcher?.fullHeight === true && !config.launcher?.sidebarMode && !isDockedMountMode(config));
|
|
4122
|
+
messagesWrapper.style.gap = fullScreenTranscript
|
|
4123
|
+
? "var(--persona-components-message-fullscreenGap, var(--persona-components-message-gap, 12px))"
|
|
4124
|
+
: "var(--persona-components-message-gap, 12px)";
|
|
4125
|
+
messagesWrapper.style.setProperty("--persona-transcript-gap", messagesWrapper.style.gap);
|
|
4038
4126
|
// Composer-bar mode owns its own sizing/chrome. Geometry comes from
|
|
4039
4127
|
// `applyComposerBarGeometry()` (per-state inline on the wrapper), the
|
|
4040
4128
|
// pill carries its own chrome via `.persona-pill-composer`, and the
|
|
@@ -4083,7 +4171,14 @@ export const createAgentExperience = (
|
|
|
4083
4171
|
const isInlineEmbed = config.launcher?.enabled === false;
|
|
4084
4172
|
/** Detached appearance: inset card over a canvas instead of flush chrome. */
|
|
4085
4173
|
const isDetached = config.launcher?.detachedPanel === true;
|
|
4086
|
-
const
|
|
4174
|
+
const lightPanelPartial = config.theme?.components?.panel;
|
|
4175
|
+
const darkPanelPartial = config.darkTheme?.components?.panel;
|
|
4176
|
+
const panelPartial: typeof lightPanelPartial = getColorScheme(config) === "dark"
|
|
4177
|
+
? deepMerge(
|
|
4178
|
+
(lightPanelPartial ?? {}) as Record<string, unknown>,
|
|
4179
|
+
(darkPanelPartial ?? {}) as Record<string, unknown>
|
|
4180
|
+
) as typeof lightPanelPartial
|
|
4181
|
+
: lightPanelPartial;
|
|
4087
4182
|
const activeTheme = getActiveTheme(config);
|
|
4088
4183
|
const resolvePanelChrome = (raw: string | undefined, fallback: string): string => {
|
|
4089
4184
|
if (raw == null || raw === "") return fallback;
|
|
@@ -4110,11 +4205,38 @@ export const createAgentExperience = (
|
|
|
4110
4205
|
// Card chrome defaults (floating look): reused to restore detached chrome.
|
|
4111
4206
|
// Defaults chain through the aliases themeToCssVariables emits so explicit
|
|
4112
4207
|
// theme.components.panel overrides and these defaults never diverge.
|
|
4113
|
-
const
|
|
4114
|
-
|
|
4115
|
-
|
|
4208
|
+
const modeChrome = (
|
|
4209
|
+
mode: "floating" | "inline" | "sidebar" | "docked" | "mobile",
|
|
4210
|
+
field: "border" | "shadow" | "borderRadius",
|
|
4211
|
+
fallback: string
|
|
4212
|
+
): string => `var(--persona-components-panel-modes-${mode}-${field}, ${fallback})`;
|
|
4213
|
+
const configuredFloatingPanel = panelPartial?.modes?.floating;
|
|
4214
|
+
const cardBorder = resolvePanelChrome(
|
|
4215
|
+
configuredFloatingPanel?.border ?? panelPartial?.border,
|
|
4216
|
+
modeChrome("floating", "border", 'var(--persona-panel-border, 1px solid var(--persona-border))')
|
|
4217
|
+
);
|
|
4218
|
+
const cardShadow = resolvePanelChrome(
|
|
4219
|
+
configuredFloatingPanel?.shadow ?? panelPartial?.shadow,
|
|
4220
|
+
modeChrome("floating", "shadow", 'var(--persona-panel-shadow, var(--persona-palette-shadows-xl, 0 25px 50px -12px rgba(0, 0, 0, 0.25)))')
|
|
4221
|
+
);
|
|
4222
|
+
const cardRadius = resolvePanelChrome(
|
|
4223
|
+
configuredFloatingPanel?.borderRadius ?? panelPartial?.borderRadius,
|
|
4224
|
+
modeChrome("floating", "borderRadius", 'var(--persona-panel-radius, var(--persona-radius-xl, 0.75rem))')
|
|
4225
|
+
);
|
|
4116
4226
|
/** Detached restores card chrome except when a host layout goes fullscreen. */
|
|
4117
4227
|
const detachedCard = isDetached && !shouldGoFullscreen && !dockedHostFullscreen;
|
|
4228
|
+
const panelMode = shouldGoFullscreen || dockedHostFullscreen
|
|
4229
|
+
? "mobile"
|
|
4230
|
+
: detachedCard
|
|
4231
|
+
? "floating"
|
|
4232
|
+
: dockedMode
|
|
4233
|
+
? "docked"
|
|
4234
|
+
: sidebarMode
|
|
4235
|
+
? "sidebar"
|
|
4236
|
+
: isInlineEmbed
|
|
4237
|
+
? "inline"
|
|
4238
|
+
: "floating";
|
|
4239
|
+
const configuredModePanel = panelPartial?.modes?.[panelMode];
|
|
4118
4240
|
// Stamp reflects rendered chrome: cleared when a fullscreen host layout
|
|
4119
4241
|
// suppresses the card, so the attribute never lies to the detached CSS.
|
|
4120
4242
|
if (detachedCard) {
|
|
@@ -4132,7 +4254,7 @@ export const createAgentExperience = (
|
|
|
4132
4254
|
: shouldGoFullscreen
|
|
4133
4255
|
? 'none'
|
|
4134
4256
|
: sidebarMode
|
|
4135
|
-
? (isLeftSidebar ? '
|
|
4257
|
+
? (isLeftSidebar ? '2px 0 12px rgba(0, 0, 0, 0.08)' : '-2px 0 12px rgba(0, 0, 0, 0.08)')
|
|
4136
4258
|
// Flush inline embeds fill their container: no elevation by default
|
|
4137
4259
|
// (detachedPanel or components.panel.shadow opts back in).
|
|
4138
4260
|
: isInlineEmbed ? 'none' : cardShadow;
|
|
@@ -4146,9 +4268,18 @@ export const createAgentExperience = (
|
|
|
4146
4268
|
: (sidebarMode || shouldGoFullscreen) ? '0' : cardRadius;
|
|
4147
4269
|
|
|
4148
4270
|
// Apply theme overrides or defaults (components.panel.*)
|
|
4149
|
-
const panelBorder = resolvePanelChrome(
|
|
4150
|
-
|
|
4151
|
-
|
|
4271
|
+
const panelBorder = resolvePanelChrome(
|
|
4272
|
+
configuredModePanel?.border ?? panelPartial?.border,
|
|
4273
|
+
modeChrome(panelMode, "border", defaultPanelBorder)
|
|
4274
|
+
);
|
|
4275
|
+
const panelShadow = resolvePanelChrome(
|
|
4276
|
+
configuredModePanel?.shadow ?? panelPartial?.shadow,
|
|
4277
|
+
modeChrome(panelMode, "shadow", defaultPanelShadow)
|
|
4278
|
+
);
|
|
4279
|
+
const panelBorderRadius = resolvePanelChrome(
|
|
4280
|
+
configuredModePanel?.borderRadius ?? panelPartial?.borderRadius,
|
|
4281
|
+
modeChrome(panelMode, "borderRadius", defaultPanelBorderRadius)
|
|
4282
|
+
);
|
|
4152
4283
|
|
|
4153
4284
|
// Split chrome: 'welded' folds the card border onto the outer panel so it
|
|
4154
4285
|
// wraps both columns as one card (shadow/radius already on the panel);
|
|
@@ -4192,7 +4323,8 @@ export const createAgentExperience = (
|
|
|
4192
4323
|
// Flush fills the container flush, so the outer panel squares off by default;
|
|
4193
4324
|
// the pane keeps its own rounded radius. An explicit panel.borderRadius wins.
|
|
4194
4325
|
const panelRadiusExplicit =
|
|
4195
|
-
|
|
4326
|
+
(configuredModePanel?.borderRadius != null && configuredModePanel.borderRadius !== '') ||
|
|
4327
|
+
(panelPartial?.borderRadius != null && panelPartial.borderRadius !== '');
|
|
4196
4328
|
const appliedPanelRadius =
|
|
4197
4329
|
chatFlush && !panelRadiusExplicit ? '0' : panelBorderRadius;
|
|
4198
4330
|
|
|
@@ -4317,15 +4449,14 @@ export const createAgentExperience = (
|
|
|
4317
4449
|
}
|
|
4318
4450
|
|
|
4319
4451
|
// Re-apply panel width/maxWidth from initial setup
|
|
4320
|
-
const
|
|
4321
|
-
const width = launcherWidth ?? DEFAULT_FLOATING_LAUNCHER_WIDTH;
|
|
4452
|
+
const geometry = resolvePanelGeometry(config);
|
|
4322
4453
|
if (!sidebarMode && !dockedMode) {
|
|
4323
4454
|
if (isInlineEmbed && fullHeight) {
|
|
4324
4455
|
panel.style.width = "100%";
|
|
4325
4456
|
panel.style.maxWidth = "100%";
|
|
4326
4457
|
} else {
|
|
4327
|
-
panel.style.width = width;
|
|
4328
|
-
panel.style.maxWidth =
|
|
4458
|
+
panel.style.width = geometry.width;
|
|
4459
|
+
panel.style.maxWidth = geometry.maxWidth;
|
|
4329
4460
|
}
|
|
4330
4461
|
} else if (dockedMode) {
|
|
4331
4462
|
const dockReveal = resolveDockConfig(config).reveal;
|
|
@@ -4375,7 +4506,9 @@ export const createAgentExperience = (
|
|
|
4375
4506
|
weldedOuterRadius = null;
|
|
4376
4507
|
}
|
|
4377
4508
|
|
|
4378
|
-
|
|
4509
|
+
const panelBorderExplicit =
|
|
4510
|
+
configuredModePanel?.border != null || panelPartial?.border != null;
|
|
4511
|
+
if (dockedMode && !shouldGoFullscreen && !detachedCard && !detachedSplitActive && !weldedSplitActive && !panelBorderExplicit) {
|
|
4379
4512
|
container.style.border = 'none';
|
|
4380
4513
|
const dockSide = resolveDockConfig(config).side;
|
|
4381
4514
|
if (dockSide === 'right') {
|
|
@@ -4389,7 +4522,7 @@ export const createAgentExperience = (
|
|
|
4389
4522
|
// mode resolves its border to none, so add the dock-facing hairline there so
|
|
4390
4523
|
// the split still separates from the host page. Mirrors the flush block's
|
|
4391
4524
|
// side choice (right dock => left edge faces the page).
|
|
4392
|
-
if (dockedMode && !shouldGoFullscreen && weldedSplitActive &&
|
|
4525
|
+
if (dockedMode && !shouldGoFullscreen && weldedSplitActive && !panelBorderExplicit) {
|
|
4393
4526
|
const dockSide = resolveDockConfig(config).side;
|
|
4394
4527
|
if (dockSide === 'right') {
|
|
4395
4528
|
panel.style.borderLeft = '1px solid var(--persona-border)';
|
|
@@ -4478,7 +4611,9 @@ export const createAgentExperience = (
|
|
|
4478
4611
|
|
|
4479
4612
|
// Apply sidebar-specific styles
|
|
4480
4613
|
if (sidebarMode) {
|
|
4481
|
-
const sidebarWidth = config.
|
|
4614
|
+
const sidebarWidth = getPanelAliasProvenance(config).sidebarWidth
|
|
4615
|
+
? config.launcher?.sidebarWidth ?? "420px"
|
|
4616
|
+
: resolveTokenValue(getActiveTheme(config), "components.panel.width") ?? "420px";
|
|
4482
4617
|
|
|
4483
4618
|
// Wrapper - fixed position. Detached insets the card off the edges and
|
|
4484
4619
|
// shrinks its height by the inset on both ends; flush hugs the edges.
|
|
@@ -4592,7 +4727,12 @@ export const createAgentExperience = (
|
|
|
4592
4727
|
let warnedComposerBarPlacement = false;
|
|
4593
4728
|
// mount.style.cssText is wiped by applyFullHeightStyles, so every
|
|
4594
4729
|
// mount-level var re-stamps here, next to applyContentMaxWidthVar.
|
|
4730
|
+
function syncTranscriptTopFade() {
|
|
4731
|
+
if (config.layout?.topFade && body.scrollTop > 1) body.setAttribute("data-persona-top-fade", "true");
|
|
4732
|
+
else body.removeAttribute("data-persona-top-fade");
|
|
4733
|
+
}
|
|
4595
4734
|
const applyComposerPlacement = () => {
|
|
4735
|
+
syncTranscriptTopFade();
|
|
4596
4736
|
const placement = resolveComposerPlacement(config, isComposerBar());
|
|
4597
4737
|
if (
|
|
4598
4738
|
isComposerBar() &&
|
|
@@ -4605,7 +4745,7 @@ export const createAgentExperience = (
|
|
|
4605
4745
|
"[persona] composer.placement is ignored in composer-bar mount mode."
|
|
4606
4746
|
);
|
|
4607
4747
|
}
|
|
4608
|
-
const welcome =
|
|
4748
|
+
const welcome = getWelcomeConfig();
|
|
4609
4749
|
mount.setAttribute("data-persona-composer-placement", placement);
|
|
4610
4750
|
mount.setAttribute("data-persona-conversation-state", conversationState);
|
|
4611
4751
|
// Root-level mirror of the welcome host's own anchor attribute, so the
|
|
@@ -4632,12 +4772,9 @@ export const createAgentExperience = (
|
|
|
4632
4772
|
const mobileFullscreen = config.launcher?.mobileFullscreen ?? true;
|
|
4633
4773
|
const mobileBreakpoint = config.launcher?.mobileBreakpoint ?? 640;
|
|
4634
4774
|
if (mobileFullscreen && ownerWindow.innerWidth <= mobileBreakpoint) return;
|
|
4635
|
-
const
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
const available = Math.max(200, viewportHeight - verticalMargin);
|
|
4639
|
-
const clamped = Math.min(640, available);
|
|
4640
|
-
panel.style.height = `${Math.max(200, clamped - heightOffset)}px`;
|
|
4775
|
+
const geometry = resolvePanelGeometry(config);
|
|
4776
|
+
panel.style.height = geometry.height;
|
|
4777
|
+
panel.style.maxHeight = geometry.maxHeight;
|
|
4641
4778
|
};
|
|
4642
4779
|
applyFullHeightStyles();
|
|
4643
4780
|
// Apply theme variables after applyFullHeightStyles since it resets mount.style.cssText
|
|
@@ -4981,12 +5118,14 @@ export const createAgentExperience = (
|
|
|
4981
5118
|
return;
|
|
4982
5119
|
}
|
|
4983
5120
|
|
|
4984
|
-
const
|
|
5121
|
+
const v5 = config.future?.v5Defaults === true;
|
|
5122
|
+
const fullscreen = isWelcomeFullscreen();
|
|
5123
|
+
const starters = config.suggestions?.starters ?? (v5 ? {} : undefined);
|
|
4985
5124
|
if (starters) {
|
|
4986
5125
|
// The starter host lives inside the welcome surface, so a pinned
|
|
4987
5126
|
// "welcome" placement has nowhere to render when that surface is hidden.
|
|
4988
5127
|
const welcomeCardVisible = isWelcomeVisible(
|
|
4989
|
-
|
|
5128
|
+
getWelcomeConfig(),
|
|
4990
5129
|
current
|
|
4991
5130
|
);
|
|
4992
5131
|
const requestedPlacement = starters.placement ?? "auto";
|
|
@@ -5014,10 +5153,10 @@ export const createAgentExperience = (
|
|
|
5014
5153
|
config.suggestionChipsConfig,
|
|
5015
5154
|
{
|
|
5016
5155
|
surface: "starter",
|
|
5017
|
-
variant: starters.variant ?? "card",
|
|
5156
|
+
variant: starters.variant ?? (v5 && !fullscreen ? "chip" : "card"),
|
|
5018
5157
|
behavior: starters.behavior ?? "send",
|
|
5019
5158
|
overflow: starters.overflow ?? "wrap",
|
|
5020
|
-
maxItems: starters.maxItems ?? 4,
|
|
5159
|
+
maxItems: starters.maxItems ?? (v5 && !fullscreen ? 3 : 4),
|
|
5021
5160
|
config,
|
|
5022
5161
|
plugins,
|
|
5023
5162
|
submitPrompt: submitSuggestionPrompt,
|
|
@@ -5038,6 +5177,7 @@ export const createAgentExperience = (
|
|
|
5038
5177
|
variant: "chip",
|
|
5039
5178
|
behavior: "send",
|
|
5040
5179
|
overflow: "wrap",
|
|
5180
|
+
maxItems: 4,
|
|
5041
5181
|
config,
|
|
5042
5182
|
plugins,
|
|
5043
5183
|
submitPrompt: submitSuggestionPrompt,
|
|
@@ -5220,7 +5360,6 @@ export const createAgentExperience = (
|
|
|
5220
5360
|
lastUserMessageWasVoice: false,
|
|
5221
5361
|
lastUserMessageId: null as string | null
|
|
5222
5362
|
};
|
|
5223
|
-
isDictationActive = () => voiceState.active;
|
|
5224
5363
|
// First stamp: an untouched composer emits no store change, so the attribute
|
|
5225
5364
|
// would otherwise not appear until the first keystroke.
|
|
5226
5365
|
syncComposerCompact();
|
|
@@ -6163,7 +6302,9 @@ export const createAgentExperience = (
|
|
|
6163
6302
|
// Only an explicit config value is stamped inline; the 85%/100% defaults
|
|
6164
6303
|
// live in widget.css so `components.message.<role>.maxWidth` can win.
|
|
6165
6304
|
const maxWidth =
|
|
6166
|
-
roleLayout?.maxWidth ?? (width === "full"
|
|
6305
|
+
roleLayout?.maxWidth ?? (width === "full"
|
|
6306
|
+
? `var(--persona-message-${sizingRole}-max-width, 100%)`
|
|
6307
|
+
: undefined);
|
|
6167
6308
|
|
|
6168
6309
|
wrapper.classList.add("persona-message-row");
|
|
6169
6310
|
wrapper.classList.remove(
|
|
@@ -6201,6 +6342,15 @@ export const createAgentExperience = (
|
|
|
6201
6342
|
messages: AgentWidgetMessage[],
|
|
6202
6343
|
transform: MessageTransform
|
|
6203
6344
|
) => {
|
|
6345
|
+
const activityIds = new Set<string>();
|
|
6346
|
+
for (const message of messages) {
|
|
6347
|
+
const kind = message.variant === "tool" ? "tool" : message.variant === "reasoning" ? "reasoning" : null;
|
|
6348
|
+
if (!kind || activityVariant(config, kind) !== "row") continue;
|
|
6349
|
+
const display = kind === "tool" ? config.features?.toolCallDisplay : config.features?.reasoningDisplay;
|
|
6350
|
+
if (display?.expandable === false) continue;
|
|
6351
|
+
activityIds.add(message.id);
|
|
6352
|
+
activityLifecycle.observe(message, kind, kind === "tool" ? toolExpansionState : reasoningExpansionState, display);
|
|
6353
|
+
}
|
|
6204
6354
|
// Build new content in a temporary container for morphing
|
|
6205
6355
|
const tempContainer = document.createElement("div");
|
|
6206
6356
|
|
|
@@ -6613,7 +6763,7 @@ export const createAgentExperience = (
|
|
|
6613
6763
|
};
|
|
6614
6764
|
liveBubble = approvalPlugin.renderApproval({
|
|
6615
6765
|
message,
|
|
6616
|
-
defaultRenderer: () => approvalMod.createApprovalBubble(message, config),
|
|
6766
|
+
defaultRenderer: () => approvalMod.createApprovalBubble(message, config, approvalDetailsExpansionState),
|
|
6617
6767
|
config,
|
|
6618
6768
|
approve: (options) => resolveDecision("approved", options),
|
|
6619
6769
|
deny: (options) => resolveDecision("denied", options)
|
|
@@ -6629,7 +6779,7 @@ export const createAgentExperience = (
|
|
|
6629
6779
|
const existing = container.querySelector<HTMLElement>(`#wrapper-${message.id}`);
|
|
6630
6780
|
existing?.removeAttribute("data-preserve-runtime");
|
|
6631
6781
|
lastApprovalBubbleFingerprint.delete(message.id);
|
|
6632
|
-
bubble = approvalMod.createApprovalBubble(message, config);
|
|
6782
|
+
bubble = approvalMod.createApprovalBubble(message, config, approvalDetailsExpansionState);
|
|
6633
6783
|
} else {
|
|
6634
6784
|
// A fresh live bubble to hydrate (needsRebuild), or fingerprint
|
|
6635
6785
|
// unchanged so we reuse the preserved live wrapper (`bubble: null`).
|
|
@@ -6649,14 +6799,14 @@ export const createAgentExperience = (
|
|
|
6649
6799
|
if (!showReasoning) return;
|
|
6650
6800
|
bubble = matchingPlugin.renderReasoning({
|
|
6651
6801
|
message,
|
|
6652
|
-
defaultRenderer: () => createReasoningBubble(message, config),
|
|
6802
|
+
defaultRenderer: () => createReasoningBubble(message, config, reasoningExpansionState),
|
|
6653
6803
|
config
|
|
6654
6804
|
});
|
|
6655
6805
|
} else if (message.variant === "tool" && message.toolCall && matchingPlugin.renderToolCall) {
|
|
6656
6806
|
if (!showToolCalls) return;
|
|
6657
6807
|
bubble = matchingPlugin.renderToolCall({
|
|
6658
6808
|
message,
|
|
6659
|
-
defaultRenderer: () => createToolBubble(message, config),
|
|
6809
|
+
defaultRenderer: () => createToolBubble(message, config, toolExpansionState),
|
|
6660
6810
|
config
|
|
6661
6811
|
});
|
|
6662
6812
|
} else if (matchingPlugin.renderMessage) {
|
|
@@ -6824,15 +6974,15 @@ export const createAgentExperience = (
|
|
|
6824
6974
|
if (!bubble) {
|
|
6825
6975
|
if (message.variant === "reasoning" && message.reasoning) {
|
|
6826
6976
|
if (!showReasoning) return;
|
|
6827
|
-
bubble = createReasoningBubble(message, config);
|
|
6977
|
+
bubble = createReasoningBubble(message, config, reasoningExpansionState);
|
|
6828
6978
|
} else if (message.variant === "tool" && message.toolCall) {
|
|
6829
6979
|
if (!showToolCalls) return;
|
|
6830
|
-
bubble = createToolBubble(message, config);
|
|
6980
|
+
bubble = createToolBubble(message, config, toolExpansionState);
|
|
6831
6981
|
} else if (message.variant === "approval" && message.approval) {
|
|
6832
6982
|
if (config.approval === false) return;
|
|
6833
6983
|
const approvalMod = ensureApprovalUi();
|
|
6834
6984
|
if (!approvalMod) return;
|
|
6835
|
-
bubble = approvalMod.createApprovalBubble(message, config);
|
|
6985
|
+
bubble = approvalMod.createApprovalBubble(message, config, approvalDetailsExpansionState);
|
|
6836
6986
|
} else {
|
|
6837
6987
|
// Check for custom message renderers in layout config
|
|
6838
6988
|
const messageLayoutConfig = config.layout?.messages;
|
|
@@ -6950,6 +7100,32 @@ export const createAgentExperience = (
|
|
|
6950
7100
|
);
|
|
6951
7101
|
groupWrapper.setAttribute("data-persona-tool-group-row", "true");
|
|
6952
7102
|
|
|
7103
|
+
if (config.features?.toolCallDisplay?.groupedMode === "collapsible") {
|
|
7104
|
+
const id = `tool-group-${group[0].id}`;
|
|
7105
|
+
const active = group.some(item => item.toolCall?.status !== "complete");
|
|
7106
|
+
const synthetic: AgentWidgetMessage = { ...group[0], id, toolCall: {
|
|
7107
|
+
id, name: `${group.length} tools`, status: active ? "running" : "complete",
|
|
7108
|
+
chunks: group.flatMap(item => item.toolCall?.chunks ?? []),
|
|
7109
|
+
success: !group.some(item => item.toolCall?.success === false),
|
|
7110
|
+
} };
|
|
7111
|
+
activityIds.add(id);
|
|
7112
|
+
activityLifecycle.observe(synthetic, "tool", toolExpansionState, config.features.toolCallDisplay);
|
|
7113
|
+
const label = `${active ? "Using" : "Used"} ${group.length} tools`;
|
|
7114
|
+
const custom = config.toolCall?.renderGroupedSummary?.({ messages: group, toolCalls: group.map(item => item.toolCall!), defaultSummary: label, config });
|
|
7115
|
+
const row = createActivityGroup(synthetic, config, toolExpansionState.has(id), custom ?? label);
|
|
7116
|
+
row.bubble.classList.add("persona-tool-group");
|
|
7117
|
+
row.body.dataset.personaToolGroupStack = "true";
|
|
7118
|
+
wrappers[0].before(groupWrapper);
|
|
7119
|
+
groupWrapper.appendChild(row.bubble);
|
|
7120
|
+
wrappers.forEach((wrapper, index) => {
|
|
7121
|
+
wrapper.style.setProperty("--persona-message-row-max-width", "100%");
|
|
7122
|
+
wrapper.style.setProperty("--persona-activity-stagger", `${index * 40}ms`);
|
|
7123
|
+
wrapper.classList.add("persona-activity-group-child");
|
|
7124
|
+
row.body.appendChild(wrapper);
|
|
7125
|
+
});
|
|
7126
|
+
return;
|
|
7127
|
+
}
|
|
7128
|
+
|
|
6953
7129
|
const groupContainer = document.createElement("div");
|
|
6954
7130
|
groupContainer.className =
|
|
6955
7131
|
"persona-tool-group persona-flex persona-w-full persona-flex-col persona-gap-2";
|
|
@@ -7006,6 +7182,8 @@ export const createAgentExperience = (
|
|
|
7006
7182
|
});
|
|
7007
7183
|
}
|
|
7008
7184
|
|
|
7185
|
+
activityLifecycle.prune(activityIds);
|
|
7186
|
+
|
|
7009
7187
|
// Remove cache entries for messages that no longer exist
|
|
7010
7188
|
pruneCache(messageCache, activeMessageIds);
|
|
7011
7189
|
|
|
@@ -8018,6 +8196,7 @@ export const createAgentExperience = (
|
|
|
8018
8196
|
statusText.setAttribute("aria-live", "polite");
|
|
8019
8197
|
statusText.setAttribute(COMPOSER_REASON_ATTR, "");
|
|
8020
8198
|
statusText.textContent = reason;
|
|
8199
|
+
applyStatusIndicatorState(statusText, config.statusIndicator ?? {}, "idle");
|
|
8021
8200
|
return;
|
|
8022
8201
|
}
|
|
8023
8202
|
if (!statusText.hasAttribute(COMPOSER_REASON_ATTR)) return;
|
|
@@ -8176,7 +8355,8 @@ export const createAgentExperience = (
|
|
|
8176
8355
|
composerLiftAnimation = animateComposerLiftChange(
|
|
8177
8356
|
footer,
|
|
8178
8357
|
next === "active" ? previousLift : readLiftPx(),
|
|
8179
|
-
next === "active" ? "drop" : "rise"
|
|
8358
|
+
next === "active" ? "drop" : "rise",
|
|
8359
|
+
getWelcomeConfig().layout === "centered" ? 320 : 260
|
|
8180
8360
|
);
|
|
8181
8361
|
}
|
|
8182
8362
|
// The reservation shrinks by the lift in one frame, above the anchor.
|
|
@@ -8194,8 +8374,8 @@ export const createAgentExperience = (
|
|
|
8194
8374
|
// so the pending stand-in the overlay body class would hide stays visible.
|
|
8195
8375
|
let welcomePluginSuppressed = false;
|
|
8196
8376
|
const updateWelcome = (messages?: AgentWidgetMessage[]) => {
|
|
8197
|
-
const resolved =
|
|
8198
|
-
const welcomeKey = `${resolved.variant}|${resolved.dismiss}|${resolved.anchor ?? "bottom"}|${resolved.align ?? ""}|${resolved.iconPlacement}|${resolved.kicker ?? ""}|${resolved.title}|${resolved.subtitle}`;
|
|
8377
|
+
const resolved = getWelcomeConfig();
|
|
8378
|
+
const welcomeKey = `${resolved.variant}|${resolved.layout}|${resolved.dismiss}|${resolved.anchor ?? "bottom"}|${resolved.align ?? ""}|${resolved.iconPlacement}|${resolved.kicker ?? ""}|${resolved.title}|${resolved.subtitle}`;
|
|
8199
8379
|
if (welcomeKey !== lastWelcomeKey || resolved.icon !== lastWelcomeIcon) {
|
|
8200
8380
|
lastWelcomeKey = welcomeKey;
|
|
8201
8381
|
lastWelcomeIcon = resolved.icon;
|
|
@@ -8348,8 +8528,8 @@ export const createAgentExperience = (
|
|
|
8348
8528
|
// A plugin calling requestRender() from inside its own render would recurse.
|
|
8349
8529
|
if (welcomeArbitrating) return;
|
|
8350
8530
|
welcomeArbitrating = true;
|
|
8351
|
-
const resolved =
|
|
8352
|
-
lastWelcomeArbitrationKey = `${resolved.variant}|${resolved.dismiss}|${resolved.title}|${resolved.subtitle}|${resolved.message ?? ""}`;
|
|
8531
|
+
const resolved = getWelcomeConfig();
|
|
8532
|
+
lastWelcomeArbitrationKey = `${resolved.variant}|${resolved.layout}|${resolved.dismiss}|${resolved.title}|${resolved.subtitle}|${resolved.message ?? ""}`;
|
|
8353
8533
|
lastWelcomeArbitrationIcon = resolved.icon;
|
|
8354
8534
|
try {
|
|
8355
8535
|
runWelcomeCleanups();
|
|
@@ -8398,8 +8578,8 @@ export const createAgentExperience = (
|
|
|
8398
8578
|
|
|
8399
8579
|
/** Re-arbitrate only when the resolved welcome config actually changed. */
|
|
8400
8580
|
const refreshWelcomePlugins = () => {
|
|
8401
|
-
const resolved =
|
|
8402
|
-
const key = `${resolved.variant}|${resolved.dismiss}|${resolved.title}|${resolved.subtitle}|${resolved.message ?? ""}`;
|
|
8581
|
+
const resolved = getWelcomeConfig();
|
|
8582
|
+
const key = `${resolved.variant}|${resolved.layout}|${resolved.dismiss}|${resolved.title}|${resolved.subtitle}|${resolved.message ?? ""}`;
|
|
8403
8583
|
if (
|
|
8404
8584
|
key === lastWelcomeArbitrationKey &&
|
|
8405
8585
|
resolved.icon === lastWelcomeArbitrationIcon
|
|
@@ -8639,7 +8819,7 @@ export const createAgentExperience = (
|
|
|
8639
8819
|
spans.forEach((span) => {
|
|
8640
8820
|
const startedAt = Number(span.getAttribute("data-tool-elapsed"));
|
|
8641
8821
|
if (!startedAt) return;
|
|
8642
|
-
span.textContent = formatElapsedMs(now - startedAt);
|
|
8822
|
+
span.textContent = span.closest(".persona-activity-row") ? activityDuration(now - startedAt) : formatElapsedMs(now - startedAt);
|
|
8643
8823
|
});
|
|
8644
8824
|
}, 100);
|
|
8645
8825
|
};
|
|
@@ -11413,9 +11593,15 @@ export const createAgentExperience = (
|
|
|
11413
11593
|
historySurface = null;
|
|
11414
11594
|
});
|
|
11415
11595
|
|
|
11416
|
-
//
|
|
11417
|
-
//
|
|
11418
|
-
|
|
11596
|
+
// Client-token init on mount is reserved for widgets that need a session at
|
|
11597
|
+
// boot: visitor history (boot reconciliation) and `sessionInit: 'mount'`.
|
|
11598
|
+
// Everything else inits on intent (the `sessionInit` triggers below) or at
|
|
11599
|
+
// send, since init is no longer free server-side (it pre-allocates the
|
|
11600
|
+
// conversation's session owner).
|
|
11601
|
+
if (
|
|
11602
|
+
config.clientToken &&
|
|
11603
|
+
(config.features?.history?.enabled === true || config.sessionInit === "mount")
|
|
11604
|
+
) {
|
|
11419
11605
|
session
|
|
11420
11606
|
.initClientSession()
|
|
11421
11607
|
.then(() => {
|
|
@@ -11435,6 +11621,73 @@ export const createAgentExperience = (
|
|
|
11435
11621
|
});
|
|
11436
11622
|
}
|
|
11437
11623
|
|
|
11624
|
+
// Early session init on visitor intent (`sessionInit`). Delegated on the
|
|
11625
|
+
// mount so composer rebuilds stay covered, and read `config` at event time so
|
|
11626
|
+
// `update()` applies without re-wiring.
|
|
11627
|
+
const warmSession = () => session.warmClientSession();
|
|
11628
|
+
// One delegated listener. `input` counts only real typing, paste, drop or
|
|
11629
|
+
// IME (an InputEvent with an inputType; the widget's programmatic edits
|
|
11630
|
+
// dispatch a plain Event) that leaves text. In 'focus' mode a pointer-down in
|
|
11631
|
+
// the composer or a Tab into it also counts; `.focus()` (autofocus on
|
|
11632
|
+
// open/mount) produces neither.
|
|
11633
|
+
const handleSessionInitIntent = (event: Event) => {
|
|
11634
|
+
const mode = config.sessionInit ?? "input";
|
|
11635
|
+
const input = composerBindings?.input ?? textarea;
|
|
11636
|
+
const target = event.target as Node;
|
|
11637
|
+
if (
|
|
11638
|
+
input &&
|
|
11639
|
+
(event.type === "input"
|
|
11640
|
+
? (mode === "input" || mode === "focus") &&
|
|
11641
|
+
target === input &&
|
|
11642
|
+
!!(event as InputEvent).inputType &&
|
|
11643
|
+
!!(input as HTMLTextAreaElement).value.trim()
|
|
11644
|
+
: mode === "focus" &&
|
|
11645
|
+
(event.type === "keyup"
|
|
11646
|
+
? target === input && (event as KeyboardEvent).key === "Tab"
|
|
11647
|
+
: !!(composerBindings?.form ?? composerForm)?.contains(target)))
|
|
11648
|
+
) {
|
|
11649
|
+
warmSession();
|
|
11650
|
+
}
|
|
11651
|
+
};
|
|
11652
|
+
const sessionInitEvents = ["input", "pointerdown", "keyup"];
|
|
11653
|
+
sessionInitEvents.forEach((type) => mount.addEventListener(type, handleSessionInitIntent));
|
|
11654
|
+
// The custom trigger (a `sessionInit` function) runs on mount and again
|
|
11655
|
+
// whenever `update()` swaps in a different function, cleaning up the old one.
|
|
11656
|
+
let sessionInitHook: AgentWidgetSessionInitHook | undefined;
|
|
11657
|
+
let sessionInitHookCleanup: ReturnType<AgentWidgetSessionInitHook>;
|
|
11658
|
+
// Host hook code may throw; neither its setup nor its cleanup may leave
|
|
11659
|
+
// mount, update() or destroy() half done. The send still inits either way.
|
|
11660
|
+
const releaseSessionInitHook = () => {
|
|
11661
|
+
const cleanup = sessionInitHookCleanup;
|
|
11662
|
+
sessionInitHookCleanup = undefined;
|
|
11663
|
+
try {
|
|
11664
|
+
if (typeof cleanup === "function") cleanup();
|
|
11665
|
+
} catch {
|
|
11666
|
+
// ignored, see above
|
|
11667
|
+
}
|
|
11668
|
+
};
|
|
11669
|
+
const syncSessionInitHook = () => {
|
|
11670
|
+
const next =
|
|
11671
|
+
config.clientToken && typeof config.sessionInit === "function"
|
|
11672
|
+
? config.sessionInit
|
|
11673
|
+
: undefined;
|
|
11674
|
+
if (next === sessionInitHook) return;
|
|
11675
|
+
releaseSessionInitHook();
|
|
11676
|
+
sessionInitHook = next;
|
|
11677
|
+
try {
|
|
11678
|
+
sessionInitHookCleanup = next?.({ warm: warmSession, mount });
|
|
11679
|
+
} catch {
|
|
11680
|
+
// ignored, see above
|
|
11681
|
+
}
|
|
11682
|
+
};
|
|
11683
|
+
syncSessionInitHook();
|
|
11684
|
+
destroyCallbacks.push(() => {
|
|
11685
|
+
sessionInitEvents.forEach((type) =>
|
|
11686
|
+
mount.removeEventListener(type, handleSessionInitIntent)
|
|
11687
|
+
);
|
|
11688
|
+
releaseSessionInitHook();
|
|
11689
|
+
});
|
|
11690
|
+
|
|
11438
11691
|
// Wire up optional SSE tap (host) + event stream buffer to capture SSE
|
|
11439
11692
|
// events. `feedEventStream` stages events while the lazy chunk is in flight.
|
|
11440
11693
|
if (showEventStreamToggle || config.onSSEEvent) {
|
|
@@ -11607,6 +11860,7 @@ export const createAgentExperience = (
|
|
|
11607
11860
|
statusText.setAttribute("role", "status");
|
|
11608
11861
|
statusText.setAttribute("aria-live", "polite");
|
|
11609
11862
|
statusText.textContent = text;
|
|
11863
|
+
applyStatusIndicatorState(statusText, config.statusIndicator ?? {}, "idle");
|
|
11610
11864
|
if (composerNoticeTimer) clearTimeout(composerNoticeTimer);
|
|
11611
11865
|
composerNoticeTimer = setTimeout(() => {
|
|
11612
11866
|
composerNoticeTimer = null;
|
|
@@ -12429,7 +12683,7 @@ export const createAgentExperience = (
|
|
|
12429
12683
|
}
|
|
12430
12684
|
emitVoiceState(source);
|
|
12431
12685
|
persistVoiceMetadata();
|
|
12432
|
-
//
|
|
12686
|
+
// Reconcile content geometry without treating recording as extra content.
|
|
12433
12687
|
syncComposerCompact();
|
|
12434
12688
|
if (micButton) {
|
|
12435
12689
|
// Store original styles (including icon info for restoration)
|
|
@@ -12728,6 +12982,62 @@ export const createAgentExperience = (
|
|
|
12728
12982
|
|
|
12729
12983
|
composerVoiceBridge = handleMicButtonClick;
|
|
12730
12984
|
|
|
12985
|
+
// Voice prewarm on mic intent (`voiceRecognition.prewarm`). Delegated on the
|
|
12986
|
+
// mount so composer rebuilds stay covered, and `config` is read at event time
|
|
12987
|
+
// so `update()` applies without re-wiring. Default: 'hover' in client token
|
|
12988
|
+
// mode, off otherwise.
|
|
12989
|
+
const warmVoice = () => session.prewarmVoice();
|
|
12990
|
+
const voicePrewarmSetting = () => {
|
|
12991
|
+
const voice = config.voiceRecognition;
|
|
12992
|
+
if (voice?.enabled !== true) return false;
|
|
12993
|
+
return voice.prewarm ?? (config.clientToken ? "hover" : false);
|
|
12994
|
+
};
|
|
12995
|
+
// `pointerover` covers mouse hover and precedes a touch's pointer-down;
|
|
12996
|
+
// `focusin` covers keyboard focus.
|
|
12997
|
+
const handleVoicePrewarmIntent = (event: Event) => {
|
|
12998
|
+
if (voicePrewarmSetting() !== "hover") return;
|
|
12999
|
+
if (micButton?.contains(event.target as Node)) warmVoice();
|
|
13000
|
+
};
|
|
13001
|
+
const voicePrewarmEvents = ["pointerover", "pointerdown", "focusin"];
|
|
13002
|
+
voicePrewarmEvents.forEach((type) => mount.addEventListener(type, handleVoicePrewarmIntent));
|
|
13003
|
+
// A custom prewarm hook is bound to one mic button: it re-runs (after its
|
|
13004
|
+
// cleanup) when the button is rebuilt or `update()` swaps the function.
|
|
13005
|
+
let voicePrewarmHook: AgentWidgetVoicePrewarmHook | undefined;
|
|
13006
|
+
let voicePrewarmHookButton: HTMLButtonElement | null = null;
|
|
13007
|
+
let voicePrewarmHookCleanup: ReturnType<AgentWidgetVoicePrewarmHook>;
|
|
13008
|
+
// Host hook code may throw; neither setup nor cleanup may break the widget.
|
|
13009
|
+
const releaseVoicePrewarmHook = () => {
|
|
13010
|
+
const cleanup = voicePrewarmHookCleanup;
|
|
13011
|
+
voicePrewarmHookCleanup = undefined;
|
|
13012
|
+
try {
|
|
13013
|
+
if (typeof cleanup === "function") cleanup();
|
|
13014
|
+
} catch {
|
|
13015
|
+
// ignored, see above
|
|
13016
|
+
}
|
|
13017
|
+
};
|
|
13018
|
+
const syncVoicePrewarmHook = () => {
|
|
13019
|
+
const setting = voicePrewarmSetting();
|
|
13020
|
+
const next = typeof setting === "function" && micButton ? setting : undefined;
|
|
13021
|
+
const button = next ? micButton : null;
|
|
13022
|
+
if (next === voicePrewarmHook && button === voicePrewarmHookButton) return;
|
|
13023
|
+
releaseVoicePrewarmHook();
|
|
13024
|
+
voicePrewarmHook = next;
|
|
13025
|
+
voicePrewarmHookButton = button;
|
|
13026
|
+
if (!next || !button) return;
|
|
13027
|
+
try {
|
|
13028
|
+
voicePrewarmHookCleanup = next({ warm: warmVoice, micButton: button, mount });
|
|
13029
|
+
} catch {
|
|
13030
|
+
// ignored, see above
|
|
13031
|
+
}
|
|
13032
|
+
};
|
|
13033
|
+
syncVoicePrewarmHook();
|
|
13034
|
+
destroyCallbacks.push(() => {
|
|
13035
|
+
voicePrewarmEvents.forEach((type) =>
|
|
13036
|
+
mount.removeEventListener(type, handleVoicePrewarmIntent)
|
|
13037
|
+
);
|
|
13038
|
+
releaseVoicePrewarmHook();
|
|
13039
|
+
});
|
|
13040
|
+
|
|
12731
13041
|
if (micButton) {
|
|
12732
13042
|
// The click listener itself is registered by `wireComposerSurface`, so a
|
|
12733
13043
|
// composer rebuild unwires it with the rest of the surface.
|
|
@@ -12846,8 +13156,10 @@ export const createAgentExperience = (
|
|
|
12846
13156
|
footerResizeObserver.disconnect();
|
|
12847
13157
|
footerResizeObserver.observe(footer);
|
|
12848
13158
|
footerResizeObserver.observe(container);
|
|
13159
|
+
footerResizeObserver.observe(welcomeHost);
|
|
12849
13160
|
};
|
|
12850
13161
|
|
|
13162
|
+
let lastWelcomeFullscreen = isWelcomeFullscreen();
|
|
12851
13163
|
const recalcPanelHeight = () => {
|
|
12852
13164
|
// Composer-bar mode lets CSS own all sizing: collapsed pill is auto-sized
|
|
12853
13165
|
// by the footer; expanded fullscreen/modal are driven by CSS attribute
|
|
@@ -12909,10 +13221,9 @@ export const createAgentExperience = (
|
|
|
12909
13221
|
|
|
12910
13222
|
// In sidebar/fullHeight mode, don't override the width - it's handled by applyFullHeightStyles
|
|
12911
13223
|
if (!sidebarMode && !dockedMode) {
|
|
12912
|
-
const
|
|
12913
|
-
|
|
12914
|
-
panel.style.
|
|
12915
|
-
panel.style.maxWidth = width;
|
|
13224
|
+
const geometry = resolvePanelGeometry(config);
|
|
13225
|
+
panel.style.width = geometry.width;
|
|
13226
|
+
panel.style.maxWidth = geometry.maxWidth;
|
|
12916
13227
|
}
|
|
12917
13228
|
applyLauncherArtifactPanelWidth();
|
|
12918
13229
|
|
|
@@ -12923,6 +13234,12 @@ export const createAgentExperience = (
|
|
|
12923
13234
|
// overwrites updateOpenState()'s display:none when docked+closed. Re-sync after every recalc.
|
|
12924
13235
|
updateScrollToBottomButtonOffset();
|
|
12925
13236
|
syncComposerOverlayMetrics();
|
|
13237
|
+
const welcomeFullscreen = isWelcomeFullscreen();
|
|
13238
|
+
if (welcomeFullscreen !== lastWelcomeFullscreen) {
|
|
13239
|
+
lastWelcomeFullscreen = welcomeFullscreen;
|
|
13240
|
+
updateWelcome();
|
|
13241
|
+
renderSuggestions();
|
|
13242
|
+
}
|
|
12926
13243
|
updateOpenState();
|
|
12927
13244
|
|
|
12928
13245
|
// Sync scroll lock and host stacking when viewport mode changes (e.g. orientation change)
|
|
@@ -12985,6 +13302,7 @@ export const createAgentExperience = (
|
|
|
12985
13302
|
let lastBottomOffset = getScrollBottomOffset(body);
|
|
12986
13303
|
|
|
12987
13304
|
const handleScroll = () => {
|
|
13305
|
+
syncTranscriptTopFade();
|
|
12988
13306
|
const scrollTop = body.scrollTop;
|
|
12989
13307
|
// When content mutates (e.g. stream-animation plugins re-rendering text)
|
|
12990
13308
|
// or the viewport grows (composer shrinking back), the maximum scroll
|
|
@@ -13203,6 +13521,8 @@ export const createAgentExperience = (
|
|
|
13203
13521
|
}
|
|
13204
13522
|
// Clear messages in session (this will trigger onMessagesChanged which re-renders)
|
|
13205
13523
|
session.clearMessages();
|
|
13524
|
+
if (config.layout?.topFade) body.scrollTop = 0;
|
|
13525
|
+
body.removeAttribute("data-persona-top-fade");
|
|
13206
13526
|
messageCache.clear();
|
|
13207
13527
|
resumeAutoScroll();
|
|
13208
13528
|
|
|
@@ -13315,6 +13635,7 @@ export const createAgentExperience = (
|
|
|
13315
13635
|
if (micButton) {
|
|
13316
13636
|
composerBindings.addListener(micButton, "click", handleMicButtonClick);
|
|
13317
13637
|
}
|
|
13638
|
+
syncVoicePrewarmHook();
|
|
13318
13639
|
};
|
|
13319
13640
|
|
|
13320
13641
|
wireComposerSurface();
|
|
@@ -13418,6 +13739,10 @@ export const createAgentExperience = (
|
|
|
13418
13739
|
updateCopy();
|
|
13419
13740
|
renderSuggestions();
|
|
13420
13741
|
setComposerDisabled(session.isStreaming());
|
|
13742
|
+
if (config.statusIndicator?.mode === "transient" && !isComposerBar()) {
|
|
13743
|
+
const status = session.getStatus();
|
|
13744
|
+
applyStatusToElement(statusText, _getStatusText(status), config.statusIndicator, status);
|
|
13745
|
+
}
|
|
13421
13746
|
// The rebuilt mic is stamped idle by its builder. A rebuild mid-recording
|
|
13422
13747
|
// has to restore the live state, which also re-arms the level loop
|
|
13423
13748
|
// against the new footer and wrapper.
|
|
@@ -13589,6 +13914,7 @@ export const createAgentExperience = (
|
|
|
13589
13914
|
|
|
13590
13915
|
const controller: Controller = {
|
|
13591
13916
|
update(nextConfig: AgentWidgetConfigPatch) {
|
|
13917
|
+
const previousDefaultsVersion = config.future?.v5Defaults === true;
|
|
13592
13918
|
const previousToolCallConfig = config.toolCall;
|
|
13593
13919
|
const previousMessageActions = config.messageActions;
|
|
13594
13920
|
const previousLayoutMessages = config.layout?.messages;
|
|
@@ -13862,7 +14188,8 @@ export const createAgentExperience = (
|
|
|
13862
14188
|
|| (config.features?.showToolCalls ?? true) !== (previousShowToolCalls ?? true)
|
|
13863
14189
|
|| JSON.stringify(config.features?.toolCallDisplay) !== JSON.stringify(previousToolCallDisplay)
|
|
13864
14190
|
|| JSON.stringify(config.features?.reasoningDisplay) !== JSON.stringify(previousReasoningDisplay);
|
|
13865
|
-
const messagesConfigChanged =
|
|
14191
|
+
const messagesConfigChanged = (config.future?.v5Defaults === true) !== previousDefaultsVersion
|
|
14192
|
+
|| toolCallConfigChanged || messageActionsChanged || layoutMessagesChanged
|
|
13866
14193
|
|| loadingIndicatorChanged || iterationDisplayChanged || featuresChanged;
|
|
13867
14194
|
if (messagesConfigChanged && session) {
|
|
13868
14195
|
configVersion++;
|
|
@@ -13899,7 +14226,7 @@ export const createAgentExperience = (
|
|
|
13899
14226
|
// Hide icon if either headerIconHidden is true OR layout.header.showIcon is false
|
|
13900
14227
|
const shouldHideIcon = headerIconHidden || layoutShowIcon === false;
|
|
13901
14228
|
const headerIconName = launcher.headerIconName;
|
|
13902
|
-
const headerIconSize = launcher.headerIconSize ??
|
|
14229
|
+
const headerIconSize = launcher.headerIconSize ?? DEFAULT_HEADER_ICON_SIZE;
|
|
13903
14230
|
|
|
13904
14231
|
if (iconHolder) {
|
|
13905
14232
|
const headerEl = header;
|
|
@@ -13940,6 +14267,8 @@ export const createAgentExperience = (
|
|
|
13940
14267
|
const iconSize = parseFloat(headerIconSize) || 24;
|
|
13941
14268
|
const iconSvg = renderLucideIcon(headerIconName, iconSize * 0.6, "currentColor", 1);
|
|
13942
14269
|
if (iconSvg) {
|
|
14270
|
+
iconSvg.style.width = `calc(${iconSize}px * var(--persona-components-header-iconScale, 0.6))`;
|
|
14271
|
+
iconSvg.style.height = `calc(${iconSize}px * var(--persona-components-header-iconScale, 0.6))`;
|
|
13943
14272
|
iconHolder.replaceChildren(iconSvg);
|
|
13944
14273
|
} else {
|
|
13945
14274
|
// Fallback to agentIconText if Lucide icon fails
|
|
@@ -14306,6 +14635,7 @@ export const createAgentExperience = (
|
|
|
14306
14635
|
// Re-key the visitor store before any client rebuild sees the new config.
|
|
14307
14636
|
syncVisitorStore();
|
|
14308
14637
|
session.updateConfig(config);
|
|
14638
|
+
syncSessionInitHook();
|
|
14309
14639
|
if (
|
|
14310
14640
|
(config.features?.history?.enabled === true) !== previousHistoryEnabled ||
|
|
14311
14641
|
config.features?.history?.provider !== previousHistoryProvider ||
|
|
@@ -14492,6 +14822,7 @@ export const createAgentExperience = (
|
|
|
14492
14822
|
}
|
|
14493
14823
|
}
|
|
14494
14824
|
}
|
|
14825
|
+
syncVoicePrewarmHook();
|
|
14495
14826
|
|
|
14496
14827
|
// Update attachment button visibility based on attachments config
|
|
14497
14828
|
const attachmentsEnabled = config.attachments?.enabled === true;
|
|
@@ -14816,6 +15147,8 @@ export const createAgentExperience = (
|
|
|
14816
15147
|
}
|
|
14817
15148
|
|
|
14818
15149
|
syncComposerLayout();
|
|
15150
|
+
// Placement may change through a live option or defaults-version switch.
|
|
15151
|
+
applyComposerPlacement();
|
|
14819
15152
|
|
|
14820
15153
|
// Modes can be added or removed live: drop selections whose mode is gone
|
|
14821
15154
|
// before anything reads `activeModeIds`, and mount/unmount the mode chips.
|
|
@@ -14900,8 +15233,8 @@ export const createAgentExperience = (
|
|
|
14900
15233
|
|
|
14901
15234
|
// Update status indicator visibility and text
|
|
14902
15235
|
const statusIndicatorConfig = config.statusIndicator ?? {};
|
|
14903
|
-
|
|
14904
|
-
statusText
|
|
15236
|
+
placeStatusIndicator(statusText, composerForm, statusIndicatorConfig);
|
|
15237
|
+
applyStatusIndicatorState(statusText, statusIndicatorConfig, session?.getStatus() ?? "idle");
|
|
14905
15238
|
|
|
14906
15239
|
// Update status text if status is currently set
|
|
14907
15240
|
if (session) {
|
|
@@ -14911,6 +15244,8 @@ export const createAgentExperience = (
|
|
|
14911
15244
|
if (s === "connecting") return statusIndicatorConfig.connectingText ?? statusCopy.connecting;
|
|
14912
15245
|
if (s === "connected") return statusIndicatorConfig.connectedText ?? statusCopy.connected;
|
|
14913
15246
|
if (s === "error") return statusIndicatorConfig.errorText ?? statusCopy.error;
|
|
15247
|
+
if (s === "paused") return statusIndicatorConfig.pausedText ?? statusCopy.paused;
|
|
15248
|
+
if (s === "resuming") return statusIndicatorConfig.resumingText ?? statusCopy.resuming;
|
|
14914
15249
|
return statusCopy[s];
|
|
14915
15250
|
};
|
|
14916
15251
|
applyStatusToElement(statusText, getCurrentStatusText(currentStatus), statusIndicatorConfig, currentStatus);
|
|
@@ -14951,7 +15286,13 @@ export const createAgentExperience = (
|
|
|
14951
15286
|
clearChat() {
|
|
14952
15287
|
// Clear messages in session (this will trigger onMessagesChanged which re-renders)
|
|
14953
15288
|
artifactsPaneUserHidden = false;
|
|
15289
|
+
activityLifecycle.clear();
|
|
15290
|
+
toolExpansionState.clear();
|
|
15291
|
+
reasoningExpansionState.clear();
|
|
15292
|
+
approvalDetailsExpansionState.clear();
|
|
14954
15293
|
session.clearMessages();
|
|
15294
|
+
if (config.layout?.topFade) body.scrollTop = 0;
|
|
15295
|
+
body.removeAttribute("data-persona-top-fade");
|
|
14955
15296
|
messageCache.clear();
|
|
14956
15297
|
resumeAutoScroll();
|
|
14957
15298
|
|
|
@@ -15207,6 +15548,9 @@ export const createAgentExperience = (
|
|
|
15207
15548
|
startNewConversation(opts?: { focus?: boolean }): Promise<void> {
|
|
15208
15549
|
return startNewConversation({ focusComposer: opts?.focus === true });
|
|
15209
15550
|
},
|
|
15551
|
+
warmSession(): void {
|
|
15552
|
+
warmSession();
|
|
15553
|
+
},
|
|
15210
15554
|
deleteConversation(conversationId: string): Promise<void> {
|
|
15211
15555
|
return deleteHistoryConversation(conversationId);
|
|
15212
15556
|
},
|
|
@@ -15475,6 +15819,10 @@ export const createAgentExperience = (
|
|
|
15475
15819
|
clearInterval(toolElapsedTimerId);
|
|
15476
15820
|
toolElapsedTimerId = null;
|
|
15477
15821
|
}
|
|
15822
|
+
activityLifecycle.clear();
|
|
15823
|
+
toolExpansionState.clear();
|
|
15824
|
+
reasoningExpansionState.clear();
|
|
15825
|
+
approvalDetailsExpansionState.clear();
|
|
15478
15826
|
destroyCallbacks.forEach((cb) => cb());
|
|
15479
15827
|
wrapper.remove();
|
|
15480
15828
|
pillRoot?.remove();
|