@runtypelabs/persona 4.7.0 → 4.9.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 +1 -0
- package/dist/animations/glyph-cycle.cjs +2 -2
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/glyph-cycle.js +2 -2
- package/dist/animations/{types-CSmiKRVa.d.cts → types-4ROVJ1gA.d.cts} +85 -3
- package/dist/animations/{types-CSmiKRVa.d.ts → types-4ROVJ1gA.d.ts} +85 -3
- package/dist/animations/wipe.cjs +2 -2
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/chunk-5EIIHQLQ.js +1 -0
- 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 +13 -13
- package/dist/codegen.js +14 -14
- 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 +99 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1507 -182
- package/dist/index.d.ts +1507 -182
- package/dist/index.global.js +88 -56
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +99 -68
- 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 +3 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/markdown-parsers.js +24 -24
- 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/runtype-tts-entry-UJAEF7NZ.js +1 -0
- package/dist/runtype-tts.js +1 -1
- package/dist/session-reconnect-JKIJBHS5.js +1 -0
- package/dist/smart-dom-reader.cjs +18 -17
- package/dist/smart-dom-reader.d.cts +1260 -16
- package/dist/smart-dom-reader.d.ts +1260 -16
- package/dist/smart-dom-reader.js +18 -17
- package/dist/testing.cjs +3 -3
- package/dist/testing.js +3 -3
- package/dist/theme-editor-preview.cjs +260 -58
- package/dist/theme-editor-preview.d.cts +1234 -15
- package/dist/theme-editor-preview.d.ts +1234 -15
- package/dist/theme-editor-preview.js +87 -58
- package/dist/theme-editor.cjs +10 -10
- package/dist/theme-editor.d.cts +1226 -15
- package/dist/theme-editor.d.ts +1226 -15
- package/dist/theme-editor.js +10 -10
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +76 -0
- package/dist/theme-reference.d.ts +76 -0
- package/dist/theme-reference.js +1 -1
- package/dist/voice-worklet-player.cjs +2 -2
- package/dist/voice-worklet-player.js +2 -2
- package/dist/webmcp-polyfill.js +2 -2
- package/dist/widget.css +1 -1
- package/package.json +16 -5
- package/src/artifacts-session.test.ts +178 -0
- package/src/client.test.ts +255 -1
- package/src/client.ts +157 -60
- package/src/components/artifact-card.test.ts +333 -0
- package/src/components/artifact-card.ts +75 -28
- package/src/components/artifact-inline.test.ts +1328 -0
- package/src/components/artifact-inline.ts +920 -0
- package/src/components/artifact-pane.test.ts +1042 -0
- package/src/components/artifact-pane.ts +440 -131
- package/src/components/artifact-preview.test.ts +1155 -0
- package/src/components/artifact-preview.ts +994 -0
- 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/components/pill-composer-builder.test.ts +6 -2
- package/src/components/pill-composer-builder.ts +6 -6
- package/src/components/reasoning-bubble.ts +1 -13
- package/src/components/registry.ts +38 -3
- package/src/components/tool-bubble.ts +1 -13
- 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/defaults.ts +1 -0
- package/src/generated/runtype-openapi-contract.ts +55 -3
- package/src/index-core.ts +46 -1
- package/src/index-global.ts +51 -0
- package/src/index.ts +8 -0
- package/src/markdown-parsers-loader.test.ts +158 -0
- package/src/markdown-parsers-loader.ts +83 -27
- package/src/plugin-kit.test.ts +40 -0
- package/src/plugin-kit.ts +39 -5
- package/src/runtime/host-layout.test.ts +163 -0
- package/src/runtime/host-layout.ts +110 -7
- package/src/runtime/init.ts +18 -61
- package/src/session.mentions.test.ts +175 -0
- package/src/session.test.ts +52 -4
- package/src/session.ts +197 -27
- 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 +1002 -152
- package/src/theme-editor/preview.ts +2 -0
- package/src/theme-editor/sections.test.ts +26 -1
- package/src/theme-editor/sections.ts +13 -5
- package/src/theme-reference.ts +2 -2
- package/src/tool-call-display-defaults.test.ts +1 -0
- package/src/types/theme.ts +79 -0
- package/src/types.ts +1041 -17
- package/src/ui.artifact-pane-gating.test.ts +636 -0
- package/src/ui.component-directive.test.ts +104 -0
- package/src/ui.composer-bar.test.ts +60 -2
- package/src/ui.detached-panel.test.ts +1049 -0
- package/src/ui.mention-submit.test.ts +235 -0
- package/src/ui.tool-display.test.ts +51 -0
- package/src/ui.ts +998 -159
- package/src/utils/artifact-custom-actions.ts +128 -0
- package/src/utils/artifact-display.test.ts +42 -0
- package/src/utils/artifact-display.ts +84 -0
- package/src/utils/artifact-file.test.ts +116 -0
- package/src/utils/artifact-file.ts +117 -0
- package/src/utils/artifact-gate.test.ts +112 -5
- package/src/utils/artifact-gate.ts +39 -14
- package/src/utils/artifact-loading-status.ts +55 -0
- package/src/utils/artifact-status-label.ts +190 -0
- package/src/utils/buttons.ts +7 -1
- package/src/utils/chunk-loader.test.ts +97 -0
- package/src/utils/chunk-loader.ts +88 -0
- package/src/utils/code-highlight.test.ts +186 -0
- package/src/utils/code-highlight.ts +400 -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/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/icons.ts +2 -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/roving-tablist.test.ts +152 -0
- package/src/utils/roving-tablist.ts +111 -0
- package/src/utils/spinner.ts +45 -0
- package/src/utils/theme.test.ts +76 -4
- package/src/utils/theme.ts +7 -0
- package/src/utils/tokens.ts +118 -11
- package/src/utils/tool-loading-animation.test.ts +32 -0
- package/src/utils/tool-loading-animation.ts +24 -0
- package/dist/chunk-DFBSCFYN.js +0 -1
- package/dist/runtype-tts-entry-HFUV2UF7.js +0 -1
- package/dist/session-reconnect-U77QFUR7.js +0 -1
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect, vi } from "vitest";
|
|
4
|
+
import { ContextMentionManager, refFromItem } from "./context-mention-manager";
|
|
5
|
+
import * as mentionLlmFormat from "./mention-llm-format";
|
|
6
|
+
import type {
|
|
7
|
+
AgentWidgetConfig,
|
|
8
|
+
AgentWidgetContextMentionConfig,
|
|
9
|
+
AgentWidgetContextMentionItem,
|
|
10
|
+
AgentWidgetContextMentionPayload,
|
|
11
|
+
AgentWidgetContextMentionSource,
|
|
12
|
+
} from "../types";
|
|
13
|
+
|
|
14
|
+
const tick = () => new Promise((r) => setTimeout(r, 0));
|
|
15
|
+
|
|
16
|
+
const item = (id: string, label = id): AgentWidgetContextMentionItem => ({ id, label });
|
|
17
|
+
|
|
18
|
+
function makeManager(
|
|
19
|
+
overrides: Partial<AgentWidgetContextMentionConfig> = {},
|
|
20
|
+
sourceResolve?: AgentWidgetContextMentionSource["resolve"]
|
|
21
|
+
) {
|
|
22
|
+
const contextRow = document.createElement("div");
|
|
23
|
+
const mentionConfig: AgentWidgetContextMentionConfig = {
|
|
24
|
+
enabled: true,
|
|
25
|
+
sources: [],
|
|
26
|
+
...overrides,
|
|
27
|
+
};
|
|
28
|
+
const announce = vi.fn();
|
|
29
|
+
const announceError = vi.fn();
|
|
30
|
+
const manager = new ContextMentionManager({
|
|
31
|
+
mentionConfig,
|
|
32
|
+
contextRow,
|
|
33
|
+
getMessages: () => [],
|
|
34
|
+
getConfig: () => ({}) as AgentWidgetConfig,
|
|
35
|
+
getComposerText: () => "hello",
|
|
36
|
+
announce,
|
|
37
|
+
announceError,
|
|
38
|
+
});
|
|
39
|
+
const source: AgentWidgetContextMentionSource = {
|
|
40
|
+
id: "files",
|
|
41
|
+
label: "Files",
|
|
42
|
+
search: () => [],
|
|
43
|
+
resolve: sourceResolve ?? (async () => ({ llmAppend: "CONTENT" })),
|
|
44
|
+
};
|
|
45
|
+
return { manager, contextRow, mentionConfig, source, announce, announceError };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe("ContextMentionManager", () => {
|
|
49
|
+
it("adds a chip and eagerly resolves on select (cached)", async () => {
|
|
50
|
+
const resolve = vi.fn(async () => ({ llmAppend: "FILE BODY" }));
|
|
51
|
+
const { manager, contextRow, source } = makeManager({}, resolve);
|
|
52
|
+
expect(manager.add(source, item("App.tsx"))).toBe(true);
|
|
53
|
+
expect(contextRow.querySelectorAll("[data-persona-mention-chip]")).toHaveLength(1);
|
|
54
|
+
await tick();
|
|
55
|
+
expect(resolve).toHaveBeenCalledTimes(1);
|
|
56
|
+
|
|
57
|
+
const bundle = await manager.collectForSubmit().finalize();
|
|
58
|
+
expect(resolve).toHaveBeenCalledTimes(1); // re-used the cached payload
|
|
59
|
+
expect(bundle.blocks).toEqual(["```App.tsx\nFILE BODY\n```"]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("shows the context row on the first add (visibility computed after tracking)", () => {
|
|
63
|
+
const { manager, contextRow, source } = makeManager();
|
|
64
|
+
expect(manager.add(source, item("App.tsx"))).toBe(true);
|
|
65
|
+
expect(contextRow.style.display).toBe("flex");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("formats multiple mentions in order via llmFormat, numbering document blocks 1-based", async () => {
|
|
69
|
+
const { manager } = makeManager({ llmFormat: "document" });
|
|
70
|
+
const source: AgentWidgetContextMentionSource = {
|
|
71
|
+
id: "files",
|
|
72
|
+
label: "Files",
|
|
73
|
+
search: () => [],
|
|
74
|
+
resolve: async (i) => ({ llmAppend: `body of ${i.label}` }),
|
|
75
|
+
};
|
|
76
|
+
manager.add(source, item("a", "A.ts"));
|
|
77
|
+
manager.add(source, item("b", "B.ts"));
|
|
78
|
+
await tick();
|
|
79
|
+
const bundle = await manager.collectForSubmit().finalize();
|
|
80
|
+
expect(bundle.blocks).toEqual([
|
|
81
|
+
'<document index="1">\n<source>A.ts</source>\n<document_content>\nbody of A.ts\n</document_content>\n</document>',
|
|
82
|
+
'<document index="2">\n<source>B.ts</source>\n<document_content>\nbody of B.ts\n</document_content>\n</document>',
|
|
83
|
+
]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("reuses the in-flight resolve at submit (no duplicate fetch, survives clear)", async () => {
|
|
87
|
+
// Resolve stays pending until we release it — mirrors a submit that fires
|
|
88
|
+
// before the select-time resolve has settled.
|
|
89
|
+
let release!: (v: AgentWidgetContextMentionPayload) => void;
|
|
90
|
+
const resolve = vi.fn(
|
|
91
|
+
() => new Promise<AgentWidgetContextMentionPayload>((r) => (release = r))
|
|
92
|
+
);
|
|
93
|
+
const { manager, contextRow, source } = makeManager({}, resolve);
|
|
94
|
+
manager.add(source, item("App.tsx"));
|
|
95
|
+
expect(resolve).toHaveBeenCalledTimes(1);
|
|
96
|
+
|
|
97
|
+
// Submit path: collect (detaches ownership) then the UI's post-send clear().
|
|
98
|
+
const { finalize } = manager.collectForSubmit();
|
|
99
|
+
expect(contextRow.querySelectorAll("[data-persona-mention-chip]")).toHaveLength(0);
|
|
100
|
+
manager.clear(); // must NOT abort the collected in-flight resolve
|
|
101
|
+
|
|
102
|
+
// The select-time resolve settles after collect+clear; finalize reuses it.
|
|
103
|
+
release({ llmAppend: "FILE BODY" });
|
|
104
|
+
const bundle = await finalize();
|
|
105
|
+
expect(resolve).toHaveBeenCalledTimes(1); // never re-fetched
|
|
106
|
+
expect(bundle.blocks).toEqual(["```App.tsx\nFILE BODY\n```"]);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("rejects duplicates", () => {
|
|
110
|
+
const onMentionRejected = vi.fn();
|
|
111
|
+
const { manager, source } = makeManager({ onMentionRejected });
|
|
112
|
+
manager.add(source, item("a"));
|
|
113
|
+
expect(manager.add(source, item("a"))).toBe(false);
|
|
114
|
+
expect(onMentionRejected).toHaveBeenCalledWith(expect.objectContaining({ id: "a" }), "duplicate");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("enforces maxMentions", () => {
|
|
118
|
+
const onMentionRejected = vi.fn();
|
|
119
|
+
const { manager, source } = makeManager({ maxMentions: 1, onMentionRejected });
|
|
120
|
+
expect(manager.add(source, item("a"))).toBe(true);
|
|
121
|
+
expect(manager.add(source, item("b"))).toBe(false);
|
|
122
|
+
expect(onMentionRejected).toHaveBeenCalledWith(expect.objectContaining({ id: "b" }), "limit");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("aborts an in-flight resolve when the chip is removed", async () => {
|
|
126
|
+
let captured: AbortSignal | undefined;
|
|
127
|
+
const resolve = vi.fn(
|
|
128
|
+
(_i, ctx) =>
|
|
129
|
+
new Promise<never>(() => {
|
|
130
|
+
captured = ctx.signal;
|
|
131
|
+
})
|
|
132
|
+
);
|
|
133
|
+
const { manager, contextRow, source } = makeManager({}, resolve as never);
|
|
134
|
+
manager.add(source, item("App.tsx"));
|
|
135
|
+
await tick();
|
|
136
|
+
expect(captured?.aborted).toBe(false);
|
|
137
|
+
(contextRow.querySelector(".persona-mention-chip-remove") as HTMLButtonElement).click();
|
|
138
|
+
expect(captured?.aborted).toBe(true);
|
|
139
|
+
expect(manager.hasMentions()).toBe(false);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("removeLast pops the most recent chip", () => {
|
|
143
|
+
const { manager, contextRow, source } = makeManager();
|
|
144
|
+
manager.add(source, item("a"));
|
|
145
|
+
manager.add(source, item("b"));
|
|
146
|
+
expect(contextRow.querySelectorAll(".persona-mention-chip")).toHaveLength(2);
|
|
147
|
+
expect(manager.removeLast()).toBe(true);
|
|
148
|
+
const labels = Array.from(
|
|
149
|
+
contextRow.querySelectorAll(".persona-mention-chip-label")
|
|
150
|
+
).map((el) => el.textContent);
|
|
151
|
+
expect(labels).toEqual(["a"]);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("defers resolve to submit for resolveOn:'submit' sources", async () => {
|
|
155
|
+
const resolve = vi.fn(async () => ({ llmAppend: "LIVE PAGE" }));
|
|
156
|
+
const { manager } = makeManager();
|
|
157
|
+
const source: AgentWidgetContextMentionSource = {
|
|
158
|
+
id: "page",
|
|
159
|
+
label: "Page",
|
|
160
|
+
search: () => [],
|
|
161
|
+
resolve,
|
|
162
|
+
resolveOn: "submit",
|
|
163
|
+
};
|
|
164
|
+
manager.add(source, item("hero"));
|
|
165
|
+
await tick();
|
|
166
|
+
expect(resolve).not.toHaveBeenCalled(); // deferred
|
|
167
|
+
const bundle = await manager.collectForSubmit().finalize();
|
|
168
|
+
expect(resolve).toHaveBeenCalledTimes(1); // resolved at submit
|
|
169
|
+
expect(bundle.blocks).toEqual(["```hero\nLIVE PAGE\n```"]);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("drops a failed select-resolve and fires onMentionResolveError; still sends", async () => {
|
|
173
|
+
const onMentionResolveError = vi.fn();
|
|
174
|
+
const resolve = vi.fn(async () => {
|
|
175
|
+
throw new Error("boom");
|
|
176
|
+
});
|
|
177
|
+
const { manager } = makeManager({ onMentionResolveError }, resolve);
|
|
178
|
+
manager.add(makeSource(resolve), item("bad"));
|
|
179
|
+
await tick();
|
|
180
|
+
expect(onMentionResolveError).toHaveBeenCalledWith(
|
|
181
|
+
expect.objectContaining({ id: "bad" }),
|
|
182
|
+
expect.any(Error)
|
|
183
|
+
);
|
|
184
|
+
const bundle = await manager.collectForSubmit().finalize();
|
|
185
|
+
expect(bundle.blocks).toEqual([]); // dropped
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("announces a select-resolve failure through the assertive region", async () => {
|
|
189
|
+
const resolve = vi.fn(async () => {
|
|
190
|
+
throw new Error("boom");
|
|
191
|
+
});
|
|
192
|
+
const { manager, announceError } = makeManager({}, resolve);
|
|
193
|
+
manager.add(makeSource(resolve), item("bad", "Bad File"));
|
|
194
|
+
await tick();
|
|
195
|
+
expect(announceError).toHaveBeenCalledWith(
|
|
196
|
+
"Couldn't attach Bad File to context"
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("forwards the resolved payload to renderMentionChip once ready", async () => {
|
|
201
|
+
const seen: (AgentWidgetContextMentionPayload | undefined)[] = [];
|
|
202
|
+
const renderMentionChip = vi.fn((ctx) => {
|
|
203
|
+
seen.push(ctx.payload);
|
|
204
|
+
const el = document.createElement("span");
|
|
205
|
+
el.className = "custom-chip";
|
|
206
|
+
el.dataset.status = ctx.status;
|
|
207
|
+
return el;
|
|
208
|
+
});
|
|
209
|
+
const resolve = vi.fn(async () => ({ llmAppend: "FILE BODY" }));
|
|
210
|
+
const { manager, source } = makeManager({ renderMentionChip }, resolve);
|
|
211
|
+
manager.add(source, item("App.tsx"));
|
|
212
|
+
// First render is the resolving state, before resolve() settles: no payload.
|
|
213
|
+
expect(seen[0]).toBeUndefined();
|
|
214
|
+
await tick();
|
|
215
|
+
// After resolve, the renderer is re-invoked with status "ready" + the payload.
|
|
216
|
+
expect(seen.at(-1)).toEqual({ llmAppend: "FILE BODY" });
|
|
217
|
+
expect(renderMentionChip).toHaveBeenCalledWith(
|
|
218
|
+
expect.objectContaining({
|
|
219
|
+
status: "ready",
|
|
220
|
+
payload: { llmAppend: "FILE BODY" },
|
|
221
|
+
})
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("remove() detaches a custom-rendered chip after setStatus swapped its node", async () => {
|
|
226
|
+
const renderMentionChip = vi.fn((ctx) => {
|
|
227
|
+
const el = document.createElement("span");
|
|
228
|
+
el.className = "custom-chip";
|
|
229
|
+
el.dataset.status = ctx.status;
|
|
230
|
+
return el;
|
|
231
|
+
});
|
|
232
|
+
const resolve = vi.fn(async () => ({ llmAppend: "FILE BODY" }));
|
|
233
|
+
const { manager, contextRow, source } = makeManager({ renderMentionChip }, resolve);
|
|
234
|
+
manager.add(source, item("App.tsx"));
|
|
235
|
+
await tick(); // resolve settles → renderer re-invoked → node replaced
|
|
236
|
+
expect(contextRow.querySelectorAll(".custom-chip")).toHaveLength(1);
|
|
237
|
+
|
|
238
|
+
manager.removeLast();
|
|
239
|
+
expect(contextRow.querySelectorAll(".custom-chip")).toHaveLength(0);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("does not expose a payload to renderMentionChip for resolveOn:'submit' sources", async () => {
|
|
243
|
+
const seen: (AgentWidgetContextMentionPayload | undefined)[] = [];
|
|
244
|
+
const renderMentionChip = vi.fn((ctx) => {
|
|
245
|
+
seen.push(ctx.payload);
|
|
246
|
+
const el = document.createElement("span");
|
|
247
|
+
el.dataset.status = ctx.status;
|
|
248
|
+
return el;
|
|
249
|
+
});
|
|
250
|
+
const { manager } = makeManager({ renderMentionChip });
|
|
251
|
+
const source: AgentWidgetContextMentionSource = {
|
|
252
|
+
id: "page",
|
|
253
|
+
label: "Page",
|
|
254
|
+
search: () => [],
|
|
255
|
+
resolve: async () => ({ llmAppend: "LIVE" }),
|
|
256
|
+
resolveOn: "submit",
|
|
257
|
+
};
|
|
258
|
+
manager.add(source, item("hero"));
|
|
259
|
+
await tick();
|
|
260
|
+
// Submit sources flip straight to "ready" with no payload until send time.
|
|
261
|
+
expect(seen.every((p) => p === undefined)).toBe(true);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("namespaces opt-in context by source + item", async () => {
|
|
265
|
+
const resolve = async () => ({ context: { path: "/x" } });
|
|
266
|
+
const { manager } = makeManager({}, resolve);
|
|
267
|
+
manager.add(makeSource(resolve, "files"), item("App.tsx"));
|
|
268
|
+
await tick();
|
|
269
|
+
const bundle = await manager.collectForSubmit().finalize();
|
|
270
|
+
expect(bundle.context).toEqual({ files: { "App.tsx": { path: "/x" } } });
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// ---- inline mode (track / admit / refFromItem) ---------------------------
|
|
274
|
+
|
|
275
|
+
it("refFromItem builds the stored ref from source + item", () => {
|
|
276
|
+
const source = makeSource(async () => ({}), "files");
|
|
277
|
+
expect(
|
|
278
|
+
refFromItem(source, {
|
|
279
|
+
id: "app",
|
|
280
|
+
label: "App.tsx",
|
|
281
|
+
iconName: "file",
|
|
282
|
+
color: "#f00",
|
|
283
|
+
})
|
|
284
|
+
).toEqual({
|
|
285
|
+
sourceId: "files",
|
|
286
|
+
itemId: "app",
|
|
287
|
+
label: "App.tsx",
|
|
288
|
+
iconName: "file",
|
|
289
|
+
color: "#f00",
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("admit enforces the limit and fires the rejection hook (inline pre-insert gate)", () => {
|
|
294
|
+
const onMentionRejected = vi.fn();
|
|
295
|
+
const { manager, source } = makeManager({ maxMentions: 1, onMentionRejected });
|
|
296
|
+
manager.track("pmention-1", source, item("a"));
|
|
297
|
+
expect(manager.admit(source, item("b"))).toBe(false);
|
|
298
|
+
expect(onMentionRejected).toHaveBeenCalledWith(
|
|
299
|
+
expect.objectContaining({ id: "b" }),
|
|
300
|
+
"limit"
|
|
301
|
+
);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("admit allows a mention under the limit", () => {
|
|
305
|
+
const { manager, source } = makeManager({ maxMentions: 2 });
|
|
306
|
+
manager.track("pmention-1", source, item("a"));
|
|
307
|
+
expect(manager.admit(source, item("b"))).toBe(true);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("admits duplicate inline mentions and dedupes the resolved payload at submit (Slack/Cursor/Claude)", async () => {
|
|
311
|
+
// Inline mode lets the same item appear as several prose tokens; admit only
|
|
312
|
+
// gates the limit, and finalize dedupes the resolved payload by (source, item)
|
|
313
|
+
// so the LLM sees the content once while every token still renders in the sent
|
|
314
|
+
// bubble (refs lists both). No rejection fires for a duplicate here.
|
|
315
|
+
const onMentionRejected = vi.fn();
|
|
316
|
+
const { manager, source } = makeManager({ onMentionRejected });
|
|
317
|
+
expect(manager.admit(source, item("a"))).toBe(true);
|
|
318
|
+
manager.track("pmention-1", source, item("a"));
|
|
319
|
+
expect(manager.admit(source, item("a"))).toBe(true); // duplicate allowed
|
|
320
|
+
manager.track("pmention-2", source, item("a"));
|
|
321
|
+
expect(onMentionRejected).not.toHaveBeenCalled();
|
|
322
|
+
|
|
323
|
+
await tick();
|
|
324
|
+
const { refs, finalize } = manager.collectForSubmit();
|
|
325
|
+
expect(refs).toHaveLength(2); // both tokens render in the bubble
|
|
326
|
+
const bundle = await finalize();
|
|
327
|
+
expect(bundle.blocks).toEqual(["```a\nCONTENT\n```"]); // deduped
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("track reports resolve status onto the inline token (resolved / error)", async () => {
|
|
331
|
+
// Success: reports "resolved" once the select-time resolve settles.
|
|
332
|
+
const ok = makeManager({}, async () => ({ llmAppend: "CONTENT" }));
|
|
333
|
+
const okReports: string[] = [];
|
|
334
|
+
ok.manager.track("pmention-1", ok.source, item("good"), "", (s) =>
|
|
335
|
+
okReports.push(s)
|
|
336
|
+
);
|
|
337
|
+
await tick();
|
|
338
|
+
expect(okReports).toContain("resolved");
|
|
339
|
+
|
|
340
|
+
// Failure: reports "error" so the token surfaces the dropped context.
|
|
341
|
+
const badResolve = vi.fn(async () => {
|
|
342
|
+
throw new Error("boom");
|
|
343
|
+
});
|
|
344
|
+
const bad = makeManager({}, badResolve);
|
|
345
|
+
const badReports: string[] = [];
|
|
346
|
+
bad.manager.track("pmention-2", bad.source, item("bad"), "", (s) =>
|
|
347
|
+
badReports.push(s)
|
|
348
|
+
);
|
|
349
|
+
await tick();
|
|
350
|
+
expect(badReports).toContain("error");
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("a throwing block formatter drops that item's block but finalize resolves the others", async () => {
|
|
354
|
+
const resolve = async (i: AgentWidgetContextMentionItem) => ({
|
|
355
|
+
llmAppend: `body of ${i.label}`,
|
|
356
|
+
});
|
|
357
|
+
const { manager } = makeManager({}, resolve);
|
|
358
|
+
manager.add(makeSource(resolve, "files"), item("good", "Good.ts"));
|
|
359
|
+
manager.add(makeSource(resolve, "files"), item("bad", "Bad.ts"));
|
|
360
|
+
await tick();
|
|
361
|
+
|
|
362
|
+
// Force formatMentionBlock to throw for one item: a bad block formatter must
|
|
363
|
+
// drop that block, not reject the whole bundle and lose every mention.
|
|
364
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
365
|
+
const spy = vi
|
|
366
|
+
.spyOn(mentionLlmFormat, "formatMentionBlock")
|
|
367
|
+
.mockImplementation((entry) => {
|
|
368
|
+
if (entry.label === "Bad.ts") throw new Error("format boom");
|
|
369
|
+
return `[[${entry.label}]] ${entry.text}`;
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
const bundle = await manager.collectForSubmit().finalize();
|
|
373
|
+
expect(bundle.blocks).toEqual(["[[Good.ts]] body of Good.ts"]);
|
|
374
|
+
|
|
375
|
+
spy.mockRestore();
|
|
376
|
+
warn.mockRestore();
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("a throwing onMentionResolveError drops that item but finalize resolves the others", async () => {
|
|
380
|
+
const onMentionResolveError = vi.fn(() => {
|
|
381
|
+
throw new Error("callback boom");
|
|
382
|
+
});
|
|
383
|
+
const { manager } = makeManager({ onMentionResolveError });
|
|
384
|
+
// Submit-resolve sources: the failing resolve throws inside finalize, so its
|
|
385
|
+
// catch fires onMentionResolveError there (the throw site the guard protects).
|
|
386
|
+
const goodSource: AgentWidgetContextMentionSource = {
|
|
387
|
+
id: "good",
|
|
388
|
+
label: "Good",
|
|
389
|
+
search: () => [],
|
|
390
|
+
resolve: async (i) => ({ llmAppend: `body of ${i.label}` }),
|
|
391
|
+
resolveOn: "submit",
|
|
392
|
+
};
|
|
393
|
+
const badSource: AgentWidgetContextMentionSource = {
|
|
394
|
+
id: "bad",
|
|
395
|
+
label: "Bad",
|
|
396
|
+
search: () => [],
|
|
397
|
+
resolve: async () => {
|
|
398
|
+
throw new Error("resolve boom");
|
|
399
|
+
},
|
|
400
|
+
resolveOn: "submit",
|
|
401
|
+
};
|
|
402
|
+
manager.add(goodSource, item("g", "Good.ts"));
|
|
403
|
+
manager.add(badSource, item("b", "Bad.ts"));
|
|
404
|
+
await tick();
|
|
405
|
+
|
|
406
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
407
|
+
const bundle = await manager.collectForSubmit().finalize();
|
|
408
|
+
expect(onMentionResolveError).toHaveBeenCalledWith(
|
|
409
|
+
expect.objectContaining({ id: "b" }),
|
|
410
|
+
expect.any(Error)
|
|
411
|
+
);
|
|
412
|
+
expect(bundle.blocks).toEqual(["```Good.ts\nbody of Good.ts\n```"]);
|
|
413
|
+
warn.mockRestore();
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
function makeSource(
|
|
418
|
+
resolve: AgentWidgetContextMentionSource["resolve"],
|
|
419
|
+
id = "files"
|
|
420
|
+
): AgentWidgetContextMentionSource {
|
|
421
|
+
return { id, label: "Files", search: () => [], resolve };
|
|
422
|
+
}
|