@runtypelabs/persona 4.8.0 → 4.10.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 +14 -1
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-BsZtXPKK.d.cts → types-4ROVJ1gA.d.cts} +42 -0
- package/dist/animations/{types-BsZtXPKK.d.ts → types-4ROVJ1gA.d.ts} +42 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/chunk-IO5VVUKP.js +3 -0
- package/dist/chunk-IPVK3KOM.js +1 -0
- package/dist/chunk-UPO4GUFC.js +1 -0
- package/dist/codegen.cjs +6 -6
- package/dist/codegen.js +8 -8
- package/dist/context-mentions-7S5KVUTG.js +169 -0
- package/dist/context-mentions-inline-TTCN7ZM2.js +4 -0
- package/dist/context-mentions-inline.cjs +4 -0
- package/dist/context-mentions-inline.d.cts +203 -0
- package/dist/context-mentions-inline.d.ts +203 -0
- package/dist/context-mentions-inline.js +4 -0
- package/dist/context-mentions.cjs +295 -0
- package/dist/context-mentions.d.cts +7025 -0
- package/dist/context-mentions.d.ts +7025 -0
- package/dist/context-mentions.js +295 -0
- package/dist/index.cjs +72 -64
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +634 -3
- package/dist/index.d.ts +634 -3
- package/dist/index.global.js +59 -51
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +64 -56
- package/dist/index.js.map +1 -1
- package/dist/launcher.global.js +2 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/plugin-kit.cjs +1 -1
- package/dist/plugin-kit.d.cts +17 -0
- package/dist/plugin-kit.d.ts +17 -0
- package/dist/plugin-kit.js +1 -1
- package/dist/smart-dom-reader.cjs +17 -16
- package/dist/smart-dom-reader.d.cts +507 -1
- package/dist/smart-dom-reader.d.ts +507 -1
- package/dist/smart-dom-reader.js +17 -16
- package/dist/theme-editor-preview.cjs +236 -57
- package/dist/theme-editor-preview.d.cts +485 -1
- package/dist/theme-editor-preview.d.ts +485 -1
- package/dist/theme-editor-preview.js +53 -47
- package/dist/theme-editor.cjs +7 -7
- package/dist/theme-editor.d.cts +473 -0
- package/dist/theme-editor.d.ts +473 -0
- package/dist/theme-editor.js +5 -5
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +2 -0
- package/dist/theme-reference.d.ts +2 -0
- package/dist/widget.css +1 -1
- package/package.json +15 -3
- package/src/client.test.ts +69 -0
- package/src/client.ts +65 -51
- package/src/components/artifact-pane.test.ts +47 -0
- package/src/components/artifact-pane.ts +25 -2
- package/src/components/composer-parts.ts +3 -12
- package/src/components/context-mention-button.test.ts +70 -0
- package/src/components/context-mention-button.ts +82 -0
- package/src/components/context-mention-chip.ts +134 -0
- package/src/components/context-mention-menu.test.ts +508 -0
- package/src/components/context-mention-menu.ts +0 -0
- package/src/components/message-bubble.test.ts +175 -0
- package/src/components/message-bubble.ts +177 -19
- package/src/components/panel.ts +7 -10
- package/src/context-mentions-bundle.test.ts +163 -0
- package/src/context-mentions-entry.ts +185 -0
- package/src/context-mentions-inline-entry.test.ts +136 -0
- package/src/context-mentions-inline-entry.ts +226 -0
- package/src/context-mentions-inline-loader.test.ts +30 -0
- package/src/context-mentions-inline-loader.ts +36 -0
- package/src/context-mentions-inline.ts +15 -0
- package/src/context-mentions-loader.ts +32 -0
- package/src/context-mentions.ts +16 -0
- package/src/index-core.ts +27 -0
- package/src/index-global.ts +51 -0
- package/src/markdown-parsers-loader.ts +35 -44
- package/src/plugin-kit.test.ts +40 -0
- package/src/plugin-kit.ts +39 -5
- package/src/runtime/init-update-reset.test.ts +81 -0
- package/src/runtime/init.test.ts +62 -0
- package/src/runtime/init.ts +7 -14
- package/src/session.mentions.test.ts +175 -0
- package/src/session.test.ts +52 -4
- package/src/session.ts +121 -5
- package/src/smart-dom-reader.test.ts +129 -2
- package/src/smart-dom-reader.ts +127 -1
- package/src/styles/context-mention-menu-css.ts +176 -0
- package/src/styles/widget.css +243 -126
- package/src/theme-editor/sections.ts +3 -3
- package/src/types/theme.ts +2 -0
- package/src/types.ts +542 -0
- package/src/ui.artifact-pane-gating.test.ts +11 -1
- package/src/ui.attachments-drop.test.ts +90 -0
- package/src/ui.header-update-stability.test.ts +149 -0
- package/src/ui.launcher-update-merge.test.ts +83 -0
- package/src/ui.mention-submit.test.ts +235 -0
- package/src/ui.send-button-stream-update.test.ts +69 -0
- package/src/ui.ts +379 -84
- package/src/utils/chunk-loader.test.ts +97 -0
- package/src/utils/chunk-loader.ts +88 -0
- package/src/utils/composer-contenteditable.test.ts +507 -0
- package/src/utils/composer-contenteditable.ts +626 -0
- package/src/utils/composer-document.test.ts +280 -0
- package/src/utils/composer-document.ts +293 -0
- package/src/utils/composer-history.test.ts +35 -7
- package/src/utils/composer-history.ts +30 -20
- package/src/utils/composer-input.ts +159 -0
- package/src/utils/config-merge.test.ts +131 -0
- package/src/utils/config-merge.ts +61 -0
- package/src/utils/context-mention-controller.test.ts +1215 -0
- package/src/utils/context-mention-controller.ts +1186 -0
- package/src/utils/context-mention-manager.test.ts +422 -0
- package/src/utils/context-mention-manager.ts +410 -0
- package/src/utils/context-mention-orchestrator.test.ts +538 -0
- package/src/utils/context-mention-orchestrator.ts +348 -0
- package/src/utils/live-region.test.ts +108 -0
- package/src/utils/live-region.ts +94 -0
- package/src/utils/mention-channels.ts +63 -0
- package/src/utils/mention-llm-format.test.ts +91 -0
- package/src/utils/mention-llm-format.ts +79 -0
- package/src/utils/mention-matcher.test.ts +86 -0
- package/src/utils/mention-matcher.ts +221 -0
- package/src/utils/mention-token.ts +72 -0
- package/src/utils/mention-trigger.test.ts +155 -0
- package/src/utils/mention-trigger.ts +156 -0
- package/src/utils/theme.test.ts +54 -4
- package/src/utils/theme.ts +6 -3
- package/src/utils/tokens.ts +27 -11
package/src/ui.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { onMarkdownParsersReady, getMarkdownParsersSync } from "./markdown-parse
|
|
|
5
5
|
import { AgentWidgetSession, AgentWidgetSessionStatus } from "./session";
|
|
6
6
|
import {
|
|
7
7
|
AgentWidgetConfig,
|
|
8
|
+
AgentWidgetConfigPatch,
|
|
8
9
|
AgentWidgetApprovalDecisionOptions,
|
|
9
10
|
AgentWidgetMessage,
|
|
10
11
|
AgentWidgetEvent,
|
|
@@ -30,9 +31,16 @@ import {
|
|
|
30
31
|
PersonaArtifactRecord,
|
|
31
32
|
PersonaArtifactManualUpsert,
|
|
32
33
|
PersonaArtifactFileMeta,
|
|
33
|
-
PersonaArtifactActionContext
|
|
34
|
+
PersonaArtifactActionContext,
|
|
35
|
+
AgentWidgetContentSegment,
|
|
36
|
+
AgentWidgetContextMentionRef
|
|
34
37
|
} from "./types";
|
|
35
38
|
import { AttachmentManager } from "./utils/attachment-manager";
|
|
39
|
+
import {
|
|
40
|
+
createContextMentionOrchestrator,
|
|
41
|
+
type ContextMentionOrchestrator,
|
|
42
|
+
} from "./utils/context-mention-orchestrator";
|
|
43
|
+
import type { MentionSubmitBundle } from "./utils/context-mention-manager";
|
|
36
44
|
import { createTextPart, ALL_SUPPORTED_MIME_TYPES } from "./utils/content";
|
|
37
45
|
import { applyThemeVariables, createThemeObserver, getActiveTheme } from "./utils/theme";
|
|
38
46
|
import { resolveTokenValue } from "./utils/tokens";
|
|
@@ -126,6 +134,7 @@ import { readFlexGapPx, resolveArtifactPaneWidthPx } from "./utils/artifact-resi
|
|
|
126
134
|
import { enhanceWithForms } from "./components/forms";
|
|
127
135
|
import { pluginRegistry } from "./plugins/registry";
|
|
128
136
|
import { mergeWithDefaults, DEFAULT_FLOATING_LAUNCHER_WIDTH } from "./defaults";
|
|
137
|
+
import { mergeConfigUpdate } from "./utils/config-merge";
|
|
129
138
|
import { createEventBus } from "./utils/events";
|
|
130
139
|
import {
|
|
131
140
|
createActionManager,
|
|
@@ -298,7 +307,7 @@ const stripStreamingFromMessages = (messages: AgentWidgetMessage[]) =>
|
|
|
298
307
|
}));
|
|
299
308
|
|
|
300
309
|
type Controller = {
|
|
301
|
-
update: (config:
|
|
310
|
+
update: (config: AgentWidgetConfigPatch) => void;
|
|
302
311
|
destroy: () => void;
|
|
303
312
|
open: () => void;
|
|
304
313
|
close: () => void;
|
|
@@ -535,6 +544,35 @@ function buildDropOverlay(
|
|
|
535
544
|
return overlay;
|
|
536
545
|
}
|
|
537
546
|
|
|
547
|
+
// Deep-merge two mention context maps (`{ [sourceId]: { [itemId]: unknown } }`).
|
|
548
|
+
// A source contributing to both bundles keeps BOTH per-item maps; a shallow
|
|
549
|
+
// spread would let one sourceId's inner map wholesale replace the other's.
|
|
550
|
+
export const mergeMentionContext = (
|
|
551
|
+
a: MentionSubmitBundle["context"],
|
|
552
|
+
b: MentionSubmitBundle["context"]
|
|
553
|
+
): MentionSubmitBundle["context"] => {
|
|
554
|
+
const out: MentionSubmitBundle["context"] = { ...a };
|
|
555
|
+
for (const [sourceId, items] of Object.entries(b)) {
|
|
556
|
+
const existing = out[sourceId];
|
|
557
|
+
out[sourceId] = existing ? { ...existing, ...items } : items;
|
|
558
|
+
}
|
|
559
|
+
return out;
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
// Concatenate the fulfilled halves of two finalize() results (either may be
|
|
563
|
+
// absent when its side rejected), deep-merging their per-source context.
|
|
564
|
+
export const mergeFinalizedMentions = (
|
|
565
|
+
bundles: MentionSubmitBundle[]
|
|
566
|
+
): MentionSubmitBundle =>
|
|
567
|
+
bundles.reduce<MentionSubmitBundle>(
|
|
568
|
+
(acc, b) => ({
|
|
569
|
+
blocks: [...acc.blocks, ...b.blocks],
|
|
570
|
+
contentParts: [...acc.contentParts, ...b.contentParts],
|
|
571
|
+
context: mergeMentionContext(acc.context, b.context),
|
|
572
|
+
}),
|
|
573
|
+
{ blocks: [], contentParts: [], context: {} }
|
|
574
|
+
);
|
|
575
|
+
|
|
538
576
|
export const createAgentExperience = (
|
|
539
577
|
mount: HTMLElement,
|
|
540
578
|
initialConfig?: AgentWidgetConfig,
|
|
@@ -840,9 +878,13 @@ export const createAgentExperience = (
|
|
|
840
878
|
header,
|
|
841
879
|
footer,
|
|
842
880
|
actionsRow: _actionsRow,
|
|
843
|
-
leftActions,
|
|
844
881
|
rightActions
|
|
845
882
|
} = panelElements;
|
|
883
|
+
// Nullable + reassignable: a plugin `replaceComposer` swaps the footer, and
|
|
884
|
+
// `bindComposerRefsFromFooter` must repoint this at the NEW left-action
|
|
885
|
+
// cluster (or null it so the composerForm fallback fires) — a stale ref would
|
|
886
|
+
// insert the mention/attachment buttons into the detached old subtree.
|
|
887
|
+
let leftActions: HTMLElement | null = panelElements.leftActions;
|
|
846
888
|
let setSendButtonMode = panelElements.setSendButtonMode;
|
|
847
889
|
|
|
848
890
|
// Use mutable references for mic button so we can update them dynamically
|
|
@@ -981,6 +1023,9 @@ export const createAgentExperience = (
|
|
|
981
1023
|
|
|
982
1024
|
// Initialized after composer plugins rebind footer DOM (see `bindComposerRefsFromFooter`)
|
|
983
1025
|
let attachmentManager: AttachmentManager | null = null;
|
|
1026
|
+
// Context mentions orchestrator (core, tiny); lazy-loads the heavy runtime on
|
|
1027
|
+
// first use. Null when `contextMentions` is disabled / has no sources.
|
|
1028
|
+
let mentionOrchestrator: ContextMentionOrchestrator | null = null;
|
|
984
1029
|
|
|
985
1030
|
/** Wired after `handleMicButtonClick` is defined; used by `renderComposer` `onVoiceToggle`. */
|
|
986
1031
|
let composerVoiceBridge: (() => void) | null = null;
|
|
@@ -1251,6 +1296,10 @@ export const createAgentExperience = (
|
|
|
1251
1296
|
".persona-widget-composer .persona-flex.persona-items-center.persona-justify-between"
|
|
1252
1297
|
);
|
|
1253
1298
|
if (ar) _actionsRow = ar;
|
|
1299
|
+
// Rebind the left-action cluster to the NEW footer (both composer builders
|
|
1300
|
+
// ship this class). Assign unconditionally: when a custom plugin composer
|
|
1301
|
+
// has no left cluster, null lets the button-insert fall back to the form.
|
|
1302
|
+
leftActions = pick<HTMLElement>(".persona-widget-composer__left-actions");
|
|
1254
1303
|
};
|
|
1255
1304
|
ensureComposerAttachmentSurface(footer);
|
|
1256
1305
|
bindComposerRefsFromFooter(footer);
|
|
@@ -1303,6 +1352,49 @@ export const createAgentExperience = (
|
|
|
1303
1352
|
container.appendChild(overlay);
|
|
1304
1353
|
}
|
|
1305
1354
|
|
|
1355
|
+
// Context mentions: render the affordance button + chip row eagerly (so the
|
|
1356
|
+
// feature is discoverable) and lazy-load the heavy runtime on first use. The
|
|
1357
|
+
// orchestrator returns null when disabled or sourceless.
|
|
1358
|
+
const mentionPrefetch = () => mentionOrchestrator?.prefetch();
|
|
1359
|
+
if (config.contextMentions?.enabled && textarea) {
|
|
1360
|
+
// Slash-command dispatch (prompt macros write text / submit; client actions
|
|
1361
|
+
// read/replace the value) and submission are owned by the composer input
|
|
1362
|
+
// surface itself — the mention runtime builds a textarea (chip) or
|
|
1363
|
+
// contenteditable (inline) adapter from this textarea. Broader host actions
|
|
1364
|
+
// (clear transcript, theme) are still wired via closures over the controller.
|
|
1365
|
+
mentionOrchestrator = createContextMentionOrchestrator({
|
|
1366
|
+
config,
|
|
1367
|
+
textarea,
|
|
1368
|
+
anchor: composerForm ?? textarea,
|
|
1369
|
+
getMessages: () => session.getMessages(),
|
|
1370
|
+
// The engine chunk creates the mention live regions in this container on
|
|
1371
|
+
// mount, keeping the live-region helper out of the core bundle.
|
|
1372
|
+
liveRegionHost: container,
|
|
1373
|
+
});
|
|
1374
|
+
|
|
1375
|
+
if (mentionOrchestrator) {
|
|
1376
|
+
// Chip row sits directly above the textarea.
|
|
1377
|
+
const ta = textarea;
|
|
1378
|
+
ta.parentElement?.insertBefore(mentionOrchestrator.contextRow, ta);
|
|
1379
|
+
// Each channel's "add context" affordance is a secondary control that
|
|
1380
|
+
// augments the outgoing message, so it joins the LEFT action cluster
|
|
1381
|
+
// (beside the attachment button) — never the right cluster with mic +
|
|
1382
|
+
// send. Buttons are inserted leftmost in channel order so they read as
|
|
1383
|
+
// "add to my message" and stay clear of the primary send action. Both
|
|
1384
|
+
// composer builders (full + pill) always ship `leftActions`; the form
|
|
1385
|
+
// fallback is purely defensive.
|
|
1386
|
+
const buttons = mentionOrchestrator.affordanceButtons;
|
|
1387
|
+
for (let i = buttons.length - 1; i >= 0; i--) {
|
|
1388
|
+
const btn = buttons[i];
|
|
1389
|
+
if (leftActions) leftActions.insertBefore(btn, leftActions.firstChild);
|
|
1390
|
+
else composerForm?.appendChild(btn);
|
|
1391
|
+
}
|
|
1392
|
+
// The focus-prefetch listener (warm the chunk on first focus so the first
|
|
1393
|
+
// `@` is instant) is registered through the shared `composerListeners`
|
|
1394
|
+
// registry below, so it survives the inline contenteditable swap too.
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1306
1398
|
// Slot system: allow custom content injection into specific regions
|
|
1307
1399
|
const renderSlots = () => {
|
|
1308
1400
|
const slots = config.layout?.slots ?? {};
|
|
@@ -6174,26 +6266,93 @@ export const createAgentExperience = (
|
|
|
6174
6266
|
setOpenState(true, "auto");
|
|
6175
6267
|
};
|
|
6176
6268
|
|
|
6177
|
-
|
|
6178
|
-
|
|
6269
|
+
// Combine `@`-chip mentions with an inline server-command's context bundle so
|
|
6270
|
+
// both reach the message in one `mentions` payload. Either side may be null.
|
|
6271
|
+
type SubmitMentions = NonNullable<
|
|
6272
|
+
ReturnType<NonNullable<typeof mentionOrchestrator>["collectForSubmit"]>
|
|
6273
|
+
>;
|
|
6274
|
+
const mergeSubmitMentions = (
|
|
6275
|
+
a: SubmitMentions | null,
|
|
6276
|
+
b: SubmitMentions | null
|
|
6277
|
+
): SubmitMentions | null => {
|
|
6278
|
+
if (!a) return b;
|
|
6279
|
+
if (!b) return a;
|
|
6280
|
+
return {
|
|
6281
|
+
refs: [...a.refs, ...b.refs],
|
|
6282
|
+
// allSettled, not all: one side rejecting must not discard the other
|
|
6283
|
+
// side's already-resolved context (the bubble already echoed its chips).
|
|
6284
|
+
finalize: async () => {
|
|
6285
|
+
const [ra, rb] = await Promise.allSettled([a.finalize(), b.finalize()]);
|
|
6286
|
+
const fulfilled: MentionSubmitBundle[] = [];
|
|
6287
|
+
if (ra.status === "fulfilled") fulfilled.push(ra.value);
|
|
6288
|
+
else console.warn("[Persona] a mention bundle failed to finalize; sending without it", ra.reason);
|
|
6289
|
+
if (rb.status === "fulfilled") fulfilled.push(rb.value);
|
|
6290
|
+
else console.warn("[Persona] a mention bundle failed to finalize; sending without it", rb.reason);
|
|
6291
|
+
return mergeFinalizedMentions(fulfilled);
|
|
6292
|
+
},
|
|
6293
|
+
};
|
|
6294
|
+
};
|
|
6179
6295
|
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6296
|
+
// Inline-mode composer element: after the contenteditable swap, `textarea`
|
|
6297
|
+
// additionally exposes `getInlineMessageFields()` (built in the inline chunk,
|
|
6298
|
+
// which owns the document model — the core never imports it). Structural shape
|
|
6299
|
+
// only, so no runtime import crosses the bundle boundary.
|
|
6300
|
+
type InlineComposerFieldsHost = {
|
|
6301
|
+
getInlineMessageFields?: () => {
|
|
6302
|
+
content: string;
|
|
6303
|
+
contextMentions: AgentWidgetContextMentionRef[];
|
|
6304
|
+
contentSegments: AgentWidgetContentSegment[];
|
|
6305
|
+
};
|
|
6306
|
+
};
|
|
6307
|
+
// Read the ordered display segments from the live inline composer, but only
|
|
6308
|
+
// when at least one mention is present: a plain-text inline message must keep
|
|
6309
|
+
// the normal markdown-rendered bubble (segments bypass that path), so we return
|
|
6310
|
+
// undefined for pure prose and let the bubble render `content` as usual.
|
|
6311
|
+
const readInlineContentSegments = (): AgentWidgetContentSegment[] | undefined => {
|
|
6312
|
+
const host = textarea as unknown as InlineComposerFieldsHost;
|
|
6313
|
+
const fields = host.getInlineMessageFields?.();
|
|
6314
|
+
if (!fields) return undefined;
|
|
6315
|
+
const hasMention = fields.contentSegments.some((s) => s.kind === "mention");
|
|
6316
|
+
return hasMention ? fields.contentSegments : undefined;
|
|
6317
|
+
};
|
|
6318
|
+
|
|
6319
|
+
// Re-entrancy guard for the async submit path: `takeInlineCommand` awaits (a
|
|
6320
|
+
// lazy chunk load + host resolve) before the composer clears, and
|
|
6321
|
+
// `isStreaming()` is still false in that window, so a second Enter would
|
|
6322
|
+
// otherwise dispatch the same text/command twice.
|
|
6323
|
+
let submitInFlight = false;
|
|
6191
6324
|
|
|
6325
|
+
const performSubmit = async (submitOptions?: { viaVoice?: boolean }) => {
|
|
6192
6326
|
const value = textarea.value.trim();
|
|
6193
6327
|
const hasAttachments = attachmentManager?.hasAttachments() ?? false;
|
|
6194
6328
|
|
|
6195
|
-
//
|
|
6196
|
-
|
|
6329
|
+
// Inline slash command (Slack-style): every `command:"server"` plus any
|
|
6330
|
+
// arg-bearing prompt/action. Resolve FIRST — a prompt command changes the
|
|
6331
|
+
// text to send, and an action sends nothing at all.
|
|
6332
|
+
const inline = value
|
|
6333
|
+
? await (mentionOrchestrator?.takeInlineCommand(value) ?? Promise.resolve(null))
|
|
6334
|
+
: null;
|
|
6335
|
+
|
|
6336
|
+
if (inline?.kind === "action") {
|
|
6337
|
+
// Ran in the browser; nothing to send. Clear the composer + any chips.
|
|
6338
|
+
textarea.value = "";
|
|
6339
|
+
textarea.style.height = "auto";
|
|
6340
|
+
resetHistoryNavigation();
|
|
6341
|
+
mentionOrchestrator?.clear();
|
|
6342
|
+
return;
|
|
6343
|
+
}
|
|
6344
|
+
|
|
6345
|
+
// Gather `@`-chip mentions synchronously (detaches chips + captures composer
|
|
6346
|
+
// text before clearing); `finalize()` resolves them inside `sendMessage`.
|
|
6347
|
+
const chipMentions = mentionOrchestrator?.collectForSubmit() ?? null;
|
|
6348
|
+
const serverMentions = inline?.kind === "server" ? inline.mentions : null;
|
|
6349
|
+
const mentions = mergeSubmitMentions(chipMentions, serverMentions);
|
|
6350
|
+
// A prompt command replaces the outgoing text with its resolved macro.
|
|
6351
|
+
const sendText = inline?.kind === "prompt" ? inline.sendText : value;
|
|
6352
|
+
|
|
6353
|
+
const hasChips = !!chipMentions && chipMentions.refs.length > 0;
|
|
6354
|
+
// Must have text, attachments, chips, or an inline server command's context.
|
|
6355
|
+
if (!sendText && !hasAttachments && !hasChips && !serverMentions) return;
|
|
6197
6356
|
|
|
6198
6357
|
maybeExpandComposerBar();
|
|
6199
6358
|
|
|
@@ -6204,22 +6363,65 @@ export const createAgentExperience = (
|
|
|
6204
6363
|
// Add image parts first
|
|
6205
6364
|
contentParts.push(...attachmentManager!.getContentParts());
|
|
6206
6365
|
// Add text part if there's text
|
|
6207
|
-
if (
|
|
6208
|
-
contentParts.push(createTextPart(
|
|
6366
|
+
if (sendText) {
|
|
6367
|
+
contentParts.push(createTextPart(sendText));
|
|
6209
6368
|
}
|
|
6210
6369
|
}
|
|
6211
6370
|
|
|
6371
|
+
// Capture the inline display segments BEFORE clearing the composer (clearing
|
|
6372
|
+
// rebuilds the document as empty text). Only set when a `prompt` macro didn't
|
|
6373
|
+
// replace the outgoing text — a macro's segments no longer match `sendText`.
|
|
6374
|
+
const contentSegments =
|
|
6375
|
+
inline?.kind === "prompt" ? undefined : readInlineContentSegments();
|
|
6376
|
+
|
|
6212
6377
|
textarea.value = "";
|
|
6213
6378
|
textarea.style.height = "auto"; // Reset height after clearing
|
|
6214
6379
|
resetHistoryNavigation();
|
|
6215
6380
|
|
|
6216
|
-
// Send message with optional content parts
|
|
6217
|
-
session.sendMessage(
|
|
6381
|
+
// Send message with optional content parts + mentions
|
|
6382
|
+
session.sendMessage(sendText, {
|
|
6383
|
+
contentParts,
|
|
6384
|
+
mentions: mentions ?? undefined,
|
|
6385
|
+
contentSegments,
|
|
6386
|
+
viaVoice: submitOptions?.viaVoice,
|
|
6387
|
+
});
|
|
6218
6388
|
|
|
6219
|
-
// Clear attachments after sending
|
|
6389
|
+
// Clear attachments + mention chips after sending
|
|
6220
6390
|
if (hasAttachments) {
|
|
6221
6391
|
attachmentManager!.clearAttachments();
|
|
6222
6392
|
}
|
|
6393
|
+
if (chipMentions) {
|
|
6394
|
+
mentionOrchestrator?.clear();
|
|
6395
|
+
}
|
|
6396
|
+
};
|
|
6397
|
+
|
|
6398
|
+
const doSubmit = async (submitOptions?: { viaVoice?: boolean }) => {
|
|
6399
|
+
if (submitInFlight) return;
|
|
6400
|
+
submitInFlight = true;
|
|
6401
|
+
try {
|
|
6402
|
+
await performSubmit(submitOptions);
|
|
6403
|
+
} finally {
|
|
6404
|
+
submitInFlight = false;
|
|
6405
|
+
}
|
|
6406
|
+
};
|
|
6407
|
+
|
|
6408
|
+
const handleSubmit = (event: Event) => {
|
|
6409
|
+
event.preventDefault();
|
|
6410
|
+
|
|
6411
|
+
// While a response is streaming, the submit button acts as a stop button.
|
|
6412
|
+
// Abort the in-flight stream and leave textarea contents / attachments
|
|
6413
|
+
// intact so the user can edit and resend without retyping.
|
|
6414
|
+
if (session.isStreaming()) {
|
|
6415
|
+
session.cancel();
|
|
6416
|
+
// Cancelling emits no terminal/error SSE frame, so reset the throughput
|
|
6417
|
+
// tracker (as clear-chat does) to avoid a stale `running` row lingering.
|
|
6418
|
+
throughputTracker?.reset();
|
|
6419
|
+
eventStreamView?.update();
|
|
6420
|
+
return;
|
|
6421
|
+
}
|
|
6422
|
+
|
|
6423
|
+
if (submitInFlight) return;
|
|
6424
|
+
void doSubmit();
|
|
6223
6425
|
};
|
|
6224
6426
|
|
|
6225
6427
|
// --- Composer message-history navigation (Up/Down arrows) ---
|
|
@@ -6258,15 +6460,30 @@ export const createAgentExperience = (
|
|
|
6258
6460
|
textarea.setSelectionRange(end, end);
|
|
6259
6461
|
};
|
|
6260
6462
|
|
|
6261
|
-
const handleComposerInput = () => {
|
|
6262
|
-
//
|
|
6463
|
+
const handleComposerInput = (event: Event) => {
|
|
6464
|
+
// The synthetic input from history recall (guarded by `suppressHistoryReset`)
|
|
6465
|
+
// is not user typing: it must neither open the mention menu (an undefined
|
|
6466
|
+
// inputType reads as menu-opening) nor exit history-navigation mode.
|
|
6263
6467
|
if (suppressHistoryReset) return;
|
|
6468
|
+
// Drive the mention menu (open/update/close) + lazy-load on first trigger.
|
|
6469
|
+
// Skip while an IME composition is active: the intermediate value isn't the
|
|
6470
|
+
// user's committed text, so it must not open or filter the menu.
|
|
6471
|
+
if (!(event as InputEvent).isComposing) {
|
|
6472
|
+
mentionOrchestrator?.handleInput((event as InputEvent).inputType ?? undefined);
|
|
6473
|
+
}
|
|
6474
|
+
// A real edit leaves history-navigation mode.
|
|
6264
6475
|
resetHistoryNavigation();
|
|
6265
6476
|
};
|
|
6266
6477
|
|
|
6267
6478
|
const handleComposerKeydown = (event: KeyboardEvent) => {
|
|
6268
6479
|
if (!textarea) return;
|
|
6269
6480
|
|
|
6481
|
+
// Mention menu takes precedence when open (↑/↓ nav, Enter/Tab select, Esc
|
|
6482
|
+
// close) and handles Backspace-removes-last-chip on an empty composer. One
|
|
6483
|
+
// handler, no competing capture-phase listener. Skip during IME composition
|
|
6484
|
+
// so Enter-to-confirm-composition isn't swallowed as a menu selection.
|
|
6485
|
+
if (!event.isComposing && mentionOrchestrator?.handleKeydown(event)) return;
|
|
6486
|
+
|
|
6270
6487
|
// Up/Down: walk through previously sent user messages.
|
|
6271
6488
|
if (
|
|
6272
6489
|
historyNavigationEnabled() &&
|
|
@@ -6304,6 +6521,12 @@ export const createAgentExperience = (
|
|
|
6304
6521
|
event.preventDefault();
|
|
6305
6522
|
return;
|
|
6306
6523
|
}
|
|
6524
|
+
// A submit is already awaiting its async pre-send work; a second Enter in
|
|
6525
|
+
// that window would dispatch the same text twice.
|
|
6526
|
+
if (submitInFlight) {
|
|
6527
|
+
event.preventDefault();
|
|
6528
|
+
return;
|
|
6529
|
+
}
|
|
6307
6530
|
resetHistoryNavigation();
|
|
6308
6531
|
event.preventDefault();
|
|
6309
6532
|
sendButton.click();
|
|
@@ -6409,9 +6632,11 @@ export const createAgentExperience = (
|
|
|
6409
6632
|
const finalValue = textarea.value.trim();
|
|
6410
6633
|
if (finalValue && speechRecognition && isRecording) {
|
|
6411
6634
|
stopVoiceRecognition();
|
|
6412
|
-
|
|
6413
|
-
|
|
6414
|
-
|
|
6635
|
+
// Route through the normal submit path so mentions are collected +
|
|
6636
|
+
// cleared like a manual send (the transcript already lives in the
|
|
6637
|
+
// composer). Sending directly would leave stale tracked mention
|
|
6638
|
+
// context to attach to the next unrelated message.
|
|
6639
|
+
void doSubmit({ viaVoice: true });
|
|
6415
6640
|
}
|
|
6416
6641
|
}, pauseDuration);
|
|
6417
6642
|
}
|
|
@@ -6429,9 +6654,9 @@ export const createAgentExperience = (
|
|
|
6429
6654
|
if (isRecording) {
|
|
6430
6655
|
const finalValue = textarea.value.trim();
|
|
6431
6656
|
if (finalValue && finalValue !== initialText.trim()) {
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
6657
|
+
// Route through the normal submit path (mentions collect + clear), same
|
|
6658
|
+
// as the pause-timer branch above.
|
|
6659
|
+
void doSubmit({ viaVoice: true });
|
|
6435
6660
|
}
|
|
6436
6661
|
stopVoiceRecognition();
|
|
6437
6662
|
}
|
|
@@ -7313,9 +7538,43 @@ export const createAgentExperience = (
|
|
|
7313
7538
|
if (composerForm) {
|
|
7314
7539
|
composerForm.addEventListener("submit", handleSubmit);
|
|
7315
7540
|
}
|
|
7316
|
-
|
|
7317
|
-
|
|
7318
|
-
|
|
7541
|
+
|
|
7542
|
+
// Single registry of composer listeners. The initial attach and the inline
|
|
7543
|
+
// contenteditable swap-reattach both consume this array, so a listener added
|
|
7544
|
+
// here is mechanically included in both — no hand-maintained enumeration to
|
|
7545
|
+
// drift. Add new composer listeners here, not as ad-hoc addEventListener calls.
|
|
7546
|
+
type ComposerListener = [event: string, handler: (event: Event) => void];
|
|
7547
|
+
const composerListeners: ComposerListener[] = [
|
|
7548
|
+
["keydown", handleComposerKeydown as unknown as (event: Event) => void],
|
|
7549
|
+
["input", handleComposerInput as (event: Event) => void],
|
|
7550
|
+
["paste", handleInputPaste as unknown as (event: Event) => void],
|
|
7551
|
+
// Warm the mention chunk on first focus so the first `@` is instant.
|
|
7552
|
+
["focus", mentionPrefetch as (event: Event) => void],
|
|
7553
|
+
];
|
|
7554
|
+
const attachComposerListeners = (el: HTMLElement | null): void => {
|
|
7555
|
+
if (!el) return;
|
|
7556
|
+
for (const [event, handler] of composerListeners) {
|
|
7557
|
+
el.addEventListener(event, handler);
|
|
7558
|
+
}
|
|
7559
|
+
};
|
|
7560
|
+
const detachComposerListeners = (el: HTMLElement | null): void => {
|
|
7561
|
+
if (!el) return;
|
|
7562
|
+
for (const [event, handler] of composerListeners) {
|
|
7563
|
+
el.removeEventListener(event, handler);
|
|
7564
|
+
}
|
|
7565
|
+
};
|
|
7566
|
+
|
|
7567
|
+
attachComposerListeners(textarea);
|
|
7568
|
+
|
|
7569
|
+
// Inline mention mode swaps the textarea for a contenteditable surface (loaded
|
|
7570
|
+
// lazily). When that happens, move the composer listeners onto the new element
|
|
7571
|
+
// and repoint `textarea` (the swapped element shims the textarea API the rest of
|
|
7572
|
+
// the composer code relies on). Fires immediately if the swap already occurred.
|
|
7573
|
+
mentionOrchestrator?.onComposerSwap((next, prev) => {
|
|
7574
|
+
detachComposerListeners(prev);
|
|
7575
|
+
textarea = next as unknown as HTMLTextAreaElement;
|
|
7576
|
+
attachComposerListeners(next);
|
|
7577
|
+
});
|
|
7319
7578
|
|
|
7320
7579
|
const escStopDoc = mount.ownerDocument ?? document;
|
|
7321
7580
|
escStopDoc.addEventListener("keydown", handleEscStop, true);
|
|
@@ -7396,10 +7655,9 @@ export const createAgentExperience = (
|
|
|
7396
7655
|
if (composerForm) {
|
|
7397
7656
|
composerForm.removeEventListener("submit", handleSubmit);
|
|
7398
7657
|
}
|
|
7399
|
-
textarea
|
|
7400
|
-
textarea?.removeEventListener("input", handleComposerInput);
|
|
7401
|
-
textarea?.removeEventListener("paste", handleInputPaste);
|
|
7658
|
+
detachComposerListeners(textarea);
|
|
7402
7659
|
escStopDoc.removeEventListener("keydown", handleEscStop, true);
|
|
7660
|
+
mentionOrchestrator?.destroy();
|
|
7403
7661
|
});
|
|
7404
7662
|
|
|
7405
7663
|
destroyCallbacks.push(() => {
|
|
@@ -7427,7 +7685,7 @@ export const createAgentExperience = (
|
|
|
7427
7685
|
}
|
|
7428
7686
|
|
|
7429
7687
|
const controller: Controller = {
|
|
7430
|
-
update(nextConfig:
|
|
7688
|
+
update(nextConfig: AgentWidgetConfigPatch) {
|
|
7431
7689
|
const previousToolCallConfig = config.toolCall;
|
|
7432
7690
|
const previousMessageActions = config.messageActions;
|
|
7433
7691
|
const previousLayoutMessages = config.layout?.messages;
|
|
@@ -7439,7 +7697,10 @@ export const createAgentExperience = (
|
|
|
7439
7697
|
const previousToolCallDisplay = config.features?.toolCallDisplay;
|
|
7440
7698
|
const previousReasoningDisplay = config.features?.reasoningDisplay;
|
|
7441
7699
|
const previousStreamAnimationType = config.features?.streamAnimation?.type;
|
|
7442
|
-
|
|
7700
|
+
// One consistent recursive patch policy across the live controller and the
|
|
7701
|
+
// init handle. See utils/config-merge.ts for the replace-leaf list and
|
|
7702
|
+
// explicit-undefined reset semantics.
|
|
7703
|
+
config = mergeConfigUpdate(config, nextConfig);
|
|
7443
7704
|
// applyFullHeightStyles resets mount.style.cssText, so call it before applyThemeVariables
|
|
7444
7705
|
applyFullHeightStyles();
|
|
7445
7706
|
applyThemeVariables(mount, config);
|
|
@@ -7609,7 +7870,10 @@ export const createAgentExperience = (
|
|
|
7609
7870
|
headerSubtitle.style.display = headerLayoutConfig.showSubtitle === false ? "none" : "";
|
|
7610
7871
|
}
|
|
7611
7872
|
if (closeButton) {
|
|
7612
|
-
|
|
7873
|
+
// showCloseButton (defaulted true) filters on top of toggleability;
|
|
7874
|
+
// it must not reveal the close button on non-closeable panels.
|
|
7875
|
+
const showClose = isPanelToggleable() && headerLayoutConfig.showCloseButton !== false;
|
|
7876
|
+
closeButton.style.display = showClose ? "" : "none";
|
|
7613
7877
|
}
|
|
7614
7878
|
if (panelElements.clearChatButtonWrapper) {
|
|
7615
7879
|
// showClearChat explicitly controls visibility when set
|
|
@@ -7670,7 +7934,7 @@ export const createAgentExperience = (
|
|
|
7670
7934
|
refreshCloseButton();
|
|
7671
7935
|
|
|
7672
7936
|
// Re-render messages if config affecting message rendering changed
|
|
7673
|
-
const toolCallConfigChanged = JSON.stringify(
|
|
7937
|
+
const toolCallConfigChanged = JSON.stringify(config.toolCall) !== JSON.stringify(previousToolCallConfig);
|
|
7674
7938
|
const messageActionsChanged = JSON.stringify(config.messageActions) !== JSON.stringify(previousMessageActions);
|
|
7675
7939
|
const layoutMessagesChanged = JSON.stringify(config.layout?.messages) !== JSON.stringify(previousLayoutMessages);
|
|
7676
7940
|
const loadingIndicatorChanged = config.loadingIndicator?.render !== previousLoadingIndicator?.render
|
|
@@ -7811,13 +8075,11 @@ export const createAgentExperience = (
|
|
|
7811
8075
|
}
|
|
7812
8076
|
|
|
7813
8077
|
if (closeButton) {
|
|
7814
|
-
//
|
|
7815
|
-
|
|
7816
|
-
|
|
7817
|
-
|
|
7818
|
-
|
|
7819
|
-
closeButton.style.display = "";
|
|
7820
|
-
}
|
|
8078
|
+
// showCloseButton (defaulted true) filters on top of toggleability;
|
|
8079
|
+
// it must not reveal the close button on non-closeable panels.
|
|
8080
|
+
const layoutShowCloseButton =
|
|
8081
|
+
isPanelToggleable() && config.layout?.header?.showCloseButton !== false;
|
|
8082
|
+
closeButton.style.display = layoutShowCloseButton ? "" : "none";
|
|
7821
8083
|
|
|
7822
8084
|
const closeButtonSize = launcher.closeButtonSize ?? "32px";
|
|
7823
8085
|
const closeButtonPlacement = launcher.closeButtonPlacement ?? "inline";
|
|
@@ -7908,6 +8170,9 @@ export const createAgentExperience = (
|
|
|
7908
8170
|
closeButton.innerHTML = "";
|
|
7909
8171
|
const iconSvg = renderLucideIcon(closeButtonIconName, "28px", "currentColor", 1);
|
|
7910
8172
|
if (iconSvg) {
|
|
8173
|
+
// display:block matches the builder; inline SVG baseline spacing
|
|
8174
|
+
// shifts the icon off-center.
|
|
8175
|
+
iconSvg.style.display = "block";
|
|
7911
8176
|
closeButton.appendChild(iconSvg);
|
|
7912
8177
|
} else {
|
|
7913
8178
|
closeButton.textContent = closeButtonIconText;
|
|
@@ -8094,8 +8359,13 @@ export const createAgentExperience = (
|
|
|
8094
8359
|
// the icon to match its 16px button.
|
|
8095
8360
|
clearChatButton.innerHTML = "";
|
|
8096
8361
|
const clearChatIconSize = isComposerBar() ? "14px" : "20px";
|
|
8097
|
-
|
|
8362
|
+
// Stroke 1 matches the mount-time builder (header-parts.ts); a
|
|
8363
|
+
// different weight here makes the icon visibly bolden on update.
|
|
8364
|
+
const iconSvg = renderLucideIcon(clearChatIconName, clearChatIconSize, "currentColor", 1);
|
|
8098
8365
|
if (iconSvg) {
|
|
8366
|
+
// display:block matches the builder; inline SVG baseline spacing
|
|
8367
|
+
// shifts the icon off-center.
|
|
8368
|
+
iconSvg.style.display = "block";
|
|
8099
8369
|
clearChatButton.appendChild(iconSvg);
|
|
8100
8370
|
}
|
|
8101
8371
|
|
|
@@ -8298,10 +8568,16 @@ export const createAgentExperience = (
|
|
|
8298
8568
|
micButton.style.minWidth = micIconSize;
|
|
8299
8569
|
micButton.style.minHeight = micIconSize;
|
|
8300
8570
|
|
|
8301
|
-
// Update icon
|
|
8302
|
-
|
|
8571
|
+
// Update icon; color chain and stroke 1.5 match the mount-time
|
|
8572
|
+
// builder (composer-parts.ts) so an unrelated update cannot restyle.
|
|
8573
|
+
const iconColor = voiceConfig.iconColor ?? sendButtonConfig.textColor;
|
|
8303
8574
|
micButton.innerHTML = "";
|
|
8304
|
-
const micIconSvg = renderLucideIcon(
|
|
8575
|
+
const micIconSvg = renderLucideIcon(
|
|
8576
|
+
micIconName,
|
|
8577
|
+
micIconSizeNum,
|
|
8578
|
+
iconColor || "currentColor",
|
|
8579
|
+
1.5
|
|
8580
|
+
);
|
|
8305
8581
|
if (micIconSvg) {
|
|
8306
8582
|
micButton.appendChild(micIconSvg);
|
|
8307
8583
|
} else {
|
|
@@ -8441,19 +8717,11 @@ export const createAgentExperience = (
|
|
|
8441
8717
|
attachmentButton.style.minHeight = attachIconSize;
|
|
8442
8718
|
attachmentButton.style.fontSize = "18px";
|
|
8443
8719
|
attachmentButton.style.lineHeight = "1";
|
|
8444
|
-
|
|
8445
|
-
|
|
8446
|
-
|
|
8447
|
-
|
|
8448
|
-
|
|
8449
|
-
|
|
8450
|
-
// Add hover effect via mouseenter/mouseleave
|
|
8451
|
-
attachmentButton.addEventListener("mouseenter", () => {
|
|
8452
|
-
attachmentButton!.style.backgroundColor = "var(--persona-palette-colors-black-alpha-50, rgba(0, 0, 0, 0.05))";
|
|
8453
|
-
});
|
|
8454
|
-
attachmentButton.addEventListener("mouseleave", () => {
|
|
8455
|
-
attachmentButton!.style.backgroundColor = "transparent";
|
|
8456
|
-
});
|
|
8720
|
+
// Appearance (bg / fg / border / radius / hover) is themed from the
|
|
8721
|
+
// shared `.persona-attachment-button` CSS rule via the
|
|
8722
|
+
// `--persona-button-ghost-*` tokens — matching the static
|
|
8723
|
+
// createAttachmentControls path. Only sizing stays inline here so
|
|
8724
|
+
// this runtime-created button restyles identically to the built-in one.
|
|
8457
8725
|
|
|
8458
8726
|
const attachIconSvg = renderLucideIcon(attachIconName, attachIconSizeNum, "currentColor", 1.5);
|
|
8459
8727
|
if (attachIconSvg) {
|
|
@@ -8475,8 +8743,10 @@ export const createAgentExperience = (
|
|
|
8475
8743
|
tooltip.textContent = attachTooltipText;
|
|
8476
8744
|
attachmentButtonWrapper.appendChild(tooltip);
|
|
8477
8745
|
|
|
8478
|
-
// Insert into left actions container
|
|
8479
|
-
|
|
8746
|
+
// Insert into left actions container (fall back to the form when a
|
|
8747
|
+
// custom composer has no left cluster).
|
|
8748
|
+
if (leftActions) leftActions.append(attachmentButtonWrapper);
|
|
8749
|
+
else composerForm.appendChild(attachmentButtonWrapper);
|
|
8480
8750
|
|
|
8481
8751
|
// Initialize attachment manager
|
|
8482
8752
|
if (!attachmentManager && attachmentInput && attachmentPreviewsContainer) {
|
|
@@ -8491,10 +8761,6 @@ export const createAgentExperience = (
|
|
|
8491
8761
|
});
|
|
8492
8762
|
}
|
|
8493
8763
|
|
|
8494
|
-
// Create drop overlay if missing
|
|
8495
|
-
if (!container.querySelector(".persona-attachment-drop-overlay")) {
|
|
8496
|
-
container.appendChild(buildDropOverlay(attachmentsConfig.dropOverlay));
|
|
8497
|
-
}
|
|
8498
8764
|
} else {
|
|
8499
8765
|
// Show existing attachment button and update config
|
|
8500
8766
|
attachmentButtonWrapper.style.display = "";
|
|
@@ -8515,6 +8781,33 @@ export const createAgentExperience = (
|
|
|
8515
8781
|
});
|
|
8516
8782
|
}
|
|
8517
8783
|
}
|
|
8784
|
+
|
|
8785
|
+
// Rebuild the overlay so live dropOverlay updates restyle it; visibility
|
|
8786
|
+
// is owned by the container's drop-active class, so this is drag-safe.
|
|
8787
|
+
container.querySelector(".persona-attachment-drop-overlay")?.remove();
|
|
8788
|
+
container.appendChild(buildDropOverlay(config.attachments?.dropOverlay));
|
|
8789
|
+
|
|
8790
|
+
// Re-render icon/tooltip so live buttonIconName/buttonTooltipText
|
|
8791
|
+
// updates apply; the button element itself is created once and kept.
|
|
8792
|
+
if (attachmentButton) {
|
|
8793
|
+
const attCfg = config.attachments ?? {};
|
|
8794
|
+
const tooltipText = attCfg.buttonTooltipText ?? "Attach file";
|
|
8795
|
+
attachmentButton.setAttribute("aria-label", tooltipText);
|
|
8796
|
+
attachmentButton.textContent = "";
|
|
8797
|
+
const btnSize = parseFloat(config.sendButton?.size ?? "40px") || 40;
|
|
8798
|
+
const iconSvg = renderLucideIcon(
|
|
8799
|
+
attCfg.buttonIconName ?? "paperclip",
|
|
8800
|
+
Math.round(btnSize * 0.6),
|
|
8801
|
+
"currentColor",
|
|
8802
|
+
1.5
|
|
8803
|
+
);
|
|
8804
|
+
if (iconSvg) attachmentButton.appendChild(iconSvg);
|
|
8805
|
+
else attachmentButton.textContent = "📎";
|
|
8806
|
+
const attachTooltip = attachmentButtonWrapper?.querySelector(
|
|
8807
|
+
".persona-send-button-tooltip"
|
|
8808
|
+
);
|
|
8809
|
+
if (attachTooltip) attachTooltip.textContent = tooltipText;
|
|
8810
|
+
}
|
|
8518
8811
|
} else {
|
|
8519
8812
|
// Hide attachment button if disabled
|
|
8520
8813
|
if (attachmentButtonWrapper) {
|
|
@@ -8549,9 +8842,6 @@ export const createAgentExperience = (
|
|
|
8549
8842
|
sendButton.style.fontSize = "18px";
|
|
8550
8843
|
sendButton.style.lineHeight = "1";
|
|
8551
8844
|
|
|
8552
|
-
// Clear existing content
|
|
8553
|
-
sendButton.innerHTML = "";
|
|
8554
|
-
|
|
8555
8845
|
// Set foreground color from config or theme token
|
|
8556
8846
|
if (textColor) {
|
|
8557
8847
|
sendButton.style.color = textColor;
|
|
@@ -8559,20 +8849,25 @@ export const createAgentExperience = (
|
|
|
8559
8849
|
sendButton.style.color = "var(--persona-button-primary-fg, #ffffff)";
|
|
8560
8850
|
}
|
|
8561
8851
|
|
|
8562
|
-
//
|
|
8563
|
-
|
|
8564
|
-
|
|
8565
|
-
|
|
8566
|
-
|
|
8567
|
-
if (
|
|
8568
|
-
|
|
8852
|
+
// Skip the icon-content re-render while streaming: the button holds the
|
|
8853
|
+
// stop icon (owned by setSendButtonMode), and clobbering it here would
|
|
8854
|
+
// show the send arrow mid-stream while the aria-label still says stop.
|
|
8855
|
+
if (!session.isStreaming()) {
|
|
8856
|
+
sendButton.innerHTML = "";
|
|
8857
|
+
if (iconName) {
|
|
8858
|
+
const iconSize = parseFloat(buttonSize) || 24;
|
|
8859
|
+
const iconColor = textColor?.trim() || "currentColor";
|
|
8860
|
+
const iconSvg = renderLucideIcon(iconName, iconSize, iconColor, 2);
|
|
8861
|
+
if (iconSvg) {
|
|
8862
|
+
sendButton.appendChild(iconSvg);
|
|
8863
|
+
} else {
|
|
8864
|
+
sendButton.textContent = iconText;
|
|
8865
|
+
}
|
|
8569
8866
|
} else {
|
|
8570
8867
|
sendButton.textContent = iconText;
|
|
8571
8868
|
}
|
|
8572
|
-
} else {
|
|
8573
|
-
sendButton.textContent = iconText;
|
|
8574
8869
|
}
|
|
8575
|
-
|
|
8870
|
+
|
|
8576
8871
|
// Update classes
|
|
8577
8872
|
sendButton.className = "persona-rounded-button persona-flex persona-items-center persona-justify-center disabled:persona-opacity-50 persona-cursor-pointer";
|
|
8578
8873
|
|