@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/index-core.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
|
|
6
6
|
export type {
|
|
7
7
|
AgentWidgetConfig,
|
|
8
|
+
AgentWidgetConfigPatch,
|
|
8
9
|
TargetResolver,
|
|
9
10
|
ResolvedTarget,
|
|
10
11
|
AgentWidgetFeatureFlags,
|
|
@@ -29,6 +30,25 @@ export type {
|
|
|
29
30
|
// Context provider types (e.g. for config.contextProviders)
|
|
30
31
|
AgentWidgetContextProvider,
|
|
31
32
|
AgentWidgetContextProviderContext,
|
|
33
|
+
// Context mention types (e.g. for config.contextMentions)
|
|
34
|
+
AgentWidgetContextMentionConfig,
|
|
35
|
+
AgentWidgetContextMentionSource,
|
|
36
|
+
AgentWidgetContextMentionItem,
|
|
37
|
+
AgentWidgetContextMentionPayload,
|
|
38
|
+
AgentWidgetContextMentionRef,
|
|
39
|
+
AgentWidgetContextMentionSearchContext,
|
|
40
|
+
AgentWidgetContextMentionResolveContext,
|
|
41
|
+
AgentWidgetContextMentionMenuRenderContext,
|
|
42
|
+
AgentWidgetContextMentionItemRenderContext,
|
|
43
|
+
AgentWidgetContextMentionChipRenderContext,
|
|
44
|
+
AgentWidgetContextMentionTokenRenderContext,
|
|
45
|
+
// Per-mention LLM block formatter entry (config.contextMentions.llmFormat)
|
|
46
|
+
AgentWidgetMentionLlmEntry,
|
|
47
|
+
// Skills / slash-commands (config.contextMentions.triggers)
|
|
48
|
+
AgentWidgetMentionTriggerChannel,
|
|
49
|
+
AgentWidgetMentionTriggerPosition,
|
|
50
|
+
AgentWidgetContextMentionCommandContext,
|
|
51
|
+
AgentWidgetContextMentionComposerCapability,
|
|
32
52
|
AgentWidgetCustomFetch,
|
|
33
53
|
AgentWidgetSSEEventParser,
|
|
34
54
|
AgentWidgetSSEEventResult,
|
|
@@ -226,6 +246,13 @@ export {
|
|
|
226
246
|
fileToImagePart,
|
|
227
247
|
validateImageFile
|
|
228
248
|
} from "./utils/content";
|
|
249
|
+
// Context mention helpers (for building config.contextMentions.sources)
|
|
250
|
+
export {
|
|
251
|
+
defaultMentionFilter,
|
|
252
|
+
createStaticMentionSource,
|
|
253
|
+
createSlashCommandsSource,
|
|
254
|
+
} from "./utils/mention-matcher";
|
|
255
|
+
export type { SlashCommandDefinition } from "./utils/mention-matcher";
|
|
229
256
|
export {
|
|
230
257
|
collectEnrichedPageContext,
|
|
231
258
|
formatEnrichedContext,
|
package/src/index-global.ts
CHANGED
|
@@ -137,3 +137,54 @@ setRuntypeTtsLoader(() => {
|
|
|
137
137
|
}
|
|
138
138
|
return import(/* @vite-ignore */ chunkUrl);
|
|
139
139
|
});
|
|
140
|
+
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// Deferred Context Mentions loading.
|
|
143
|
+
//
|
|
144
|
+
// This bundle is built with `./context-mentions-entry` external (see
|
|
145
|
+
// `tsup.global.config.ts`): the mention controller/manager/menu runtime is kept
|
|
146
|
+
// out of the CDN payload. Register a loader that imports the self-contained
|
|
147
|
+
// `context-mentions.js` chunk from a sibling URL; the orchestrator calls it on
|
|
148
|
+
// the first `@`/affordance-button interaction (and can prefetch on composer
|
|
149
|
+
// focus) only when `contextMentions.enabled`. Same pattern as the chunks above.
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
import { setContextMentionsLoader } from "./context-mentions-loader";
|
|
153
|
+
import { setContextMentionsInlineLoader } from "./context-mentions-inline-loader";
|
|
154
|
+
|
|
155
|
+
setContextMentionsLoader(() => {
|
|
156
|
+
const chunkUrl = widgetScriptSrc?.replace(
|
|
157
|
+
/index\.global\.js($|\?)/,
|
|
158
|
+
"context-mentions.js$1",
|
|
159
|
+
);
|
|
160
|
+
if (!chunkUrl || chunkUrl === widgetScriptSrc) {
|
|
161
|
+
return Promise.reject(
|
|
162
|
+
new Error(
|
|
163
|
+
"Could not derive the context-mentions.js URL from the widget script URL " +
|
|
164
|
+
`(${widgetScriptSrc ?? "unavailable"}). Self-hosted deployments that ` +
|
|
165
|
+
"rename index.global.js should host context-mentions.js alongside it.",
|
|
166
|
+
),
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
return import(/* @vite-ignore */ chunkUrl);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Sibling loader for the inline-mention contenteditable chunk, loaded on composer
|
|
173
|
+
// mount when `contextMentions.display === "inline"`. Same sibling-URL scheme.
|
|
174
|
+
setContextMentionsInlineLoader(() => {
|
|
175
|
+
const chunkUrl = widgetScriptSrc?.replace(
|
|
176
|
+
/index\.global\.js($|\?)/,
|
|
177
|
+
"context-mentions-inline.js$1",
|
|
178
|
+
);
|
|
179
|
+
if (!chunkUrl || chunkUrl === widgetScriptSrc) {
|
|
180
|
+
return Promise.reject(
|
|
181
|
+
new Error(
|
|
182
|
+
"Could not derive the context-mentions-inline.js URL from the widget " +
|
|
183
|
+
`script URL (${widgetScriptSrc ?? "unavailable"}). Self-hosted ` +
|
|
184
|
+
"deployments that rename index.global.js should host " +
|
|
185
|
+
"context-mentions-inline.js alongside it.",
|
|
186
|
+
),
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
return import(/* @vite-ignore */ chunkUrl);
|
|
190
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createChunkLoader } from "./utils/chunk-loader";
|
|
1
2
|
import type { Marked } from "marked";
|
|
2
3
|
import type DOMPurify from "dompurify";
|
|
3
4
|
|
|
@@ -6,19 +7,22 @@ export type MarkdownParsersModule = {
|
|
|
6
7
|
DOMPurify: typeof DOMPurify;
|
|
7
8
|
};
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
// Memoization + rejection-retry semantics live in `createChunkLoader`. The
|
|
11
|
+
// ESM/CJS fallback imports `./markdown-parsers-entry` directly; the IIFE/CDN
|
|
12
|
+
// build registers a loader that fetches the `markdown-parsers.js` chunk.
|
|
13
|
+
const { setLoader, load, provide, getSync } =
|
|
14
|
+
createChunkLoader<MarkdownParsersModule>({
|
|
15
|
+
fallbackImport: () => import("./markdown-parsers-entry"),
|
|
16
|
+
});
|
|
12
17
|
|
|
13
18
|
// Surfaces that want to self-heal when the lazy `markdown-parsers.js` chunk
|
|
14
19
|
// lands. See `onMarkdownParsersReady` below for why this is centralized.
|
|
15
20
|
const readySubscribers = new Set<() => void>();
|
|
16
21
|
|
|
17
|
-
//
|
|
18
|
-
//
|
|
22
|
+
// Fan out to everyone waiting to re-render, exactly once per subscriber.
|
|
23
|
+
// Called from BOTH the eager (`provideMarkdownParsers`) and lazy
|
|
19
24
|
// (`loadMarkdownParsers`) paths so a subscriber can't miss the transition.
|
|
20
|
-
const
|
|
21
|
-
moduleCache = mod;
|
|
25
|
+
const notifyParsersReady = (): void => {
|
|
22
26
|
// Snapshot + clear first: fire-once semantics, and a callback that re-subscribes
|
|
23
27
|
// (it won't — `onMarkdownParsersReady` no-ops once ready) can't loop.
|
|
24
28
|
const subs = [...readySubscribers];
|
|
@@ -31,11 +35,25 @@ const markParsersReady = (mod: MarkdownParsersModule): MarkdownParsersModule =>
|
|
|
31
35
|
/* subscriber threw: swallow so the remaining re-renders still run */
|
|
32
36
|
}
|
|
33
37
|
}
|
|
34
|
-
return mod;
|
|
35
38
|
};
|
|
36
39
|
|
|
37
|
-
export const setMarkdownParsersLoader =
|
|
38
|
-
|
|
40
|
+
export const setMarkdownParsersLoader = setLoader;
|
|
41
|
+
|
|
42
|
+
// On a failed chunk load (404, ad blocker, offline), `createChunkLoader`
|
|
43
|
+
// resets its cached promise so a later call retries. Pending subscribers are
|
|
44
|
+
// KEPT — a surface that rendered escaped during the failed attempt must still
|
|
45
|
+
// heal when a retry (kicked by a new subscriber or an explicit
|
|
46
|
+
// loadMarkdownParsers() call) succeeds; dropping them left that surface
|
|
47
|
+
// escaped forever after a transient failure. They're only retained while the
|
|
48
|
+
// parsers stay unloaded — success releases them (fire-once in
|
|
49
|
+
// `notifyParsersReady`), and callers can unsubscribe.
|
|
50
|
+
export const loadMarkdownParsers = (): Promise<MarkdownParsersModule> => {
|
|
51
|
+
const cached = getSync();
|
|
52
|
+
if (cached) return Promise.resolve(cached);
|
|
53
|
+
return load().then((mod) => {
|
|
54
|
+
notifyParsersReady();
|
|
55
|
+
return mod;
|
|
56
|
+
});
|
|
39
57
|
};
|
|
40
58
|
|
|
41
59
|
/**
|
|
@@ -58,12 +76,13 @@ export const setMarkdownParsersLoader = (l: () => Promise<MarkdownParsersModule>
|
|
|
58
76
|
* Fires at most once per subscription.
|
|
59
77
|
*/
|
|
60
78
|
export const onMarkdownParsersReady = (cb: () => void): (() => void) => {
|
|
61
|
-
if (
|
|
79
|
+
if (getSync()) return () => {};
|
|
62
80
|
readySubscribers.add(cb);
|
|
63
81
|
// Ensure the chunk is actually being fetched; harmless if already in flight.
|
|
64
82
|
// Swallow rejection here — on failure the subscriber stays registered (see
|
|
65
|
-
// `
|
|
66
|
-
// succeeds; this catch only avoids an unhandled
|
|
83
|
+
// the note on `loadMarkdownParsers`) and the surface keeps its escaped
|
|
84
|
+
// fallback until a retry succeeds; this catch only avoids an unhandled
|
|
85
|
+
// rejection.
|
|
67
86
|
void loadMarkdownParsers().catch(() => {});
|
|
68
87
|
return () => {
|
|
69
88
|
readySubscribers.delete(cb);
|
|
@@ -79,36 +98,8 @@ export const onMarkdownParsersReady = (cb: () => void): (() => void) => {
|
|
|
79
98
|
* it lazy-loads the `markdown-parsers.js` chunk instead.
|
|
80
99
|
*/
|
|
81
100
|
export const provideMarkdownParsers = (mod: MarkdownParsersModule): void => {
|
|
82
|
-
|
|
101
|
+
provide(mod);
|
|
102
|
+
notifyParsersReady();
|
|
83
103
|
};
|
|
84
104
|
|
|
85
|
-
|
|
86
|
-
// promise: reset it so a later subscribe/load call can retry. Pending
|
|
87
|
-
// subscribers are KEPT — a surface that rendered escaped during the failed
|
|
88
|
-
// attempt must still heal when a retry (kicked by a new subscriber or an
|
|
89
|
-
// explicit loadMarkdownParsers() call) succeeds; dropping them here left that
|
|
90
|
-
// surface escaped forever after a transient failure. They're only retained
|
|
91
|
-
// while the parsers stay unloaded — success releases them (fire-once in
|
|
92
|
-
// `markParsersReady`), and callers can unsubscribe. On a permanently failing
|
|
93
|
-
// setup they persist for the page's life, which is the price of healing.
|
|
94
|
-
// Re-throw so awaiters of loadMarkdownParsers() still observe the rejection.
|
|
95
|
-
const onLoadFailure = (err: unknown): never => {
|
|
96
|
-
loadPromise = null;
|
|
97
|
-
throw err;
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
export const loadMarkdownParsers = (): Promise<MarkdownParsersModule> => {
|
|
101
|
-
if (moduleCache) return Promise.resolve(moduleCache);
|
|
102
|
-
if (loadPromise) return loadPromise;
|
|
103
|
-
if (!loader) {
|
|
104
|
-
// Fallback for regular ESM/CJS consumers (they import directly)
|
|
105
|
-
loadPromise = import("./markdown-parsers-entry").then(markParsersReady, onLoadFailure);
|
|
106
|
-
return loadPromise;
|
|
107
|
-
}
|
|
108
|
-
loadPromise = loader().then(markParsersReady, onLoadFailure);
|
|
109
|
-
return loadPromise;
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
export const getMarkdownParsersSync = (): MarkdownParsersModule | null => {
|
|
113
|
-
return moduleCache;
|
|
114
|
-
};
|
|
105
|
+
export const getMarkdownParsersSync = getSync;
|
package/src/plugin-kit.test.ts
CHANGED
|
@@ -188,6 +188,46 @@ describe("createPopover", () => {
|
|
|
188
188
|
expect(content.getRootNode()).toBe(shadow);
|
|
189
189
|
});
|
|
190
190
|
|
|
191
|
+
it("anchors vertically to a verticalOffset when placement is top-start", () => {
|
|
192
|
+
const { anchor, content } = setup();
|
|
193
|
+
anchor.getBoundingClientRect = () =>
|
|
194
|
+
({
|
|
195
|
+
left: 100,
|
|
196
|
+
right: 400,
|
|
197
|
+
top: 200,
|
|
198
|
+
bottom: 260,
|
|
199
|
+
width: 300,
|
|
200
|
+
height: 60,
|
|
201
|
+
x: 100,
|
|
202
|
+
y: 200,
|
|
203
|
+
toJSON: () => ({}),
|
|
204
|
+
}) as DOMRect;
|
|
205
|
+
content.getBoundingClientRect = () =>
|
|
206
|
+
({
|
|
207
|
+
width: 180,
|
|
208
|
+
height: 100,
|
|
209
|
+
left: 0,
|
|
210
|
+
right: 180,
|
|
211
|
+
top: 0,
|
|
212
|
+
bottom: 100,
|
|
213
|
+
x: 0,
|
|
214
|
+
y: 0,
|
|
215
|
+
toJSON: () => ({}),
|
|
216
|
+
}) as DOMRect;
|
|
217
|
+
|
|
218
|
+
const popover = createPopover({
|
|
219
|
+
anchor,
|
|
220
|
+
content,
|
|
221
|
+
placement: "top-start",
|
|
222
|
+
offset: 6,
|
|
223
|
+
verticalOffset: () => 40,
|
|
224
|
+
});
|
|
225
|
+
popover.open();
|
|
226
|
+
|
|
227
|
+
// anchorTop = 200 + 40 = 240 → top = 240 - 6 - 100 = 134
|
|
228
|
+
expect(content.style.top).toBe("134px");
|
|
229
|
+
});
|
|
230
|
+
|
|
191
231
|
it("destroy removes content and detaches listeners", () => {
|
|
192
232
|
vi.useFakeTimers();
|
|
193
233
|
const onDismiss = vi.fn();
|
package/src/plugin-kit.ts
CHANGED
|
@@ -117,6 +117,23 @@ export interface PopoverOptions {
|
|
|
117
117
|
offset?: number;
|
|
118
118
|
/** Set `content`'s `min-width` to the anchor's width. Default `false`. */
|
|
119
119
|
matchAnchorWidth?: boolean;
|
|
120
|
+
/**
|
|
121
|
+
* Horizontal anchoring override for `*-start` placements. Returns the desired
|
|
122
|
+
* left offset in px measured from the anchor's LEFT edge (e.g. the `@` trigger's
|
|
123
|
+
* x within the composer), or `null` to left-align with the anchor as usual. When
|
|
124
|
+
* this option is present the content is content-sized and capped to the anchor's
|
|
125
|
+
* width, and the resolved left is clamped so the content never overflows the
|
|
126
|
+
* anchor's left/right edges (Slack-style: a near-right trigger shifts the menu
|
|
127
|
+
* left to fit). Omit it entirely for plain anchor-left/right placement.
|
|
128
|
+
*/
|
|
129
|
+
horizontalOffset?: () => number | null;
|
|
130
|
+
/**
|
|
131
|
+
* Vertical anchoring override for `top-*` placements. Returns the desired
|
|
132
|
+
* anchor point's top offset in px measured from the anchor's TOP edge (e.g.
|
|
133
|
+
* the trigger line's top within the composer), or `null` to use the anchor's
|
|
134
|
+
* top edge as usual.
|
|
135
|
+
*/
|
|
136
|
+
verticalOffset?: () => number | null;
|
|
120
137
|
/**
|
|
121
138
|
* Inline `z-index` for `content`. Default `2147483000` so it overlays the rest
|
|
122
139
|
* of the widget. Pass `null` to leave z-index to your own CSS.
|
|
@@ -175,6 +192,8 @@ export function createPopover(options: PopoverOptions): PopoverHandle {
|
|
|
175
192
|
placement = "bottom-start",
|
|
176
193
|
offset = 6,
|
|
177
194
|
matchAnchorWidth = false,
|
|
195
|
+
horizontalOffset,
|
|
196
|
+
verticalOffset,
|
|
178
197
|
zIndex = 2147483000,
|
|
179
198
|
onOpen,
|
|
180
199
|
onDismiss,
|
|
@@ -189,16 +208,31 @@ export function createPopover(options: PopoverOptions): PopoverHandle {
|
|
|
189
208
|
const rect = anchor.getBoundingClientRect();
|
|
190
209
|
content.style.position = "fixed";
|
|
191
210
|
if (matchAnchorWidth) content.style.minWidth = `${rect.width}px`;
|
|
211
|
+
// Trigger-anchored mode: content is sized to its own contents but never wider
|
|
212
|
+
// than the composer, so it can shift horizontally within the composer's span.
|
|
213
|
+
if (horizontalOffset) content.style.maxWidth = `${rect.width}px`;
|
|
214
|
+
|
|
215
|
+
// Single content measurement per reposition (after width constraints apply).
|
|
216
|
+
const contentRect = content.getBoundingClientRect();
|
|
192
217
|
|
|
218
|
+
const offY = verticalOffset?.() ?? null;
|
|
219
|
+
const anchorTop = offY != null ? rect.top + offY : rect.top;
|
|
193
220
|
const top =
|
|
194
221
|
placement === "top-start" || placement === "top-end"
|
|
195
|
-
?
|
|
222
|
+
? anchorTop - offset - contentRect.height
|
|
196
223
|
: rect.bottom + offset;
|
|
197
224
|
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
225
|
+
const isEnd = placement === "bottom-end" || placement === "top-end";
|
|
226
|
+
// Default: anchor left (or anchor right for `*-end`). Trigger-anchored mode
|
|
227
|
+
// (offset present and non-null) overrides with the clamped trigger position so
|
|
228
|
+
// the menu never overflows the anchor's edges — a near-right trigger shifts it
|
|
229
|
+
// left to fit (Slack-style). A null offset falls back to the default.
|
|
230
|
+
let left = isEnd ? rect.right - contentRect.width : rect.left;
|
|
231
|
+
const off = horizontalOffset?.() ?? null;
|
|
232
|
+
if (off != null) {
|
|
233
|
+
const maxLeft = Math.max(rect.left, rect.right - contentRect.width);
|
|
234
|
+
left = Math.min(Math.max(rect.left + off, rect.left), maxLeft);
|
|
235
|
+
}
|
|
202
236
|
|
|
203
237
|
content.style.top = `${top}px`;
|
|
204
238
|
content.style.left = `${left}px`;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
// End-to-end handle.update() through the REAL ui module (init.test.ts mocks it,
|
|
4
|
+
// so it cannot see the controller's own patch merge). Regression: explicit
|
|
5
|
+
// undefined resets must reach the controller, not be materialized as absent
|
|
6
|
+
// keys by the handle's pre-merge.
|
|
7
|
+
|
|
8
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
9
|
+
|
|
10
|
+
import { initAgentWidget } from "./init";
|
|
11
|
+
|
|
12
|
+
describe("handle.update explicit-undefined reset reaches the controller", () => {
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
document.body.innerHTML = "";
|
|
15
|
+
vi.restoreAllMocks();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("round-trips artifact pane appearance seamless -> panel via explicit undefined", () => {
|
|
19
|
+
window.scrollTo = vi.fn();
|
|
20
|
+
document.body.innerHTML = `<div id="target"></div>`;
|
|
21
|
+
|
|
22
|
+
const handle = initAgentWidget({
|
|
23
|
+
target: "#target",
|
|
24
|
+
config: {
|
|
25
|
+
apiUrl: "https://api.example.com/chat",
|
|
26
|
+
launcher: { enabled: false },
|
|
27
|
+
features: { artifacts: { enabled: true } },
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const root = () => document.querySelector<HTMLElement>("[data-persona-root]")!;
|
|
32
|
+
expect(root().classList.contains("persona-artifact-appearance-panel")).toBe(true);
|
|
33
|
+
|
|
34
|
+
handle.update({
|
|
35
|
+
features: {
|
|
36
|
+
artifacts: {
|
|
37
|
+
layout: { paneAppearance: "seamless", splitGap: "0", paneShadow: "none" },
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
expect(root().classList.contains("persona-artifact-appearance-seamless")).toBe(true);
|
|
42
|
+
|
|
43
|
+
handle.update({
|
|
44
|
+
features: {
|
|
45
|
+
artifacts: {
|
|
46
|
+
layout: { paneAppearance: undefined, splitGap: undefined, paneShadow: undefined },
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
expect(root().classList.contains("persona-artifact-appearance-panel")).toBe(true);
|
|
51
|
+
expect(root().classList.contains("persona-artifact-appearance-seamless")).toBe(false);
|
|
52
|
+
expect(root().style.getPropertyValue("--persona-artifact-pane-shadow")).toBe("");
|
|
53
|
+
|
|
54
|
+
handle.destroy();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("handle and controller stored configs converge on the same merged result", () => {
|
|
58
|
+
window.scrollTo = vi.fn();
|
|
59
|
+
document.body.innerHTML = `<div id="target"></div>`;
|
|
60
|
+
|
|
61
|
+
const handle = initAgentWidget({
|
|
62
|
+
target: "#target",
|
|
63
|
+
config: {
|
|
64
|
+
apiUrl: "https://api.example.com/chat",
|
|
65
|
+
launcher: { enabled: false, clearChat: { backgroundColor: "#123456" } },
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Sibling override must survive an unrelated patch in BOTH layers: the
|
|
70
|
+
// controller renders it now, and the handle's stored config must carry it
|
|
71
|
+
// into a mount-mode rebuild. tooltipText also renames the aria-label.
|
|
72
|
+
handle.update({ launcher: { clearChat: { tooltipText: "Wipe" } } });
|
|
73
|
+
const clear = () => document.querySelector<HTMLButtonElement>('button[aria-label="Wipe"]')!;
|
|
74
|
+
expect(clear().style.backgroundColor).toBe("rgb(18, 52, 86)");
|
|
75
|
+
|
|
76
|
+
handle.update({ launcher: { mountMode: "docked" } });
|
|
77
|
+
expect(clear().style.backgroundColor).toBe("rgb(18, 52, 86)");
|
|
78
|
+
|
|
79
|
+
handle.destroy();
|
|
80
|
+
});
|
|
81
|
+
});
|
package/src/runtime/init.test.ts
CHANGED
|
@@ -483,3 +483,65 @@ describe("initAgentWidget docked mode", () => {
|
|
|
483
483
|
handleB.destroy();
|
|
484
484
|
});
|
|
485
485
|
});
|
|
486
|
+
|
|
487
|
+
describe("initAgentWidget update merge policy", () => {
|
|
488
|
+
beforeEach(() => {
|
|
489
|
+
document.body.innerHTML = "";
|
|
490
|
+
createAgentExperienceMock.mockReset();
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it("passes the raw patch to the controller so explicit-undefined resets survive", async () => {
|
|
494
|
+
const { initAgentWidget } = await import("./init");
|
|
495
|
+
document.body.innerHTML = `<div id="target"></div>`;
|
|
496
|
+
|
|
497
|
+
let controllerRef: ReturnType<typeof createMockController> | undefined;
|
|
498
|
+
createAgentExperienceMock.mockImplementation((_mount, config) => {
|
|
499
|
+
controllerRef = createMockController(config);
|
|
500
|
+
return controllerRef;
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
const handle = initAgentWidget({
|
|
504
|
+
target: "#target",
|
|
505
|
+
config: {
|
|
506
|
+
apiUrl: "https://api.example.com/chat",
|
|
507
|
+
launcher: { enabled: false, clearChat: { backgroundColor: "#123456" } },
|
|
508
|
+
},
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
// The merged config materializes explicit-undefined as absence, which the
|
|
512
|
+
// controller's patch merge would preserve; only the raw patch carries resets.
|
|
513
|
+
const patch = { launcher: { clearChat: { tooltipText: "Clear" } } };
|
|
514
|
+
handle.update(patch);
|
|
515
|
+
|
|
516
|
+
const passed = controllerRef!.update.mock.calls.at(-1)?.[0] as any;
|
|
517
|
+
expect(passed).toBe(patch);
|
|
518
|
+
|
|
519
|
+
handle.destroy();
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it("retains prior nested overrides when a mount-mode change rebuilds the controller", async () => {
|
|
523
|
+
const { initAgentWidget } = await import("./init");
|
|
524
|
+
document.body.innerHTML = `<div id="target"></div>`;
|
|
525
|
+
|
|
526
|
+
createAgentExperienceMock.mockImplementation((_mount, config) => createMockController(config));
|
|
527
|
+
|
|
528
|
+
const handle = initAgentWidget({
|
|
529
|
+
target: "#target",
|
|
530
|
+
config: {
|
|
531
|
+
apiUrl: "https://api.example.com/chat",
|
|
532
|
+
launcher: { enabled: false, clearChat: { backgroundColor: "#123456" } },
|
|
533
|
+
},
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
// Patch a sibling, then flip the mount mode to force a rebuild.
|
|
537
|
+
handle.update({ launcher: { clearChat: { tooltipText: "Clear" } } });
|
|
538
|
+
handle.update({ launcher: { mountMode: "docked" } });
|
|
539
|
+
|
|
540
|
+
const rebuildConfig = createAgentExperienceMock.mock.calls.at(-1)?.[1] as any;
|
|
541
|
+
expect(rebuildConfig.launcher.clearChat.backgroundColor).toBe("#123456");
|
|
542
|
+
expect(rebuildConfig.launcher.clearChat.tooltipText).toBe("Clear");
|
|
543
|
+
expect(rebuildConfig.launcher.mountMode).toBe("docked");
|
|
544
|
+
|
|
545
|
+
handle.destroy();
|
|
546
|
+
});
|
|
547
|
+
});
|
package/src/runtime/init.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createAgentExperience, AgentWidgetController } from "../ui";
|
|
2
|
-
import { AgentWidgetConfig as _AgentWidgetConfig, AgentWidgetInitOptions, AgentWidgetEvent as _AgentWidgetEvent } from "../types";
|
|
2
|
+
import { AgentWidgetConfig as _AgentWidgetConfig, AgentWidgetConfigPatch, AgentWidgetInitOptions, AgentWidgetEvent as _AgentWidgetEvent } from "../types";
|
|
3
3
|
import { isComposerBarMountMode, isDockedMountMode } from "../utils/dock";
|
|
4
|
+
import { mergeConfigUpdate } from "../utils/config-merge";
|
|
4
5
|
import { createWidgetHostLayout } from "./host-layout";
|
|
5
6
|
|
|
6
7
|
const ensureTarget = (target: string | HTMLElement): HTMLElement => {
|
|
@@ -126,19 +127,8 @@ export const initAgentWidget = (
|
|
|
126
127
|
};
|
|
127
128
|
|
|
128
129
|
const handleBase = {
|
|
129
|
-
update(nextConfig:
|
|
130
|
-
const mergedConfig = {
|
|
131
|
-
...config,
|
|
132
|
-
...nextConfig,
|
|
133
|
-
launcher: {
|
|
134
|
-
...(config?.launcher ?? {}),
|
|
135
|
-
...(nextConfig?.launcher ?? {}),
|
|
136
|
-
dock: {
|
|
137
|
-
...(config?.launcher?.dock ?? {}),
|
|
138
|
-
...(nextConfig?.launcher?.dock ?? {}),
|
|
139
|
-
},
|
|
140
|
-
},
|
|
141
|
-
} as _AgentWidgetConfig;
|
|
130
|
+
update(nextConfig: AgentWidgetConfigPatch) {
|
|
131
|
+
const mergedConfig = mergeConfigUpdate(config ?? ({} as _AgentWidgetConfig), nextConfig);
|
|
142
132
|
const previousDocked = isDockedMountMode(config);
|
|
143
133
|
const nextDocked = isDockedMountMode(mergedConfig);
|
|
144
134
|
const previousComposerBar = isComposerBarMountMode(config);
|
|
@@ -151,6 +141,9 @@ export const initAgentWidget = (
|
|
|
151
141
|
|
|
152
142
|
config = mergedConfig;
|
|
153
143
|
hostLayout.updateConfig(config);
|
|
144
|
+
// Pass the raw patch: mergedConfig materializes explicit-undefined resets
|
|
145
|
+
// as absent keys, which the controller's patch merge would preserve
|
|
146
|
+
// instead of clearing. Both layers share mergeConfigUpdate, so they converge.
|
|
154
147
|
controller.update(nextConfig);
|
|
155
148
|
syncHostState();
|
|
156
149
|
},
|