@runtypelabs/persona 3.34.1 → 3.36.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 +56 -12
- package/src/client.ts +35 -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
package/src/client.ts
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
createXmlParser
|
|
34
34
|
} from "./utils/formatting";
|
|
35
35
|
import { SequenceReorderBuffer } from "./utils/sequence-buffer";
|
|
36
|
+
import { VERSION } from "./version";
|
|
36
37
|
// artifactsSidebarEnabled is used in ui.ts to gate the sidebar pane rendering;
|
|
37
38
|
// artifact events are always processed here regardless of config.
|
|
38
39
|
|
|
@@ -180,7 +181,7 @@ export class AgentWidgetClient {
|
|
|
180
181
|
private lastSentClientToolsFingerprint: string | null = null;
|
|
181
182
|
private clientToolsFingerprintSessionId: string | null = null;
|
|
182
183
|
|
|
183
|
-
// WebMCP
|
|
184
|
+
// WebMCP: page-discovered tool consumption (see ./webmcp-bridge).
|
|
184
185
|
// Constructed lazily: null when `config.webmcp?.enabled !== true`.
|
|
185
186
|
private readonly webMcpBridge: WebMcpBridge | null;
|
|
186
187
|
|
|
@@ -188,6 +189,7 @@ export class AgentWidgetClient {
|
|
|
188
189
|
this.apiUrl = config.apiUrl ?? DEFAULT_ENDPOINT;
|
|
189
190
|
this.headers = {
|
|
190
191
|
"Content-Type": "application/json",
|
|
192
|
+
"X-Persona-Version": VERSION,
|
|
191
193
|
...config.headers
|
|
192
194
|
};
|
|
193
195
|
this.debug = Boolean(config.debug);
|
|
@@ -206,8 +208,8 @@ export class AgentWidgetClient {
|
|
|
206
208
|
* Refresh config in place WITHOUT tearing down the live connection or the
|
|
207
209
|
* WebMCP bridge. `AgentWidgetSession.updateConfig` calls this when only
|
|
208
210
|
* 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
|
|
211
|
+
* so a UI update that lands mid-turn: e.g. a `webmcp:*` tool restyling the
|
|
212
|
+
* widget while the agent's turn is still streaming: doesn't abandon the
|
|
211
213
|
* in-flight stream/resume. Connection or request-shaping changes (apiUrl,
|
|
212
214
|
* clientToken, webmcp, headers, parser, …) take the full client rebuild path
|
|
213
215
|
* in the session instead, which is the only place the bridge is recreated.
|
|
@@ -215,7 +217,7 @@ export class AgentWidgetClient {
|
|
|
215
217
|
* Only the live-read `config` is refreshed (e.g. `iterationDisplay`); the
|
|
216
218
|
* constructor-derived request-shaping fields (apiUrl, headers, parser,
|
|
217
219
|
* contextProviders, middleware, …) are left untouched because the session
|
|
218
|
-
* routes any change to those down the full-rebuild path instead
|
|
220
|
+
* routes any change to those down the full-rebuild path instead, so they are
|
|
219
221
|
* guaranteed unchanged here. The `webMcpBridge` instance and its
|
|
220
222
|
* installed-polyfill memo are deliberately preserved, which keeps any
|
|
221
223
|
* in-flight resolve alive.
|
|
@@ -254,7 +256,7 @@ export class AgentWidgetClient {
|
|
|
254
256
|
* WebMCP: execute a returned `webmcp:<name>` tool call against the page's
|
|
255
257
|
* registry and return the normalized MCP-shaped result for `/resume`. The
|
|
256
258
|
* bridge handles confirm-bubble gating, the 30s timeout, error
|
|
257
|
-
* normalization, and `signal`-driven abort
|
|
259
|
+
* normalization, and `signal`-driven abort: callers never see throws.
|
|
258
260
|
*
|
|
259
261
|
* Returns `null` when WebMCP is not enabled on this client (signal to the
|
|
260
262
|
* session that it should fall back to the legacy local-tool resume path,
|
|
@@ -355,6 +357,7 @@ export class AgentWidgetClient {
|
|
|
355
357
|
method: 'POST',
|
|
356
358
|
headers: {
|
|
357
359
|
'Content-Type': 'application/json',
|
|
360
|
+
'X-Persona-Version': VERSION,
|
|
358
361
|
},
|
|
359
362
|
body: JSON.stringify(requestBody),
|
|
360
363
|
});
|
|
@@ -495,6 +498,7 @@ export class AgentWidgetClient {
|
|
|
495
498
|
method: 'POST',
|
|
496
499
|
headers: {
|
|
497
500
|
'Content-Type': 'application/json',
|
|
501
|
+
'X-Persona-Version': VERSION,
|
|
498
502
|
},
|
|
499
503
|
body: JSON.stringify(requestBody),
|
|
500
504
|
});
|
|
@@ -682,6 +686,7 @@ export class AgentWidgetClient {
|
|
|
682
686
|
method: 'POST',
|
|
683
687
|
headers: {
|
|
684
688
|
'Content-Type': 'application/json',
|
|
689
|
+
'X-Persona-Version': VERSION,
|
|
685
690
|
},
|
|
686
691
|
body: JSON.stringify(chatRequest),
|
|
687
692
|
signal: controller.signal,
|
|
@@ -701,7 +706,7 @@ export class AgentWidgetClient {
|
|
|
701
706
|
this.lastSentClientToolsFingerprint = null;
|
|
702
707
|
continue;
|
|
703
708
|
}
|
|
704
|
-
// Some other 409
|
|
709
|
+
// Some other 409: keep the parsed body for the handler below.
|
|
705
710
|
errorData = body ?? { error: 'Chat request failed' };
|
|
706
711
|
}
|
|
707
712
|
break;
|
|
@@ -971,10 +976,10 @@ export class AgentWidgetClient {
|
|
|
971
976
|
* Routes by mode:
|
|
972
977
|
* - **client-token mode**: POST `${apiBase}/v1/client/resume` (the
|
|
973
978
|
* session-authenticated sibling of `/v1/client/chat`; runtypelabs/core#3889),
|
|
974
|
-
* with the active `sessionId` in the body and no Bearer key
|
|
979
|
+
* with the active `sessionId` in the body and no Bearer key: a browser
|
|
975
980
|
* client-token page holds no secret. `clientTools` are already persisted
|
|
976
981
|
* server-side from the dispatch turn, so only `toolOutputs` is re-sent.
|
|
977
|
-
* - **dispatch / proxy mode**: POST `${apiUrl}/resume
|
|
982
|
+
* - **dispatch / proxy mode**: POST `${apiUrl}/resume`: Runtype mounts
|
|
978
983
|
* resume as a child of `/v1/dispatch`, so the URL is `${apiUrl}/resume`,
|
|
979
984
|
* and proxies follow the same shape (`/api/chat/dispatch/resume`).
|
|
980
985
|
*
|
|
@@ -998,8 +1003,8 @@ export class AgentWidgetClient {
|
|
|
998
1003
|
// The client-token resume route authenticates the session, not a Bearer
|
|
999
1004
|
// key. A WebMCP approval can sit awaiting user input for a long time, so by
|
|
1000
1005
|
// the time we resume the original session may have expired. Re-validate (and
|
|
1001
|
-
// silently re-init if needed) via initSession()
|
|
1002
|
-
// session when `new Date() < expiresAt`, else mints a fresh one
|
|
1006
|
+
// silently re-init if needed) via initSession(): which returns the live
|
|
1007
|
+
// session when `new Date() < expiresAt`, else mints a fresh one: instead of
|
|
1003
1008
|
// trusting the possibly-stale `this.clientSession`. (core#3889; BugBot
|
|
1004
1009
|
// PR #214 r3367875360.)
|
|
1005
1010
|
let resumeSessionId: string | undefined;
|
|
@@ -1069,7 +1074,7 @@ export class AgentWidgetClient {
|
|
|
1069
1074
|
|
|
1070
1075
|
// Client tools: built-in widget tools (ask_user_question, when exposed)
|
|
1071
1076
|
// plus the per-turn WebMCP page-registry snapshot. Name collisions are
|
|
1072
|
-
// impossible
|
|
1077
|
+
// impossible: WebMCP entries are `webmcp:`-prefixed server-side while
|
|
1073
1078
|
// `sdk`-origin built-ins keep bare names. Both kinds ride the same
|
|
1074
1079
|
// diff-only fingerprint path in client-token mode. Kept to a single await
|
|
1075
1080
|
// so dispatch microtask timing is unchanged.
|
|
@@ -1185,7 +1190,7 @@ export class AgentWidgetClient {
|
|
|
1185
1190
|
// dropping `clientTools` accidentally; the WebMCP wire surface
|
|
1186
1191
|
// is invisible to them. The integrator can still set
|
|
1187
1192
|
// `clientTools: []` or `clientTools: undefined` explicitly to
|
|
1188
|
-
// strip them on purpose
|
|
1193
|
+
// strip them on purpose: we only fall back when the field is
|
|
1189
1194
|
// entirely absent from the returned object.
|
|
1190
1195
|
if (
|
|
1191
1196
|
payload.clientTools !== undefined &&
|
|
@@ -1258,7 +1263,7 @@ export class AgentWidgetClient {
|
|
|
1258
1263
|
createNewAssistant(result.partId);
|
|
1259
1264
|
}
|
|
1260
1265
|
|
|
1261
|
-
// Update partId tracking (only when partId is provided
|
|
1266
|
+
// Update partId tracking (only when partId is provided: backward compatible)
|
|
1262
1267
|
if (result.partId !== undefined) {
|
|
1263
1268
|
partIdState.current = result.partId;
|
|
1264
1269
|
}
|
|
@@ -1381,7 +1386,7 @@ export class AgentWidgetClient {
|
|
|
1381
1386
|
// Messages produced by steps inside a nested flow executed as a tool.
|
|
1382
1387
|
// Keyed by `${parentToolId}::${nestedStepId}::${partId}` so each nested
|
|
1383
1388
|
// step (send-stream, prompt) gets its own assistant message, and prompts
|
|
1384
|
-
// with inner tool calls split into one message per text segment
|
|
1389
|
+
// with inner tool calls split into one message per text segment: still
|
|
1385
1390
|
// attributable to the parent tool call.
|
|
1386
1391
|
const nestedStepMessages = new Map<string, AgentWidgetMessage>();
|
|
1387
1392
|
// Most-recent partId seen for a given `${toolId}::${stepId}` scope, used
|
|
@@ -1406,7 +1411,7 @@ export class AgentWidgetClient {
|
|
|
1406
1411
|
) => `${toolId}::${stepId}::${partId}`;
|
|
1407
1412
|
|
|
1408
1413
|
// Prefix used to sweep every nested message belonging to a single
|
|
1409
|
-
// (toolId, stepId) scope
|
|
1414
|
+
// (toolId, stepId) scope: needed on step_complete to seal any segments
|
|
1410
1415
|
// that are still streaming.
|
|
1411
1416
|
const getNestedStepPrefix = (toolId: string, stepId: string) =>
|
|
1412
1417
|
`${toolId}::${stepId}::`;
|
|
@@ -1728,7 +1733,7 @@ export class AgentWidgetClient {
|
|
|
1728
1733
|
const cur = msg.content ?? "";
|
|
1729
1734
|
if (mergedDisplay.trim() === "") return;
|
|
1730
1735
|
// Only replace when empty, or when the stream left a strict prefix of the
|
|
1731
|
-
// authoritative final (truncation). Do not use length alone
|
|
1736
|
+
// authoritative final (truncation). Do not use length alone: multi-segment
|
|
1732
1737
|
// flows can have a short last bubble whose content is not a prefix of the
|
|
1733
1738
|
// full step response.
|
|
1734
1739
|
if (
|
|
@@ -1810,7 +1815,7 @@ export class AgentWidgetClient {
|
|
|
1810
1815
|
// Declared here so scheduleReadyQueueDrain can reference it; assigned
|
|
1811
1816
|
// after all handler-scoped variables are initialised (before the SSE loop).
|
|
1812
1817
|
let drainReadyQueue: () => void;
|
|
1813
|
-
// Two drain paths
|
|
1818
|
+
// Two drain paths: both are intentional, do not remove either:
|
|
1814
1819
|
// 1. Microtask drain (scheduleReadyQueueDrain): required when the
|
|
1815
1820
|
// buffer's emitter fires from the gap-timeout setTimeout callback,
|
|
1816
1821
|
// because there is no surrounding synchronous drain site there.
|
|
@@ -1931,7 +1936,7 @@ export class AgentWidgetClient {
|
|
|
1931
1936
|
const toolId =
|
|
1932
1937
|
resolveToolId(payload, true) ?? `tool-${nextSequence()}`;
|
|
1933
1938
|
const toolName = payload.toolName ?? payload.name;
|
|
1934
|
-
// Suppress tool UI for artifact emit tools
|
|
1939
|
+
// Suppress tool UI for artifact emit tools: artifacts are handled via artifact_* events
|
|
1935
1940
|
if (isArtifactEmitToolName(toolName)) {
|
|
1936
1941
|
artifactToolCallIds.add(toolId);
|
|
1937
1942
|
continue;
|
|
@@ -2056,7 +2061,7 @@ export class AgentWidgetClient {
|
|
|
2056
2061
|
// Key the message by the per-call `toolCallId` (provider `toolu_…`;
|
|
2057
2062
|
// core#3878) when present. Two PARALLEL calls to the SAME tool in one
|
|
2058
2063
|
// turn collapse to an identical `toolId` (`runtime_webmcp:<name>_<ms>`)
|
|
2059
|
-
// and `index: 0
|
|
2064
|
+
// and `index: 0`: only `toolCallId` distinguishes them. Keying on it
|
|
2060
2065
|
// (a) keeps the two awaits as DISTINCT messages with their own args
|
|
2061
2066
|
// instead of the second clobbering the first, and (b) merges each
|
|
2062
2067
|
// await into the matching `tool_start` bubble (also keyed by
|
|
@@ -2105,7 +2110,7 @@ export class AgentWidgetClient {
|
|
|
2105
2110
|
emitMessage(toolMessage);
|
|
2106
2111
|
} else if (payloadType === "text_start") {
|
|
2107
2112
|
// Lifecycle event: a new text segment is beginning (emitted at tool boundaries).
|
|
2108
|
-
// When toolContext is present this fired inside a nested flow
|
|
2113
|
+
// When toolContext is present this fired inside a nested flow: it must not
|
|
2109
2114
|
// seal or rotate the outer assistant message. Nested prompt segmentation is
|
|
2110
2115
|
// handled via nestedStepMessages keyed by (toolId, stepId).
|
|
2111
2116
|
if ((payload as any).toolContext?.toolId) {
|
|
@@ -2127,7 +2132,7 @@ export class AgentWidgetClient {
|
|
|
2127
2132
|
}
|
|
2128
2133
|
} else if (payloadType === "text_end") {
|
|
2129
2134
|
// Lifecycle event: current text segment ended (tool call about to start).
|
|
2130
|
-
// When toolContext is present the boundary belongs to a nested flow
|
|
2135
|
+
// When toolContext is present the boundary belongs to a nested flow: leave
|
|
2131
2136
|
// outer assistant state alone so the outer stream is never interrupted by
|
|
2132
2137
|
// nested activity.
|
|
2133
2138
|
if ((payload as any).toolContext?.toolId) {
|
|
@@ -2155,7 +2160,7 @@ export class AgentWidgetClient {
|
|
|
2155
2160
|
// originated inside a nested flow executed as a tool. Surface it as
|
|
2156
2161
|
// its own assistant message keyed by the nested step id, so authors
|
|
2157
2162
|
// who add send-stream / prompt steps inside their flow see them as
|
|
2158
|
-
// real messages in the timeline, in order
|
|
2163
|
+
// real messages in the timeline, in order: rather than merging
|
|
2159
2164
|
// into the outer assistant bubble or getting buried in the tool
|
|
2160
2165
|
// card. Each nested step id gets its own message; the parent tool
|
|
2161
2166
|
// bubble continues to represent the invocation via tool_* events.
|
|
@@ -2304,7 +2309,7 @@ export class AgentWidgetClient {
|
|
|
2304
2309
|
const text = typeof result === 'string' ? result : result?.text ?? null;
|
|
2305
2310
|
|
|
2306
2311
|
if (text !== null && text.trim() !== "") {
|
|
2307
|
-
// Parser successfully extracted text
|
|
2312
|
+
// Parser successfully extracted text: update the chunk's assistant
|
|
2308
2313
|
// (not assistantMessage; text_end may have cleared that ref before microtasks run)
|
|
2309
2314
|
assistant.content = text;
|
|
2310
2315
|
emitMessage(assistant);
|
|
@@ -2478,14 +2483,14 @@ export class AgentWidgetClient {
|
|
|
2478
2483
|
|
|
2479
2484
|
// Capture optional per-step stopReason emitted by the runtime
|
|
2480
2485
|
// (e.g. `'max_tool_calls'`, `'length'`). This is the dispatch-mode
|
|
2481
|
-
// fallback
|
|
2486
|
+
// fallback: `agent_turn_complete` will overwrite it later in
|
|
2482
2487
|
// agent-loop streams.
|
|
2483
2488
|
const stepStopReason = (payload as any).stopReason as
|
|
2484
2489
|
| StopReasonKind
|
|
2485
2490
|
| undefined;
|
|
2486
2491
|
|
|
2487
2492
|
if (didSplitByPartId) {
|
|
2488
|
-
// Sealed segment(s)
|
|
2493
|
+
// Sealed segment(s): do not create a second bubble from step_complete.
|
|
2489
2494
|
// Merge authoritative final response into the last sealed segment (fixes async lag).
|
|
2490
2495
|
if (assistantMessage !== null) {
|
|
2491
2496
|
const msg: AgentWidgetMessage = assistantMessage;
|
|
@@ -2635,7 +2640,7 @@ export class AgentWidgetClient {
|
|
|
2635
2640
|
} else if (payloadType === "flow_complete") {
|
|
2636
2641
|
const finalContent = payload.result?.response;
|
|
2637
2642
|
if (didSplitByPartId) {
|
|
2638
|
-
// Content was split into multiple assistant messages
|
|
2643
|
+
// Content was split into multiple assistant messages: the full response
|
|
2639
2644
|
// in flow_complete would overwrite the last segment. Just finalize streaming.
|
|
2640
2645
|
if (assistantMessage !== null) {
|
|
2641
2646
|
const msg: AgentWidgetMessage = assistantMessage;
|
|
@@ -2808,8 +2813,7 @@ export class AgentWidgetClient {
|
|
|
2808
2813
|
}
|
|
2809
2814
|
|
|
2810
2815
|
// Attach the turn-level stopReason to the assistant message
|
|
2811
|
-
// produced by this turn. Only overwrite the current message
|
|
2812
|
-
// prior turns already sealed their own stopReason via step_complete.
|
|
2816
|
+
// produced by this turn. Only overwrite the current message: // prior turns already sealed their own stopReason via step_complete.
|
|
2813
2817
|
// Falls back to lastAssistantInTurn when the current bubble was
|
|
2814
2818
|
// sealed at a tool-call boundary mid-turn, so the notice still
|
|
2815
2819
|
// attaches to the final visible text segment.
|
|
@@ -2895,7 +2899,7 @@ export class AgentWidgetClient {
|
|
|
2895
2899
|
} else if (payloadType === "agent_media") {
|
|
2896
2900
|
// A tool produced media (image / audio / video / file). Render it
|
|
2897
2901
|
// as a synthetic assistant message inserted at the point the tool
|
|
2898
|
-
// completed
|
|
2902
|
+
// completed: between the tool bubble and the next text turn.
|
|
2899
2903
|
//
|
|
2900
2904
|
// Wire format is the AI SDK–aligned `MediaContentPart` from
|
|
2901
2905
|
// @runtypelabs/shared:
|
|
@@ -2922,7 +2926,7 @@ export class AgentWidgetClient {
|
|
|
2922
2926
|
const data = typeof rec.data === "string" ? rec.data : undefined;
|
|
2923
2927
|
if (!data) continue;
|
|
2924
2928
|
// Empty/missing mediaType yields `data:;base64,...` which RFC 2397
|
|
2925
|
-
// resolves to `text/plain
|
|
2929
|
+
// resolves to `text/plain`: stamp a default so the data URI is
|
|
2926
2930
|
// well-formed and the part lands in the file bucket.
|
|
2927
2931
|
mediaType = rawMediaType.length > 0 ? rawMediaType : "application/octet-stream";
|
|
2928
2932
|
src = `data:${mediaType};base64,${data}`;
|
|
@@ -2999,7 +3003,7 @@ export class AgentWidgetClient {
|
|
|
2999
3003
|
|
|
3000
3004
|
// Seal any in-flight assistant text bubble before splitting the
|
|
3001
3005
|
// stream. Without this, an orphan bubble retains `streaming: true`
|
|
3002
|
-
// forever
|
|
3006
|
+
// forever: `agent_complete` only finalizes the latest
|
|
3003
3007
|
// `assistantMessage`, so the typing/caret indicator would stay on
|
|
3004
3008
|
// the prior bubble even though no more deltas will arrive.
|
|
3005
3009
|
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"
|