@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
|
@@ -39,6 +39,26 @@ function clearDocumentToolbarLayoutVars(mount: HTMLElement): void {
|
|
|
39
39
|
mount.style.removeProperty("--persona-artifact-doc-toggle-active-border");
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
const ARTIFACT_APPEARANCE_MODES = ["panel", "seamless", "detached"] as const;
|
|
43
|
+
type ArtifactAppearance = (typeof ARTIFACT_APPEARANCE_MODES)[number];
|
|
44
|
+
|
|
45
|
+
/** Resolve the effective pane appearance, applying the detached-panel coordinated default. */
|
|
46
|
+
function resolveArtifactPaneAppearance(config: AgentWidgetConfig): ArtifactAppearance {
|
|
47
|
+
const raw = config.features?.artifacts?.layout?.paneAppearance;
|
|
48
|
+
if (raw && (ARTIFACT_APPEARANCE_MODES as readonly string[]).includes(raw)) {
|
|
49
|
+
return raw as ArtifactAppearance;
|
|
50
|
+
}
|
|
51
|
+
if (raw) return "panel";
|
|
52
|
+
// Coordinated default: a detached panel gets a detached pane unless overridden.
|
|
53
|
+
return config.launcher?.detachedPanel ? "detached" : "panel";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** True when the effective pane appearance resolves to "detached" (explicit or coordinated default). */
|
|
57
|
+
export function isArtifactPaneAppearanceDetached(config: AgentWidgetConfig | undefined): boolean {
|
|
58
|
+
if (!config || !artifactsSidebarEnabled(config)) return false;
|
|
59
|
+
return resolveArtifactPaneAppearance(config) === "detached";
|
|
60
|
+
}
|
|
61
|
+
|
|
42
62
|
export function applyArtifactLayoutCssVars(mount: HTMLElement, config: AgentWidgetConfig): void {
|
|
43
63
|
if (!artifactsSidebarEnabled(config)) {
|
|
44
64
|
mount.style.removeProperty("--persona-artifact-split-gap");
|
|
@@ -52,7 +72,12 @@ export function applyArtifactLayoutCssVars(mount: HTMLElement, config: AgentWidg
|
|
|
52
72
|
return;
|
|
53
73
|
}
|
|
54
74
|
const l = config.features?.artifacts?.layout;
|
|
55
|
-
|
|
75
|
+
// Detached shows the canvas between columns; panel/seamless weld at gap 0.
|
|
76
|
+
const gapDefault =
|
|
77
|
+
resolveArtifactPaneAppearance(config) === "detached"
|
|
78
|
+
? "var(--persona-panel-inset)"
|
|
79
|
+
: "0";
|
|
80
|
+
mount.style.setProperty("--persona-artifact-split-gap", l?.splitGap ?? gapDefault);
|
|
56
81
|
mount.style.setProperty("--persona-artifact-pane-width", l?.paneWidth ?? "40%");
|
|
57
82
|
mount.style.setProperty("--persona-artifact-pane-max-width", l?.paneMaxWidth ?? "28rem");
|
|
58
83
|
if (l?.paneMinWidth) {
|
|
@@ -95,22 +120,18 @@ export function applyArtifactLayoutCssVars(mount: HTMLElement, config: AgentWidg
|
|
|
95
120
|
applyArtifactPaneBorderTheme(mount, config);
|
|
96
121
|
}
|
|
97
122
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
/** Toggle root classes for artifact pane appearance, radius, shadow, and unified chrome. */
|
|
123
|
+
/** Toggle root classes for artifact pane appearance, radius, and shadow vars. */
|
|
101
124
|
export function applyArtifactPaneAppearance(mount: HTMLElement, config: AgentWidgetConfig): void {
|
|
102
125
|
for (const m of ARTIFACT_APPEARANCE_MODES) {
|
|
103
126
|
mount.classList.remove(`persona-artifact-appearance-${m}`);
|
|
104
127
|
}
|
|
105
|
-
mount.classList.remove("persona-artifact-unified-split");
|
|
106
128
|
mount.style.removeProperty("--persona-artifact-pane-radius");
|
|
107
129
|
mount.style.removeProperty("--persona-artifact-pane-shadow");
|
|
108
|
-
mount.style.removeProperty("--persona-artifact-
|
|
130
|
+
mount.style.removeProperty("--persona-artifact-chat-shadow");
|
|
109
131
|
if (!artifactsSidebarEnabled(config)) return;
|
|
110
132
|
|
|
111
133
|
const layout = config.features?.artifacts?.layout;
|
|
112
|
-
const
|
|
113
|
-
const mode = (ARTIFACT_APPEARANCE_MODES as readonly string[]).includes(raw) ? raw : "panel";
|
|
134
|
+
const mode = resolveArtifactPaneAppearance(config);
|
|
114
135
|
mount.classList.add(`persona-artifact-appearance-${mode}`);
|
|
115
136
|
|
|
116
137
|
const radius = layout?.paneBorderRadius?.trim();
|
|
@@ -123,13 +144,17 @@ export function applyArtifactPaneAppearance(mount: HTMLElement, config: AgentWid
|
|
|
123
144
|
mount.style.setProperty("--persona-artifact-pane-shadow", shadow);
|
|
124
145
|
}
|
|
125
146
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
147
|
+
// Chat card gains its own front shadow lookup so a detached split can flatten
|
|
148
|
+
// the chat column while the pane stays raised. Unset falls back to the pane chain.
|
|
149
|
+
const chatShadow = layout?.chatShadow?.trim();
|
|
150
|
+
if (chatShadow) {
|
|
151
|
+
mount.style.setProperty("--persona-artifact-chat-shadow", chatShadow);
|
|
132
152
|
}
|
|
153
|
+
|
|
154
|
+
// Panel/seamless splits weld by default; `unifiedSplitChrome` is a deprecated
|
|
155
|
+
// no-op. The welded outer-right radius (`--persona-artifact-welded-outer-radius`)
|
|
156
|
+
// is owned by ui.ts syncPanelChrome so it derives from the same resolved panel
|
|
157
|
+
// radius as the chat card; `unifiedSplitOuterRadius` / `paneBorderRadius` win there.
|
|
133
158
|
}
|
|
134
159
|
|
|
135
160
|
/** Widen floating panel when artifacts show (default true); `false` opts out. */
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { AgentWidgetArtifactsFeature } from "../types";
|
|
2
|
+
import { appendCharSpans } from "./tool-loading-animation";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Apply the artifact "Generating …" streaming animation to a status element.
|
|
6
|
+
*
|
|
7
|
+
* Shared by the artifact reference card and the inline chrome so both surfaces
|
|
8
|
+
* honor `features.artifacts.loadingAnimation` (and the color / duration knobs)
|
|
9
|
+
* identically. Character-by-character modes (`shimmer`, `shimmer-color`,
|
|
10
|
+
* `rainbow`) wrap each character in a `persona-tool-char` span; `pulse` fades
|
|
11
|
+
* the whole element; `none` renders plain text.
|
|
12
|
+
*
|
|
13
|
+
* The caller owns the element and its reset — pass a fresh (or cleared) span so
|
|
14
|
+
* stale animation classes from a previous streaming render do not linger.
|
|
15
|
+
*/
|
|
16
|
+
export function applyArtifactLoadingStatus(
|
|
17
|
+
statusText: HTMLElement,
|
|
18
|
+
text: string,
|
|
19
|
+
artifactsCfg: AgentWidgetArtifactsFeature | undefined
|
|
20
|
+
): void {
|
|
21
|
+
const loadingAnimation = artifactsCfg?.loadingAnimation ?? "shimmer";
|
|
22
|
+
const duration = artifactsCfg?.loadingAnimationDuration ?? 2000;
|
|
23
|
+
|
|
24
|
+
if (loadingAnimation === "none") {
|
|
25
|
+
statusText.textContent = text;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (loadingAnimation === "pulse") {
|
|
30
|
+
statusText.setAttribute("data-preserve-animation", "true");
|
|
31
|
+
statusText.classList.add("persona-tool-loading-pulse");
|
|
32
|
+
statusText.style.setProperty("--persona-tool-anim-duration", `${duration}ms`);
|
|
33
|
+
statusText.textContent = text;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
statusText.setAttribute("data-preserve-animation", "true");
|
|
38
|
+
statusText.classList.add(`persona-tool-loading-${loadingAnimation}`);
|
|
39
|
+
statusText.style.setProperty("--persona-tool-anim-duration", `${duration}ms`);
|
|
40
|
+
if (loadingAnimation === "shimmer-color") {
|
|
41
|
+
if (artifactsCfg?.loadingAnimationColor) {
|
|
42
|
+
statusText.style.setProperty(
|
|
43
|
+
"--persona-tool-anim-color",
|
|
44
|
+
artifactsCfg.loadingAnimationColor
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
if (artifactsCfg?.loadingAnimationSecondaryColor) {
|
|
48
|
+
statusText.style.setProperty(
|
|
49
|
+
"--persona-tool-anim-secondary-color",
|
|
50
|
+
artifactsCfg.loadingAnimationSecondaryColor
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
appendCharSpans(statusText, text, 0);
|
|
55
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentWidgetArtifactsFeature,
|
|
3
|
+
PersonaArtifactRecord,
|
|
4
|
+
PersonaArtifactStatusLabelContext
|
|
5
|
+
} from "../types";
|
|
6
|
+
import { createElement } from "./dom";
|
|
7
|
+
import { fileTypeLabel } from "./artifact-file";
|
|
8
|
+
import { applyArtifactLoadingStatus } from "./artifact-loading-status";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Shared resolver + applier for the artifact streaming status label
|
|
12
|
+
* (`features.artifacts.statusLabel`).
|
|
13
|
+
*
|
|
14
|
+
* Three surfaces render the "Generating …" status while an artifact streams:
|
|
15
|
+
* the reference card status line, the inline chrome meta span, and the inline
|
|
16
|
+
* streaming status body. All three resolve their text through
|
|
17
|
+
* {@link resolveArtifactStatusLabel} and paint it through
|
|
18
|
+
* {@link applyArtifactStatus}, so a host `statusLabel` string / function reads
|
|
19
|
+
* identically everywhere.
|
|
20
|
+
*
|
|
21
|
+
* Animation-stability contract: the animated label span is (re)built only when
|
|
22
|
+
* its text changes, so a per-delta detail update (live counters) never restarts
|
|
23
|
+
* the shimmer/pulse/typewriter animation. `applyArtifactStatus` enforces this by
|
|
24
|
+
* tracking the last-applied label text on the label span and by keeping the
|
|
25
|
+
* plain detail span as a separate sibling the animation never touches.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export type ArtifactStatusSurface = "card" | "inline-chrome" | "status-body";
|
|
29
|
+
|
|
30
|
+
export type ResolvedArtifactStatus = { label: string; detail?: string };
|
|
31
|
+
|
|
32
|
+
/** Class on the animated label span managed inside a status container. */
|
|
33
|
+
export const STATUS_LABEL_CLASS = "persona-artifact-status-label";
|
|
34
|
+
/** Class on the plain, freely-updating detail span. */
|
|
35
|
+
export const STATUS_DETAIL_CLASS = "persona-artifact-status-detail";
|
|
36
|
+
/** Attribute recording the last label text applied to the label span. */
|
|
37
|
+
const APPLIED_LABEL_ATTR = "data-artifact-status-label";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* First-seen streaming timestamp per artifact id, so `elapsedMs` starts near 0
|
|
41
|
+
* and a completed → re-streamed artifact restarts cleanly. Set on the first
|
|
42
|
+
* streaming resolve; cleared on complete (see {@link clearArtifactStatusTracking})
|
|
43
|
+
* so the map cannot grow without bound.
|
|
44
|
+
*/
|
|
45
|
+
const firstSeenAt = new Map<string, number>();
|
|
46
|
+
|
|
47
|
+
const now = (): number =>
|
|
48
|
+
typeof performance !== "undefined" && typeof performance.now === "function"
|
|
49
|
+
? performance.now()
|
|
50
|
+
: Date.now();
|
|
51
|
+
|
|
52
|
+
/** Drop the elapsed-time tracking for an artifact. Call on complete. */
|
|
53
|
+
export function clearArtifactStatusTracking(id: string): void {
|
|
54
|
+
if (id) firstSeenAt.delete(id);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Type subject used to build the default label; mirrors the card/chrome logic. */
|
|
58
|
+
function typeLabelFor(record: PersonaArtifactRecord): string {
|
|
59
|
+
const file = record.artifactType === "markdown" ? record.file : undefined;
|
|
60
|
+
if (file) return fileTypeLabel(file);
|
|
61
|
+
return record.artifactType === "component" ? "Component" : "Document";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolve the streaming status label + optional detail for one surface.
|
|
66
|
+
*
|
|
67
|
+
* - `statusLabel` unset → the default `Generating <type>...`.
|
|
68
|
+
* - string → that string as the label (no detail).
|
|
69
|
+
* - function → called with a per-surface context; a string return is the label,
|
|
70
|
+
* an object return is `{ label, detail }`. A throw falls back to the default.
|
|
71
|
+
*/
|
|
72
|
+
export function resolveArtifactStatusLabel(
|
|
73
|
+
record: PersonaArtifactRecord,
|
|
74
|
+
cfg: AgentWidgetArtifactsFeature | undefined,
|
|
75
|
+
surface: ArtifactStatusSurface
|
|
76
|
+
): ResolvedArtifactStatus {
|
|
77
|
+
const typeLabel = typeLabelFor(record);
|
|
78
|
+
const defaultLabel = `Generating ${typeLabel.toLowerCase()}...`;
|
|
79
|
+
|
|
80
|
+
const statusLabel = cfg?.statusLabel;
|
|
81
|
+
if (typeof statusLabel === "string") {
|
|
82
|
+
return { label: statusLabel };
|
|
83
|
+
}
|
|
84
|
+
if (typeof statusLabel !== "function") {
|
|
85
|
+
return { label: defaultLabel };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Track first-seen only while streaming (the surfaces only resolve while
|
|
89
|
+
// streaming); elapsedMs measures from that instant.
|
|
90
|
+
const id = record.id;
|
|
91
|
+
let firstSeen = firstSeenAt.get(id);
|
|
92
|
+
if (record.status !== "complete" && firstSeen === undefined) {
|
|
93
|
+
firstSeen = now();
|
|
94
|
+
firstSeenAt.set(id, firstSeen);
|
|
95
|
+
}
|
|
96
|
+
const elapsedMs = firstSeen === undefined ? 0 : Math.max(0, now() - firstSeen);
|
|
97
|
+
|
|
98
|
+
const isComponent = record.artifactType === "component";
|
|
99
|
+
const markdown =
|
|
100
|
+
!isComponent && typeof record.markdown === "string" ? record.markdown : "";
|
|
101
|
+
const chars = isComponent ? 0 : markdown.length;
|
|
102
|
+
const lines = isComponent || markdown === "" ? 0 : markdown.split("\n").length;
|
|
103
|
+
const file = record.artifactType === "markdown" ? record.file : undefined;
|
|
104
|
+
|
|
105
|
+
const ctx: PersonaArtifactStatusLabelContext = {
|
|
106
|
+
artifactId: id,
|
|
107
|
+
artifactType: record.artifactType,
|
|
108
|
+
title: record.title,
|
|
109
|
+
typeLabel,
|
|
110
|
+
file,
|
|
111
|
+
chars,
|
|
112
|
+
lines,
|
|
113
|
+
elapsedMs,
|
|
114
|
+
// Lazy: only materialize the accumulated source if the host asks for it.
|
|
115
|
+
content: () => (isComponent ? "" : markdown),
|
|
116
|
+
surface
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const result = statusLabel(ctx);
|
|
121
|
+
if (typeof result === "string") return { label: result };
|
|
122
|
+
if (
|
|
123
|
+
result &&
|
|
124
|
+
typeof result === "object" &&
|
|
125
|
+
typeof result.label === "string"
|
|
126
|
+
) {
|
|
127
|
+
return {
|
|
128
|
+
label: result.label,
|
|
129
|
+
detail: typeof result.detail === "string" ? result.detail : undefined
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
// A malformed return (null/number/etc.) falls back like a throw.
|
|
133
|
+
return { label: defaultLabel };
|
|
134
|
+
} catch {
|
|
135
|
+
// A bad host callback must never break rendering.
|
|
136
|
+
return { label: defaultLabel };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Restore the label span to its base state before re-applying the animation. */
|
|
141
|
+
function resetLabelSpan(labelEl: HTMLElement): void {
|
|
142
|
+
labelEl.className = STATUS_LABEL_CLASS;
|
|
143
|
+
labelEl.removeAttribute("data-preserve-animation");
|
|
144
|
+
labelEl.style.removeProperty("--persona-tool-anim-duration");
|
|
145
|
+
labelEl.style.removeProperty("--persona-tool-anim-color");
|
|
146
|
+
labelEl.style.removeProperty("--persona-tool-anim-secondary-color");
|
|
147
|
+
labelEl.replaceChildren();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Paint a resolved status into `container`, which owns exactly two children: an
|
|
152
|
+
* animated label span and an optional plain detail span.
|
|
153
|
+
*
|
|
154
|
+
* The label span is rebuilt (and its animation re-applied) ONLY when its text
|
|
155
|
+
* differs from the last application, so a detail-only update never restarts the
|
|
156
|
+
* loading animation. The detail span updates its `textContent` freely and is
|
|
157
|
+
* removed when the resolved detail is empty.
|
|
158
|
+
*/
|
|
159
|
+
export function applyArtifactStatus(
|
|
160
|
+
container: HTMLElement,
|
|
161
|
+
resolved: ResolvedArtifactStatus,
|
|
162
|
+
cfg: AgentWidgetArtifactsFeature | undefined
|
|
163
|
+
): void {
|
|
164
|
+
let labelEl = container.querySelector<HTMLElement>(
|
|
165
|
+
`:scope > .${STATUS_LABEL_CLASS}`
|
|
166
|
+
);
|
|
167
|
+
if (!labelEl) {
|
|
168
|
+
labelEl = createElement("span", STATUS_LABEL_CLASS);
|
|
169
|
+
container.appendChild(labelEl);
|
|
170
|
+
}
|
|
171
|
+
if (labelEl.getAttribute(APPLIED_LABEL_ATTR) !== resolved.label) {
|
|
172
|
+
resetLabelSpan(labelEl);
|
|
173
|
+
applyArtifactLoadingStatus(labelEl, resolved.label, cfg);
|
|
174
|
+
labelEl.setAttribute(APPLIED_LABEL_ATTR, resolved.label);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let detailEl = container.querySelector<HTMLElement>(
|
|
178
|
+
`:scope > .${STATUS_DETAIL_CLASS}`
|
|
179
|
+
);
|
|
180
|
+
const detail = resolved.detail;
|
|
181
|
+
if (detail) {
|
|
182
|
+
if (!detailEl) {
|
|
183
|
+
detailEl = createElement("span", STATUS_DETAIL_CLASS);
|
|
184
|
+
container.appendChild(detailEl);
|
|
185
|
+
}
|
|
186
|
+
if (detailEl.textContent !== detail) detailEl.textContent = detail;
|
|
187
|
+
} else if (detailEl) {
|
|
188
|
+
detailEl.remove();
|
|
189
|
+
}
|
|
190
|
+
}
|
package/src/utils/buttons.ts
CHANGED
|
@@ -149,6 +149,8 @@ export interface ToggleGroupItem {
|
|
|
149
149
|
icon?: string;
|
|
150
150
|
/** Accessible label for the button. */
|
|
151
151
|
label: string;
|
|
152
|
+
/** Extra CSS class(es) appended after "persona-icon-btn". */
|
|
153
|
+
className?: string;
|
|
152
154
|
}
|
|
153
155
|
|
|
154
156
|
/** Options for {@link createToggleGroup}. */
|
|
@@ -202,6 +204,7 @@ export function createToggleGroup(options: CreateToggleGroupOptions): ToggleGrou
|
|
|
202
204
|
btn = createIconButton({
|
|
203
205
|
icon: item.icon,
|
|
204
206
|
label: item.label,
|
|
207
|
+
className: item.className,
|
|
205
208
|
onClick: () => {
|
|
206
209
|
currentId = item.id;
|
|
207
210
|
updatePressed();
|
|
@@ -209,7 +212,10 @@ export function createToggleGroup(options: CreateToggleGroupOptions): ToggleGrou
|
|
|
209
212
|
},
|
|
210
213
|
});
|
|
211
214
|
} else {
|
|
212
|
-
btn = createElement(
|
|
215
|
+
btn = createElement(
|
|
216
|
+
"button",
|
|
217
|
+
"persona-icon-btn" + (item.className ? " " + item.className : ""),
|
|
218
|
+
);
|
|
213
219
|
btn.type = "button";
|
|
214
220
|
btn.setAttribute("aria-label", item.label);
|
|
215
221
|
btn.title = item.label;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { createChunkLoader } from "./chunk-loader";
|
|
3
|
+
|
|
4
|
+
type FakeModule = { value: number };
|
|
5
|
+
|
|
6
|
+
describe("createChunkLoader", () => {
|
|
7
|
+
it("uses the fallback import when no loader is registered", async () => {
|
|
8
|
+
const mod: FakeModule = { value: 1 };
|
|
9
|
+
const fallbackImport = vi.fn(async () => mod);
|
|
10
|
+
const { load } = createChunkLoader<FakeModule>({ fallbackImport });
|
|
11
|
+
|
|
12
|
+
await expect(load()).resolves.toBe(mod);
|
|
13
|
+
expect(fallbackImport).toHaveBeenCalledTimes(1);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("memoizes the resolved module and shares the in-flight promise", async () => {
|
|
17
|
+
const mod: FakeModule = { value: 2 };
|
|
18
|
+
const loader = vi.fn(async () => mod);
|
|
19
|
+
const { setLoader, load } = createChunkLoader<FakeModule>({
|
|
20
|
+
fallbackImport: async () => ({ value: -1 }),
|
|
21
|
+
});
|
|
22
|
+
setLoader(loader);
|
|
23
|
+
|
|
24
|
+
// Concurrent callers share one import.
|
|
25
|
+
const [a, b] = await Promise.all([load(), load()]);
|
|
26
|
+
expect(a).toBe(mod);
|
|
27
|
+
expect(b).toBe(mod);
|
|
28
|
+
// A later call returns the cached module without importing again.
|
|
29
|
+
await expect(load()).resolves.toBe(mod);
|
|
30
|
+
expect(loader).toHaveBeenCalledTimes(1);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("retries after a rejected load instead of caching the failure", async () => {
|
|
34
|
+
const mod: FakeModule = { value: 3 };
|
|
35
|
+
const loader = vi
|
|
36
|
+
.fn<() => Promise<FakeModule>>()
|
|
37
|
+
.mockRejectedValueOnce(new Error("network"))
|
|
38
|
+
.mockResolvedValueOnce(mod);
|
|
39
|
+
const { setLoader, load } = createChunkLoader<FakeModule>({
|
|
40
|
+
fallbackImport: async () => ({ value: -1 }),
|
|
41
|
+
});
|
|
42
|
+
setLoader(loader);
|
|
43
|
+
|
|
44
|
+
await expect(load()).rejects.toThrow("network");
|
|
45
|
+
// The failed promise was cleared, so a later call retries and resolves.
|
|
46
|
+
await expect(load()).resolves.toBe(mod);
|
|
47
|
+
expect(loader).toHaveBeenCalledTimes(2);
|
|
48
|
+
// Once resolved, the module is cached (no third loader call).
|
|
49
|
+
await expect(load()).resolves.toBe(mod);
|
|
50
|
+
expect(loader).toHaveBeenCalledTimes(2);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("does not invalidate the cache on setLoader by default", async () => {
|
|
54
|
+
const first: FakeModule = { value: 4 };
|
|
55
|
+
const second: FakeModule = { value: 5 };
|
|
56
|
+
const { setLoader, load } = createChunkLoader<FakeModule>({
|
|
57
|
+
fallbackImport: async () => ({ value: -1 }),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
setLoader(async () => first);
|
|
61
|
+
await expect(load()).resolves.toBe(first);
|
|
62
|
+
// Registering a new loader after a resolved load has no effect: cache holds.
|
|
63
|
+
setLoader(async () => second);
|
|
64
|
+
await expect(load()).resolves.toBe(first);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("invalidates the cache on setLoader when resetOnSetLoader is set", async () => {
|
|
68
|
+
const first: FakeModule = { value: 6 };
|
|
69
|
+
const second: FakeModule = { value: 7 };
|
|
70
|
+
const { setLoader, load } = createChunkLoader<FakeModule>({
|
|
71
|
+
fallbackImport: async () => ({ value: -1 }),
|
|
72
|
+
resetOnSetLoader: true,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
setLoader(async () => first);
|
|
76
|
+
await expect(load()).resolves.toBe(first);
|
|
77
|
+
// A swapped loader takes effect: the previous module is discarded.
|
|
78
|
+
setLoader(async () => second);
|
|
79
|
+
await expect(load()).resolves.toBe(second);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("provide() seeds the cache and getSync() reads it", async () => {
|
|
83
|
+
const mod: FakeModule = { value: 8 };
|
|
84
|
+
const loader = vi.fn(async () => ({ value: -1 }));
|
|
85
|
+
const { setLoader, load, provide, getSync } = createChunkLoader<FakeModule>({
|
|
86
|
+
fallbackImport: async () => ({ value: -1 }),
|
|
87
|
+
});
|
|
88
|
+
setLoader(loader);
|
|
89
|
+
|
|
90
|
+
expect(getSync()).toBeNull();
|
|
91
|
+
provide(mod);
|
|
92
|
+
expect(getSync()).toBe(mod);
|
|
93
|
+
// A provided module short-circuits load(): the loader is never called.
|
|
94
|
+
await expect(load()).resolves.toBe(mod);
|
|
95
|
+
expect(loader).not.toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic memoized loader for a lazy code-split chunk.
|
|
3
|
+
*
|
|
4
|
+
* Backs the near-identical loader indirections in the core/eager bundle
|
|
5
|
+
* (`context-mentions-loader.ts`, `context-mentions-inline-loader.ts`,
|
|
6
|
+
* `markdown-parsers-loader.ts`): each registers a runtime loader (IIFE/CDN
|
|
7
|
+
* sibling-URL chunk) and falls back to an external/relative dynamic import for
|
|
8
|
+
* ESM/CJS consumers. The shared semantics are:
|
|
9
|
+
*
|
|
10
|
+
* - `load()` memoizes the resolved module (`moduleCache`) and the in-flight
|
|
11
|
+
* promise (`loadPromise`) so concurrent callers share one import.
|
|
12
|
+
* - A rejected load clears `loadPromise` so a later call retries after a
|
|
13
|
+
* transient failure (one dropped fetch must not disable the feature for the
|
|
14
|
+
* whole session); the current caller still sees the rejection.
|
|
15
|
+
* - The registered loader wins over `fallbackImport`; if no loader is
|
|
16
|
+
* registered, `fallbackImport` is used.
|
|
17
|
+
*
|
|
18
|
+
* IMPORTANT: this module must import nothing beyond types. It sits in the core
|
|
19
|
+
* bundle, so any value import here could drag chunk code back into core and
|
|
20
|
+
* defeat the split (see `context-mentions-bundle.test.ts`).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export type ChunkLoaderOptions<T> = {
|
|
24
|
+
/**
|
|
25
|
+
* Dynamic import used when no runtime loader has been registered. Keep the
|
|
26
|
+
* `import(...)` literal in the calling module (not here) so the bundler
|
|
27
|
+
* resolves it as an external subpath / relative chunk at that call site.
|
|
28
|
+
*/
|
|
29
|
+
fallbackImport: () => Promise<T>;
|
|
30
|
+
/**
|
|
31
|
+
* When true, registering a new loader clears the cached module and any
|
|
32
|
+
* in-flight promise, so a swapped loader (test injection) takes effect
|
|
33
|
+
* deterministically. Production registers its loader once before any load, so
|
|
34
|
+
* this only matters mid-session.
|
|
35
|
+
*/
|
|
36
|
+
resetOnSetLoader?: boolean;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type ChunkLoader<T> = {
|
|
40
|
+
setLoader: (l: () => Promise<T>) => void;
|
|
41
|
+
load: () => Promise<T>;
|
|
42
|
+
/** Register a resolved module synchronously (eager builds that bundle it). */
|
|
43
|
+
provide: (mod: T) => void;
|
|
44
|
+
/** The cached module, or null if not loaded/provided yet. */
|
|
45
|
+
getSync: () => T | null;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const createChunkLoader = <T>(
|
|
49
|
+
options: ChunkLoaderOptions<T>
|
|
50
|
+
): ChunkLoader<T> => {
|
|
51
|
+
const { fallbackImport, resetOnSetLoader = false } = options;
|
|
52
|
+
|
|
53
|
+
let loader: (() => Promise<T>) | null = null;
|
|
54
|
+
let moduleCache: T | null = null;
|
|
55
|
+
let loadPromise: Promise<T> | null = null;
|
|
56
|
+
|
|
57
|
+
const setLoader = (l: () => Promise<T>): void => {
|
|
58
|
+
loader = l;
|
|
59
|
+
if (resetOnSetLoader) {
|
|
60
|
+
moduleCache = null;
|
|
61
|
+
loadPromise = null;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const load = (): Promise<T> => {
|
|
66
|
+
if (moduleCache) return Promise.resolve(moduleCache);
|
|
67
|
+
if (loadPromise) return loadPromise;
|
|
68
|
+
const importChunk = loader ?? fallbackImport;
|
|
69
|
+
loadPromise = importChunk()
|
|
70
|
+
.then((mod) => {
|
|
71
|
+
moduleCache = mod;
|
|
72
|
+
return mod;
|
|
73
|
+
})
|
|
74
|
+
.catch((err) => {
|
|
75
|
+
loadPromise = null;
|
|
76
|
+
throw err;
|
|
77
|
+
});
|
|
78
|
+
return loadPromise;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const provide = (mod: T): void => {
|
|
82
|
+
moduleCache = mod;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const getSync = (): T | null => moduleCache;
|
|
86
|
+
|
|
87
|
+
return { setLoader, load, provide, getSync };
|
|
88
|
+
};
|