@runtypelabs/persona 4.8.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.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 +59 -51
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +622 -2
- package/dist/index.d.ts +622 -2
- package/dist/index.global.js +54 -46
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +59 -51
- 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 +234 -55
- package/dist/theme-editor-preview.d.cts +473 -0
- package/dist/theme-editor-preview.d.ts +473 -0
- package/dist/theme-editor-preview.js +48 -42
- 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/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 +26 -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/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 +229 -126
- package/src/theme-editor/sections.ts +3 -3
- package/src/types/theme.ts +2 -0
- package/src/types.ts +525 -0
- package/src/ui.mention-submit.test.ts +235 -0
- package/src/ui.ts +301 -51
- 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/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 +28 -4
- package/src/utils/tokens.ts +27 -11
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect, vi } from "vitest";
|
|
4
|
+
import { mountInlineComposer } from "./context-mentions-inline-entry";
|
|
5
|
+
import type { AgentWidgetContextMentionRef } from "./types";
|
|
6
|
+
|
|
7
|
+
const renderToken = (ref: AgentWidgetContextMentionRef): HTMLElement => {
|
|
8
|
+
const el = document.createElement("span");
|
|
9
|
+
el.textContent = `@${ref.label}`;
|
|
10
|
+
return el;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const makeTextarea = (): HTMLTextAreaElement => {
|
|
14
|
+
const ta = document.createElement("textarea");
|
|
15
|
+
document.body.appendChild(ta);
|
|
16
|
+
return ta;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const appRef: AgentWidgetContextMentionRef = {
|
|
20
|
+
sourceId: "files",
|
|
21
|
+
itemId: "app",
|
|
22
|
+
label: "App.tsx",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
describe("mountInlineComposer", () => {
|
|
26
|
+
it("mirrors the textarea's inline styles onto the contenteditable (no visual pop)", () => {
|
|
27
|
+
const ta = makeTextarea();
|
|
28
|
+
ta.className = "persona-composer-textarea";
|
|
29
|
+
ta.style.maxHeight = "60px";
|
|
30
|
+
ta.style.overflowY = "auto";
|
|
31
|
+
ta.style.fontWeight = "500";
|
|
32
|
+
ta.setAttribute("placeholder", "Ask about a file…");
|
|
33
|
+
|
|
34
|
+
const handle = mountInlineComposer({ textarea: ta, renderToken });
|
|
35
|
+
const el = handle.element;
|
|
36
|
+
|
|
37
|
+
expect(el.style.maxHeight).toBe("60px");
|
|
38
|
+
expect(el.style.overflowY).toBe("auto");
|
|
39
|
+
expect(el.style.fontWeight).toBe("500");
|
|
40
|
+
expect(el.className).toContain("persona-composer-textarea");
|
|
41
|
+
// Placeholder + accessible name mirror across.
|
|
42
|
+
expect(el.getAttribute("data-placeholder")).toBe("Ask about a file…");
|
|
43
|
+
expect(el.getAttribute("aria-label")).toBe("Ask about a file…");
|
|
44
|
+
handle.destroy();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("maps the shimmed `placeholder` property onto data-placeholder", () => {
|
|
48
|
+
const ta = makeTextarea();
|
|
49
|
+
const handle = mountInlineComposer({ textarea: ta, renderToken });
|
|
50
|
+
const el = handle.element as unknown as HTMLTextAreaElement;
|
|
51
|
+
|
|
52
|
+
el.placeholder = "Type here";
|
|
53
|
+
expect(handle.element.getAttribute("data-placeholder")).toBe("Type here");
|
|
54
|
+
expect(handle.element.getAttribute("aria-label")).toBe("Type here");
|
|
55
|
+
expect(el.placeholder).toBe("Type here");
|
|
56
|
+
handle.destroy();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("preserves an explicit host aria-label when placeholder updates land after swap", () => {
|
|
60
|
+
const ta = makeTextarea();
|
|
61
|
+
ta.setAttribute("aria-label", "Message support");
|
|
62
|
+
ta.setAttribute("placeholder", "Ask about a file…");
|
|
63
|
+
|
|
64
|
+
const handle = mountInlineComposer({ textarea: ta, renderToken });
|
|
65
|
+
const el = handle.element as unknown as HTMLTextAreaElement;
|
|
66
|
+
expect(handle.element.getAttribute("aria-label")).toBe("Message support");
|
|
67
|
+
|
|
68
|
+
// ui.ts updateCopy assigns truthy placeholder text on every config update;
|
|
69
|
+
// the host's accessible name must survive it.
|
|
70
|
+
el.placeholder = "Ask about a file…";
|
|
71
|
+
expect(handle.element.getAttribute("aria-label")).toBe("Message support");
|
|
72
|
+
expect(handle.element.getAttribute("data-placeholder")).toBe("Ask about a file…");
|
|
73
|
+
handle.destroy();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("keeps a placeholder-derived aria-label in sync and clears it on empty", () => {
|
|
77
|
+
const ta = makeTextarea();
|
|
78
|
+
ta.setAttribute("placeholder", "Type a message");
|
|
79
|
+
const handle = mountInlineComposer({ textarea: ta, renderToken });
|
|
80
|
+
const el = handle.element as unknown as HTMLTextAreaElement;
|
|
81
|
+
expect(handle.element.getAttribute("aria-label")).toBe("Type a message");
|
|
82
|
+
|
|
83
|
+
el.placeholder = "First update";
|
|
84
|
+
expect(handle.element.getAttribute("aria-label")).toBe("First update");
|
|
85
|
+
el.placeholder = "Second update";
|
|
86
|
+
expect(handle.element.getAttribute("aria-label")).toBe("Second update");
|
|
87
|
+
|
|
88
|
+
// Empty placeholder leaves no stale accessible name behind.
|
|
89
|
+
el.placeholder = "";
|
|
90
|
+
expect(handle.element.getAttribute("aria-label")).toBeNull();
|
|
91
|
+
handle.destroy();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("exposes ordered contentSegments via getInlineMessageFields", () => {
|
|
95
|
+
const ta = makeTextarea();
|
|
96
|
+
const handle = mountInlineComposer({ textarea: ta, renderToken });
|
|
97
|
+
handle.input.setValueWithCaret("Check ", 6);
|
|
98
|
+
handle.input.insertMentionAtSelection!(appRef);
|
|
99
|
+
|
|
100
|
+
const fields = (
|
|
101
|
+
handle.element as unknown as {
|
|
102
|
+
getInlineMessageFields: () => {
|
|
103
|
+
contentSegments: Array<{
|
|
104
|
+
kind: string;
|
|
105
|
+
text?: string;
|
|
106
|
+
ref?: { label?: string };
|
|
107
|
+
}>;
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
).getInlineMessageFields();
|
|
111
|
+
|
|
112
|
+
const kinds = fields.contentSegments.map((s) => s.kind);
|
|
113
|
+
expect(kinds).toContain("mention");
|
|
114
|
+
const mention = fields.contentSegments.find((s) => s.kind === "mention");
|
|
115
|
+
expect(mention?.ref?.label).toBe("App.tsx");
|
|
116
|
+
handle.destroy();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("untracks tokens when the shim value setter clears the composer", () => {
|
|
120
|
+
const ta = makeTextarea();
|
|
121
|
+
const onMentionRemoved = vi.fn();
|
|
122
|
+
const handle = mountInlineComposer({
|
|
123
|
+
textarea: ta,
|
|
124
|
+
renderToken,
|
|
125
|
+
onMentionRemoved,
|
|
126
|
+
});
|
|
127
|
+
const el = handle.element as unknown as HTMLTextAreaElement;
|
|
128
|
+
const id = handle.input.insertMentionAtSelection!(appRef);
|
|
129
|
+
|
|
130
|
+
// Programmatic `value =` (voice/clear path) drops the token DOM; the shim must
|
|
131
|
+
// fire the adapter's reconcile so the manager aborts that mention's resolve.
|
|
132
|
+
el.value = "";
|
|
133
|
+
expect(onMentionRemoved).toHaveBeenCalledWith(id);
|
|
134
|
+
handle.destroy();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline-mention chunk entry — the HEAVY, DOM-bearing half of inline mode. Loaded
|
|
3
|
+
* on composer mount when `contextMentions.display === "inline"` and NEVER
|
|
4
|
+
* statically imported on the core path (the IIFE build marks
|
|
5
|
+
* `@runtypelabs/persona/context-mentions-inline` external; ESM/CJS mark the same
|
|
6
|
+
* subpath external via `build:client`). This is the only place the
|
|
7
|
+
* contenteditable engine + `composer-document` enter the widget at runtime.
|
|
8
|
+
*
|
|
9
|
+
* `mountInlineComposer` builds a contenteditable `ComposerInputCapability` that
|
|
10
|
+
* visually mirrors the live `<textarea>` and migrates its current value + caret.
|
|
11
|
+
* It does NOT touch the DOM tree or rebind listeners — the caller (`ui.ts`) swaps
|
|
12
|
+
* the element in place and re-points the composer's event handlers, because those
|
|
13
|
+
* live in the core and the chunk must stay free of that wiring.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
createContentEditableComposerInput,
|
|
18
|
+
type ContentEditableComposerInputOptions,
|
|
19
|
+
} from "./utils/composer-contenteditable";
|
|
20
|
+
import { documentToMessageFields } from "./utils/composer-document";
|
|
21
|
+
import type { ComposerInputCapability } from "./utils/composer-input";
|
|
22
|
+
import type {
|
|
23
|
+
AgentWidgetContentSegment,
|
|
24
|
+
AgentWidgetContextMentionRef,
|
|
25
|
+
} from "./types";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Display/transcript fields a sent inline-mode message carries, derived from the
|
|
29
|
+
* live composer document. Computed in THIS chunk (it owns `composer-document.ts`
|
|
30
|
+
* at runtime) and read by the core `ui.ts` submit path via `getInlineMessageFields`
|
|
31
|
+
* below, so the core never imports the document model.
|
|
32
|
+
*/
|
|
33
|
+
export interface InlineComposerMessageFields {
|
|
34
|
+
content: string;
|
|
35
|
+
contextMentions: AgentWidgetContextMentionRef[];
|
|
36
|
+
contentSegments: AgentWidgetContentSegment[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The `<textarea>`-shaped element the inline composer swaps in. Beyond the
|
|
41
|
+
* textarea API shims it also exposes `getInlineMessageFields()` so the core
|
|
42
|
+
* submit path can read the ordered document segments without importing the
|
|
43
|
+
* document model. `ui.ts` references this via a structural (type-only) shape.
|
|
44
|
+
*/
|
|
45
|
+
export type InlineComposerElement = HTMLElement & {
|
|
46
|
+
getInlineMessageFields: () => InlineComposerMessageFields;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export interface InlineComposerMountContext {
|
|
50
|
+
/** The live composer textarea to migrate from (value + caret + look). */
|
|
51
|
+
textarea: HTMLTextAreaElement;
|
|
52
|
+
/**
|
|
53
|
+
* Build a token's full element. Supplied by the CORE orchestrator (which owns
|
|
54
|
+
* `createMentionTokenElement` + the shared icon renderer) so this lazy chunk
|
|
55
|
+
* never bundles the icon set — it already ships in core. The adapter marks the
|
|
56
|
+
* returned element `contenteditable="false"` and stamps `data-mention-id`.
|
|
57
|
+
*/
|
|
58
|
+
renderToken: (ref: AgentWidgetContextMentionRef) => HTMLElement;
|
|
59
|
+
/**
|
|
60
|
+
* Fired when a token is deleted from the composer (backspace/cut). The caller
|
|
61
|
+
* routes this to `engine.untrackMention(id)` so the manager aborts its resolve.
|
|
62
|
+
*/
|
|
63
|
+
onMentionRemoved?: (id: string) => void;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface InlineComposerHandle {
|
|
67
|
+
/** The contenteditable input surface; hand this to `mountContextMentions`. */
|
|
68
|
+
input: ComposerInputCapability;
|
|
69
|
+
/** The contenteditable root element to swap in for the textarea. */
|
|
70
|
+
element: HTMLElement;
|
|
71
|
+
/** Detach the adapter's listeners on unmount. */
|
|
72
|
+
destroy: () => void;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let idSeq = 0;
|
|
76
|
+
/** Page-unique mention id (composer-scoped; only needs uniqueness within a doc). */
|
|
77
|
+
const generateId = (): string => `pmention-${++idSeq}`;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Copy the visual hooks that make the contenteditable read like the textarea it
|
|
81
|
+
* replaces. The composer sets most of its look via INLINE styles at runtime (font
|
|
82
|
+
* vars from config, the `maxHeight`/`overflowY` auto-resize cap, border/outline
|
|
83
|
+
* stripping — see `composer-parts.ts` and `ui.ts` `updateCopy`), not just utility
|
|
84
|
+
* classes, so copy every inline style property across as well; otherwise the
|
|
85
|
+
* contenteditable falls back to its CSS defaults (a taller max-height, a different
|
|
86
|
+
* font) and visibly pops on swap. Also mirror the classes, the placeholder, and
|
|
87
|
+
* the accessible name.
|
|
88
|
+
*/
|
|
89
|
+
function mirrorTextareaLook(
|
|
90
|
+
textarea: HTMLTextAreaElement,
|
|
91
|
+
el: HTMLElement
|
|
92
|
+
): void {
|
|
93
|
+
el.className = `${textarea.className} persona-composer-contenteditable`.trim();
|
|
94
|
+
// Copy every inline style property (font*, lineHeight, maxHeight, overflowY,
|
|
95
|
+
// border, outline, background, padding, color, …) so the box model + typography
|
|
96
|
+
// match exactly. Iterating keeps this in sync as the composer's inline styles
|
|
97
|
+
// evolve, without re-enumerating a fixed property list here. Skip `height`: the
|
|
98
|
+
// textarea's auto-resize sets it to a fixed px on input, but the contenteditable
|
|
99
|
+
// grows with its content (capped by the copied `maxHeight`/`overflowY`), so a
|
|
100
|
+
// frozen height would clip it.
|
|
101
|
+
for (let i = 0; i < textarea.style.length; i++) {
|
|
102
|
+
const prop = textarea.style.item(i);
|
|
103
|
+
if (prop === "height") continue;
|
|
104
|
+
el.style.setProperty(
|
|
105
|
+
prop,
|
|
106
|
+
textarea.style.getPropertyValue(prop),
|
|
107
|
+
textarea.style.getPropertyPriority(prop)
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
const placeholder = textarea.getAttribute("placeholder");
|
|
111
|
+
if (placeholder) {
|
|
112
|
+
el.setAttribute("data-placeholder", placeholder);
|
|
113
|
+
// Carry the placeholder in aria-placeholder (its correct role) rather than
|
|
114
|
+
// overloading the accessible name with it.
|
|
115
|
+
el.setAttribute("aria-placeholder", placeholder);
|
|
116
|
+
}
|
|
117
|
+
// Stable accessible name: prefer an explicit host/config aria-label on the
|
|
118
|
+
// textarea (ui.ts stamps config input copy there); fall back to the placeholder
|
|
119
|
+
// text for parity with chip mode (the plain textarea's name is its placeholder)
|
|
120
|
+
// so inline mode never regresses to an unnamed field.
|
|
121
|
+
const label = textarea.getAttribute("aria-label") ?? placeholder;
|
|
122
|
+
if (label) el.setAttribute("aria-label", label);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Make the contenteditable element quack like a `<textarea>` for the specific API
|
|
127
|
+
* surface the host composer code touches (`value`, `selectionStart/End`,
|
|
128
|
+
* `setSelectionRange`). This lets `ui.ts` keep reading/writing the composer the
|
|
129
|
+
* same way after the swap without threading the capability through every call
|
|
130
|
+
* site (doSubmit clears `value`; history sets `value` + caret; the send gate
|
|
131
|
+
* reads `value`). Everything is delegated to the capability so the document stays
|
|
132
|
+
* the source of truth. NOTE: `value` writes render PLAIN TEXT (no tokens) — inline
|
|
133
|
+
* history recall is plain-text in v1; tokens survive only within a live session.
|
|
134
|
+
*/
|
|
135
|
+
function shimTextareaApi(
|
|
136
|
+
el: HTMLElement,
|
|
137
|
+
input: ComposerInputCapability,
|
|
138
|
+
// True when the host stamped an explicit aria-label on the textarea. Its
|
|
139
|
+
// accessible name is authoritative, so placeholder writes must not clobber it.
|
|
140
|
+
hasHostLabel: boolean
|
|
141
|
+
): void {
|
|
142
|
+
Object.defineProperty(el, "value", {
|
|
143
|
+
configurable: true,
|
|
144
|
+
get: () => input.getValue(),
|
|
145
|
+
set: (v: string) => {
|
|
146
|
+
input.setValueWithCaret(v, v.length);
|
|
147
|
+
// `setValueWithCaret` rebuilds the DOM as plain text (dropping any token
|
|
148
|
+
// spans) but does NOT itself run the adapter's removal reconcile. Fire an
|
|
149
|
+
// input event so vanished tokens untrack (the manager aborts their resolve)
|
|
150
|
+
// — otherwise a programmatic `value =` (voice transcript, clear, history
|
|
151
|
+
// recall) leaves stale resolved context tracked against the next send.
|
|
152
|
+
input.dispatchInput();
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
// The contenteditable reads its placeholder from `data-placeholder`, so a plain
|
|
156
|
+
// `.placeholder =` (e.g. `ui.ts` `updateCopy` after the swap, which assigns
|
|
157
|
+
// truthy placeholder text on every config update) would no-op. Map the textarea
|
|
158
|
+
// `placeholder` property onto the attribute. Only touch the accessible name when
|
|
159
|
+
// it is placeholder-derived — an explicit host aria-label must survive.
|
|
160
|
+
Object.defineProperty(el, "placeholder", {
|
|
161
|
+
configurable: true,
|
|
162
|
+
get: () => el.getAttribute("data-placeholder") ?? "",
|
|
163
|
+
set: (v: string) => {
|
|
164
|
+
if (v) {
|
|
165
|
+
el.setAttribute("data-placeholder", v);
|
|
166
|
+
el.setAttribute("aria-placeholder", v);
|
|
167
|
+
if (!hasHostLabel) el.setAttribute("aria-label", v);
|
|
168
|
+
} else {
|
|
169
|
+
el.removeAttribute("data-placeholder");
|
|
170
|
+
el.removeAttribute("aria-placeholder");
|
|
171
|
+
// Clear the placeholder-derived name too, so no stale label lingers.
|
|
172
|
+
if (!hasHostLabel) el.removeAttribute("aria-label");
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
Object.defineProperty(el, "selectionStart", {
|
|
177
|
+
configurable: true,
|
|
178
|
+
get: () => input.getSelection().start,
|
|
179
|
+
});
|
|
180
|
+
Object.defineProperty(el, "selectionEnd", {
|
|
181
|
+
configurable: true,
|
|
182
|
+
get: () => input.getSelection().end,
|
|
183
|
+
});
|
|
184
|
+
(el as unknown as HTMLTextAreaElement).setSelectionRange = (
|
|
185
|
+
start: number,
|
|
186
|
+
end: number
|
|
187
|
+
) => input.setSelection(start ?? 0, end ?? start ?? 0);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function mountInlineComposer(
|
|
191
|
+
ctx: InlineComposerMountContext
|
|
192
|
+
): InlineComposerHandle {
|
|
193
|
+
const { textarea } = ctx;
|
|
194
|
+
const options: ContentEditableComposerInputOptions = {
|
|
195
|
+
generateId,
|
|
196
|
+
// Token DOM (incl. icon + any host `renderMentionToken`) is built in core and
|
|
197
|
+
// passed in, so this chunk stays free of the icon renderer.
|
|
198
|
+
renderToken: ctx.renderToken,
|
|
199
|
+
onMentionRemoved: ctx.onMentionRemoved,
|
|
200
|
+
placeholder: textarea.getAttribute("placeholder") ?? undefined,
|
|
201
|
+
};
|
|
202
|
+
const capability = createContentEditableComposerInput(options);
|
|
203
|
+
const el = capability.element;
|
|
204
|
+
// Capture before mirroring/shimming: whether the accessible name is an explicit
|
|
205
|
+
// host label (authoritative) or placeholder-derived (kept in sync with copy).
|
|
206
|
+
const hasHostLabel = textarea.getAttribute("aria-label") != null;
|
|
207
|
+
mirrorTextareaLook(textarea, el);
|
|
208
|
+
shimTextareaApi(el, capability, hasHostLabel);
|
|
209
|
+
|
|
210
|
+
// Expose the ordered document as sent-message fields for the core submit path.
|
|
211
|
+
// Computed here (this chunk owns `composer-document.ts`) so `ui.ts` can attach
|
|
212
|
+
// `contentSegments` to the user message without importing the document model.
|
|
213
|
+
(el as InlineComposerElement).getInlineMessageFields = () =>
|
|
214
|
+
documentToMessageFields(capability.getDocument?.() ?? { blocks: [] });
|
|
215
|
+
|
|
216
|
+
// Migrate current text + caret. Anything typed before the swap is plain text
|
|
217
|
+
// (tokens can't exist yet — the menu chunk gates selection), so this is lossless.
|
|
218
|
+
const caret = textarea.selectionStart ?? textarea.value.length;
|
|
219
|
+
capability.setValueWithCaret(textarea.value, caret);
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
input: capability,
|
|
223
|
+
element: el,
|
|
224
|
+
destroy: capability.destroy,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
loadContextMentionsInline,
|
|
4
|
+
setContextMentionsInlineLoader,
|
|
5
|
+
type ContextMentionsInlineModule,
|
|
6
|
+
} from "./context-mentions-inline-loader";
|
|
7
|
+
|
|
8
|
+
const fakeModule = {
|
|
9
|
+
mountInlineComposer: vi.fn(),
|
|
10
|
+
} as unknown as ContextMentionsInlineModule;
|
|
11
|
+
|
|
12
|
+
describe("loadContextMentionsInline", () => {
|
|
13
|
+
it("retries after a rejected load instead of caching the failure forever", async () => {
|
|
14
|
+
const loader = vi
|
|
15
|
+
.fn<() => Promise<ContextMentionsInlineModule>>()
|
|
16
|
+
.mockRejectedValueOnce(new Error("network"))
|
|
17
|
+
.mockResolvedValueOnce(fakeModule);
|
|
18
|
+
setContextMentionsInlineLoader(loader);
|
|
19
|
+
|
|
20
|
+
// First call surfaces the rejection to the caller.
|
|
21
|
+
await expect(loadContextMentionsInline()).rejects.toThrow("network");
|
|
22
|
+
// A later call retries (the failed promise was not cached) and resolves.
|
|
23
|
+
await expect(loadContextMentionsInline()).resolves.toBe(fakeModule);
|
|
24
|
+
expect(loader).toHaveBeenCalledTimes(2);
|
|
25
|
+
|
|
26
|
+
// Once resolved, the module is cached (no third loader call).
|
|
27
|
+
await expect(loadContextMentionsInline()).resolves.toBe(fakeModule);
|
|
28
|
+
expect(loader).toHaveBeenCalledTimes(2);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loader indirection for the lazy inline-mention chunk (core bundle, tiny).
|
|
3
|
+
*
|
|
4
|
+
* Sibling of `context-mentions-loader.ts`. The IIFE/CDN entry (`index-global.ts`)
|
|
5
|
+
* registers a loader that imports the standalone `context-mentions-inline.js`
|
|
6
|
+
* chunk from a sibling URL; ESM/CJS consumers fall back to the package's
|
|
7
|
+
* `./context-mentions-inline` subpath — which `build:client` marks external, so
|
|
8
|
+
* the contenteditable engine is NOT inlined into `dist/index.{js,cjs}` and stays
|
|
9
|
+
* out of the core bundle. (A relative `./context-mentions-inline-entry` import
|
|
10
|
+
* would be inlined by the no-splitting build, defeating the split.)
|
|
11
|
+
*
|
|
12
|
+
* This is loaded on composer mount when `contextMentions.display === "inline"`,
|
|
13
|
+
* distinct from the chip menu chunk which loads on the first `@`/click.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { createChunkLoader } from "./utils/chunk-loader";
|
|
17
|
+
import type {
|
|
18
|
+
InlineComposerMountContext,
|
|
19
|
+
InlineComposerHandle,
|
|
20
|
+
} from "./context-mentions-inline-entry";
|
|
21
|
+
|
|
22
|
+
export type ContextMentionsInlineModule = {
|
|
23
|
+
mountInlineComposer: (ctx: InlineComposerMountContext) => InlineComposerHandle;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Memoization + rejection-retry semantics live in `createChunkLoader`.
|
|
27
|
+
// `resetOnSetLoader` mirrors this loader's original behavior: registering a new
|
|
28
|
+
// loader source invalidates whatever the previous loader produced (so a
|
|
29
|
+
// mid-session swap for test injection takes effect deterministically).
|
|
30
|
+
const { setLoader, load } = createChunkLoader<ContextMentionsInlineModule>({
|
|
31
|
+
fallbackImport: () => import("@runtypelabs/persona/context-mentions-inline"),
|
|
32
|
+
resetOnSetLoader: true,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export const setContextMentionsInlineLoader = setLoader;
|
|
36
|
+
export const loadContextMentionsInline = load;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public entry for the lazy inline-mention chunk (`dist/context-mentions-inline.js`).
|
|
3
|
+
*
|
|
4
|
+
* Loaded on composer mount when `contextMentions.display === "inline"`. Carries
|
|
5
|
+
* the contenteditable engine (`composer-document`, `composer-contenteditable`)
|
|
6
|
+
* so chip-only and feature-off sites never download it. See
|
|
7
|
+
* `context-mentions-inline-loader.ts` and the loader registration in
|
|
8
|
+
* `index-global.ts`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export { mountInlineComposer } from "./context-mentions-inline-entry";
|
|
12
|
+
export type {
|
|
13
|
+
InlineComposerMountContext,
|
|
14
|
+
InlineComposerHandle,
|
|
15
|
+
} from "./context-mentions-inline-entry";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loader indirection for the lazy context-mentions chunk (core bundle, tiny).
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `markdown-parsers-loader.ts`: the IIFE/CDN entry (`index-global.ts`)
|
|
5
|
+
* registers a loader that imports the standalone `context-mentions.js` chunk
|
|
6
|
+
* from a sibling URL. ESM/CJS consumers fall back to importing the package's
|
|
7
|
+
* `./context-mentions` subpath — which `build:client` marks external, so the
|
|
8
|
+
* runtime is NOT inlined into `dist/index.{js,cjs}` and stays out of the core
|
|
9
|
+
* bundle until first use. (A relative `./context-mentions-entry` import would be
|
|
10
|
+
* inlined by the no-splitting build, defeating the whole point.)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createChunkLoader } from "./utils/chunk-loader";
|
|
14
|
+
import type {
|
|
15
|
+
ContextMentionMountContext,
|
|
16
|
+
ContextMentionEngine,
|
|
17
|
+
} from "./context-mentions-entry";
|
|
18
|
+
|
|
19
|
+
export type ContextMentionsModule = {
|
|
20
|
+
mountContextMentions: (ctx: ContextMentionMountContext) => ContextMentionEngine;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// IIFE/CDN: sibling-URL chunk via the registered loader.
|
|
24
|
+
// ESM/CJS fallback: the package's own `./context-mentions` subpath (external, so
|
|
25
|
+
// the runtime chunk is code-split out of dist/index.{js,cjs} rather than
|
|
26
|
+
// inlined). Memoization + rejection-retry semantics live in `createChunkLoader`.
|
|
27
|
+
const { setLoader, load } = createChunkLoader<ContextMentionsModule>({
|
|
28
|
+
fallbackImport: () => import("@runtypelabs/persona/context-mentions"),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export const setContextMentionsLoader = setLoader;
|
|
32
|
+
export const loadContextMentions = load;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone context-mentions chunk (`dist/context-mentions.js`).
|
|
3
|
+
*
|
|
4
|
+
* The IIFE/CDN widget bundle marks `./context-mentions-entry` external so the
|
|
5
|
+
* mention runtime is excluded from the core payload; this file is built as a
|
|
6
|
+
* self-contained sibling chunk (`tsup.context-mentions.config.ts`) and loaded on
|
|
7
|
+
* demand by the loader registered in `index-global.ts`. ESM/CJS consumers import
|
|
8
|
+
* the entry directly. See `docs/context-mentions-plan.md`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export { mountContextMentions } from "./context-mentions-entry";
|
|
12
|
+
export type {
|
|
13
|
+
ContextMentionMountContext,
|
|
14
|
+
ContextMentionEngine,
|
|
15
|
+
MentionSubmitBundle,
|
|
16
|
+
} from "./context-mentions-entry";
|
package/src/index-core.ts
CHANGED
|
@@ -29,6 +29,25 @@ export type {
|
|
|
29
29
|
// Context provider types (e.g. for config.contextProviders)
|
|
30
30
|
AgentWidgetContextProvider,
|
|
31
31
|
AgentWidgetContextProviderContext,
|
|
32
|
+
// Context mention types (e.g. for config.contextMentions)
|
|
33
|
+
AgentWidgetContextMentionConfig,
|
|
34
|
+
AgentWidgetContextMentionSource,
|
|
35
|
+
AgentWidgetContextMentionItem,
|
|
36
|
+
AgentWidgetContextMentionPayload,
|
|
37
|
+
AgentWidgetContextMentionRef,
|
|
38
|
+
AgentWidgetContextMentionSearchContext,
|
|
39
|
+
AgentWidgetContextMentionResolveContext,
|
|
40
|
+
AgentWidgetContextMentionMenuRenderContext,
|
|
41
|
+
AgentWidgetContextMentionItemRenderContext,
|
|
42
|
+
AgentWidgetContextMentionChipRenderContext,
|
|
43
|
+
AgentWidgetContextMentionTokenRenderContext,
|
|
44
|
+
// Per-mention LLM block formatter entry (config.contextMentions.llmFormat)
|
|
45
|
+
AgentWidgetMentionLlmEntry,
|
|
46
|
+
// Skills / slash-commands (config.contextMentions.triggers)
|
|
47
|
+
AgentWidgetMentionTriggerChannel,
|
|
48
|
+
AgentWidgetMentionTriggerPosition,
|
|
49
|
+
AgentWidgetContextMentionCommandContext,
|
|
50
|
+
AgentWidgetContextMentionComposerCapability,
|
|
32
51
|
AgentWidgetCustomFetch,
|
|
33
52
|
AgentWidgetSSEEventParser,
|
|
34
53
|
AgentWidgetSSEEventResult,
|
|
@@ -226,6 +245,13 @@ export {
|
|
|
226
245
|
fileToImagePart,
|
|
227
246
|
validateImageFile
|
|
228
247
|
} from "./utils/content";
|
|
248
|
+
// Context mention helpers (for building config.contextMentions.sources)
|
|
249
|
+
export {
|
|
250
|
+
defaultMentionFilter,
|
|
251
|
+
createStaticMentionSource,
|
|
252
|
+
createSlashCommandsSource,
|
|
253
|
+
} from "./utils/mention-matcher";
|
|
254
|
+
export type { SlashCommandDefinition } from "./utils/mention-matcher";
|
|
229
255
|
export {
|
|
230
256
|
collectEnrichedPageContext,
|
|
231
257
|
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
|
+
});
|