@runtypelabs/persona 3.34.0 → 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 +67 -12
- package/src/client.ts +40 -32
- 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
package/src/client.ts
CHANGED
|
@@ -180,7 +180,7 @@ export class AgentWidgetClient {
|
|
|
180
180
|
private lastSentClientToolsFingerprint: string | null = null;
|
|
181
181
|
private clientToolsFingerprintSessionId: string | null = null;
|
|
182
182
|
|
|
183
|
-
// WebMCP
|
|
183
|
+
// WebMCP: page-discovered tool consumption (see ./webmcp-bridge).
|
|
184
184
|
// Constructed lazily: null when `config.webmcp?.enabled !== true`.
|
|
185
185
|
private readonly webMcpBridge: WebMcpBridge | null;
|
|
186
186
|
|
|
@@ -206,8 +206,8 @@ export class AgentWidgetClient {
|
|
|
206
206
|
* Refresh config in place WITHOUT tearing down the live connection or the
|
|
207
207
|
* WebMCP bridge. `AgentWidgetSession.updateConfig` calls this when only
|
|
208
208
|
* connection-irrelevant fields changed (theme, copy, layout, suggestions, …),
|
|
209
|
-
* so a UI update that lands mid-turn
|
|
210
|
-
* widget while the agent's turn is still streaming
|
|
209
|
+
* so a UI update that lands mid-turn: e.g. a `webmcp:*` tool restyling the
|
|
210
|
+
* widget while the agent's turn is still streaming: doesn't abandon the
|
|
211
211
|
* in-flight stream/resume. Connection or request-shaping changes (apiUrl,
|
|
212
212
|
* clientToken, webmcp, headers, parser, …) take the full client rebuild path
|
|
213
213
|
* in the session instead, which is the only place the bridge is recreated.
|
|
@@ -215,7 +215,7 @@ export class AgentWidgetClient {
|
|
|
215
215
|
* Only the live-read `config` is refreshed (e.g. `iterationDisplay`); the
|
|
216
216
|
* constructor-derived request-shaping fields (apiUrl, headers, parser,
|
|
217
217
|
* contextProviders, middleware, …) are left untouched because the session
|
|
218
|
-
* routes any change to those down the full-rebuild path instead
|
|
218
|
+
* routes any change to those down the full-rebuild path instead, so they are
|
|
219
219
|
* guaranteed unchanged here. The `webMcpBridge` instance and its
|
|
220
220
|
* installed-polyfill memo are deliberately preserved, which keeps any
|
|
221
221
|
* in-flight resolve alive.
|
|
@@ -254,7 +254,7 @@ export class AgentWidgetClient {
|
|
|
254
254
|
* WebMCP: execute a returned `webmcp:<name>` tool call against the page's
|
|
255
255
|
* registry and return the normalized MCP-shaped result for `/resume`. The
|
|
256
256
|
* bridge handles confirm-bubble gating, the 30s timeout, error
|
|
257
|
-
* normalization, and `signal`-driven abort
|
|
257
|
+
* normalization, and `signal`-driven abort: callers never see throws.
|
|
258
258
|
*
|
|
259
259
|
* Returns `null` when WebMCP is not enabled on this client (signal to the
|
|
260
260
|
* session that it should fall back to the legacy local-tool resume path,
|
|
@@ -482,12 +482,21 @@ export class AgentWidgetClient {
|
|
|
482
482
|
console.debug("[AgentWidgetClient] sending feedback", feedback);
|
|
483
483
|
}
|
|
484
484
|
|
|
485
|
+
// Scope the feedback request to the caller's client token, sourced the same
|
|
486
|
+
// way as the chat/init requests. sendFeedback is client-token-mode only
|
|
487
|
+
// (guarded above), so clientToken is always present here and an API key can
|
|
488
|
+
// never leak into the body. Left undefined only when the embed has none.
|
|
489
|
+
const requestBody = {
|
|
490
|
+
...feedback,
|
|
491
|
+
...(this.config.clientToken && { token: this.config.clientToken }),
|
|
492
|
+
};
|
|
493
|
+
|
|
485
494
|
const response = await fetch(this.getFeedbackApiUrl(), {
|
|
486
495
|
method: 'POST',
|
|
487
496
|
headers: {
|
|
488
497
|
'Content-Type': 'application/json',
|
|
489
498
|
},
|
|
490
|
-
body: JSON.stringify(
|
|
499
|
+
body: JSON.stringify(requestBody),
|
|
491
500
|
});
|
|
492
501
|
|
|
493
502
|
if (!response.ok) {
|
|
@@ -692,7 +701,7 @@ export class AgentWidgetClient {
|
|
|
692
701
|
this.lastSentClientToolsFingerprint = null;
|
|
693
702
|
continue;
|
|
694
703
|
}
|
|
695
|
-
// Some other 409
|
|
704
|
+
// Some other 409: keep the parsed body for the handler below.
|
|
696
705
|
errorData = body ?? { error: 'Chat request failed' };
|
|
697
706
|
}
|
|
698
707
|
break;
|
|
@@ -962,10 +971,10 @@ export class AgentWidgetClient {
|
|
|
962
971
|
* Routes by mode:
|
|
963
972
|
* - **client-token mode**: POST `${apiBase}/v1/client/resume` (the
|
|
964
973
|
* session-authenticated sibling of `/v1/client/chat`; runtypelabs/core#3889),
|
|
965
|
-
* with the active `sessionId` in the body and no Bearer key
|
|
974
|
+
* with the active `sessionId` in the body and no Bearer key: a browser
|
|
966
975
|
* client-token page holds no secret. `clientTools` are already persisted
|
|
967
976
|
* server-side from the dispatch turn, so only `toolOutputs` is re-sent.
|
|
968
|
-
* - **dispatch / proxy mode**: POST `${apiUrl}/resume
|
|
977
|
+
* - **dispatch / proxy mode**: POST `${apiUrl}/resume`: Runtype mounts
|
|
969
978
|
* resume as a child of `/v1/dispatch`, so the URL is `${apiUrl}/resume`,
|
|
970
979
|
* and proxies follow the same shape (`/api/chat/dispatch/resume`).
|
|
971
980
|
*
|
|
@@ -989,8 +998,8 @@ export class AgentWidgetClient {
|
|
|
989
998
|
// The client-token resume route authenticates the session, not a Bearer
|
|
990
999
|
// key. A WebMCP approval can sit awaiting user input for a long time, so by
|
|
991
1000
|
// the time we resume the original session may have expired. Re-validate (and
|
|
992
|
-
// silently re-init if needed) via initSession()
|
|
993
|
-
// session when `new Date() < expiresAt`, else mints a fresh one
|
|
1001
|
+
// silently re-init if needed) via initSession(): which returns the live
|
|
1002
|
+
// session when `new Date() < expiresAt`, else mints a fresh one: instead of
|
|
994
1003
|
// trusting the possibly-stale `this.clientSession`. (core#3889; BugBot
|
|
995
1004
|
// PR #214 r3367875360.)
|
|
996
1005
|
let resumeSessionId: string | undefined;
|
|
@@ -1060,7 +1069,7 @@ export class AgentWidgetClient {
|
|
|
1060
1069
|
|
|
1061
1070
|
// Client tools: built-in widget tools (ask_user_question, when exposed)
|
|
1062
1071
|
// plus the per-turn WebMCP page-registry snapshot. Name collisions are
|
|
1063
|
-
// impossible
|
|
1072
|
+
// impossible: WebMCP entries are `webmcp:`-prefixed server-side while
|
|
1064
1073
|
// `sdk`-origin built-ins keep bare names. Both kinds ride the same
|
|
1065
1074
|
// diff-only fingerprint path in client-token mode. Kept to a single await
|
|
1066
1075
|
// so dispatch microtask timing is unchanged.
|
|
@@ -1176,7 +1185,7 @@ export class AgentWidgetClient {
|
|
|
1176
1185
|
// dropping `clientTools` accidentally; the WebMCP wire surface
|
|
1177
1186
|
// is invisible to them. The integrator can still set
|
|
1178
1187
|
// `clientTools: []` or `clientTools: undefined` explicitly to
|
|
1179
|
-
// strip them on purpose
|
|
1188
|
+
// strip them on purpose: we only fall back when the field is
|
|
1180
1189
|
// entirely absent from the returned object.
|
|
1181
1190
|
if (
|
|
1182
1191
|
payload.clientTools !== undefined &&
|
|
@@ -1249,7 +1258,7 @@ export class AgentWidgetClient {
|
|
|
1249
1258
|
createNewAssistant(result.partId);
|
|
1250
1259
|
}
|
|
1251
1260
|
|
|
1252
|
-
// Update partId tracking (only when partId is provided
|
|
1261
|
+
// Update partId tracking (only when partId is provided: backward compatible)
|
|
1253
1262
|
if (result.partId !== undefined) {
|
|
1254
1263
|
partIdState.current = result.partId;
|
|
1255
1264
|
}
|
|
@@ -1372,7 +1381,7 @@ export class AgentWidgetClient {
|
|
|
1372
1381
|
// Messages produced by steps inside a nested flow executed as a tool.
|
|
1373
1382
|
// Keyed by `${parentToolId}::${nestedStepId}::${partId}` so each nested
|
|
1374
1383
|
// step (send-stream, prompt) gets its own assistant message, and prompts
|
|
1375
|
-
// with inner tool calls split into one message per text segment
|
|
1384
|
+
// with inner tool calls split into one message per text segment: still
|
|
1376
1385
|
// attributable to the parent tool call.
|
|
1377
1386
|
const nestedStepMessages = new Map<string, AgentWidgetMessage>();
|
|
1378
1387
|
// Most-recent partId seen for a given `${toolId}::${stepId}` scope, used
|
|
@@ -1397,7 +1406,7 @@ export class AgentWidgetClient {
|
|
|
1397
1406
|
) => `${toolId}::${stepId}::${partId}`;
|
|
1398
1407
|
|
|
1399
1408
|
// Prefix used to sweep every nested message belonging to a single
|
|
1400
|
-
// (toolId, stepId) scope
|
|
1409
|
+
// (toolId, stepId) scope: needed on step_complete to seal any segments
|
|
1401
1410
|
// that are still streaming.
|
|
1402
1411
|
const getNestedStepPrefix = (toolId: string, stepId: string) =>
|
|
1403
1412
|
`${toolId}::${stepId}::`;
|
|
@@ -1719,7 +1728,7 @@ export class AgentWidgetClient {
|
|
|
1719
1728
|
const cur = msg.content ?? "";
|
|
1720
1729
|
if (mergedDisplay.trim() === "") return;
|
|
1721
1730
|
// Only replace when empty, or when the stream left a strict prefix of the
|
|
1722
|
-
// authoritative final (truncation). Do not use length alone
|
|
1731
|
+
// authoritative final (truncation). Do not use length alone: multi-segment
|
|
1723
1732
|
// flows can have a short last bubble whose content is not a prefix of the
|
|
1724
1733
|
// full step response.
|
|
1725
1734
|
if (
|
|
@@ -1801,7 +1810,7 @@ export class AgentWidgetClient {
|
|
|
1801
1810
|
// Declared here so scheduleReadyQueueDrain can reference it; assigned
|
|
1802
1811
|
// after all handler-scoped variables are initialised (before the SSE loop).
|
|
1803
1812
|
let drainReadyQueue: () => void;
|
|
1804
|
-
// Two drain paths
|
|
1813
|
+
// Two drain paths: both are intentional, do not remove either:
|
|
1805
1814
|
// 1. Microtask drain (scheduleReadyQueueDrain): required when the
|
|
1806
1815
|
// buffer's emitter fires from the gap-timeout setTimeout callback,
|
|
1807
1816
|
// because there is no surrounding synchronous drain site there.
|
|
@@ -1922,7 +1931,7 @@ export class AgentWidgetClient {
|
|
|
1922
1931
|
const toolId =
|
|
1923
1932
|
resolveToolId(payload, true) ?? `tool-${nextSequence()}`;
|
|
1924
1933
|
const toolName = payload.toolName ?? payload.name;
|
|
1925
|
-
// Suppress tool UI for artifact emit tools
|
|
1934
|
+
// Suppress tool UI for artifact emit tools: artifacts are handled via artifact_* events
|
|
1926
1935
|
if (isArtifactEmitToolName(toolName)) {
|
|
1927
1936
|
artifactToolCallIds.add(toolId);
|
|
1928
1937
|
continue;
|
|
@@ -2047,7 +2056,7 @@ export class AgentWidgetClient {
|
|
|
2047
2056
|
// Key the message by the per-call `toolCallId` (provider `toolu_…`;
|
|
2048
2057
|
// core#3878) when present. Two PARALLEL calls to the SAME tool in one
|
|
2049
2058
|
// turn collapse to an identical `toolId` (`runtime_webmcp:<name>_<ms>`)
|
|
2050
|
-
// and `index: 0
|
|
2059
|
+
// and `index: 0`: only `toolCallId` distinguishes them. Keying on it
|
|
2051
2060
|
// (a) keeps the two awaits as DISTINCT messages with their own args
|
|
2052
2061
|
// instead of the second clobbering the first, and (b) merges each
|
|
2053
2062
|
// await into the matching `tool_start` bubble (also keyed by
|
|
@@ -2096,7 +2105,7 @@ export class AgentWidgetClient {
|
|
|
2096
2105
|
emitMessage(toolMessage);
|
|
2097
2106
|
} else if (payloadType === "text_start") {
|
|
2098
2107
|
// Lifecycle event: a new text segment is beginning (emitted at tool boundaries).
|
|
2099
|
-
// When toolContext is present this fired inside a nested flow
|
|
2108
|
+
// When toolContext is present this fired inside a nested flow: it must not
|
|
2100
2109
|
// seal or rotate the outer assistant message. Nested prompt segmentation is
|
|
2101
2110
|
// handled via nestedStepMessages keyed by (toolId, stepId).
|
|
2102
2111
|
if ((payload as any).toolContext?.toolId) {
|
|
@@ -2118,7 +2127,7 @@ export class AgentWidgetClient {
|
|
|
2118
2127
|
}
|
|
2119
2128
|
} else if (payloadType === "text_end") {
|
|
2120
2129
|
// Lifecycle event: current text segment ended (tool call about to start).
|
|
2121
|
-
// When toolContext is present the boundary belongs to a nested flow
|
|
2130
|
+
// When toolContext is present the boundary belongs to a nested flow: leave
|
|
2122
2131
|
// outer assistant state alone so the outer stream is never interrupted by
|
|
2123
2132
|
// nested activity.
|
|
2124
2133
|
if ((payload as any).toolContext?.toolId) {
|
|
@@ -2146,7 +2155,7 @@ export class AgentWidgetClient {
|
|
|
2146
2155
|
// originated inside a nested flow executed as a tool. Surface it as
|
|
2147
2156
|
// its own assistant message keyed by the nested step id, so authors
|
|
2148
2157
|
// who add send-stream / prompt steps inside their flow see them as
|
|
2149
|
-
// real messages in the timeline, in order
|
|
2158
|
+
// real messages in the timeline, in order: rather than merging
|
|
2150
2159
|
// into the outer assistant bubble or getting buried in the tool
|
|
2151
2160
|
// card. Each nested step id gets its own message; the parent tool
|
|
2152
2161
|
// bubble continues to represent the invocation via tool_* events.
|
|
@@ -2295,7 +2304,7 @@ export class AgentWidgetClient {
|
|
|
2295
2304
|
const text = typeof result === 'string' ? result : result?.text ?? null;
|
|
2296
2305
|
|
|
2297
2306
|
if (text !== null && text.trim() !== "") {
|
|
2298
|
-
// Parser successfully extracted text
|
|
2307
|
+
// Parser successfully extracted text: update the chunk's assistant
|
|
2299
2308
|
// (not assistantMessage; text_end may have cleared that ref before microtasks run)
|
|
2300
2309
|
assistant.content = text;
|
|
2301
2310
|
emitMessage(assistant);
|
|
@@ -2469,14 +2478,14 @@ export class AgentWidgetClient {
|
|
|
2469
2478
|
|
|
2470
2479
|
// Capture optional per-step stopReason emitted by the runtime
|
|
2471
2480
|
// (e.g. `'max_tool_calls'`, `'length'`). This is the dispatch-mode
|
|
2472
|
-
// fallback
|
|
2481
|
+
// fallback: `agent_turn_complete` will overwrite it later in
|
|
2473
2482
|
// agent-loop streams.
|
|
2474
2483
|
const stepStopReason = (payload as any).stopReason as
|
|
2475
2484
|
| StopReasonKind
|
|
2476
2485
|
| undefined;
|
|
2477
2486
|
|
|
2478
2487
|
if (didSplitByPartId) {
|
|
2479
|
-
// Sealed segment(s)
|
|
2488
|
+
// Sealed segment(s): do not create a second bubble from step_complete.
|
|
2480
2489
|
// Merge authoritative final response into the last sealed segment (fixes async lag).
|
|
2481
2490
|
if (assistantMessage !== null) {
|
|
2482
2491
|
const msg: AgentWidgetMessage = assistantMessage;
|
|
@@ -2626,7 +2635,7 @@ export class AgentWidgetClient {
|
|
|
2626
2635
|
} else if (payloadType === "flow_complete") {
|
|
2627
2636
|
const finalContent = payload.result?.response;
|
|
2628
2637
|
if (didSplitByPartId) {
|
|
2629
|
-
// Content was split into multiple assistant messages
|
|
2638
|
+
// Content was split into multiple assistant messages: the full response
|
|
2630
2639
|
// in flow_complete would overwrite the last segment. Just finalize streaming.
|
|
2631
2640
|
if (assistantMessage !== null) {
|
|
2632
2641
|
const msg: AgentWidgetMessage = assistantMessage;
|
|
@@ -2799,8 +2808,7 @@ export class AgentWidgetClient {
|
|
|
2799
2808
|
}
|
|
2800
2809
|
|
|
2801
2810
|
// Attach the turn-level stopReason to the assistant message
|
|
2802
|
-
// produced by this turn. Only overwrite the current message
|
|
2803
|
-
// prior turns already sealed their own stopReason via step_complete.
|
|
2811
|
+
// produced by this turn. Only overwrite the current message: // prior turns already sealed their own stopReason via step_complete.
|
|
2804
2812
|
// Falls back to lastAssistantInTurn when the current bubble was
|
|
2805
2813
|
// sealed at a tool-call boundary mid-turn, so the notice still
|
|
2806
2814
|
// attaches to the final visible text segment.
|
|
@@ -2886,7 +2894,7 @@ export class AgentWidgetClient {
|
|
|
2886
2894
|
} else if (payloadType === "agent_media") {
|
|
2887
2895
|
// A tool produced media (image / audio / video / file). Render it
|
|
2888
2896
|
// as a synthetic assistant message inserted at the point the tool
|
|
2889
|
-
// completed
|
|
2897
|
+
// completed: between the tool bubble and the next text turn.
|
|
2890
2898
|
//
|
|
2891
2899
|
// Wire format is the AI SDK–aligned `MediaContentPart` from
|
|
2892
2900
|
// @runtypelabs/shared:
|
|
@@ -2913,7 +2921,7 @@ export class AgentWidgetClient {
|
|
|
2913
2921
|
const data = typeof rec.data === "string" ? rec.data : undefined;
|
|
2914
2922
|
if (!data) continue;
|
|
2915
2923
|
// Empty/missing mediaType yields `data:;base64,...` which RFC 2397
|
|
2916
|
-
// resolves to `text/plain
|
|
2924
|
+
// resolves to `text/plain`: stamp a default so the data URI is
|
|
2917
2925
|
// well-formed and the part lands in the file bucket.
|
|
2918
2926
|
mediaType = rawMediaType.length > 0 ? rawMediaType : "application/octet-stream";
|
|
2919
2927
|
src = `data:${mediaType};base64,${data}`;
|
|
@@ -2990,7 +2998,7 @@ export class AgentWidgetClient {
|
|
|
2990
2998
|
|
|
2991
2999
|
// Seal any in-flight assistant text bubble before splitting the
|
|
2992
3000
|
// stream. Without this, an orphan bubble retains `streaming: true`
|
|
2993
|
-
// forever
|
|
3001
|
+
// forever: `agent_complete` only finalizes the latest
|
|
2994
3002
|
// `assistantMessage`, so the typing/caret indicator would stay on
|
|
2995
3003
|
// the prior bubble even though no more deltas will arrive.
|
|
2996
3004
|
const prevAssistant = assistantMessage as AgentWidgetMessage | null;
|
package/src/codegen.test.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { VERSION } from "./version";
|
|
|
5
5
|
|
|
6
6
|
// The `@runtypelabs/persona/codegen` subpath is the server/Worker-safe entry for
|
|
7
7
|
// snippet generation. It must expose the exact same generator as the internal
|
|
8
|
-
// module (and, transitively, the main barrel)
|
|
8
|
+
// module (and, transitively, the main barrel): no fork, no drift.
|
|
9
9
|
describe("codegen subpath", () => {
|
|
10
10
|
const config = {
|
|
11
11
|
apiUrl: "https://api.example.com/chat",
|
package/src/codegen.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure code-snippet generation entry (`@runtypelabs/persona/codegen`).
|
|
3
3
|
*
|
|
4
|
-
* `generateCodeSnippet` is pure string-templating
|
|
4
|
+
* `generateCodeSnippet` is pure string-templating: it builds install snippets
|
|
5
5
|
* from a widget config and depends only on a type import + the `VERSION`
|
|
6
6
|
* constant. The main `index.ts` barrel re-exports it too, but that barrel pulls
|
|
7
7
|
* in the full widget runtime (`index-core` → idiomorph / marked / DOM), which is
|
|
@@ -280,7 +280,7 @@ describe("createApprovalBubble agent reason", () => {
|
|
|
280
280
|
expect(reasonLine.textContent).toContain(
|
|
281
281
|
"Adds the <b>large</b> blue tee to your cart as you asked."
|
|
282
282
|
);
|
|
283
|
-
// Plain text rendering
|
|
283
|
+
// Plain text rendering: markup in the agent-authored string must not
|
|
284
284
|
// become elements (prompt-injection surface).
|
|
285
285
|
expect(reasonLine.querySelector("b")).toBeNull();
|
|
286
286
|
});
|
|
@@ -173,7 +173,7 @@ export const createApprovalBubble = (
|
|
|
173
173
|
bubble.id = `bubble-${message.id}`;
|
|
174
174
|
bubble.setAttribute("data-message-id", message.id);
|
|
175
175
|
|
|
176
|
-
// Apply styling
|
|
176
|
+
// Apply styling: use semantic tokens with config overrides
|
|
177
177
|
bubble.style.backgroundColor = approvalConfig?.backgroundColor ?? "var(--persona-approval-bg, #fefce8)";
|
|
178
178
|
bubble.style.borderColor = approvalConfig?.borderColor ?? "var(--persona-approval-border, #fef08a)";
|
|
179
179
|
bubble.style.boxShadow =
|
|
@@ -242,7 +242,7 @@ export const createApprovalBubble = (
|
|
|
242
242
|
|
|
243
243
|
// User-facing summary line. The wire `description` is the tool's
|
|
244
244
|
// agent-facing description (prompt prose, usage rules), so it is not shown
|
|
245
|
-
// here
|
|
245
|
+
// here: it lives in the collapsible details section below. Label priority:
|
|
246
246
|
// formatDescription → declared WebMCP `title` → humanized tool name →
|
|
247
247
|
// raw description (no tool name at all).
|
|
248
248
|
const isWebMcpTool =
|
|
@@ -276,7 +276,7 @@ export const createApprovalBubble = (
|
|
|
276
276
|
|
|
277
277
|
// Agent-authored justification for this specific call. It is the agent's
|
|
278
278
|
// own claim about its intent (attacker-writable under prompt injection), so
|
|
279
|
-
// it is rendered as plain text via textContent
|
|
279
|
+
// it is rendered as plain text via textContent, never markdown/HTML, and
|
|
280
280
|
// explicitly attributed to the agent rather than spoken in system voice.
|
|
281
281
|
if (approval.reason) {
|
|
282
282
|
const reasonLine = createElement("p", "persona-text-sm persona-mt-1 persona-text-persona-muted");
|
|
@@ -36,7 +36,7 @@ export function createArtifactPane(
|
|
|
36
36
|
config: AgentWidgetConfig,
|
|
37
37
|
options: {
|
|
38
38
|
onSelect: (id: string) => void;
|
|
39
|
-
/** User closed the pane (mobile drawer or split sidebar)
|
|
39
|
+
/** User closed the pane (mobile drawer or split sidebar): parent should persist “hidden until reopened”. */
|
|
40
40
|
onDismiss?: () => void;
|
|
41
41
|
}
|
|
42
42
|
): ArtifactPaneApi {
|
|
@@ -203,7 +203,7 @@ export function createArtifactPane(
|
|
|
203
203
|
});
|
|
204
204
|
|
|
205
205
|
if (copyMenuChevronBtn && copyMenuItems?.length) {
|
|
206
|
-
// Resolve the portal target
|
|
206
|
+
// Resolve the portal target: widget root for CSS var inheritance, escaping overflow: hidden
|
|
207
207
|
const resolvePortal = (): HTMLElement => shell.closest("[data-persona-root]") as HTMLElement ?? document.body;
|
|
208
208
|
|
|
209
209
|
const initDropdown = () => {
|
|
@@ -414,7 +414,7 @@ export function createArtifactPane(
|
|
|
414
414
|
backdrop.classList.add("persona-hidden");
|
|
415
415
|
shell.classList.remove("persona-artifact-drawer-open");
|
|
416
416
|
} else {
|
|
417
|
-
// isMobile && !(has && mobileOpen): e.g. dismissed drawer
|
|
417
|
+
// isMobile && !(has && mobileOpen): e.g. dismissed drawer: keep closed chrome in sync
|
|
418
418
|
backdrop.classList.add("persona-hidden");
|
|
419
419
|
shell.classList.remove("persona-artifact-drawer-open");
|
|
420
420
|
}
|
|
@@ -103,11 +103,11 @@ describe("ensureAskUserQuestionSheet", () => {
|
|
|
103
103
|
expect(sheet).not.toBeNull();
|
|
104
104
|
const pills = sheet!.querySelectorAll('[data-ask-user-action="pick"]');
|
|
105
105
|
expect(pills.length).toBe(3);
|
|
106
|
-
// Default rows layout
|
|
106
|
+
// Default rows layout: label is in `.persona-ask-row-label`, alongside
|
|
107
107
|
// a number-badge affordance. Read the dedicated label slot.
|
|
108
108
|
const firstLabel = pills[0].querySelector(".persona-ask-row-label");
|
|
109
109
|
expect(firstLabel?.textContent).toBe("Hobbyists");
|
|
110
|
-
// Free-text affordance present by default
|
|
110
|
+
// Free-text affordance present by default: rows mode embeds the input
|
|
111
111
|
// inside the Other row via `focus-free-text` (digit shortcut + click chrome).
|
|
112
112
|
const custom = sheet!.querySelector('[data-ask-user-action="focus-free-text"]');
|
|
113
113
|
expect(custom).not.toBeNull();
|
|
@@ -144,7 +144,7 @@ describe("ensureAskUserQuestionSheet", () => {
|
|
|
144
144
|
});
|
|
145
145
|
ensureAskUserQuestionSheet(msg, {} as AgentWidgetConfig, overlay);
|
|
146
146
|
|
|
147
|
-
// Simulate more chunks arriving
|
|
147
|
+
// Simulate more chunks arriving: options now parseable
|
|
148
148
|
msg.toolCall!.chunks = ['{"questions":[{"question":"X","options":[{"label":"A"},{"label":"B"}'];
|
|
149
149
|
ensureAskUserQuestionSheet(msg, {} as AgentWidgetConfig, overlay);
|
|
150
150
|
|
|
@@ -155,7 +155,7 @@ describe("ensureAskUserQuestionSheet", () => {
|
|
|
155
155
|
expect(pills[1].querySelector(".persona-ask-row-label")?.textContent).toBe("B");
|
|
156
156
|
});
|
|
157
157
|
|
|
158
|
-
it("is idempotent
|
|
158
|
+
it("is idempotent: re-invoking does not duplicate sheets", () => {
|
|
159
159
|
const overlay = makeOverlay();
|
|
160
160
|
ensureAskUserQuestionSheet(makeMessage(), {} as AgentWidgetConfig, overlay);
|
|
161
161
|
ensureAskUserQuestionSheet(makeMessage(), {} as AgentWidgetConfig, overlay);
|
|
@@ -163,7 +163,7 @@ describe("ensureAskUserQuestionSheet", () => {
|
|
|
163
163
|
expect(sheets.length).toBe(1);
|
|
164
164
|
});
|
|
165
165
|
|
|
166
|
-
it("respects enabled: false
|
|
166
|
+
it("respects enabled: false: sheet is not mounted", () => {
|
|
167
167
|
const overlay = makeOverlay();
|
|
168
168
|
ensureAskUserQuestionSheet(
|
|
169
169
|
makeMessage(),
|
|
@@ -192,7 +192,7 @@ describe("ensureAskUserQuestionSheet", () => {
|
|
|
192
192
|
expect(sheet.getAttribute("data-multi-select")).toBe("true");
|
|
193
193
|
});
|
|
194
194
|
|
|
195
|
-
it("does not render a dismiss button
|
|
195
|
+
it("does not render a dismiss button: Skip in the nav row is the canonical escape", () => {
|
|
196
196
|
const overlay = makeOverlay();
|
|
197
197
|
ensureAskUserQuestionSheet(makeMessage(), {} as AgentWidgetConfig, overlay);
|
|
198
198
|
const sheet = overlay.querySelector('[data-persona-ask-sheet-for="tool-1"]')!;
|
|
@@ -221,7 +221,7 @@ describe("removeAskUserQuestionSheet", () => {
|
|
|
221
221
|
ensureAskUserQuestionSheet(makeMessage(), {} as AgentWidgetConfig, overlay);
|
|
222
222
|
expect(overlay.querySelector('[data-persona-ask-sheet-for="tool-1"]')).not.toBeNull();
|
|
223
223
|
removeAskUserQuestionSheet(overlay, "tool-1");
|
|
224
|
-
// The implementation defers removal with setTimeout
|
|
224
|
+
// The implementation defers removal with setTimeout: flush it.
|
|
225
225
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
226
226
|
expect(overlay.querySelector('[data-persona-ask-sheet-for="tool-1"]')).toBeNull();
|
|
227
227
|
});
|
|
@@ -249,7 +249,7 @@ const makeGroupedMessage = (
|
|
|
249
249
|
const sheetFor = (overlay: Element, toolCallId: string): HTMLElement =>
|
|
250
250
|
overlay.querySelector<HTMLElement>(`[data-persona-ask-sheet-for="${toolCallId}"]`)!;
|
|
251
251
|
|
|
252
|
-
describe("grouped questions
|
|
252
|
+
describe("grouped questions: stepper UI", () => {
|
|
253
253
|
it("renders 'Question 1 of N' chip and Back/Next nav row when N > 1", () => {
|
|
254
254
|
const overlay = makeOverlay();
|
|
255
255
|
const msg = makeGroupedMessage([
|
|
@@ -384,7 +384,7 @@ describe("grouped questions — stepper UI", () => {
|
|
|
384
384
|
});
|
|
385
385
|
});
|
|
386
386
|
|
|
387
|
-
it("hydrates from agentMetadata
|
|
387
|
+
it("hydrates from agentMetadata: restores index and prior answers on a fresh mount", () => {
|
|
388
388
|
const overlay = makeOverlay();
|
|
389
389
|
const msg = makeGroupedMessage(
|
|
390
390
|
[
|
|
@@ -464,7 +464,7 @@ describe("rows layout (default)", () => {
|
|
|
464
464
|
expect(firstRow.querySelector(".persona-ask-row-description")?.textContent).toBe(
|
|
465
465
|
"Real Discord invite to link"
|
|
466
466
|
);
|
|
467
|
-
// No title-attr fallback in rows layout
|
|
467
|
+
// No title-attr fallback in rows layout: description is inline.
|
|
468
468
|
expect(firstRow.getAttribute("title")).toBeNull();
|
|
469
469
|
});
|
|
470
470
|
|
|
@@ -507,11 +507,11 @@ describe("rows layout (default)", () => {
|
|
|
507
507
|
expect(rows[0].querySelector(".persona-ask-row-badge")).toBeNull();
|
|
508
508
|
});
|
|
509
509
|
|
|
510
|
-
it("embeds the free-text input INSIDE the Other row (rows mode)
|
|
510
|
+
it("embeds the free-text input INSIDE the Other row (rows mode): no separate row, no Send button", () => {
|
|
511
511
|
const overlay = makeOverlay();
|
|
512
512
|
ensureAskUserQuestionSheet(makeMessage(), {} as AgentWidgetConfig, overlay);
|
|
513
513
|
const sheet = overlay.querySelector('[data-persona-ask-sheet-for="tool-1"]')!;
|
|
514
|
-
// No standalone free-text row in rows mode
|
|
514
|
+
// No standalone free-text row in rows mode: input lives inside the Other row.
|
|
515
515
|
expect(sheet.querySelector('[data-ask-free-text-row="true"]')).toBeNull();
|
|
516
516
|
const otherRow = sheet.querySelector<HTMLElement>('[data-ask-other-row="true"]')!;
|
|
517
517
|
expect(otherRow).not.toBeNull();
|
|
@@ -520,12 +520,12 @@ describe("rows layout (default)", () => {
|
|
|
520
520
|
expect(otherRow.querySelector('[data-ask-user-action="submit-free-text"]')).toBeNull();
|
|
521
521
|
// Other row carries the focus-free-text action (digit shortcut + click chrome).
|
|
522
522
|
expect(otherRow.getAttribute("data-ask-user-action")).toBe("focus-free-text");
|
|
523
|
-
// Number badge for the Other row (N+1
|
|
523
|
+
// Number badge for the Other row (N+1: 3 real options + Other = 4).
|
|
524
524
|
const badge = otherRow.querySelector<HTMLElement>(".persona-ask-row-badge");
|
|
525
525
|
expect(badge?.textContent).toBe("4");
|
|
526
526
|
});
|
|
527
527
|
|
|
528
|
-
it("respects layout: 'pills' opt-out
|
|
528
|
+
it("respects layout: 'pills' opt-out: no row affordances, free-text starts hidden, Send button still rendered", () => {
|
|
529
529
|
const overlay = makeOverlay();
|
|
530
530
|
const config = {
|
|
531
531
|
features: { askUserQuestion: { layout: "pills" } },
|
|
@@ -60,7 +60,7 @@ const resolveFeature = (config?: AgentWidgetConfig): AgentWidgetAskUserQuestionF
|
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
62
|
* Parse an `ask_user_question` tool-variant message into a partial payload.
|
|
63
|
-
* Safe to call mid-stream
|
|
63
|
+
* Safe to call mid-stream: will walk the tool call's `chunks` via
|
|
64
64
|
* `partial-json` and return `{ payload: null, complete: false }` when there
|
|
65
65
|
* isn't enough data yet. `complete` flips to `true` once the tool call
|
|
66
66
|
* reports status `"complete"`.
|
|
@@ -570,7 +570,7 @@ const syncNavState = (sheet: HTMLElement): void => {
|
|
|
570
570
|
);
|
|
571
571
|
if (next) next.disabled = !hasAnswer;
|
|
572
572
|
|
|
573
|
-
// Multi-select submit (1-question mode)
|
|
573
|
+
// Multi-select submit (1-question mode): keep existing behavior.
|
|
574
574
|
const multi = sheet.querySelector<HTMLButtonElement>('[data-ask-user-action="submit-multi"]');
|
|
575
575
|
if (multi) {
|
|
576
576
|
const labels = Array.from(
|
|
@@ -625,7 +625,7 @@ const renderCurrentPage = (
|
|
|
625
625
|
pillList.replaceWith(fresh);
|
|
626
626
|
}
|
|
627
627
|
|
|
628
|
-
// Free-text row
|
|
628
|
+
// Free-text row: re-build to clear stale input value across pages.
|
|
629
629
|
// Only present in pills (legacy) mode; in rows mode the input lives inside
|
|
630
630
|
// the Other row of the pill list, which is rebuilt above.
|
|
631
631
|
if (layout !== "rows") {
|
|
@@ -633,7 +633,7 @@ const renderCurrentPage = (
|
|
|
633
633
|
if (oldFree) oldFree.replaceWith(buildFreeTextRow(feature, layout));
|
|
634
634
|
}
|
|
635
635
|
|
|
636
|
-
// Multi-select action row
|
|
636
|
+
// Multi-select action row: only relevant in 1-question mode.
|
|
637
637
|
const oldMulti = sheet.querySelector<HTMLElement>('[data-ask-multi-actions="true"]');
|
|
638
638
|
if (!grouped && multiSelect && !oldMulti) {
|
|
639
639
|
sheet.appendChild(buildMultiSelectActions(feature));
|
|
@@ -698,7 +698,7 @@ const buildSheet = (
|
|
|
698
698
|
|
|
699
699
|
// Header: question text (flex-1) + compact "N/M" stepper indicator on the
|
|
700
700
|
// right (grouped only). Skip in the nav row is the canonical escape hatch
|
|
701
|
-
|
|
701
|
+
//: plugins that want a different escape model render their own UX.
|
|
702
702
|
const header = createElement(
|
|
703
703
|
"div",
|
|
704
704
|
"persona-ask-sheet-header persona-flex persona-items-center persona-gap-3"
|
|
@@ -721,7 +721,7 @@ const buildSheet = (
|
|
|
721
721
|
|
|
722
722
|
sheet.appendChild(header);
|
|
723
723
|
|
|
724
|
-
// Skeleton placeholders
|
|
724
|
+
// Skeleton placeholders: these get replaced wholesale by renderCurrentPage.
|
|
725
725
|
const skeletonClass =
|
|
726
726
|
layout === "rows"
|
|
727
727
|
? "persona-ask-pills persona-ask-pills--rows persona-flex persona-flex-col persona-gap-2"
|
|
@@ -768,7 +768,7 @@ const syncSheetFromMessage = (
|
|
|
768
768
|
|
|
769
769
|
/**
|
|
770
770
|
* Create the small in-transcript stub for an `ask_user_question` tool call.
|
|
771
|
-
* The stub is passive
|
|
771
|
+
* The stub is passive: the interactive sheet is mounted separately into
|
|
772
772
|
* the composer overlay via `ensureAskUserQuestionSheet`.
|
|
773
773
|
*/
|
|
774
774
|
export const createAskUserQuestionBubble = (
|
|
@@ -796,7 +796,7 @@ export const createAskUserQuestionBubble = (
|
|
|
796
796
|
|
|
797
797
|
/**
|
|
798
798
|
* Mount or update the interactive answer-pill sheet for a given message.
|
|
799
|
-
* Idempotent
|
|
799
|
+
* Idempotent: if a sheet already exists for the tool-call id, it is hydrated
|
|
800
800
|
* in-place instead of remounted, so streaming updates don't flicker.
|
|
801
801
|
*/
|
|
802
802
|
export const ensureAskUserQuestionSheet = (
|
|
@@ -812,7 +812,7 @@ export const ensureAskUserQuestionSheet = (
|
|
|
812
812
|
|
|
813
813
|
const toolCallId = message.toolCall!.id;
|
|
814
814
|
|
|
815
|
-
// Only keep the latest sheet in the overlay
|
|
815
|
+
// Only keep the latest sheet in the overlay: clear any stale siblings.
|
|
816
816
|
const siblings = overlay.querySelectorAll<HTMLElement>(`[${SHEET_SENTINEL}]`);
|
|
817
817
|
siblings.forEach((el) => {
|
|
818
818
|
if (el.getAttribute(SHEET_SENTINEL) !== toolCallId) {
|
|
@@ -916,7 +916,7 @@ export const refreshNavState = (sheet: HTMLElement): void => {
|
|
|
916
916
|
};
|
|
917
917
|
|
|
918
918
|
/**
|
|
919
|
-
* Test seam
|
|
919
|
+
* Test seam: reset the one-shot truncation warning so each test can assert
|
|
920
920
|
* the warn fires exactly once.
|
|
921
921
|
*/
|
|
922
922
|
export const __resetTruncateWarn = (): void => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createElement } from "../utils/dom";
|
|
1
|
+
import { createElement, createNode } from "../utils/dom";
|
|
2
2
|
import { AgentWidgetConfig, ContentPart } from "../types";
|
|
3
3
|
import {
|
|
4
4
|
createAttachmentControls,
|
|
@@ -57,20 +57,20 @@ export interface ComposerBuildContext {
|
|
|
57
57
|
export const buildComposer = (context: ComposerBuildContext): ComposerElements => {
|
|
58
58
|
const { config } = context;
|
|
59
59
|
|
|
60
|
-
const footer =
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
const footer = createNode("div", {
|
|
61
|
+
className:
|
|
62
|
+
"persona-widget-footer persona-border-t-persona-divider persona-bg-persona-surface persona-px-6 persona-py-4",
|
|
63
|
+
attrs: { "data-persona-theme-zone": "composer" },
|
|
64
|
+
});
|
|
65
65
|
|
|
66
66
|
const suggestions = createSuggestionsRow();
|
|
67
67
|
|
|
68
|
-
const composerForm =
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
68
|
+
const composerForm = createNode("form", {
|
|
69
|
+
className:
|
|
70
|
+
"persona-widget-composer persona-flex persona-flex-col persona-gap-2 persona-rounded-2xl persona-border persona-border-gray-200 persona-bg-persona-input-background persona-px-4 persona-py-3",
|
|
71
|
+
attrs: { "data-persona-composer-form": "" },
|
|
72
|
+
style: { outline: "none" },
|
|
73
|
+
});
|
|
74
74
|
|
|
75
75
|
const { textarea, attachAutoResize } = createComposerTextarea(config);
|
|
76
76
|
attachAutoResize();
|
|
@@ -93,10 +93,11 @@ export const buildComposer = (context: ComposerBuildContext): ComposerElements =
|
|
|
93
93
|
// The bare class names (persona-widget-composer__actions / __left-actions /
|
|
94
94
|
// __right-actions) are stable CSS hooks. The pill composer reuses
|
|
95
95
|
// __left-actions / __right-actions as semantic markers in its grid.
|
|
96
|
-
const actionsRow =
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
const actionsRow = createNode("div", {
|
|
97
|
+
className:
|
|
98
|
+
"persona-widget-composer__actions persona-flex persona-items-center persona-justify-between persona-w-full",
|
|
99
|
+
attrs: { "data-persona-composer-actions": "" },
|
|
100
|
+
});
|
|
100
101
|
const leftActions = createElement(
|
|
101
102
|
"div",
|
|
102
103
|
"persona-widget-composer__left-actions persona-flex persona-items-center persona-gap-2"
|