@runtypelabs/persona 3.34.1 → 3.35.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 +11 -11
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-CthJFfNx.d.cts → types-DgYsuwXL.d.cts} +15 -14
- package/dist/animations/{types-CthJFfNx.d.ts → types-DgYsuwXL.d.ts} +15 -14
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/codegen.cjs +11 -11
- package/dist/codegen.js +10 -10
- package/dist/index.cjs +46 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +514 -158
- package/dist/index.d.ts +514 -158
- package/dist/index.global.js +41 -89
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +46 -44
- 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/markdown-parsers.js +53 -0
- package/dist/plugin-kit.d.cts +4 -4
- package/dist/plugin-kit.d.ts +4 -4
- package/dist/runtype-tts.js +1 -0
- package/dist/smart-dom-reader.cjs +1 -1
- package/dist/smart-dom-reader.d.cts +352 -103
- package/dist/smart-dom-reader.d.ts +352 -103
- package/dist/smart-dom-reader.js +1 -1
- package/dist/theme-editor-preview.cjs +203 -0
- package/dist/theme-editor-preview.d.cts +5748 -0
- package/dist/theme-editor-preview.d.ts +5748 -0
- package/dist/theme-editor-preview.js +203 -0
- package/dist/theme-editor.cjs +14 -131
- package/dist/theme-editor.d.cts +362 -574
- package/dist/theme-editor.d.ts +362 -574
- package/dist/theme-editor.js +14 -131
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +1 -1
- package/dist/theme-reference.d.ts +1 -1
- package/dist/theme-reference.js +1 -1
- package/dist/voice-worklet-player.cjs +80 -0
- package/dist/voice-worklet-player.d.cts +215 -0
- package/dist/voice-worklet-player.d.ts +215 -0
- package/dist/voice-worklet-player.js +80 -0
- package/dist/widget.css +54 -36
- package/package.json +16 -2
- package/src/animations/glyph-cycle.ts +11 -14
- package/src/animations/wipe.ts +1 -1
- package/src/ask-user-question-tool.test.ts +1 -1
- package/src/ask-user-question-tool.ts +6 -8
- package/src/client.test.ts +12 -12
- package/src/client.ts +30 -31
- package/src/codegen.test.ts +1 -1
- package/src/codegen.ts +1 -1
- package/src/components/approval-bubble.test.ts +1 -1
- package/src/components/approval-bubble.ts +3 -3
- package/src/components/artifact-pane.ts +3 -3
- package/src/components/ask-user-question-bubble.test.ts +14 -14
- package/src/components/ask-user-question-bubble.ts +10 -10
- package/src/components/composer-builder.ts +17 -16
- package/src/components/composer-parts.test.ts +54 -0
- package/src/components/composer-parts.ts +95 -126
- package/src/components/demo-carousel.ts +1 -1
- package/src/components/event-stream-view.test.ts +7 -7
- package/src/components/event-stream-view.ts +5 -5
- package/src/components/header-builder.ts +33 -27
- package/src/components/header-layouts.ts +1 -1
- package/src/components/header-parts.test.ts +90 -0
- package/src/components/header-parts.ts +62 -93
- package/src/components/launcher.test.ts +11 -0
- package/src/components/launcher.ts +1 -1
- package/src/components/message-bubble.test.ts +74 -2
- package/src/components/message-bubble.ts +25 -11
- package/src/components/messages.ts +2 -2
- package/src/components/panel.test.ts +1 -1
- package/src/components/panel.ts +92 -97
- package/src/components/pill-composer-builder.ts +28 -24
- package/src/components/reasoning-bubble.ts +2 -2
- package/src/components/suggestions.ts +1 -1
- package/src/components/tool-bubble.ts +1 -1
- package/src/components/widget-view.test.ts +181 -0
- package/src/components/widget-view.ts +234 -0
- package/src/defaults.ts +54 -51
- package/src/generated/runtype-openapi-contract.ts +5 -0
- package/src/index-core.ts +30 -4
- package/src/index-global.ts +73 -6
- package/src/index.ts +7 -1
- package/src/install.test.ts +11 -11
- package/src/install.ts +28 -18
- package/src/launcher-global.ts +25 -14
- package/src/markdown-parsers-eager.ts +20 -0
- package/src/markdown-parsers-entry.ts +4 -0
- package/src/markdown-parsers-loader.ts +49 -0
- package/src/plugin-kit.test.ts +1 -1
- package/src/plugin-kit.ts +7 -7
- package/src/plugins/types.ts +5 -5
- package/src/postprocessors.ts +27 -14
- package/src/runtime/host-layout.test.ts +1 -1
- package/src/runtime/host-layout.ts +4 -5
- package/src/runtime/init.ts +2 -2
- package/src/session.test.ts +5 -6
- package/src/session.ts +333 -227
- package/src/session.voice.test.ts +146 -0
- package/src/session.webmcp.test.ts +24 -25
- package/src/smart-dom-reader.ts +3 -4
- package/src/styles/widget.css +54 -36
- package/src/suggest-replies-tool.test.ts +3 -3
- package/src/suggest-replies-tool.ts +5 -5
- package/src/testing/index.ts +1 -1
- package/src/theme-editor/color-utils.ts +2 -2
- package/src/theme-editor/index.ts +4 -13
- package/src/theme-editor/presets.ts +1 -1
- package/src/theme-editor/preview-utils.ts +22 -20
- package/src/theme-editor/preview.ts +4 -1
- package/src/theme-editor/role-mappings.ts +3 -3
- package/src/theme-editor/sections.ts +21 -20
- package/src/theme-editor/types.ts +1 -1
- package/src/theme-editor/webmcp/index.ts +1 -1
- package/src/theme-editor/webmcp/summary.ts +1 -2
- package/src/theme-editor/webmcp/tools.ts +30 -30
- package/src/theme-editor/webmcp/types.ts +2 -2
- package/src/theme-editor-preview.ts +10 -0
- package/src/theme-reference.ts +26 -26
- package/src/types.ts +368 -99
- package/src/ui.approval-plugin.test.ts +3 -3
- package/src/ui.ask-user-question-plugin.test.ts +9 -9
- package/src/ui.component-directive.test.ts +4 -4
- package/src/ui.composer-bar.test.ts +10 -11
- package/src/ui.composer-keyboard.test.ts +5 -5
- package/src/ui.header-icon-color.test.ts +59 -0
- package/src/ui.scroll.test.ts +5 -6
- package/src/ui.stop-button.test.ts +3 -4
- package/src/ui.suggest-replies.test.ts +2 -2
- package/src/ui.ts +292 -131
- package/src/utils/buttons.ts +1 -1
- package/src/utils/code-generators.ts +1 -1
- package/src/utils/composer-history.ts +3 -3
- package/src/utils/dom-context.test.ts +2 -2
- package/src/utils/dom-context.ts +7 -7
- package/src/utils/dom.test.ts +67 -0
- package/src/utils/dom.ts +75 -0
- package/src/utils/dropdown.ts +2 -2
- package/src/utils/event-stream-buffer.test.ts +2 -2
- package/src/utils/message-fingerprint.test.ts +9 -0
- package/src/utils/message-fingerprint.ts +5 -0
- package/src/utils/morph.ts +1 -1
- package/src/utils/sanitize.ts +32 -16
- package/src/utils/sequence-buffer.test.ts +8 -8
- package/src/utils/sequence-buffer.ts +4 -4
- package/src/utils/smart-dom-adapter.test.ts +1 -1
- package/src/utils/smart-dom-adapter.ts +4 -5
- package/src/utils/speech-text.test.ts +122 -0
- package/src/utils/speech-text.ts +101 -0
- package/src/utils/stream-animation.test.ts +3 -3
- package/src/utils/stream-animation.ts +6 -6
- package/src/utils/theme.test.ts +1 -1
- package/src/utils/throughput-tracker.test.ts +11 -11
- package/src/utils/throughput-tracker.ts +9 -9
- package/src/utils/tokens.ts +13 -13
- package/src/utils/virtual-scroller.ts +1 -1
- package/src/vendor/smart-dom-reader/README.md +4 -4
- package/src/vendor/smart-dom-reader/index.d.ts +1 -1
- package/src/vendor/smart-dom-reader/index.js +2 -2
- package/src/version.ts +1 -1
- package/src/voice/audio-playback-manager.test.ts +159 -0
- package/src/voice/audio-playback-manager.ts +145 -30
- package/src/voice/browser-speech-engine.ts +130 -0
- package/src/voice/fallback-speech-engine.ts +89 -0
- package/src/voice/index.ts +21 -0
- package/src/voice/read-aloud-controller.test.ts +143 -0
- package/src/voice/read-aloud-controller.ts +136 -0
- package/src/voice/runtype-speech-engine.test.ts +280 -0
- package/src/voice/runtype-speech-engine.ts +211 -0
- package/src/voice/runtype-tts-entry.ts +10 -0
- package/src/voice/runtype-tts-loader.test.ts +47 -0
- package/src/voice/runtype-tts-loader.ts +42 -0
- package/src/voice/runtype-voice-provider.ts +332 -537
- package/src/voice/voice-factory.ts +26 -4
- package/src/voice/voice.test.ts +321 -7
- package/src/voice/worklet-playback-engine.ts +272 -0
- package/src/voice-worklet-player.ts +39 -0
- package/src/webmcp-bridge.test.ts +6 -6
- package/src/webmcp-bridge.ts +24 -25
- package/src/webmcp-polyfill.ts +1 -1
- package/src/voice/voice-activity-detector.ts +0 -90
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
* Default copy for the inline notice rendered when a turn ends with a
|
|
29
29
|
* non-natural stop reason. Deployers override per-reason via
|
|
30
30
|
* `config.copy.stopReasonNotice`. Returns `null` for natural completions
|
|
31
|
-
* (`end_turn`) and uninformative reasons (`unknown`)
|
|
31
|
+
* (`end_turn`) and uninformative reasons (`unknown`): those never render
|
|
32
32
|
* an affordance.
|
|
33
33
|
*/
|
|
34
34
|
export const getDefaultStopReasonNoticeCopy = (
|
|
@@ -62,7 +62,7 @@ export const resolveStopReasonNoticeText = (
|
|
|
62
62
|
): string | null => {
|
|
63
63
|
if (!stopReason) return null;
|
|
64
64
|
const fallback = getDefaultStopReasonNoticeCopy(stopReason);
|
|
65
|
-
// Reasons without a default (end_turn, unknown) never render
|
|
65
|
+
// Reasons without a default (end_turn, unknown) never render: overrides
|
|
66
66
|
// for those keys are intentionally ignored.
|
|
67
67
|
if (fallback === null) return null;
|
|
68
68
|
const override = overrides?.[stopReason];
|
|
@@ -498,10 +498,11 @@ const createAvatar = (
|
|
|
498
498
|
*/
|
|
499
499
|
const createTimestamp = (
|
|
500
500
|
message: AgentWidgetMessage,
|
|
501
|
-
timestampConfig: AgentWidgetTimestampConfig
|
|
501
|
+
timestampConfig: AgentWidgetTimestampConfig,
|
|
502
|
+
tagName: "div" | "span" = "div"
|
|
502
503
|
): HTMLElement => {
|
|
503
504
|
const timestamp = createElement(
|
|
504
|
-
|
|
505
|
+
tagName,
|
|
505
506
|
"persona-text-xs persona-text-persona-muted"
|
|
506
507
|
);
|
|
507
508
|
|
|
@@ -629,9 +630,10 @@ export const createMessageActions = (
|
|
|
629
630
|
const showCopy = actionsConfig.showCopy ?? true;
|
|
630
631
|
const showUpvote = actionsConfig.showUpvote ?? true;
|
|
631
632
|
const showDownvote = actionsConfig.showDownvote ?? true;
|
|
633
|
+
const showReadAloud = actionsConfig.showReadAloud ?? false;
|
|
632
634
|
|
|
633
635
|
// Don't render the container at all when no actions are visible
|
|
634
|
-
if (!showCopy && !showUpvote && !showDownvote) {
|
|
636
|
+
if (!showCopy && !showUpvote && !showDownvote && !showReadAloud) {
|
|
635
637
|
const empty = createElement("div");
|
|
636
638
|
empty.style.display = "none";
|
|
637
639
|
empty.id = `actions-${message.id}`;
|
|
@@ -686,6 +688,12 @@ export const createMessageActions = (
|
|
|
686
688
|
container.appendChild(createActionButton("copy", "Copy message", "copy"));
|
|
687
689
|
}
|
|
688
690
|
|
|
691
|
+
// Read-aloud button. Click handling and play/pause/resume state are managed
|
|
692
|
+
// in ui.ts (event delegation + ReadAloudController) so they survive morphs.
|
|
693
|
+
if (showReadAloud) {
|
|
694
|
+
container.appendChild(createActionButton("volume-2", "Read aloud", "read-aloud"));
|
|
695
|
+
}
|
|
696
|
+
|
|
689
697
|
// Upvote button
|
|
690
698
|
if (showUpvote) {
|
|
691
699
|
container.appendChild(createActionButton("thumbs-up", "Upvote", "upvote"));
|
|
@@ -768,7 +776,7 @@ export const createStandardBubble = (
|
|
|
768
776
|
? resolveStreamAnimationPlugin(streamAnimation.type, streamPluginOverrides)
|
|
769
777
|
: null;
|
|
770
778
|
// Stay in "streaming-animated" mode while the plugin reports in-flight
|
|
771
|
-
// work for this message
|
|
779
|
+
// work for this message: e.g. glyph-cycle's tick loops still walking
|
|
772
780
|
// through the tail after the last token arrived. Without this, the final
|
|
773
781
|
// non-animated render rips out the cycling spans mid-animation.
|
|
774
782
|
const pluginStillAnimating =
|
|
@@ -861,11 +869,17 @@ export const createStandardBubble = (
|
|
|
861
869
|
}
|
|
862
870
|
}
|
|
863
871
|
|
|
864
|
-
//
|
|
872
|
+
// Usung <span>, not <div> for timestamp
|
|
873
|
+
// because a block child of a <p> gets re-parented outside of it on re-render (idiomorph).
|
|
865
874
|
if (showTimestamp && timestampPosition === "inline" && message.createdAt) {
|
|
866
|
-
const timestamp = createTimestamp(message, timestampConfig
|
|
867
|
-
timestamp.classList.add("persona-
|
|
868
|
-
contentDiv.
|
|
875
|
+
const timestamp = createTimestamp(message, timestampConfig!, "span");
|
|
876
|
+
timestamp.classList.add("persona-timestamp-inline");
|
|
877
|
+
const lastBlock = contentDiv.lastElementChild;
|
|
878
|
+
if (lastBlock) {
|
|
879
|
+
lastBlock.appendChild(timestamp);
|
|
880
|
+
} else {
|
|
881
|
+
contentDiv.appendChild(timestamp);
|
|
882
|
+
}
|
|
869
883
|
}
|
|
870
884
|
|
|
871
885
|
if (imageParts.length > 0) {
|
|
@@ -930,7 +944,7 @@ export const createStandardBubble = (
|
|
|
930
944
|
: null;
|
|
931
945
|
|
|
932
946
|
// Add typing indicator (or skeleton placeholder) for streaming assistant
|
|
933
|
-
// messages. Check the buffered content
|
|
947
|
+
// messages. Check the buffered content: a plugin's `bufferContent` may
|
|
934
948
|
// hold back the first N chars (e.g. glyph-cycle waits for 50 chars), during
|
|
935
949
|
// which the bubble would otherwise appear empty.
|
|
936
950
|
//
|
|
@@ -34,7 +34,7 @@ export const renderMessages = (
|
|
|
34
34
|
if (!showReasoning) return;
|
|
35
35
|
bubble = createReasoningBubble(message, config);
|
|
36
36
|
} else if (isAskUserQuestionMessage(message)) {
|
|
37
|
-
// No transcript bubble
|
|
37
|
+
// No transcript bubble: the overlay sheet is the only question UI.
|
|
38
38
|
if (config?.features?.askUserQuestion?.enabled === false) return;
|
|
39
39
|
if (!message.agentMetadata?.askUserQuestionAnswered) {
|
|
40
40
|
if (message.toolCall?.id) liveAskToolIds.add(message.toolCall.id);
|
|
@@ -45,7 +45,7 @@ export const renderMessages = (
|
|
|
45
45
|
isSuggestRepliesMessage(message) &&
|
|
46
46
|
config?.features?.suggestReplies?.enabled !== false
|
|
47
47
|
) {
|
|
48
|
-
// No transcript bubble
|
|
48
|
+
// No transcript bubble: the chips above the composer are the only UI.
|
|
49
49
|
// When the feature is disabled the message falls through to the generic
|
|
50
50
|
// tool bubble below (and is never auto-resumed), making the parked
|
|
51
51
|
// execution visible instead of silently swallowed.
|
|
@@ -4,7 +4,7 @@ import { describe, expect, it } from "vitest";
|
|
|
4
4
|
import { createWrapper } from "./panel";
|
|
5
5
|
import type { AgentWidgetConfig } from "../types";
|
|
6
6
|
|
|
7
|
-
describe("createWrapper
|
|
7
|
+
describe("createWrapper: composer-bar mode", () => {
|
|
8
8
|
it("marks the wrapper with composer-bar data-attrs and leaves geometry to updateOpenState", () => {
|
|
9
9
|
const config: AgentWidgetConfig = {
|
|
10
10
|
apiUrl: "/api",
|
package/src/components/panel.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createElement } from "../utils/dom";
|
|
1
|
+
import { createElement, createNode } from "../utils/dom";
|
|
2
2
|
import { DEFAULT_FLOATING_LAUNCHER_WIDTH } from "../defaults";
|
|
3
3
|
import { AgentWidgetConfig } from "../types";
|
|
4
4
|
import { positionMap } from "../utils/positioning";
|
|
@@ -16,8 +16,7 @@ export interface PanelWrapper {
|
|
|
16
16
|
/**
|
|
17
17
|
* Composer-bar mode only: viewport-fixed sibling of `wrapper` that owns
|
|
18
18
|
* the persistent pill (`footer`) and peek banner. Lives outside the
|
|
19
|
-
* wrapper so it never inherits the wrapper's geometry transitions
|
|
20
|
-
* critical for modal mode where the wrapper is `transform: translate(-50%, -50%)`
|
|
19
|
+
* wrapper so it never inherits the wrapper's geometry transitions: * critical for modal mode where the wrapper is `transform: translate(-50%, -50%)`
|
|
21
20
|
* (a transformed ancestor establishes a containing block for `position: fixed`
|
|
22
21
|
* descendants, which would trap the pill inside the wrapper otherwise).
|
|
23
22
|
*/
|
|
@@ -32,7 +31,7 @@ export const createWrapper = (config?: AgentWidgetConfig): PanelWrapper => {
|
|
|
32
31
|
if (composerBarMode) {
|
|
33
32
|
const cb = config?.launcher?.composerBar ?? {};
|
|
34
33
|
// Geometry (left/transform/bottom/top/width/max-width) is intentionally
|
|
35
|
-
// NOT set here
|
|
34
|
+
// NOT set here: it's owned entirely by `applyComposerBarGeometry()` in
|
|
36
35
|
// ui.ts so that collapsed → expanded transitions can clear the previous
|
|
37
36
|
// state's inline styles cleanly. Setting geometry here would persist
|
|
38
37
|
// across state changes and override the per-state values, which
|
|
@@ -55,7 +54,7 @@ export const createWrapper = (config?: AgentWidgetConfig): PanelWrapper => {
|
|
|
55
54
|
panel.style.width = "100%";
|
|
56
55
|
wrapper.appendChild(panel);
|
|
57
56
|
|
|
58
|
-
// Pill lives in a separate viewport-fixed sibling
|
|
57
|
+
// Pill lives in a separate viewport-fixed sibling: see PanelWrapper
|
|
59
58
|
// docs above. ui.ts appends `peekBanner` and `footer` here, then
|
|
60
59
|
// appends pillRoot to `mount` immediately after the wrapper.
|
|
61
60
|
const pillRoot = createElement("div", "persona-widget-pill-root");
|
|
@@ -182,7 +181,7 @@ export interface PanelElements {
|
|
|
182
181
|
/**
|
|
183
182
|
* Composer-bar panel: minimal close-only header (× in the top-right of the
|
|
184
183
|
* chat chrome). The pill (`footer`) and `peekBanner` are NOT children of
|
|
185
|
-
* the panel
|
|
184
|
+
* the panel: caller (`ui.ts`) appends them to the `pillRoot` returned by
|
|
186
185
|
* `createWrapper`, which is a viewport-fixed sibling of the wrapper.
|
|
187
186
|
* This decouples the pill from the wrapper's geometry transitions (so the
|
|
188
187
|
* pill stays anchored at the viewport bottom regardless of expanded mode).
|
|
@@ -200,7 +199,7 @@ const buildComposerBarPanel = (
|
|
|
200
199
|
);
|
|
201
200
|
container.setAttribute("data-persona-theme-zone", "container");
|
|
202
201
|
|
|
203
|
-
// Minimal header
|
|
202
|
+
// Minimal header: just an absolutely-positioned close button.
|
|
204
203
|
// The wrapper uses inline styles (top/right/z-index values) because the
|
|
205
204
|
// widget's hand-authored CSS doesn't ship every Tailwind utility.
|
|
206
205
|
// Composer-bar's defaults are roughly half the floating-launcher's
|
|
@@ -220,7 +219,7 @@ const buildComposerBarPanel = (
|
|
|
220
219
|
closeButtonWrapper.style.right = "8px";
|
|
221
220
|
closeButtonWrapper.style.zIndex = "10";
|
|
222
221
|
|
|
223
|
-
// Clear / "start over" button
|
|
222
|
+
// Clear / "start over" button: sits immediately to the left of the close
|
|
224
223
|
// button in the panel chrome's top-right corner. Same minimal sizing as
|
|
225
224
|
// the close icon (16px button, 14px icon) so the two read as a paired
|
|
226
225
|
// action group rather than a header strip. Wired by `setupClearChatButton()`
|
|
@@ -246,46 +245,50 @@ const buildComposerBarPanel = (
|
|
|
246
245
|
}
|
|
247
246
|
// Placeholder header element so PanelElements.header exists (some downstream
|
|
248
247
|
// code reads it). It carries `data-persona-widget-header` for plugin /
|
|
249
|
-
// selector parity but renders nothing visible
|
|
248
|
+
// selector parity but renders nothing visible: the close button is the only
|
|
250
249
|
// header chrome in composer-bar mode.
|
|
251
|
-
const headerPlaceholder =
|
|
252
|
-
|
|
253
|
-
|
|
250
|
+
const headerPlaceholder = createNode("span", {
|
|
251
|
+
className: "persona-widget-header",
|
|
252
|
+
attrs: { "data-persona-theme-zone": "header" },
|
|
253
|
+
style: { display: "none" },
|
|
254
|
+
});
|
|
254
255
|
|
|
255
|
-
// Body
|
|
256
|
+
// Body: extra top padding (set inline so the hand-authored widget.css
|
|
256
257
|
// doesn't need a `pt-12` utility) so the absolute close button doesn't
|
|
257
258
|
// overlap the welcome card / first message.
|
|
258
|
-
const body =
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
body.setAttribute("data-persona-theme-zone", "messages");
|
|
259
|
+
const body = createNode("div", {
|
|
260
|
+
className:
|
|
261
|
+
"persona-widget-body persona-flex persona-flex-1 persona-min-h-0 persona-flex-col persona-gap-6 persona-overflow-y-auto persona-bg-persona-container persona-px-6 persona-py-6",
|
|
262
|
+
attrs: { id: "persona-scroll-container", "data-persona-theme-zone": "messages" },
|
|
263
|
+
style: { paddingTop: "48px" },
|
|
264
|
+
});
|
|
265
265
|
// Reserve the scrollbar gutter so the transcript doesn't shift horizontally
|
|
266
266
|
// when streaming content first overflows and the scrollbar appears.
|
|
267
267
|
body.style.setProperty("scrollbar-gutter", "stable");
|
|
268
268
|
|
|
269
|
-
const
|
|
269
|
+
const introTitle = createNode("h2", {
|
|
270
|
+
className: "persona-text-lg persona-font-semibold persona-text-persona-primary",
|
|
271
|
+
text: config?.copy?.welcomeTitle ?? "Hello 👋",
|
|
272
|
+
});
|
|
273
|
+
const introSubtitle = createNode("p", {
|
|
274
|
+
className: "persona-mt-2 persona-text-sm persona-text-persona-muted",
|
|
275
|
+
text:
|
|
276
|
+
config?.copy?.welcomeSubtitle ??
|
|
277
|
+
"Ask anything about your account or products.",
|
|
278
|
+
});
|
|
279
|
+
const introCard = createNode(
|
|
270
280
|
"div",
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const introSubtitle = createElement(
|
|
282
|
-
"p",
|
|
283
|
-
"persona-mt-2 persona-text-sm persona-text-persona-muted"
|
|
281
|
+
{
|
|
282
|
+
className: "persona-rounded-2xl persona-bg-persona-surface persona-p-6",
|
|
283
|
+
attrs: { "data-persona-intro-card": "" },
|
|
284
|
+
style: {
|
|
285
|
+
boxShadow:
|
|
286
|
+
"var(--persona-intro-card-shadow, 0 5px 15px rgba(15, 23, 42, 0.08))",
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
introTitle,
|
|
290
|
+
introSubtitle
|
|
284
291
|
);
|
|
285
|
-
introSubtitle.textContent =
|
|
286
|
-
config?.copy?.welcomeSubtitle ??
|
|
287
|
-
"Ask anything about your account or products.";
|
|
288
|
-
introCard.append(introTitle, introSubtitle);
|
|
289
292
|
|
|
290
293
|
const messagesWrapper = createElement(
|
|
291
294
|
"div",
|
|
@@ -309,23 +312,18 @@ const buildComposerBarPanel = (
|
|
|
309
312
|
|
|
310
313
|
// Composer overlay (interactive sheets like ask_user_question slide up here).
|
|
311
314
|
// Anchored to the bottom of the container (which is `position: relative`),
|
|
312
|
-
// so sheets render at the bottom of the chat chrome
|
|
315
|
+
// so sheets render at the bottom of the chat chrome: just above the gap +
|
|
313
316
|
// pill that sit below the container.
|
|
314
|
-
const composerOverlay =
|
|
315
|
-
"
|
|
316
|
-
"persona-composer-overlay
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
composerOverlay.style.right = "0";
|
|
322
|
-
composerOverlay.style.bottom = "0";
|
|
323
|
-
composerOverlay.style.zIndex = "20";
|
|
324
|
-
|
|
325
|
-
// Pill composer — caller appends as a sibling of container in the panel.
|
|
317
|
+
const composerOverlay = createNode("div", {
|
|
318
|
+
className: "persona-composer-overlay persona-pointer-events-none",
|
|
319
|
+
attrs: { "data-persona-composer-overlay": "" },
|
|
320
|
+
style: { position: "absolute", left: "0", right: "0", bottom: "0", zIndex: "20" },
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Pill composer: caller appends as a sibling of container in the panel.
|
|
326
324
|
const composerElements: ComposerElements = buildPillComposer({ config });
|
|
327
325
|
|
|
328
|
-
// Peek banner
|
|
326
|
+
// Peek banner: caller inserts as a sibling of container/footer between
|
|
329
327
|
// them in the panel. Hidden by default; ui.ts toggles visibility.
|
|
330
328
|
const { root: peekBanner, textNode: peekTextNode } = buildPillPeekBanner();
|
|
331
329
|
|
|
@@ -382,11 +380,11 @@ export const buildPanel = (config?: AgentWidgetConfig, showClose = true): PanelE
|
|
|
382
380
|
return buildComposerBarPanel(config, showClose);
|
|
383
381
|
}
|
|
384
382
|
|
|
385
|
-
const container =
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
383
|
+
const container = createNode("div", {
|
|
384
|
+
className:
|
|
385
|
+
"persona-widget-container persona-flex persona-h-full persona-w-full persona-flex-1 persona-min-h-0 persona-flex-col persona-text-persona-primary persona-bg-persona-surface persona-rounded-2xl persona-overflow-hidden persona-border persona-border-persona-border",
|
|
386
|
+
attrs: { "data-persona-theme-zone": "container" },
|
|
387
|
+
});
|
|
390
388
|
|
|
391
389
|
// Build header using layout config if available, otherwise use standard builder
|
|
392
390
|
const headerLayoutConfig = config?.layout?.header;
|
|
@@ -396,40 +394,43 @@ export const buildPanel = (config?: AgentWidgetConfig, showClose = true): PanelE
|
|
|
396
394
|
: buildHeader({ config, showClose });
|
|
397
395
|
|
|
398
396
|
// Build body with intro card and messages wrapper
|
|
399
|
-
const body =
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
body.setAttribute("data-persona-theme-zone", "messages");
|
|
397
|
+
const body = createNode("div", {
|
|
398
|
+
className:
|
|
399
|
+
"persona-widget-body persona-flex persona-flex-1 persona-min-h-0 persona-flex-col persona-gap-6 persona-overflow-y-auto persona-bg-persona-container persona-px-6 persona-py-6",
|
|
400
|
+
attrs: { id: "persona-scroll-container", "data-persona-theme-zone": "messages" },
|
|
401
|
+
});
|
|
405
402
|
// Reserve the scrollbar gutter so the transcript doesn't shift horizontally
|
|
406
403
|
// when streaming content first overflows and the scrollbar appears.
|
|
407
404
|
body.style.setProperty("scrollbar-gutter", "stable");
|
|
408
405
|
|
|
409
|
-
const
|
|
410
|
-
"
|
|
411
|
-
|
|
412
|
-
);
|
|
406
|
+
const introTitle = createNode("h2", {
|
|
407
|
+
className: "persona-text-lg persona-font-semibold persona-text-persona-primary",
|
|
408
|
+
text: config?.copy?.welcomeTitle ?? "Hello 👋",
|
|
409
|
+
});
|
|
410
|
+
const introSubtitle = createNode("p", {
|
|
411
|
+
className: "persona-mt-2 persona-text-sm persona-text-persona-muted",
|
|
412
|
+
text:
|
|
413
|
+
config?.copy?.welcomeSubtitle ??
|
|
414
|
+
"Ask anything about your account or products.",
|
|
415
|
+
});
|
|
413
416
|
// Box-shadow flows through the themable `components.introCard.shadow` token
|
|
414
417
|
// (--persona-intro-card-shadow). Docked mode keeps a flat look by default;
|
|
415
418
|
// floating mode falls back to the legacy `persona-shadow-sm` value when no
|
|
416
419
|
// token is set.
|
|
417
|
-
introCard
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
420
|
+
const introCard = createNode(
|
|
421
|
+
"div",
|
|
422
|
+
{
|
|
423
|
+
className: "persona-rounded-2xl persona-bg-persona-surface persona-p-6",
|
|
424
|
+
attrs: { "data-persona-intro-card": "" },
|
|
425
|
+
style: {
|
|
426
|
+
boxShadow: isDockedMountMode(config)
|
|
427
|
+
? "none"
|
|
428
|
+
: "var(--persona-intro-card-shadow, 0 5px 15px rgba(15, 23, 42, 0.08))",
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
introTitle,
|
|
432
|
+
introSubtitle
|
|
428
433
|
);
|
|
429
|
-
introSubtitle.textContent =
|
|
430
|
-
config?.copy?.welcomeSubtitle ??
|
|
431
|
-
"Ask anything about your account or products.";
|
|
432
|
-
introCard.append(introTitle, introSubtitle);
|
|
433
434
|
|
|
434
435
|
const messagesWrapper = createElement(
|
|
435
436
|
"div",
|
|
@@ -444,7 +445,6 @@ export const buildPanel = (config?: AgentWidgetConfig, showClose = true): PanelE
|
|
|
444
445
|
messagesWrapper.style.width = "100%";
|
|
445
446
|
}
|
|
446
447
|
|
|
447
|
-
introCard.setAttribute("data-persona-intro-card", "");
|
|
448
448
|
const showWelcomeCard = config?.copy?.showWelcomeCard !== false;
|
|
449
449
|
if (!showWelcomeCard) {
|
|
450
450
|
introCard.style.display = "none";
|
|
@@ -473,21 +473,16 @@ export const buildPanel = (config?: AgentWidgetConfig, showClose = true): PanelE
|
|
|
473
473
|
// above the composer so sheets (e.g. the ask_user_question answer-pill sheet)
|
|
474
474
|
// can slide up without reflowing the chat transcript above. Uses inline
|
|
475
475
|
// styles for left/right/bottom because widget.css is hand-authored and
|
|
476
|
-
// doesn't ship `.persona-left-0` / `.persona-right-0` rules
|
|
476
|
+
// doesn't ship `.persona-left-0` / `.persona-right-0` rules: without
|
|
477
477
|
// them the overlay shrink-wraps to content and collapses the sheet width.
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
composerOverlay.style.bottom = "0";
|
|
487
|
-
// Above .persona-scroll-to-bottom-indicator (z-index 10, sibling in the
|
|
488
|
-
// container) so suggestion chips and the ask-user-question sheet are not
|
|
489
|
-
// covered by the "jump to latest" button.
|
|
490
|
-
composerOverlay.style.zIndex = "20";
|
|
478
|
+
// zIndex 20 sits above .persona-scroll-to-bottom-indicator (z-index 10,
|
|
479
|
+
// sibling in the container) so suggestion chips and the ask-user-question
|
|
480
|
+
// sheet are not covered by the "jump to latest" button.
|
|
481
|
+
const composerOverlay = createNode("div", {
|
|
482
|
+
className: "persona-composer-overlay persona-pointer-events-none",
|
|
483
|
+
attrs: { "data-persona-composer-overlay": "" },
|
|
484
|
+
style: { position: "absolute", left: "0", right: "0", bottom: "0", zIndex: "20" },
|
|
485
|
+
});
|
|
491
486
|
|
|
492
487
|
if (showFooter) {
|
|
493
488
|
container.append(composerElements.footer);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createElement } from "../utils/dom";
|
|
1
|
+
import { createElement, createNode } from "../utils/dom";
|
|
2
2
|
import { renderLucideIcon } from "../utils/icons";
|
|
3
3
|
import { ComposerBuildContext, ComposerElements } from "./composer-builder";
|
|
4
4
|
import {
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
|
|
13
13
|
export interface PillPeekBanner {
|
|
14
14
|
/**
|
|
15
|
-
* The peek button itself
|
|
15
|
+
* The peek button itself: a chrome-less row that floats above the pill,
|
|
16
16
|
* showing a chat-bubble icon, a trailing-100-char preview of the most
|
|
17
17
|
* recent assistant message, and a chevron-up. Rendered hidden by default
|
|
18
18
|
* (opacity 0, pointer-events none); ui.ts toggles
|
|
@@ -25,8 +25,7 @@ export interface PillPeekBanner {
|
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* Build the peek banner for `launcher.mountMode: "composer-bar"`. The peek
|
|
28
|
-
* is the user's path back into the expanded chat from the collapsed pill
|
|
29
|
-
* it fades in during streaming OR on composer hover, and clicking it opens
|
|
28
|
+
* is the user's path back into the expanded chat from the collapsed pill: * it fades in during streaming OR on composer hover, and clicking it opens
|
|
30
29
|
* the panel. ui.ts owns visibility + content updates via
|
|
31
30
|
* `syncComposerBarPeek`; this factory just produces the inert DOM shell.
|
|
32
31
|
*
|
|
@@ -34,11 +33,15 @@ export interface PillPeekBanner {
|
|
|
34
33
|
* just above the pill in the collapsed-state UI.
|
|
35
34
|
*/
|
|
36
35
|
export const buildPillPeekBanner = (): PillPeekBanner => {
|
|
37
|
-
const root =
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
const root = createNode("button", {
|
|
37
|
+
className: "persona-pill-peek",
|
|
38
|
+
attrs: {
|
|
39
|
+
type: "button",
|
|
40
|
+
"data-persona-pill-peek": "",
|
|
41
|
+
"aria-label": "Show conversation",
|
|
42
|
+
tabindex: "-1",
|
|
43
|
+
},
|
|
44
|
+
});
|
|
42
45
|
|
|
43
46
|
const iconHolder = createElement("span", "persona-pill-peek__icon");
|
|
44
47
|
const messageIcon = renderLucideIcon("message-square", 16, "currentColor", 1.5);
|
|
@@ -61,7 +64,7 @@ export const buildPillPeekBanner = (): PillPeekBanner => {
|
|
|
61
64
|
/**
|
|
62
65
|
* Single-row pill composer for `launcher.mountMode: "composer-bar"`.
|
|
63
66
|
*
|
|
64
|
-
* Same control factories as `buildComposer
|
|
67
|
+
* Same control factories as `buildComposer`: the only difference is the
|
|
65
68
|
* layout shell + className. The form ships with `persona-pill-composer`
|
|
66
69
|
* (no `persona-flex-col` / `persona-rounded-2xl` baggage), so the CSS
|
|
67
70
|
* layout rules apply at normal specificity without `!important` fights.
|
|
@@ -71,7 +74,7 @@ export const buildPillPeekBanner = (): PillPeekBanner => {
|
|
|
71
74
|
*
|
|
72
75
|
* Suggestions row + status text are built (so plugin code that mutates
|
|
73
76
|
* them keeps working and `bindComposerRefsFromFooter` finds them) but are
|
|
74
|
-
* `display: none` by default
|
|
77
|
+
* `display: none` by default: pill UX is just textarea + 3 buttons.
|
|
75
78
|
*
|
|
76
79
|
* Attachment previews float ABOVE the pill in their own row when
|
|
77
80
|
* AttachmentManager toggles the previews container's `display` property
|
|
@@ -80,8 +83,10 @@ export const buildPillPeekBanner = (): PillPeekBanner => {
|
|
|
80
83
|
export const buildPillComposer = (context: ComposerBuildContext): ComposerElements => {
|
|
81
84
|
const { config } = context;
|
|
82
85
|
|
|
83
|
-
const footer =
|
|
84
|
-
|
|
86
|
+
const footer = createNode("div", {
|
|
87
|
+
className: "persona-widget-footer persona-widget-footer--pill",
|
|
88
|
+
attrs: { "data-persona-theme-zone": "composer" },
|
|
89
|
+
});
|
|
85
90
|
|
|
86
91
|
const suggestions = createSuggestionsRow();
|
|
87
92
|
suggestions.style.display = "none";
|
|
@@ -105,12 +110,11 @@ export const buildPillComposer = (context: ComposerBuildContext): ComposerElemen
|
|
|
105
110
|
|
|
106
111
|
// Pill form: NO `persona-flex-col`. Only the marker classes that the rest
|
|
107
112
|
// of the codebase queries by name.
|
|
108
|
-
const composerForm =
|
|
109
|
-
"
|
|
110
|
-
"persona-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
composerForm.style.outline = "none";
|
|
113
|
+
const composerForm = createNode("form", {
|
|
114
|
+
className: "persona-widget-composer persona-pill-composer",
|
|
115
|
+
attrs: { "data-persona-composer-form": "" },
|
|
116
|
+
style: { outline: "none" },
|
|
117
|
+
});
|
|
114
118
|
|
|
115
119
|
// Three columns of the grid: [paperclip?] · textarea · mic + send.
|
|
116
120
|
// The empty leftActions wrapper still ships when attachments are off so
|
|
@@ -154,11 +158,11 @@ export const buildPillComposer = (context: ComposerBuildContext): ComposerElemen
|
|
|
154
158
|
|
|
155
159
|
// The pill flattens left/right into the form's grid; there's no separate
|
|
156
160
|
// wrapper. Surface the form itself as `actionsRow` to satisfy the
|
|
157
|
-
// ComposerElements contract
|
|
158
|
-
// opaque ref.
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
161
|
+
// ComposerElements contract: downstream code only treats it as an
|
|
162
|
+
// opaque ref. The pill form intentionally carries no
|
|
163
|
+
// `data-persona-composer-actions` marker, so `bindComposerRefsFromFooter`
|
|
164
|
+
// finds no actions row in pill mode; that lookup writes to `_actionsRow`
|
|
165
|
+
// (the underscore prefix marks it as soft-optional).
|
|
162
166
|
const actionsRow = composerForm;
|
|
163
167
|
|
|
164
168
|
return {
|
|
@@ -114,7 +114,7 @@ export const createReasoningBubble = (message: AgentWidgetMessage, config?: Agen
|
|
|
114
114
|
const defaultSummary = "Thinking...";
|
|
115
115
|
const reasoningConfig = config?.reasoning ?? {};
|
|
116
116
|
|
|
117
|
-
// Elapsed helpers
|
|
117
|
+
// Elapsed helpers: defined early so they're available to renderCollapsedSummary
|
|
118
118
|
const startedAt = String(reasoning.startedAt ?? Date.now());
|
|
119
119
|
|
|
120
120
|
const createElapsedSpan = (): HTMLElement => {
|
|
@@ -144,7 +144,7 @@ export const createReasoningBubble = (message: AgentWidgetMessage, config?: Agen
|
|
|
144
144
|
headerContent.appendChild(title);
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
// Status span
|
|
147
|
+
// Status span: used in the legacy (no-template) path
|
|
148
148
|
const status = createElement("span", "persona-text-xs persona-text-persona-primary");
|
|
149
149
|
status.textContent = describeReasonStatus(reasoning);
|
|
150
150
|
headerContent.appendChild(status);
|
|
@@ -45,7 +45,7 @@ export const createSuggestions = (container: HTMLElement): SuggestionButtons =>
|
|
|
45
45
|
if (!chips || !chips.length) return;
|
|
46
46
|
|
|
47
47
|
// Hide config suggestions after the first user message is sent.
|
|
48
|
-
// Agent-pushed chips skip this gate
|
|
48
|
+
// Agent-pushed chips skip this gate: their visibility is the caller's
|
|
49
49
|
// latest-turn rule (last suggest_replies call with no user message after).
|
|
50
50
|
// Use provided messages or get from session
|
|
51
51
|
if (!agentPushed) {
|
|
@@ -197,7 +197,7 @@ export const createToolBubble = (message: AgentWidgetMessage, config?: AgentWidg
|
|
|
197
197
|
title.style.color = toolCallConfig.headerTextColor;
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
// Elapsed helpers
|
|
200
|
+
// Elapsed helpers: defined early so they're available to renderCollapsedSummary
|
|
201
201
|
const startedAt = String(tool.startedAt ?? Date.now());
|
|
202
202
|
|
|
203
203
|
// Helper: build a <span data-tool-elapsed> that the global timer in ui.ts updates
|