@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,91 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { formatMentionBlock } from "./mention-llm-format";
|
|
3
|
+
import type { AgentWidgetMentionLlmEntry } from "../types";
|
|
4
|
+
|
|
5
|
+
function entry(label: string, text: string): AgentWidgetMentionLlmEntry {
|
|
6
|
+
return {
|
|
7
|
+
label,
|
|
8
|
+
text,
|
|
9
|
+
ref: { sourceId: "files", itemId: label, label },
|
|
10
|
+
item: { id: label, label },
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("formatMentionBlock", () => {
|
|
15
|
+
it("fenced (default): label in the info string, body inside a ``` fence", () => {
|
|
16
|
+
expect(formatMentionBlock(entry("App.tsx", "FILE BODY"), 0)).toBe(
|
|
17
|
+
"```App.tsx\nFILE BODY\n```"
|
|
18
|
+
);
|
|
19
|
+
// Passing "fenced" explicitly is identical to the default.
|
|
20
|
+
expect(formatMentionBlock(entry("App.tsx", "FILE BODY"), 0, "fenced")).toBe(
|
|
21
|
+
"```App.tsx\nFILE BODY\n```"
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("fenced: escalates to four backticks when the body contains a ``` fence", () => {
|
|
26
|
+
const body = "before\n```\ncode\n```\nafter";
|
|
27
|
+
expect(formatMentionBlock(entry("Doc", body), 0)).toBe(
|
|
28
|
+
"````Doc\n" + body + "\n````"
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("fenced: escalates past the longest run when the body contains a ```` fence", () => {
|
|
33
|
+
const body = "````\nnested\n````";
|
|
34
|
+
expect(formatMentionBlock(entry("Doc", body), 0)).toBe(
|
|
35
|
+
"`````Doc\n" + body + "\n`````"
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("fenced: escalates past fences indented up to three spaces (CommonMark)", () => {
|
|
40
|
+
// A closing fence indented by up to three spaces is valid CommonMark, so an
|
|
41
|
+
// indented ``` inside the body must still force a four-backtick wrapper.
|
|
42
|
+
const body = "- item\n ```\n code\n ```";
|
|
43
|
+
expect(formatMentionBlock(entry("Doc", body), 0)).toBe(
|
|
44
|
+
"````Doc\n" + body + "\n````"
|
|
45
|
+
);
|
|
46
|
+
// Four or more spaces is an indented code block, not a fence: no escalation.
|
|
47
|
+
expect(formatMentionBlock(entry("Doc", " ```"), 0)).toBe(
|
|
48
|
+
"```Doc\n ```\n```"
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("function form: a throwing template falls back to fenced instead of rejecting", () => {
|
|
53
|
+
const block = formatMentionBlock(entry("App.tsx", "FILE BODY"), 0, () => {
|
|
54
|
+
throw new Error("host template bug");
|
|
55
|
+
});
|
|
56
|
+
// One bad host template must not reject finalize() and drop every
|
|
57
|
+
// mention's context from the outgoing message.
|
|
58
|
+
expect(block).toBe("```App.tsx\nFILE BODY\n```");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("document: Anthropic shape with a 1-based index", () => {
|
|
62
|
+
expect(formatMentionBlock(entry("App.tsx", "FILE BODY"), 0, "document")).toBe(
|
|
63
|
+
'<document index="1">\n' +
|
|
64
|
+
"<source>App.tsx</source>\n" +
|
|
65
|
+
"<document_content>\nFILE BODY\n</document_content>\n" +
|
|
66
|
+
"</document>"
|
|
67
|
+
);
|
|
68
|
+
// index is 0-based in, rendered 1-based out.
|
|
69
|
+
expect(formatMentionBlock(entry("Two", "B"), 1, "document")).toContain(
|
|
70
|
+
'<document index="2">'
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("document: falls back to the fenced block when the body carries the closing tag", () => {
|
|
75
|
+
const body = "spoofed </document_content> injection";
|
|
76
|
+
// Would break the XML boundary → fenced instead.
|
|
77
|
+
expect(formatMentionBlock(entry("Doc", body), 0, "document")).toBe(
|
|
78
|
+
"```Doc\n" + body + "\n```"
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("function form: receives {entry, index} and its return is used verbatim", () => {
|
|
83
|
+
const seen: { label: string; index: number }[] = [];
|
|
84
|
+
const out = formatMentionBlock(entry("App.tsx", "BODY"), 3, (e, i) => {
|
|
85
|
+
seen.push({ label: e.label, index: i });
|
|
86
|
+
return `CUSTOM(${e.item.id}):${e.text}`;
|
|
87
|
+
});
|
|
88
|
+
expect(out).toBe("CUSTOM(App.tsx):BODY");
|
|
89
|
+
expect(seen).toEqual([{ label: "App.tsx", index: 3 }]);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentWidgetContextMentionConfig,
|
|
3
|
+
AgentWidgetMentionLlmEntry,
|
|
4
|
+
} from "../types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Longest run of leading backticks on any line of `text`. A fenced block whose
|
|
8
|
+
* fence is longer than this can safely wrap the body without the body's own
|
|
9
|
+
* fences closing it early. CommonMark treats a closing fence indented by up to
|
|
10
|
+
* three spaces as valid, so indented runs count too.
|
|
11
|
+
*/
|
|
12
|
+
function longestLeadingBacktickRun(text: string): number {
|
|
13
|
+
let longest = 0;
|
|
14
|
+
for (const line of text.split("\n")) {
|
|
15
|
+
const match = /^ {0,3}(`+)/.exec(line);
|
|
16
|
+
if (match && match[1].length > longest) longest = match[1].length;
|
|
17
|
+
}
|
|
18
|
+
return longest;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Wrap `text` in a fenced code block carrying `label` in the info string. The
|
|
23
|
+
* fence escalates past any backtick run inside the body (three backticks by
|
|
24
|
+
* default, four/five/… when the body itself contains a fence) so a mention body
|
|
25
|
+
* can never terminate its own wrapper.
|
|
26
|
+
*/
|
|
27
|
+
function fencedBlock(label: string, text: string): string {
|
|
28
|
+
const fence = "`".repeat(Math.max(3, longestLeadingBacktickRun(text) + 1));
|
|
29
|
+
return `${fence}${label}\n${text}\n${fence}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Anthropic's documented long-context document shape. `index` is 0-based here
|
|
34
|
+
* and rendered 1-based (`index + 1`) to match Anthropic's example numbering.
|
|
35
|
+
* A body containing the literal `</document_content>` closing tag would break
|
|
36
|
+
* the XML boundary, so that one entry falls back to the fenced block, which has
|
|
37
|
+
* no such collision.
|
|
38
|
+
*/
|
|
39
|
+
function documentBlock(label: string, text: string, index: number): string {
|
|
40
|
+
if (text.includes("</document_content>")) return fencedBlock(label, text);
|
|
41
|
+
return (
|
|
42
|
+
`<document index="${index + 1}">\n` +
|
|
43
|
+
`<source>${label}</source>\n` +
|
|
44
|
+
`<document_content>\n${text}\n</document_content>\n` +
|
|
45
|
+
`</document>`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Format one resolved mention into the ready-to-join LLM block string, per the
|
|
51
|
+
* widget's `contextMentions.llmFormat`. `index` is the 0-based position of this
|
|
52
|
+
* block among the message's contributed mention blocks (used by the `"document"`
|
|
53
|
+
* preset's 1-based numbering and passed through to the function form).
|
|
54
|
+
*
|
|
55
|
+
* A throwing function-form template falls back to the fenced preset for that
|
|
56
|
+
* entry instead of propagating: `finalize()` runs the whole assembly in one
|
|
57
|
+
* pass, so an uncaught throw here would reject the entire bundle and silently
|
|
58
|
+
* drop every mention's context (plus contentParts and structured context) from
|
|
59
|
+
* the outgoing message — far worse than one host-formatted block degrading.
|
|
60
|
+
*/
|
|
61
|
+
export function formatMentionBlock(
|
|
62
|
+
entry: AgentWidgetMentionLlmEntry,
|
|
63
|
+
index: number,
|
|
64
|
+
format: AgentWidgetContextMentionConfig["llmFormat"] = "fenced"
|
|
65
|
+
): string {
|
|
66
|
+
if (typeof format === "function") {
|
|
67
|
+
try {
|
|
68
|
+
return format(entry, index);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.warn(
|
|
71
|
+
"[persona] contextMentions.llmFormat threw; falling back to the fenced format for this mention",
|
|
72
|
+
error
|
|
73
|
+
);
|
|
74
|
+
return fencedBlock(entry.label, entry.text);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (format === "document") return documentBlock(entry.label, entry.text, index);
|
|
78
|
+
return fencedBlock(entry.label, entry.text);
|
|
79
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { defaultMentionFilter, createStaticMentionSource } from "./mention-matcher";
|
|
3
|
+
import type { AgentWidgetContextMentionItem } from "../types";
|
|
4
|
+
|
|
5
|
+
const item = (
|
|
6
|
+
label: string,
|
|
7
|
+
extra: Partial<AgentWidgetContextMentionItem> = {}
|
|
8
|
+
): AgentWidgetContextMentionItem => ({ id: label, label, ...extra });
|
|
9
|
+
|
|
10
|
+
describe("defaultMentionFilter", () => {
|
|
11
|
+
it("returns all items recency-ordered for an empty query", () => {
|
|
12
|
+
const items = [
|
|
13
|
+
item("alpha", { recencyScore: 1 }),
|
|
14
|
+
item("beta", { recencyScore: 5 }),
|
|
15
|
+
item("gamma", { recencyScore: 3 }),
|
|
16
|
+
];
|
|
17
|
+
expect(defaultMentionFilter(items, "").map((i) => i.label)).toEqual([
|
|
18
|
+
"beta",
|
|
19
|
+
"gamma",
|
|
20
|
+
"alpha",
|
|
21
|
+
]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("ranks prefix > word-boundary > subsequence", () => {
|
|
25
|
+
const items = [
|
|
26
|
+
item("subsequence_app_x"), // 'app' as subsequence only? actually contains 'app' as substring
|
|
27
|
+
item("my-app-config"), // word boundary: 'app' starts the 'app' word
|
|
28
|
+
item("application"), // prefix
|
|
29
|
+
item("apricot pie"), // subsequence a-p-p? 'apricot pie' -> a,p,...,p (yes)
|
|
30
|
+
];
|
|
31
|
+
const ranked = defaultMentionFilter(items, "app").map((i) => i.label);
|
|
32
|
+
expect(ranked[0]).toBe("application"); // prefix wins
|
|
33
|
+
expect(ranked.indexOf("my-app-config")).toBeLessThan(
|
|
34
|
+
ranked.indexOf("apricot pie")
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("drops non-matching items", () => {
|
|
39
|
+
const items = [item("dashboard"), item("settings")];
|
|
40
|
+
expect(defaultMentionFilter(items, "xyz")).toHaveLength(0);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("is case-insensitive", () => {
|
|
44
|
+
const items = [item("README.md")];
|
|
45
|
+
expect(defaultMentionFilter(items, "readme")).toHaveLength(1);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("treats camelCase boundaries as word starts", () => {
|
|
49
|
+
const items = [item("getUserName")];
|
|
50
|
+
const ranked = defaultMentionFilter(items, "user");
|
|
51
|
+
expect(ranked).toHaveLength(1);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("breaks ties by recency then label", () => {
|
|
55
|
+
const items = [
|
|
56
|
+
item("appB", { recencyScore: 1 }),
|
|
57
|
+
item("appA", { recencyScore: 1 }),
|
|
58
|
+
item("appC", { recencyScore: 9 }),
|
|
59
|
+
];
|
|
60
|
+
expect(defaultMentionFilter(items, "app").map((i) => i.label)).toEqual([
|
|
61
|
+
"appC",
|
|
62
|
+
"appA",
|
|
63
|
+
"appB",
|
|
64
|
+
]);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("createStaticMentionSource", () => {
|
|
69
|
+
it("wires search() to defaultMentionFilter and passes resolve through", async () => {
|
|
70
|
+
const resolve = async () => ({ llmAppend: "x" });
|
|
71
|
+
const src = createStaticMentionSource({
|
|
72
|
+
id: "files",
|
|
73
|
+
label: "Files",
|
|
74
|
+
items: [item("App.tsx"), item("index.ts")],
|
|
75
|
+
resolve,
|
|
76
|
+
resolveOn: "submit",
|
|
77
|
+
});
|
|
78
|
+
expect(src.id).toBe("files");
|
|
79
|
+
expect(src.label).toBe("Files");
|
|
80
|
+
expect(src.resolveOn).toBe("submit");
|
|
81
|
+
const ctx = {} as never;
|
|
82
|
+
const results = await src.search("app", ctx);
|
|
83
|
+
expect(results.map((i) => i.label)).toEqual(["App.tsx"]);
|
|
84
|
+
expect(src.resolve).toBe(resolve);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default fuzzy matcher + static-source helper for context mentions.
|
|
3
|
+
*
|
|
4
|
+
* Pure and dependency-free so it can be exported for hosts (to filter
|
|
5
|
+
* client-side sources) and unit-tested in isolation. Ranking tiers, best first:
|
|
6
|
+
* 1. prefix — label starts with the query
|
|
7
|
+
* 2. word-boundary — a word inside the label starts with the query
|
|
8
|
+
* 3. subsequence — query chars appear in order anywhere in the label
|
|
9
|
+
* Ties break by `recencyScore` (desc) then label (asc). An empty query returns
|
|
10
|
+
* every item ordered by recency (recents/popular first).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
AgentWidgetContextMentionCommandContext,
|
|
15
|
+
AgentWidgetContextMentionItem,
|
|
16
|
+
AgentWidgetContextMentionSource,
|
|
17
|
+
} from "../types";
|
|
18
|
+
|
|
19
|
+
// Match tiers, best (lowest) first. Plain numbers rather than a const enum so
|
|
20
|
+
// the module stays isolatedModules-safe.
|
|
21
|
+
const TIER_PREFIX = 0;
|
|
22
|
+
const TIER_WORD_BOUNDARY = 1;
|
|
23
|
+
const TIER_SUBSEQUENCE = 2;
|
|
24
|
+
const TIER_NONE = 3;
|
|
25
|
+
|
|
26
|
+
function isSubsequence(needle: string, haystack: string): boolean {
|
|
27
|
+
if (!needle) return true;
|
|
28
|
+
let n = 0;
|
|
29
|
+
for (let h = 0; h < haystack.length && n < needle.length; h++) {
|
|
30
|
+
if (haystack[h] === needle[n]) n++;
|
|
31
|
+
}
|
|
32
|
+
return n === needle.length;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function scoreItem(label: string, query: string): number {
|
|
36
|
+
const l = label.toLowerCase();
|
|
37
|
+
const q = query.toLowerCase();
|
|
38
|
+
if (l.startsWith(q)) return TIER_PREFIX;
|
|
39
|
+
// Word boundaries: whitespace, hyphen, underscore, slash, dot, camelCase.
|
|
40
|
+
// Split camelCase by inserting a space at lower→upper transitions first
|
|
41
|
+
// (a lookbehind-free rewrite — lookbehind is a parse error on Safari < 16.4,
|
|
42
|
+
// and this module ships in the core bundle, so a regex literal there would
|
|
43
|
+
// break the whole widget on those browsers even with mentions disabled).
|
|
44
|
+
const words = label.replace(/([a-z])([A-Z])/g, "$1 $2").split(/[\s\-_/.]+/);
|
|
45
|
+
if (words.some((w) => w.toLowerCase().startsWith(q))) return TIER_WORD_BOUNDARY;
|
|
46
|
+
if (isSubsequence(q, l)) return TIER_SUBSEQUENCE;
|
|
47
|
+
return TIER_NONE;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function compareWithinTier(
|
|
51
|
+
a: AgentWidgetContextMentionItem,
|
|
52
|
+
b: AgentWidgetContextMentionItem
|
|
53
|
+
): number {
|
|
54
|
+
const ra = a.recencyScore ?? 0;
|
|
55
|
+
const rb = b.recencyScore ?? 0;
|
|
56
|
+
if (rb !== ra) return rb - ra;
|
|
57
|
+
return a.label.localeCompare(b.label);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Filter + rank items for the current `@query`. An empty query keeps all items,
|
|
62
|
+
* recency-ordered. Non-matching items are dropped.
|
|
63
|
+
*/
|
|
64
|
+
export function defaultMentionFilter(
|
|
65
|
+
items: AgentWidgetContextMentionItem[],
|
|
66
|
+
query: string
|
|
67
|
+
): AgentWidgetContextMentionItem[] {
|
|
68
|
+
const q = query.trim();
|
|
69
|
+
if (!q) {
|
|
70
|
+
return [...items].sort(compareWithinTier);
|
|
71
|
+
}
|
|
72
|
+
const scored: { item: AgentWidgetContextMentionItem; tier: number }[] = [];
|
|
73
|
+
for (const item of items) {
|
|
74
|
+
const tier = scoreItem(item.label, q);
|
|
75
|
+
if (tier !== TIER_NONE) scored.push({ item, tier });
|
|
76
|
+
}
|
|
77
|
+
scored.sort((a, b) => {
|
|
78
|
+
if (a.tier !== b.tier) return a.tier - b.tier;
|
|
79
|
+
return compareWithinTier(a.item, b.item);
|
|
80
|
+
});
|
|
81
|
+
return scored.map((s) => s.item);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Split a command-channel query into its command NAME (first token) and ARGS
|
|
86
|
+
* (everything after, trimmed). `"deploy staging"` → `{ name: "deploy", args:
|
|
87
|
+
* "staging" }`; `"deploy"` → `{ name: "deploy", args: "" }`. Shared by the
|
|
88
|
+
* slash-command source (matches on `name`) and the controller (captures `args`).
|
|
89
|
+
*/
|
|
90
|
+
export function splitCommandQuery(query: string): { name: string; args: string } {
|
|
91
|
+
const trimmed = query.replace(/^\s+/, "");
|
|
92
|
+
const sp = trimmed.search(/\s/);
|
|
93
|
+
if (sp === -1) return { name: trimmed, args: "" };
|
|
94
|
+
return { name: trimmed.slice(0, sp), args: trimmed.slice(sp + 1).trim() };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The easy path: a static, in-memory list filtered client-side with
|
|
99
|
+
* {@link defaultMentionFilter}. Supply your own `resolve()` to turn a selected
|
|
100
|
+
* item into a dispatch payload (lead with `llmAppend`).
|
|
101
|
+
*/
|
|
102
|
+
export function createStaticMentionSource(opts: {
|
|
103
|
+
id: string;
|
|
104
|
+
label: string;
|
|
105
|
+
items: AgentWidgetContextMentionItem[];
|
|
106
|
+
resolve: AgentWidgetContextMentionSource["resolve"];
|
|
107
|
+
resolveOn?: "select" | "submit";
|
|
108
|
+
}): AgentWidgetContextMentionSource {
|
|
109
|
+
return {
|
|
110
|
+
id: opts.id,
|
|
111
|
+
label: opts.label,
|
|
112
|
+
resolveOn: opts.resolveOn,
|
|
113
|
+
search: (query) => defaultMentionFilter(opts.items, query),
|
|
114
|
+
resolve: opts.resolve,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* A single slash-command definition for {@link createSlashCommandsSource}. Maps
|
|
120
|
+
* to a menu item plus its dispatch behavior (see `AgentWidgetContextMentionItem.command`):
|
|
121
|
+
* - `"prompt"` (default): `prompt` text is written into the composer (a macro).
|
|
122
|
+
* - `"action"`: `action()` runs in the browser (no message sent).
|
|
123
|
+
* - `"server"`: `data` is sent to the backend via request `context.mentions`.
|
|
124
|
+
*/
|
|
125
|
+
export type SlashCommandDefinition = {
|
|
126
|
+
/** Command name shown after the trigger (e.g. "summarize" → `/summarize`). */
|
|
127
|
+
name: string;
|
|
128
|
+
/** Menu subtitle. */
|
|
129
|
+
description?: string;
|
|
130
|
+
/** Lucide icon name for the menu row / chip. */
|
|
131
|
+
iconName?: string;
|
|
132
|
+
/** Dispatch kind. @default "prompt" */
|
|
133
|
+
kind?: "prompt" | "action" | "server";
|
|
134
|
+
/** `kind:"prompt"` — composer text (static, or built from the typed args). */
|
|
135
|
+
prompt?: string | ((args: string) => string);
|
|
136
|
+
/** `kind:"prompt"` — insertion mode. @default "replace" */
|
|
137
|
+
insertMode?: "replace" | "insert-at-caret";
|
|
138
|
+
/** `kind:"prompt"` — submit the composer immediately after inserting. */
|
|
139
|
+
submitOnSelect?: boolean;
|
|
140
|
+
/**
|
|
141
|
+
* Free-text argument hint (e.g. "order id"). Shown as ghost text in the menu
|
|
142
|
+
* row (`lookup ‹order id›`) and switches the command to INLINE COMPLETION:
|
|
143
|
+
* selecting it fills `/name ` into the composer so you type the arg inline,
|
|
144
|
+
* and the command runs at SUBMIT with the typed args (Slack-style). Optional
|
|
145
|
+
* for `"prompt"`/`"action"`; `"server"` commands always complete inline.
|
|
146
|
+
*/
|
|
147
|
+
argsPlaceholder?: string;
|
|
148
|
+
/** `kind:"action"` — the browser handler (receives parsed `args`). */
|
|
149
|
+
action?: (ctx: AgentWidgetContextMentionCommandContext) => void | Promise<void>;
|
|
150
|
+
/** `kind:"server"` — structured payload sent via `context.mentions.<sourceId>.<name>`. */
|
|
151
|
+
data?:
|
|
152
|
+
| Record<string, unknown>
|
|
153
|
+
| ((args: string) => Record<string, unknown>);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Build a `/`-slash-command SOURCE for `contextMentions.triggers`. Unlike a
|
|
158
|
+
* mention source, its items are COMMANDS (verbs): selecting one runs a prompt
|
|
159
|
+
* macro, a client action, or a server skill (see {@link SlashCommandDefinition}).
|
|
160
|
+
*
|
|
161
|
+
* Matching is on the command NAME only (the first token of the query), so
|
|
162
|
+
* `/deploy staging` still matches the `deploy` command while `staging` becomes
|
|
163
|
+
* the command's `args`. Pair it with a `"/"` channel, typically at line-start:
|
|
164
|
+
*
|
|
165
|
+
* ```ts
|
|
166
|
+
* contextMentions: {
|
|
167
|
+
* enabled: true,
|
|
168
|
+
* sources: [ ...mentionSources ], // @ context
|
|
169
|
+
* triggers: [{
|
|
170
|
+
* trigger: "/",
|
|
171
|
+
* triggerPosition: "line-start",
|
|
172
|
+
* sources: [createSlashCommandsSource({ id: "cmd", label: "Commands", commands })],
|
|
173
|
+
* }],
|
|
174
|
+
* }
|
|
175
|
+
* ```
|
|
176
|
+
*/
|
|
177
|
+
export function createSlashCommandsSource(opts: {
|
|
178
|
+
id: string;
|
|
179
|
+
label: string;
|
|
180
|
+
commands: SlashCommandDefinition[];
|
|
181
|
+
}): AgentWidgetContextMentionSource {
|
|
182
|
+
const items: AgentWidgetContextMentionItem[] = opts.commands.map((c) => ({
|
|
183
|
+
id: c.name,
|
|
184
|
+
label: c.name,
|
|
185
|
+
description: c.description,
|
|
186
|
+
iconName: c.iconName,
|
|
187
|
+
command: c.kind ?? "prompt",
|
|
188
|
+
insertMode: c.insertMode,
|
|
189
|
+
submitOnSelect: c.submitOnSelect,
|
|
190
|
+
action: c.action,
|
|
191
|
+
commandArgsPlaceholder: c.argsPlaceholder,
|
|
192
|
+
}));
|
|
193
|
+
const byName = new Map(opts.commands.map((c) => [c.name, c]));
|
|
194
|
+
const itemByName = new Map(items.map((it) => [it.id, it]));
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
id: opts.id,
|
|
198
|
+
label: opts.label,
|
|
199
|
+
// Exact-name lookup for submit-time inline-command dispatch (the query's
|
|
200
|
+
// first token) — not a fuzzy search.
|
|
201
|
+
matchCommand: (name) => itemByName.get(name),
|
|
202
|
+
// Server commands defer resolve to submit (args captured at select). Prompt
|
|
203
|
+
// commands call resolve synchronously from the controller; action commands
|
|
204
|
+
// never resolve. So "submit" is the correct source-level default here.
|
|
205
|
+
resolveOn: "submit",
|
|
206
|
+
search: (query) => defaultMentionFilter(items, splitCommandQuery(query).name),
|
|
207
|
+
resolve: (item, ctx) => {
|
|
208
|
+
const def = byName.get(item.id);
|
|
209
|
+
if (!def) return {};
|
|
210
|
+
if (def.kind === "server") {
|
|
211
|
+
const data =
|
|
212
|
+
typeof def.data === "function" ? def.data(ctx.args) : def.data;
|
|
213
|
+
return { context: data ?? {} };
|
|
214
|
+
}
|
|
215
|
+
// prompt (and the default): the composer-insert text.
|
|
216
|
+
const text =
|
|
217
|
+
typeof def.prompt === "function" ? def.prompt(ctx.args) : def.prompt ?? "";
|
|
218
|
+
return { insertText: text };
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared builder for the inline mention TOKEN — the atomic styled pill that sits
|
|
3
|
+
* inside the sentence (Slack/Linear/Cursor style), used by both the contenteditable
|
|
4
|
+
* composer (inline chunk) and the read-only sent bubble (`message-bubble`, core).
|
|
5
|
+
*
|
|
6
|
+
* The default is a tinted pill: icon + `@label`, colored by
|
|
7
|
+
* `--persona-mention-token-accent` (from `ref.color`, else the theme accent), with
|
|
8
|
+
* a `data-mention-source` attribute so hosts can theme per source/type in CSS
|
|
9
|
+
* (`.persona-mention-token[data-mention-source="files"] { … }`). A host can replace
|
|
10
|
+
* the whole element via `render` (the `renderMentionToken` config hook).
|
|
11
|
+
*
|
|
12
|
+
* Display-only: no document model, safe to bundle in both the core and the inline
|
|
13
|
+
* chunk (a small duplicate, like other shared helpers).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { createElement, createNode, cx } from "./dom";
|
|
17
|
+
import { renderLucideIcon } from "./icons";
|
|
18
|
+
import type { AgentWidgetContextMentionRef } from "../types";
|
|
19
|
+
|
|
20
|
+
export interface AgentWidgetContextMentionTokenRenderContext {
|
|
21
|
+
ref: AgentWidgetContextMentionRef;
|
|
22
|
+
readonly: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CreateMentionTokenOptions {
|
|
26
|
+
/** True in the sent bubble (adds `persona-mention-token-readonly`). */
|
|
27
|
+
readonly?: boolean;
|
|
28
|
+
/** Full-custom token DOM (the `renderMentionToken` config hook). */
|
|
29
|
+
render?: (ctx: AgentWidgetContextMentionTokenRenderContext) => HTMLElement;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createMentionTokenElement(
|
|
33
|
+
ref: AgentWidgetContextMentionRef,
|
|
34
|
+
opts: CreateMentionTokenOptions = {}
|
|
35
|
+
): HTMLElement {
|
|
36
|
+
if (opts.render) return opts.render({ ref, readonly: !!opts.readonly });
|
|
37
|
+
|
|
38
|
+
const token = createNode("span", {
|
|
39
|
+
className: cx(
|
|
40
|
+
"persona-mention-token",
|
|
41
|
+
opts.readonly && "persona-mention-token-readonly"
|
|
42
|
+
),
|
|
43
|
+
// role="img" + aria-label makes the icon + `@label` announce as one atomic
|
|
44
|
+
// unit ("App.tsx mention") instead of the icon and text being read
|
|
45
|
+
// separately; the resolve-status hook may later swap this label (see
|
|
46
|
+
// composer-contenteditable `setMentionStatus`).
|
|
47
|
+
attrs: {
|
|
48
|
+
"data-mention-source": ref.sourceId,
|
|
49
|
+
title: ref.label,
|
|
50
|
+
role: "img",
|
|
51
|
+
"aria-label": `${ref.label} mention`,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
// Per-item / per-type accent recolors the whole pill (bg tint + icon + text).
|
|
55
|
+
if (ref.color) {
|
|
56
|
+
token.style.setProperty("--persona-mention-token-accent", ref.color);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const iconEl = renderLucideIcon(ref.iconName ?? "at-sign", 13, "currentColor", 2);
|
|
60
|
+
if (iconEl) {
|
|
61
|
+
const iconHost = createElement("span", "persona-mention-token-icon");
|
|
62
|
+
iconHost.appendChild(iconEl);
|
|
63
|
+
token.appendChild(iconHost);
|
|
64
|
+
}
|
|
65
|
+
token.appendChild(
|
|
66
|
+
createNode("span", {
|
|
67
|
+
className: "persona-mention-token-label",
|
|
68
|
+
text: `@${ref.label}`,
|
|
69
|
+
})
|
|
70
|
+
);
|
|
71
|
+
return token;
|
|
72
|
+
}
|