@runtypelabs/persona 4.32.2 → 4.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/activity-ui.d.cts +3 -3
- package/dist/activity-ui.d.ts +3 -3
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/approval-ui.d.cts +2 -2
- package/dist/approval-ui.d.ts +2 -2
- package/dist/artifacts-ui.d.cts +3 -3
- package/dist/artifacts-ui.d.ts +3 -3
- package/dist/background-runtime.d.cts +1 -1
- package/dist/background-runtime.d.ts +1 -1
- package/dist/{chunk-BbXc58OA.js → chunk-6CUrH_jm.js} +1 -1
- package/dist/{chunk-B_H0l3u2.js → chunk-BRM3bHcJ.js} +1 -1
- package/dist/chunk-CEPM3FOp.js +1 -0
- package/dist/{chunk-D0tcIZJM.js → chunk-CXQJG7pb.js} +1 -1
- package/dist/{chunk-UWMfWW2a.js → chunk-D1G8tgYc.js} +1 -1
- package/dist/{chunk-BpjtKnGI.js → chunk-D6U0E82p.js} +1 -1
- package/dist/{chunk-2FnmZ9AA.js → chunk-DOZmcve8.js} +1 -1
- package/dist/chunk-DVgzoVCB.js +1 -0
- package/dist/{chunk-B9Zu-wrk.js → chunk-DW7Vg_l2.js} +1 -1
- package/dist/{chunk-B3l7TOOv.js → chunk-DbGLjU2P.js} +1 -1
- package/dist/{chunk-DTGvDx8o.js → chunk-Dj2a9f9w.js} +1 -1
- package/dist/{chunk-DgZZqy14.js → chunk-d54BJdKd.js} +1 -1
- package/dist/chunk-dLJWKfOb.js +1 -0
- package/dist/{chunk-DNdeIFJ6.js → chunk-gu34lr-P.js} +1 -1
- package/dist/chunk-i4qGOJAL.js +1 -0
- package/dist/{chunk-DVZ8ri1z.js → chunk-ql5N2I_t.js} +1 -1
- package/dist/client-dispatch.js +1 -1
- package/dist/client-history.js +1 -1
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/composer-actions-runtime.d.cts +2 -2
- package/dist/composer-actions-runtime.d.ts +2 -2
- package/dist/{composer-input-dwg_o9Z-.d.cts → composer-input-DnqGCXZz.d.cts} +1 -1
- package/dist/{composer-input-dwg_o9Z-.d.ts → composer-input-DnqGCXZz.d.ts} +1 -1
- package/dist/{composer-store-Dw7Zbqxr.d.cts → composer-store-6Iwy6WLI.d.cts} +1 -1
- package/dist/{composer-store-Dw7Zbqxr.d.ts → composer-store-6Iwy6WLI.d.ts} +1 -1
- package/dist/{context-mention-manager-D2p0-JH-.d.cts → context-mention-manager-B-rHF98n.d.cts} +2 -2
- package/dist/{context-mention-manager-D2p0-JH-.d.ts → context-mention-manager-B-rHF98n.d.ts} +2 -2
- package/dist/context-mentions-inline.d.cts +2 -2
- package/dist/context-mentions-inline.d.ts +2 -2
- package/dist/context-mentions.d.cts +3 -3
- package/dist/context-mentions.d.ts +3 -3
- package/dist/{defaults-CjX0kKzb.d.ts → defaults-BuOjqUAG.d.cts} +2 -1
- package/dist/{defaults-CjX0kKzb.d.cts → defaults-BuOjqUAG.d.ts} +2 -1
- package/dist/dictation-runtime.d.cts +2 -2
- package/dist/dictation-runtime.d.ts +2 -2
- package/dist/event-stream-view.d.cts +1 -1
- package/dist/event-stream-view.d.ts +1 -1
- package/dist/{fallback-speech-engine-DmF7MczI.d.cts → fallback-speech-engine-SqEfkGda.d.cts} +1 -1
- package/dist/{fallback-speech-engine-DmF7MczI.d.ts → fallback-speech-engine-SqEfkGda.d.ts} +1 -1
- package/dist/{feature-preferences-CVFDt9Xf.d.cts → feature-preferences-DqAmje-2.d.cts} +1 -1
- package/dist/{feature-preferences-CVFDt9Xf.d.ts → feature-preferences-DqAmje-2.d.ts} +1 -1
- package/dist/forms-ui.d.cts +2 -2
- package/dist/forms-ui.d.ts +2 -2
- package/dist/{history-confirm-dialog-DJEXsTyE.d.cts → history-confirm-dialog-B3L7QNcO.d.cts} +1 -1
- package/dist/{history-confirm-dialog-DJEXsTyE.d.ts → history-confirm-dialog-B3L7QNcO.d.ts} +1 -1
- package/dist/history-shell.cjs +1 -1
- package/dist/history-shell.d.cts +6 -6
- package/dist/history-shell.d.ts +6 -6
- package/dist/history-shell.js +1 -1
- package/dist/history-view.d.cts +1 -1
- package/dist/history-view.d.ts +1 -1
- package/dist/index.cjs +38 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -9
- package/dist/index.d.ts +9 -9
- package/dist/index.global.js +1 -1
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +52 -52
- package/dist/index.js.map +1 -1
- package/dist/install.global.js +1 -1
- package/dist/launcher.global.js +2 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/{loading-label-runtime-loader-CPxnoaoi.d.cts → loading-label-runtime-loader-JecjOfJS.d.cts} +1 -1
- package/dist/{loading-label-runtime-loader-CPxnoaoi.d.ts → loading-label-runtime-loader-JecjOfJS.d.ts} +1 -1
- package/dist/loading-label-runtime.d.cts +1 -1
- package/dist/loading-label-runtime.d.ts +1 -1
- package/dist/{message-bubble-CkZGlCLC.d.ts → message-bubble-CBYtpDsT.d.cts} +2 -2
- package/dist/{message-bubble-CkZGlCLC.d.cts → message-bubble-CBYtpDsT.d.ts} +2 -2
- package/dist/preferences-runtime.d.cts +1 -1
- package/dist/preferences-runtime.d.ts +1 -1
- package/dist/{preview-DdUvoLb4.d.cts → preview-_UJwwt2L.d.cts} +2 -2
- package/dist/{preview-DdUvoLb4.d.ts → preview-_UJwwt2L.d.ts} +2 -2
- package/dist/{registry-Dz4iHNST.d.cts → registry-B5ZV3K-h.d.cts} +1 -1
- package/dist/{registry-Dz4iHNST.d.ts → registry-B5ZV3K-h.d.ts} +1 -1
- package/dist/{session-Baxp9iRK.d.cts → session-Bys8_XuB.d.cts} +10 -9
- package/dist/{session-Baxp9iRK.d.ts → session-Bys8_XuB.d.ts} +10 -9
- package/dist/session-reconnect.d.cts +2 -2
- package/dist/session-reconnect.d.ts +2 -2
- package/dist/smart-dom-reader.d.cts +1 -1
- package/dist/smart-dom-reader.d.ts +1 -1
- package/dist/theme-editor-preview.cjs +31 -31
- package/dist/theme-editor-preview.d.cts +1 -1
- package/dist/theme-editor-preview.d.ts +1 -1
- package/dist/theme-editor-preview.js +19 -19
- package/dist/theme-editor.cjs +1 -1
- package/dist/theme-editor.d.cts +2 -2
- package/dist/theme-editor.d.ts +2 -2
- package/dist/theme-editor.js +1 -1
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +4 -2
- package/dist/theme-reference.d.ts +4 -2
- package/dist/theme-reference.js +1 -1
- package/dist/{tool-loading-animation-un3kA5Ur.d.cts → tool-loading-animation-d5qAQcwv.d.cts} +1 -1
- package/dist/{tool-loading-animation-un3kA5Ur.d.ts → tool-loading-animation-d5qAQcwv.d.ts} +1 -1
- package/dist/{turn-activity-ui-BGNhtMcH.d.cts → turn-activity-ui-q6fHVnx-.d.cts} +2 -2
- package/dist/{turn-activity-ui-BGNhtMcH.d.ts → turn-activity-ui-q6fHVnx-.d.ts} +2 -2
- package/dist/turn-activity-ui.d.cts +1 -1
- package/dist/turn-activity-ui.d.ts +1 -1
- package/dist/{types-BOZHQIk0.d.cts → types-B_mj21jg.d.cts} +91 -7
- package/dist/{types-BOZHQIk0.d.ts → types-B_mj21jg.d.ts} +91 -7
- package/dist/voice-client-tools.d.cts +1 -1
- package/dist/voice-client-tools.d.ts +1 -1
- package/dist/voice-runtime.cjs +1 -1
- package/dist/voice-runtime.d.cts +2 -2
- package/dist/voice-runtime.d.ts +2 -2
- package/dist/voice-runtime.js +1 -1
- package/dist/voice-worklet-player.d.cts +2 -2
- package/dist/voice-worklet-player.d.ts +2 -2
- package/dist/{webmcp-runtime-entry-DuAG1_yF.d.cts → webmcp-runtime-entry-ClEO3qdI.d.cts} +1 -1
- package/dist/{webmcp-runtime-entry-DuAG1_yF.d.ts → webmcp-runtime-entry-ClEO3qdI.d.ts} +1 -1
- package/dist/webmcp-runtime.d.cts +1 -1
- package/dist/webmcp-runtime.d.ts +1 -1
- package/dist/widget.css +1 -1
- package/package.json +1 -1
- package/src/client-dispatch.ts +69 -16
- package/src/client.operator-role.test.ts +217 -0
- package/src/components/message-bubble.test.ts +169 -0
- package/src/components/message-bubble.ts +97 -28
- package/src/defaults-parity.test.ts +14 -0
- package/src/index-core.ts +7 -0
- package/src/session.resubmit-from.test.ts +27 -0
- package/src/session.test.ts +10 -0
- package/src/session.ts +8 -5
- package/src/styles/widget.css +23 -1
- package/src/theme-reference.ts +3 -1
- package/src/types/theme.ts +21 -0
- package/src/types.ts +71 -6
- package/src/ui.ask-user-question-plugin.test.ts +154 -0
- package/src/ui.operator-role.test.ts +336 -0
- package/src/ui.ts +37 -12
- package/src/utils/__snapshots__/tokens-parity.test.ts.snap +236 -0
- package/src/utils/brand.test.ts +14 -0
- package/src/utils/brand.ts +8 -4
- package/src/utils/history-messages.test.ts +30 -0
- package/src/utils/history-messages.ts +26 -8
- package/src/utils/message-fingerprint.ts +4 -0
- package/src/utils/resubmit-turn.test.ts +13 -0
- package/src/utils/resubmit-turn.ts +12 -1
- package/src/utils/tokens.ts +31 -0
- package/src/welcome.test.ts +16 -0
- package/src/welcome.ts +10 -2
- package/dist/chunk-BNE40cEG.js +0 -1
- package/dist/chunk-Bz9jAgba.js +0 -1
- package/dist/chunk-CdpVP6LZ.js +0 -1
- package/dist/chunk-CjkechxR.js +0 -1
|
@@ -689,3 +689,157 @@ describe("renderAskUserQuestion plugin hook", () => {
|
|
|
689
689
|
controller.destroy();
|
|
690
690
|
});
|
|
691
691
|
});
|
|
692
|
+
|
|
693
|
+
describe("typing indicator while ask_user_question arguments stream", () => {
|
|
694
|
+
afterEach(() => {
|
|
695
|
+
document.body.innerHTML = "";
|
|
696
|
+
if (typeof localStorage !== "undefined") localStorage.clear();
|
|
697
|
+
vi.restoreAllMocks();
|
|
698
|
+
vi.unstubAllGlobals();
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
// Streaming transcript updates render once per animation frame; queue the
|
|
702
|
+
// frames so each injection can be rendered synchronously.
|
|
703
|
+
let frames = new Map<number, (time: number) => void>();
|
|
704
|
+
let nextFrameId = 1;
|
|
705
|
+
const renderFrame = () => {
|
|
706
|
+
const pending = [...frames.values()];
|
|
707
|
+
frames.clear();
|
|
708
|
+
pending.forEach((callback) => callback(0));
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
const mountStreaming = async (config: Record<string, unknown> = {}) => {
|
|
712
|
+
frames = new Map();
|
|
713
|
+
vi.stubGlobal("requestAnimationFrame", (callback: (time: number) => void) => {
|
|
714
|
+
const id = nextFrameId++;
|
|
715
|
+
frames.set(id, callback);
|
|
716
|
+
return id;
|
|
717
|
+
});
|
|
718
|
+
vi.stubGlobal("cancelAnimationFrame", (id: number) => frames.delete(id));
|
|
719
|
+
global.fetch = vi.fn().mockImplementation(
|
|
720
|
+
() => new Promise(() => {})
|
|
721
|
+
) as unknown as typeof fetch;
|
|
722
|
+
|
|
723
|
+
const mount = createMount();
|
|
724
|
+
const controller = createAgentExperience(mount, {
|
|
725
|
+
apiUrl: "https://api.example.com/chat",
|
|
726
|
+
launcher: { enabled: false },
|
|
727
|
+
...config,
|
|
728
|
+
} as unknown as Parameters<typeof createAgentExperience>[1]);
|
|
729
|
+
|
|
730
|
+
controller.submitMessage("hello");
|
|
731
|
+
await Promise.resolve();
|
|
732
|
+
await Promise.resolve();
|
|
733
|
+
return { mount, controller };
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
// Stamped in the future so the rows sort after the submitted user message.
|
|
737
|
+
const sequence = Date.now() + 60_000;
|
|
738
|
+
const createdAt = new Date(sequence).toISOString();
|
|
739
|
+
const fullArgs = {
|
|
740
|
+
questions: [
|
|
741
|
+
{ question: "Which audience?", options: [{ label: "Hobbyists" }, { label: "Pros" }] },
|
|
742
|
+
],
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
const injectStreamingAsk = (
|
|
746
|
+
controller: ReturnType<typeof createAgentExperience>,
|
|
747
|
+
{ chunks = [], args }: { chunks?: string[]; args?: unknown } = {}
|
|
748
|
+
) => {
|
|
749
|
+
controller.injectTestMessage({
|
|
750
|
+
type: "message",
|
|
751
|
+
message: {
|
|
752
|
+
id: "ask-1",
|
|
753
|
+
role: "assistant",
|
|
754
|
+
content: "",
|
|
755
|
+
createdAt,
|
|
756
|
+
sequence,
|
|
757
|
+
streaming: true,
|
|
758
|
+
variant: "tool",
|
|
759
|
+
toolCall: { id: "ask-1", name: "ask_user_question", status: "running", chunks, args },
|
|
760
|
+
},
|
|
761
|
+
});
|
|
762
|
+
renderFrame();
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
const injectAwaitingAsk = (controller: ReturnType<typeof createAgentExperience>) => {
|
|
766
|
+
controller.injectTestMessage({
|
|
767
|
+
type: "message",
|
|
768
|
+
message: {
|
|
769
|
+
id: "ask-1",
|
|
770
|
+
role: "assistant",
|
|
771
|
+
content: "",
|
|
772
|
+
createdAt,
|
|
773
|
+
sequence,
|
|
774
|
+
streaming: false,
|
|
775
|
+
variant: "tool",
|
|
776
|
+
toolCall: { id: "ask-1", name: "ask_user_question", status: "complete", chunks: [], args: fullArgs },
|
|
777
|
+
agentMetadata: { executionId: "exec_123", awaitingLocalTool: true },
|
|
778
|
+
},
|
|
779
|
+
});
|
|
780
|
+
renderFrame();
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
const typingIndicator = (mount: HTMLElement) =>
|
|
784
|
+
mount.querySelector('[data-typing-indicator="true"]');
|
|
785
|
+
const askSheet = (mount: HTMLElement) => mount.querySelector("[data-persona-ask-sheet-for]");
|
|
786
|
+
|
|
787
|
+
it("keeps the dots until the built-in sheet mounts when no plugin renders the question", async () => {
|
|
788
|
+
const { mount, controller } = await mountStreaming();
|
|
789
|
+
|
|
790
|
+
injectStreamingAsk(controller);
|
|
791
|
+
expect(askSheet(mount)).toBeNull();
|
|
792
|
+
expect(typingIndicator(mount)).not.toBeNull();
|
|
793
|
+
|
|
794
|
+
injectAwaitingAsk(controller);
|
|
795
|
+
expect(askSheet(mount)).not.toBeNull();
|
|
796
|
+
expect(typingIndicator(mount)).toBeNull();
|
|
797
|
+
|
|
798
|
+
controller.destroy();
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
it("keeps the dots while the plugin returns null for a partial payload", async () => {
|
|
802
|
+
const plugin: AgentWidgetPlugin = {
|
|
803
|
+
id: "partial-asker",
|
|
804
|
+
renderAskUserQuestion: ({ payload }) => {
|
|
805
|
+
if (!payload?.questions?.[0]?.options?.length) return null;
|
|
806
|
+
const root = document.createElement("div");
|
|
807
|
+
root.setAttribute("data-test-id", "partial-ask");
|
|
808
|
+
return root;
|
|
809
|
+
},
|
|
810
|
+
};
|
|
811
|
+
const { mount, controller } = await mountStreaming({ plugins: [plugin] });
|
|
812
|
+
|
|
813
|
+
injectStreamingAsk(controller, { chunks: ['{"questions":[{"question":"Which'] });
|
|
814
|
+
expect(mount.querySelector('[data-test-id="partial-ask"]')).toBeNull();
|
|
815
|
+
expect(askSheet(mount)).toBeNull();
|
|
816
|
+
expect(typingIndicator(mount)).not.toBeNull();
|
|
817
|
+
|
|
818
|
+
injectStreamingAsk(controller, { args: fullArgs });
|
|
819
|
+
expect(mount.querySelector('[data-test-id="partial-ask"]')).not.toBeNull();
|
|
820
|
+
expect(typingIndicator(mount)).toBeNull();
|
|
821
|
+
|
|
822
|
+
controller.destroy();
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
it.each([true, false])("hides the dots as soon as the plugin renders a bubble (showToolCalls=%s)", async (showToolCalls) => {
|
|
826
|
+
const plugin: AgentWidgetPlugin = {
|
|
827
|
+
id: "eager-asker",
|
|
828
|
+
renderAskUserQuestion: () => {
|
|
829
|
+
const root = document.createElement("div");
|
|
830
|
+
root.setAttribute("data-test-id", "eager-ask");
|
|
831
|
+
return root;
|
|
832
|
+
},
|
|
833
|
+
};
|
|
834
|
+
const { mount, controller } = await mountStreaming({
|
|
835
|
+
plugins: [plugin],
|
|
836
|
+
features: { showToolCalls },
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
injectStreamingAsk(controller);
|
|
840
|
+
expect(mount.querySelector('[data-test-id="eager-ask"]')).not.toBeNull();
|
|
841
|
+
expect(typingIndicator(mount)).toBeNull();
|
|
842
|
+
|
|
843
|
+
controller.destroy();
|
|
844
|
+
});
|
|
845
|
+
});
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { createAgentExperience } from "./ui";
|
|
7
|
+
import type { AgentWidgetMessage, AgentWidgetStoredState } from "./types";
|
|
8
|
+
import type { AgentWidgetPlugin } from "./plugins/types";
|
|
9
|
+
|
|
10
|
+
const mounts: HTMLElement[] = [];
|
|
11
|
+
const controllers: ReturnType<typeof createAgentExperience>[] = [];
|
|
12
|
+
|
|
13
|
+
const makeController = (config: Record<string, unknown> = {}) => {
|
|
14
|
+
const mount = document.createElement("div");
|
|
15
|
+
document.body.appendChild(mount);
|
|
16
|
+
mounts.push(mount);
|
|
17
|
+
const controller = createAgentExperience(mount, {
|
|
18
|
+
apiUrl: "https://api.example.com/chat",
|
|
19
|
+
launcher: { enabled: false },
|
|
20
|
+
persistState: false,
|
|
21
|
+
suggestionChips: [],
|
|
22
|
+
...config,
|
|
23
|
+
} as unknown as Parameters<typeof createAgentExperience>[1]);
|
|
24
|
+
controllers.push(controller);
|
|
25
|
+
return { mount, controller };
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** Let microtasks and the coalesced render frame run. */
|
|
29
|
+
const settle = async () => {
|
|
30
|
+
for (let i = 0; i < 8; i += 1) {
|
|
31
|
+
// eslint-disable-next-line no-await-in-loop
|
|
32
|
+
await Promise.resolve();
|
|
33
|
+
}
|
|
34
|
+
await new Promise((resolve) => setTimeout(resolve, 32));
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const row = (mount: HTMLElement, id: string) =>
|
|
38
|
+
mount.querySelector<HTMLElement>(`[data-wrapper-id="${id}"]`);
|
|
39
|
+
|
|
40
|
+
describe("operator role in the transcript", () => {
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
window.scrollTo = vi.fn();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
controllers.splice(0).forEach((controller) => {
|
|
47
|
+
try {
|
|
48
|
+
controller.destroy();
|
|
49
|
+
} catch {
|
|
50
|
+
/* already destroyed */
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
mounts.splice(0).forEach((mount) => mount.remove());
|
|
54
|
+
document.body.innerHTML = "";
|
|
55
|
+
window.localStorage.clear();
|
|
56
|
+
vi.restoreAllMocks();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("renders an operator row on the assistant side with its own bubble", async () => {
|
|
60
|
+
const { mount, controller } = makeController();
|
|
61
|
+
controller.injectUserMessage({ id: "u1", content: "Where is my order?" });
|
|
62
|
+
controller.injectAssistantMessage({ id: "a1", content: "Let me check." });
|
|
63
|
+
controller.injectMessage({ id: "op1", role: "operator", content: "Sam here, I refunded it." });
|
|
64
|
+
await settle();
|
|
65
|
+
|
|
66
|
+
const operatorRow = row(mount, "op1")!;
|
|
67
|
+
expect(operatorRow.getAttribute("data-message-role")).toBe("operator");
|
|
68
|
+
expect(operatorRow.classList.contains("persona-message-row-operator")).toBe(true);
|
|
69
|
+
expect(operatorRow.classList.contains("persona-justify-end")).toBe(false);
|
|
70
|
+
expect(operatorRow.querySelector(".persona-message-operator-bubble")).not.toBeNull();
|
|
71
|
+
expect(operatorRow.querySelector(".persona-message-assistant-bubble")).toBeNull();
|
|
72
|
+
|
|
73
|
+
// Existing roles are untouched.
|
|
74
|
+
expect(row(mount, "u1")!.classList.contains("persona-justify-end")).toBe(true);
|
|
75
|
+
expect(row(mount, "a1")!.querySelector(".persona-message-assistant-bubble")).not.toBeNull();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("names human replies with avatars off and updates provenance on reinjection", async () => {
|
|
79
|
+
const { mount, controller } = makeController();
|
|
80
|
+
controller.injectMessage({ id: "human", role: "operator", content: "Refunded" });
|
|
81
|
+
await settle();
|
|
82
|
+
expect(row(mount, "human")!.querySelector('[role="group"]')?.getAttribute("aria-label")).toBe("Human operator");
|
|
83
|
+
controller.injectMessage({ id: "human", role: "operator", content: "Refunded", author: { id: "sam", displayName: "Sam", kind: "human" } });
|
|
84
|
+
await settle();
|
|
85
|
+
expect(row(mount, "human")!.querySelector(".persona-message-author")?.textContent).toBe("Sam · Human operator");
|
|
86
|
+
expect(row(mount, "human")!.querySelector('[role="group"]')?.getAttribute("aria-label")).toBe("Sam · Human operator");
|
|
87
|
+
expect(controller.getMessages().find(message => message.id === "human")?.author).toEqual({ id: "sam", displayName: "Sam", kind: "human" });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("persists and restores structured author provenance", async () => {
|
|
91
|
+
let state: AgentWidgetStoredState | null = null;
|
|
92
|
+
const adapter = {
|
|
93
|
+
load: () => state,
|
|
94
|
+
save: (value: AgentWidgetStoredState) => { state = JSON.parse(JSON.stringify(value)); },
|
|
95
|
+
clear: () => { state = null; },
|
|
96
|
+
};
|
|
97
|
+
const { controller } = makeController({ persistState: true, storageAdapter: adapter });
|
|
98
|
+
controller.injectMessage({ id: "saved", role: "operator", content: "Refunded", author: { id: "sam", displayName: "Sam", kind: "human" } });
|
|
99
|
+
await vi.waitFor(() => expect(state?.messages?.some(message => message.id === "saved")).toBe(true));
|
|
100
|
+
const { mount, controller: restored } = makeController({ persistState: true, storageAdapter: adapter });
|
|
101
|
+
await settle();
|
|
102
|
+
expect(restored.getMessages().find(message => message.id === "saved")?.author).toEqual({ id: "sam", displayName: "Sam", kind: "human" });
|
|
103
|
+
expect(row(mount, "saved")!.querySelector(".persona-message-author")?.textContent).toBe("Sam · Human operator");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("hides destructive retry and edit before human replies while allowing a subsequent turn", async () => {
|
|
107
|
+
const { mount, controller } = makeController({ messageActions: { enabled: true, showEdit: true, showRegenerate: true } });
|
|
108
|
+
controller.injectUserMessage({ id: "before", content: "Question" });
|
|
109
|
+
controller.injectAssistantMessage({ id: "answer", content: "Answer" });
|
|
110
|
+
await settle();
|
|
111
|
+
expect(row(mount, "answer")!.querySelector('[data-action="regenerate"]')).not.toBeNull();
|
|
112
|
+
controller.injectMessage({ id: "human", role: "operator", content: "Refunded" });
|
|
113
|
+
await settle();
|
|
114
|
+
expect(row(mount, "before")!.querySelector('[data-action="edit"]')).toBeNull();
|
|
115
|
+
expect(row(mount, "answer")!.querySelector('[data-action="regenerate"]')).toBeNull();
|
|
116
|
+
controller.injectUserMessage({ id: "after", content: "Follow up" });
|
|
117
|
+
controller.injectAssistantMessage({ id: "next", content: "Next answer" });
|
|
118
|
+
await settle();
|
|
119
|
+
expect(row(mount, "after")!.querySelector('[data-action="edit"]')).not.toBeNull();
|
|
120
|
+
expect(row(mount, "next")!.querySelector('[data-action="regenerate"]')).not.toBeNull();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("does not report operator messages as assistant messages", async () => {
|
|
124
|
+
const { controller } = makeController();
|
|
125
|
+
const assistantMessages: AgentWidgetMessage[] = [];
|
|
126
|
+
controller.on("assistant:message", (message) => assistantMessages.push(message));
|
|
127
|
+
controller.injectMessage({ id: "op1", role: "operator", content: "Hello from support" });
|
|
128
|
+
controller.injectAssistantMessage({ id: "a1", content: "Agent reply" });
|
|
129
|
+
await settle();
|
|
130
|
+
expect(assistantMessages.map((message) => message.id)).toEqual(["a1"]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("renders operator messages from initialMessages", async () => {
|
|
134
|
+
const { mount } = makeController({
|
|
135
|
+
initialMessages: [
|
|
136
|
+
{ id: "in1", role: "user", content: "Hi", createdAt: "2026-01-01T00:00:00.000Z" },
|
|
137
|
+
{ id: "op1", role: "operator", content: "Hello, Sam here.", createdAt: "2026-01-01T00:00:01.000Z" },
|
|
138
|
+
] satisfies AgentWidgetMessage[],
|
|
139
|
+
});
|
|
140
|
+
await settle();
|
|
141
|
+
expect(row(mount, "in1")!.getAttribute("data-message-role")).toBe("user");
|
|
142
|
+
expect(row(mount, "op1")!.getAttribute("data-message-role")).toBe("operator");
|
|
143
|
+
expect(row(mount, "op1")!.querySelector(".persona-message-operator-bubble")).not.toBeNull();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("shrink-wraps operator rows even when the assistant is full width", async () => {
|
|
147
|
+
const { mount, controller } = makeController({
|
|
148
|
+
layout: { messages: { assistant: { width: "full" } } },
|
|
149
|
+
});
|
|
150
|
+
controller.injectAssistantMessage({ id: "a1", content: "Let me check." });
|
|
151
|
+
controller.injectMessage({ id: "op1", role: "operator", content: "Sam here." });
|
|
152
|
+
await settle();
|
|
153
|
+
expect(row(mount, "a1")!.getAttribute("data-message-width")).toBe("full");
|
|
154
|
+
const operatorRow = row(mount, "op1")!;
|
|
155
|
+
expect(operatorRow.getAttribute("data-message-width")).toBe("content");
|
|
156
|
+
// It keeps the assistant's full-width cap, so a long message can still fill the column.
|
|
157
|
+
expect(operatorRow.style.getPropertyValue("--persona-message-row-max-width")).toBe(
|
|
158
|
+
"var(--persona-message-operator-max-width, var(--persona-message-assistant-max-width, 100%))"
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("operator bubble styles", () => {
|
|
164
|
+
let stylesheet: HTMLStyleElement;
|
|
165
|
+
|
|
166
|
+
beforeEach(() => {
|
|
167
|
+
window.scrollTo = vi.fn();
|
|
168
|
+
stylesheet = document.createElement("style");
|
|
169
|
+
stylesheet.textContent = readFileSync("src/styles/widget.css", "utf8");
|
|
170
|
+
document.head.append(stylesheet);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
afterEach(() => {
|
|
174
|
+
controllers.splice(0).forEach((controller) => controller.destroy());
|
|
175
|
+
mounts.splice(0).forEach((mount) => mount.remove());
|
|
176
|
+
stylesheet.remove();
|
|
177
|
+
document.body.innerHTML = "";
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it.each(["bubble", "flat", "minimal"] as const)(
|
|
181
|
+
"outlines the V5 operator bubble in the %s layout",
|
|
182
|
+
async (layout) => {
|
|
183
|
+
const { mount, controller } = makeController({
|
|
184
|
+
future: { v5Defaults: true },
|
|
185
|
+
layout: { messages: { layout } },
|
|
186
|
+
});
|
|
187
|
+
controller.injectMessage({ id: "op1", role: "operator", content: "Sam here." });
|
|
188
|
+
await settle();
|
|
189
|
+
const bubble = row(mount, "op1")!.querySelector<HTMLElement>("#bubble-op1")!;
|
|
190
|
+
const style = getComputedStyle(bubble);
|
|
191
|
+
expect(style.borderTopStyle).toBe("solid");
|
|
192
|
+
// Width and fill come from the V5 operator tokens on the themed ancestor.
|
|
193
|
+
const token = (name: string) => {
|
|
194
|
+
for (let node: HTMLElement | null = bubble; node; node = node.parentElement) {
|
|
195
|
+
const value = node.style.getPropertyValue(name).trim();
|
|
196
|
+
if (value) return value;
|
|
197
|
+
}
|
|
198
|
+
return "";
|
|
199
|
+
};
|
|
200
|
+
expect(token("--persona-components-message-operator-borderWidth")).toBe("1px");
|
|
201
|
+
expect(token("--persona-components-message-operator-background")).toBe("transparent");
|
|
202
|
+
}
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
it.each(["flat", "minimal"] as const)("keeps the V4 %s operator borderless", async (layout) => {
|
|
206
|
+
const { mount, controller } = makeController({ layout: { messages: { layout } } });
|
|
207
|
+
controller.injectMessage({ id: "op1", role: "operator", content: "Sam here." });
|
|
208
|
+
await settle();
|
|
209
|
+
const bubble = row(mount, "op1")!.querySelector<HTMLElement>("#bubble-op1")!;
|
|
210
|
+
expect(getComputedStyle(bubble).borderTopStyle).not.toBe("solid");
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Hosts draw per-message badges (delivery status, author label, errors) with a
|
|
216
|
+
* `renderMessage` plugin: it wraps the default bubble, appends a
|
|
217
|
+
* `data-preserve-runtime` slot, and the host fills that slot itself. These
|
|
218
|
+
* tests pin the route for operator rows.
|
|
219
|
+
*/
|
|
220
|
+
describe("operator messages through a renderMessage plugin slot", () => {
|
|
221
|
+
beforeEach(() => {
|
|
222
|
+
window.scrollTo = vi.fn();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
afterEach(() => {
|
|
226
|
+
controllers.splice(0).forEach((controller) => controller.destroy());
|
|
227
|
+
mounts.splice(0).forEach((mount) => mount.remove());
|
|
228
|
+
document.body.innerHTML = "";
|
|
229
|
+
vi.restoreAllMocks();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const slotPlugin = (seen: AgentWidgetMessage["role"][]): AgentWidgetPlugin => ({
|
|
233
|
+
id: "delivery-status",
|
|
234
|
+
renderMessage: ({ message, defaultRenderer }) => {
|
|
235
|
+
seen.push(message.role);
|
|
236
|
+
if (message.role !== "operator" && message.role !== "assistant") return null;
|
|
237
|
+
const element = defaultRenderer();
|
|
238
|
+
const slot = document.createElement("div");
|
|
239
|
+
slot.setAttribute("data-preserve-runtime", "true");
|
|
240
|
+
slot.setAttribute("data-status-slot", message.id);
|
|
241
|
+
element.insertBefore(slot, element.querySelector(":scope > .persona-message-actions"));
|
|
242
|
+
return element;
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const slot = (mount: HTMLElement, id: string) =>
|
|
247
|
+
row(mount, id)?.querySelector<HTMLElement>(`[data-status-slot="${id}"]`) ?? null;
|
|
248
|
+
|
|
249
|
+
/** What a host does once the row is on screen: fill the slot with its own UI. */
|
|
250
|
+
const fillSlot = (target: HTMLElement, text: string) => {
|
|
251
|
+
const badge = document.createElement("span");
|
|
252
|
+
badge.className = "host-status";
|
|
253
|
+
badge.textContent = text;
|
|
254
|
+
target.replaceChildren(badge);
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
it("runs the hook for operator rows and uses the element it returns", async () => {
|
|
258
|
+
const seen: AgentWidgetMessage["role"][] = [];
|
|
259
|
+
const { mount, controller } = makeController({ plugins: [slotPlugin(seen)] });
|
|
260
|
+
controller.injectUserMessage({ id: "u1", content: "Where is my order?" });
|
|
261
|
+
controller.injectMessage({ id: "op1", role: "operator", content: "Sam here, I refunded it." });
|
|
262
|
+
await settle();
|
|
263
|
+
|
|
264
|
+
expect(seen).toContain("operator");
|
|
265
|
+
expect(seen).toContain("user");
|
|
266
|
+
const operatorRow = row(mount, "op1")!;
|
|
267
|
+
expect(operatorRow.getAttribute("data-message-role")).toBe("operator");
|
|
268
|
+
expect(slot(mount, "op1")).not.toBeNull();
|
|
269
|
+
// The plugin returned null for the user row, so it keeps the default bubble.
|
|
270
|
+
expect(row(mount, "u1")!.querySelector("[data-status-slot]")).toBeNull();
|
|
271
|
+
expect(row(mount, "u1")!.querySelector(".persona-message-user-bubble")).not.toBeNull();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("keeps the operator bubble class when a plugin wraps the default renderer", async () => {
|
|
275
|
+
const { mount, controller } = makeController({ plugins: [slotPlugin([])] });
|
|
276
|
+
controller.injectMessage({ id: "op1", role: "operator", content: "Sam here." });
|
|
277
|
+
await settle();
|
|
278
|
+
|
|
279
|
+
const bubble = row(mount, "op1")!.querySelector<HTMLElement>("#bubble-op1")!;
|
|
280
|
+
expect(bubble.classList.contains("persona-message-operator-bubble")).toBe(true);
|
|
281
|
+
expect(bubble.classList.contains("persona-message-assistant-bubble")).toBe(false);
|
|
282
|
+
expect(bubble.getAttribute("data-persona-theme-zone")).toBe("operator-message");
|
|
283
|
+
expect(bubble.contains(slot(mount, "op1"))).toBe(true);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("keeps the host-filled slot node across controller.update()", async () => {
|
|
287
|
+
const seen: AgentWidgetMessage["role"][] = [];
|
|
288
|
+
const { mount, controller } = makeController({ plugins: [slotPlugin(seen)] });
|
|
289
|
+
controller.injectMessage({ id: "op1", role: "operator", content: "Sam here." });
|
|
290
|
+
await settle();
|
|
291
|
+
const first = slot(mount, "op1")!;
|
|
292
|
+
fillSlot(first, "Delivered");
|
|
293
|
+
const rendersBefore = seen.length;
|
|
294
|
+
|
|
295
|
+
controller.update({ copy: { welcomeTitle: "Support" } });
|
|
296
|
+
await settle();
|
|
297
|
+
controller.update({ theme: { components: { message: { operator: { borderRadius: "4px" } } } } });
|
|
298
|
+
await settle();
|
|
299
|
+
|
|
300
|
+
// The plugin rendered the row again, and the morph kept the live slot.
|
|
301
|
+
expect(seen.length).toBeGreaterThan(rendersBefore);
|
|
302
|
+
expect(slot(mount, "op1")).toBe(first);
|
|
303
|
+
expect(first.isConnected).toBe(true);
|
|
304
|
+
expect(first.querySelector(".host-status")?.textContent).toBe("Delivered");
|
|
305
|
+
expect(row(mount, "op1")!.querySelector(".persona-message-operator-bubble")).not.toBeNull();
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("keeps the host-filled slot node when injectMessage upserts the same id", async () => {
|
|
309
|
+
const seen: AgentWidgetMessage["role"][] = [];
|
|
310
|
+
const { mount, controller } = makeController({ plugins: [slotPlugin(seen)] });
|
|
311
|
+
controller.injectMessage({ id: "op1", role: "operator", content: "Sam here." });
|
|
312
|
+
await settle();
|
|
313
|
+
const first = slot(mount, "op1")!;
|
|
314
|
+
fillSlot(first, "Sending");
|
|
315
|
+
|
|
316
|
+
// Same id, same text: a status-only change on the host side.
|
|
317
|
+
controller.injectMessage({ id: "op1", role: "operator", content: "Sam here." });
|
|
318
|
+
await settle();
|
|
319
|
+
expect(slot(mount, "op1")).toBe(first);
|
|
320
|
+
|
|
321
|
+
// Same id, new text: the plugin renders the row again and the slot survives.
|
|
322
|
+
const rendersBefore = seen.length;
|
|
323
|
+
controller.injectMessage({ id: "op1", role: "operator", content: "Sam here, refund sent." });
|
|
324
|
+
await settle();
|
|
325
|
+
expect(seen.length).toBeGreaterThan(rendersBefore);
|
|
326
|
+
|
|
327
|
+
expect(mount.querySelectorAll('[data-wrapper-id="op1"]')).toHaveLength(1);
|
|
328
|
+
expect(slot(mount, "op1")).toBe(first);
|
|
329
|
+
// The host's content is still there; the host then updates it in place.
|
|
330
|
+
expect(first.querySelector(".host-status")?.textContent).toBe("Sending");
|
|
331
|
+
fillSlot(first, "Delivered");
|
|
332
|
+
expect(slot(mount, "op1")?.querySelector(".host-status")?.textContent).toBe("Delivered");
|
|
333
|
+
expect(row(mount, "op1")!.querySelector("#bubble-op1")?.textContent).toContain("refund sent");
|
|
334
|
+
expect(row(mount, "op1")!.querySelector(".persona-message-operator-bubble")).not.toBeNull();
|
|
335
|
+
});
|
|
336
|
+
});
|
package/src/ui.ts
CHANGED
|
@@ -112,6 +112,7 @@ import {
|
|
|
112
112
|
type MessageEditEditor
|
|
113
113
|
} from "./components/message-edit-editor";
|
|
114
114
|
import {
|
|
115
|
+
canResubmitUserMessage,
|
|
115
116
|
findPrecedingUserMessageId,
|
|
116
117
|
findRetryableAssistantMessageId,
|
|
117
118
|
isEditableUserMessage
|
|
@@ -1408,8 +1409,9 @@ export const createAgentExperience = (
|
|
|
1408
1409
|
const messageActionCallbacks: MessageActionCallbacks = {
|
|
1409
1410
|
onCopy: (message: AgentWidgetMessage) => {
|
|
1410
1411
|
eventBus.emit("message:copy", message);
|
|
1411
|
-
// Send copy feedback to API if in client token mode
|
|
1412
|
-
|
|
1412
|
+
// Send copy feedback to API if in client token mode. Operator messages
|
|
1413
|
+
// are host-injected and unknown to the API, so they never report.
|
|
1414
|
+
if (session?.isClientTokenMode() && message.role !== "operator") {
|
|
1413
1415
|
session.submitMessageFeedback(message.id, 'copy').catch((error) => {
|
|
1414
1416
|
if (config.debug) {
|
|
1415
1417
|
// eslint-disable-next-line no-console
|
|
@@ -6097,21 +6099,26 @@ export const createAgentExperience = (
|
|
|
6097
6099
|
wrapper: HTMLElement,
|
|
6098
6100
|
role: AgentWidgetMessage["role"]
|
|
6099
6101
|
): void => {
|
|
6102
|
+
// Operator rows sit on the assistant's side and share its max width, but
|
|
6103
|
+
// always shrink-wrap: a full-column operator bubble reads as an input.
|
|
6100
6104
|
const sizingRole = role === "user" ? "user" : "assistant";
|
|
6101
6105
|
const roleLayout = config.layout?.messages?.[sizingRole];
|
|
6102
|
-
const
|
|
6106
|
+
const configuredWidth = roleLayout?.width ?? "content";
|
|
6107
|
+
const width = role === "operator" ? "content" : configuredWidth;
|
|
6103
6108
|
// Only an explicit config value is stamped inline; the 85%/100% defaults
|
|
6104
6109
|
// live in widget.css so `components.message.<role>.maxWidth` can win.
|
|
6110
|
+
const fullMaxWidth = role === "operator"
|
|
6111
|
+
? "var(--persona-message-operator-max-width, var(--persona-message-assistant-max-width, 100%))"
|
|
6112
|
+
: `var(--persona-message-${sizingRole}-max-width, 100%)`;
|
|
6105
6113
|
const maxWidth =
|
|
6106
|
-
roleLayout?.maxWidth ?? (
|
|
6107
|
-
? `var(--persona-message-${sizingRole}-max-width, 100%)`
|
|
6108
|
-
: undefined);
|
|
6114
|
+
roleLayout?.maxWidth ?? (configuredWidth === "full" ? fullMaxWidth : undefined);
|
|
6109
6115
|
|
|
6110
6116
|
wrapper.classList.add("persona-message-row");
|
|
6111
6117
|
wrapper.classList.remove(
|
|
6112
6118
|
"persona-message-row-user",
|
|
6113
6119
|
"persona-message-row-assistant",
|
|
6114
6120
|
"persona-message-row-system",
|
|
6121
|
+
"persona-message-row-operator",
|
|
6115
6122
|
"persona-message-width-content",
|
|
6116
6123
|
"persona-message-width-full"
|
|
6117
6124
|
);
|
|
@@ -6248,6 +6255,10 @@ export const createAgentExperience = (
|
|
|
6248
6255
|
? findRetryableAssistantMessageId(messages)
|
|
6249
6256
|
: null;
|
|
6250
6257
|
const editEnabled = actionsCfg?.showEdit === true && actionsCfg.enabled !== false;
|
|
6258
|
+
let lastOperatorIndex = -1;
|
|
6259
|
+
messages.forEach((message, index) => {
|
|
6260
|
+
if (message.role === "operator") lastOperatorIndex = index;
|
|
6261
|
+
});
|
|
6251
6262
|
const quoteEnabled = actionsCfg?.showQuote === true && actionsCfg.enabled !== false;
|
|
6252
6263
|
/** The live editor, re-grafted into its morphed wrapper after the pass. */
|
|
6253
6264
|
let editHydrate: { messageId: string; element: HTMLElement } | null = null;
|
|
@@ -6289,7 +6300,7 @@ export const createAgentExperience = (
|
|
|
6289
6300
|
);
|
|
6290
6301
|
};
|
|
6291
6302
|
|
|
6292
|
-
messages.forEach((message) => {
|
|
6303
|
+
messages.forEach((message, messageIndex) => {
|
|
6293
6304
|
if (isDroppedCompletedRow(message)) {
|
|
6294
6305
|
return;
|
|
6295
6306
|
}
|
|
@@ -6356,7 +6367,7 @@ export const createAgentExperience = (
|
|
|
6356
6367
|
isLastAssistant: message.id === lastAssistantId,
|
|
6357
6368
|
canRegenerate: retryableAssistantId === message.id &&
|
|
6358
6369
|
(actionsCfg?.showRegenerate === true || message.dispatchError === true),
|
|
6359
|
-
canEdit: editEnabled && isEditableUserMessage(message),
|
|
6370
|
+
canEdit: editEnabled && messageIndex > lastOperatorIndex && isEditableUserMessage(message),
|
|
6360
6371
|
canQuote:
|
|
6361
6372
|
quoteEnabled && !message.variant && (message.content?.trim().length ?? 0) > 0,
|
|
6362
6373
|
};
|
|
@@ -6957,10 +6968,23 @@ export const createAgentExperience = (
|
|
|
6957
6968
|
// "the assistant is already visibly responding": otherwise the transcript
|
|
6958
6969
|
// goes blank for the whole tool phase. When those rows are shown, their own
|
|
6959
6970
|
// bubble carries the loading animation and the standalone dots stay hidden.
|
|
6971
|
+
//
|
|
6972
|
+
// A pending `ask_user_question` call is visible only once its question has
|
|
6973
|
+
// rendered: a plugin bubble mounted for it this pass, or the built-in sheet
|
|
6974
|
+
// mounted for its tool call. Until then (arguments still streaming, a plugin
|
|
6975
|
+
// returning null for a partial payload, the sheet waiting on the pause) it
|
|
6976
|
+
// renders nothing, so it must not hide the dots either.
|
|
6977
|
+
const isUnrenderedAskRow = (msg: AgentWidgetMessage): boolean =>
|
|
6978
|
+
!(msg.toolCall?.id && liveAskToolIds.has(msg.toolCall.id)) &&
|
|
6979
|
+
!askPluginHydrate.some((entry) => entry.messageId === msg.id);
|
|
6960
6980
|
const isHiddenActivityRow = (msg: AgentWidgetMessage): boolean =>
|
|
6961
|
-
(msg
|
|
6962
|
-
|
|
6963
|
-
|
|
6981
|
+
isAskUserQuestionMessage(msg) &&
|
|
6982
|
+
msg.agentMetadata?.askUserQuestionAnswered !== true &&
|
|
6983
|
+
config.features?.askUserQuestion?.enabled !== false
|
|
6984
|
+
? isUnrenderedAskRow(msg)
|
|
6985
|
+
: (msg.variant === "tool" && !showToolCalls) ||
|
|
6986
|
+
(msg.variant === "reasoning" && !showReasoning) ||
|
|
6987
|
+
isDroppedCompletedRow(msg);
|
|
6964
6988
|
const hasStreamingAssistantMessage = messages.some(
|
|
6965
6989
|
(msg) => msg.role === "assistant" && msg.streaming && !isHiddenActivityRow(msg)
|
|
6966
6990
|
);
|
|
@@ -10053,7 +10077,8 @@ export const createAgentExperience = (
|
|
|
10053
10077
|
function beginMessageEdit(messageId: string): void {
|
|
10054
10078
|
if (config.messageActions?.showEdit !== true) return;
|
|
10055
10079
|
const message = session.getMessages().find((m) => m.id === messageId);
|
|
10056
|
-
if (!message || !isEditableUserMessage(message)
|
|
10080
|
+
if (!message || !isEditableUserMessage(message) ||
|
|
10081
|
+
!canResubmitUserMessage(session.getMessages(), messageId)) return;
|
|
10057
10082
|
if (editingMessageId === messageId) return;
|
|
10058
10083
|
cancelMessageEdit();
|
|
10059
10084
|
editingMessageId = messageId;
|