@runtypelabs/persona 4.7.0 → 4.8.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/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-BsZtXPKK.d.cts} +43 -3
- package/dist/animations/{types-CSmiKRVa.d.ts → types-BsZtXPKK.d.ts} +43 -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/codegen.cjs +12 -12
- package/dist/codegen.js +14 -14
- package/dist/index.cjs +91 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +877 -172
- package/dist/index.d.ts +877 -172
- package/dist/index.global.js +81 -57
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +91 -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.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 +17 -17
- package/dist/smart-dom-reader.d.cts +753 -15
- package/dist/smart-dom-reader.d.ts +753 -15
- package/dist/smart-dom-reader.js +17 -17
- package/dist/testing.cjs +3 -3
- package/dist/testing.js +3 -3
- package/dist/theme-editor-preview.cjs +81 -58
- package/dist/theme-editor-preview.d.cts +761 -15
- package/dist/theme-editor-preview.d.ts +761 -15
- package/dist/theme-editor-preview.js +81 -58
- package/dist/theme-editor.cjs +6 -6
- package/dist/theme-editor.d.cts +753 -15
- package/dist/theme-editor.d.ts +753 -15
- package/dist/theme-editor.js +10 -10
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +74 -0
- package/dist/theme-reference.d.ts +74 -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 +2 -3
- package/src/artifacts-session.test.ts +178 -0
- package/src/client.test.ts +186 -1
- package/src/client.ts +92 -9
- 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/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/defaults.ts +1 -0
- package/src/generated/runtype-openapi-contract.ts +55 -3
- package/src/index-core.ts +20 -1
- package/src/index.ts +8 -0
- package/src/markdown-parsers-loader.test.ts +158 -0
- package/src/markdown-parsers-loader.ts +74 -9
- 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.ts +76 -22
- package/src/styles/widget.css +773 -26
- package/src/theme-editor/preview.ts +2 -0
- package/src/theme-editor/sections.test.ts +26 -1
- package/src/theme-editor/sections.ts +10 -2
- package/src/theme-reference.ts +2 -2
- package/src/tool-call-display-defaults.test.ts +1 -0
- package/src/types/theme.ts +77 -0
- package/src/types.ts +516 -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.tool-display.test.ts +51 -0
- package/src/ui.ts +698 -109
- 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/code-highlight.test.ts +186 -0
- package/src/utils/code-highlight.ts +400 -0
- package/src/utils/icons.ts +2 -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 +48 -0
- package/src/utils/theme.ts +7 -0
- package/src/utils/tokens.ts +91 -0
- 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
package/src/ui.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { escapeHtml, createMarkdownProcessorFromConfig } from "./postprocessors";
|
|
2
2
|
import { resolveSanitizer } from "./utils/sanitize";
|
|
3
3
|
import { stabilizeStreamingTables } from "./utils/streaming-table";
|
|
4
|
-
import {
|
|
4
|
+
import { onMarkdownParsersReady, getMarkdownParsersSync } from "./markdown-parsers-loader";
|
|
5
5
|
import { AgentWidgetSession, AgentWidgetSessionStatus } from "./session";
|
|
6
6
|
import {
|
|
7
7
|
AgentWidgetConfig,
|
|
@@ -28,7 +28,9 @@ import {
|
|
|
28
28
|
VoiceStatus,
|
|
29
29
|
ReadAloudState,
|
|
30
30
|
PersonaArtifactRecord,
|
|
31
|
-
PersonaArtifactManualUpsert
|
|
31
|
+
PersonaArtifactManualUpsert,
|
|
32
|
+
PersonaArtifactFileMeta,
|
|
33
|
+
PersonaArtifactActionContext
|
|
32
34
|
} from "./types";
|
|
33
35
|
import { AttachmentManager } from "./utils/attachment-manager";
|
|
34
36
|
import { createTextPart, ALL_SUPPORTED_MIME_TYPES } from "./utils/content";
|
|
@@ -36,6 +38,8 @@ import { applyThemeVariables, createThemeObserver, getActiveTheme } from "./util
|
|
|
36
38
|
import { resolveTokenValue } from "./utils/tokens";
|
|
37
39
|
import { renderLucideIcon } from "./utils/icons";
|
|
38
40
|
import { createElement, createElementInDocument } from "./utils/dom";
|
|
41
|
+
import { downloadInfoFor } from "./utils/artifact-file";
|
|
42
|
+
import { artifactCopyText } from "./components/artifact-preview";
|
|
39
43
|
import { morphMessages } from "./utils/morph";
|
|
40
44
|
import { normalizeCopiedSelectionText } from "./utils/copy-selection";
|
|
41
45
|
import {
|
|
@@ -106,12 +110,18 @@ import { EventStreamStore } from "./utils/event-stream-store";
|
|
|
106
110
|
import { ThroughputTracker } from "./utils/throughput-tracker";
|
|
107
111
|
import { createEventStreamView } from "./components/event-stream-view";
|
|
108
112
|
import { createArtifactPane, type ArtifactPaneApi } from "./components/artifact-pane";
|
|
113
|
+
import {
|
|
114
|
+
hasLiveInlineArtifactBlock,
|
|
115
|
+
updateInlineArtifactBlocks
|
|
116
|
+
} from "./components/artifact-inline";
|
|
109
117
|
import {
|
|
110
118
|
artifactsSidebarEnabled,
|
|
111
119
|
applyArtifactLayoutCssVars,
|
|
112
120
|
applyArtifactPaneAppearance,
|
|
121
|
+
isArtifactPaneAppearanceDetached,
|
|
113
122
|
shouldExpandLauncherForArtifacts
|
|
114
123
|
} from "./utils/artifact-gate";
|
|
124
|
+
import { resolveArtifactDisplayMode } from "./utils/artifact-display";
|
|
115
125
|
import { readFlexGapPx, resolveArtifactPaneWidthPx } from "./utils/artifact-resize";
|
|
116
126
|
import { enhanceWithForms } from "./components/forms";
|
|
117
127
|
import { pluginRegistry } from "./plugins/registry";
|
|
@@ -139,6 +149,8 @@ import {
|
|
|
139
149
|
// Default localStorage key for chat history (automatically cleared on clear chat)
|
|
140
150
|
const DEFAULT_CHAT_HISTORY_STORAGE_KEY = "persona-chat-history";
|
|
141
151
|
const VOICE_STATE_RESTORE_WINDOW = 30 * 1000;
|
|
152
|
+
// Split desktop boundary; must match widget.css artifact media queries (min-width:641px).
|
|
153
|
+
const ARTIFACT_SPLIT_DESKTOP_MIN = 641;
|
|
142
154
|
|
|
143
155
|
const IMAGE_FILE_EXTENSION_BY_MIME_TYPE: Record<string, string> = {
|
|
144
156
|
"image/png": "png",
|
|
@@ -1670,32 +1682,66 @@ export const createAgentExperience = (
|
|
|
1670
1682
|
|
|
1671
1683
|
let artifactPaneApi: ArtifactPaneApi | null = null;
|
|
1672
1684
|
let artifactPanelResizeObs: ResizeObserver | null = null;
|
|
1685
|
+
// Re-runs panel chrome when the split-chrome mode flips (pane
|
|
1686
|
+
// open/close/appearance change). Assigned once applyFullHeightStyles exists.
|
|
1687
|
+
let syncPanelChrome: () => void = () => {};
|
|
1688
|
+
let appliedSplitMode: "none" | "welded" | "detached" = "none";
|
|
1689
|
+
// Resolved welded outer-right radius stashed by applyFullHeightStyles and
|
|
1690
|
+
// applied in syncPanelChrome (after applyArtifactPaneAppearance, which no
|
|
1691
|
+
// longer manages the var, so the two never fight). Null clears it.
|
|
1692
|
+
let weldedOuterRadius: string | null = null;
|
|
1673
1693
|
let lastArtifactsState: {
|
|
1674
1694
|
artifacts: PersonaArtifactRecord[];
|
|
1675
1695
|
selectedId: string | null;
|
|
1676
1696
|
} = { artifacts: [], selectedId: null };
|
|
1677
1697
|
let artifactsPaneUserHidden = false;
|
|
1698
|
+
// Runtime-only expand state: pane fills the split root and the chat column
|
|
1699
|
+
// hides. Reset whenever the pane stops being visible (syncArtifactPane).
|
|
1700
|
+
let artifactPaneExpanded = false;
|
|
1701
|
+
// Set when the expansion came from an inline block's Expand button: that is
|
|
1702
|
+
// an explicit "fullscreen this file" request, so it survives the
|
|
1703
|
+
// showExpandToggle gate below (which otherwise collapses the pane for hosts
|
|
1704
|
+
// without the toolbar toggle). Cleared whenever the pane collapses or hides.
|
|
1705
|
+
let artifactPaneExpandedPinned = false;
|
|
1706
|
+
// Whether the user explicitly opened the pane (card click, inline Expand,
|
|
1707
|
+
// showArtifacts(), programmatic upsert). Auto-open is otherwise reserved for
|
|
1708
|
+
// artifacts whose resolved display mode is "panel": "card" keeps the card as
|
|
1709
|
+
// the only affordance and "inline" renders in the transcript. "inline" no
|
|
1710
|
+
// longer *auto*-opens the pane, but its Expand control is a deliberate,
|
|
1711
|
+
// user-driven open (setting artifactsPaneUserOpened, same as a card click);
|
|
1712
|
+
// every mode still writes the session artifact registry (download /
|
|
1713
|
+
// getArtifacts / hydration).
|
|
1714
|
+
let artifactsPaneUserOpened = false;
|
|
1715
|
+
const artifactPaneCanShow = () =>
|
|
1716
|
+
artifactsPaneUserOpened ||
|
|
1717
|
+
lastArtifactsState.artifacts.some(
|
|
1718
|
+
(a) =>
|
|
1719
|
+
resolveArtifactDisplayMode(config.features?.artifacts, a.artifactType) === "panel"
|
|
1720
|
+
);
|
|
1721
|
+
const artifactPaneVisible = () =>
|
|
1722
|
+
lastArtifactsState.artifacts.length > 0 &&
|
|
1723
|
+
!artifactsPaneUserHidden &&
|
|
1724
|
+
artifactPaneCanShow();
|
|
1678
1725
|
const sessionRef: { current: AgentWidgetSession | null } = { current: null };
|
|
1679
1726
|
|
|
1680
|
-
//
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
if (!artifactId) return;
|
|
1689
|
-
// Let integrator intercept
|
|
1690
|
-
const dlPrevented = config.features?.artifacts?.onArtifactAction?.({ type: 'download', artifactId });
|
|
1691
|
-
if (dlPrevented === true) return;
|
|
1692
|
-
// Try session state first, fall back to content stored in the card's rawContent props
|
|
1727
|
+
// Resolve an artifact's content for card actions (download + custom actions).
|
|
1728
|
+
// Tries the live session registry first, then falls back to the content
|
|
1729
|
+
// persisted in the card message's rawContent props (session state is gone
|
|
1730
|
+
// after a page refresh). `fromEl` is the clicked element inside the card.
|
|
1731
|
+
const resolveCardArtifactContent = (
|
|
1732
|
+
fromEl: HTMLElement,
|
|
1733
|
+
artifactId: string
|
|
1734
|
+
): { markdown?: string; title: string; file?: PersonaArtifactFileMeta; artifactType: string } => {
|
|
1693
1735
|
const artifact = session.getArtifactById(artifactId);
|
|
1694
1736
|
let markdown = artifact?.markdown;
|
|
1695
1737
|
let title = artifact?.title || 'artifact';
|
|
1738
|
+
let file: PersonaArtifactFileMeta | undefined = artifact?.file;
|
|
1739
|
+
let artifactType: string = artifact?.artifactType ?? 'markdown';
|
|
1696
1740
|
if (!markdown) {
|
|
1697
|
-
//
|
|
1698
|
-
|
|
1741
|
+
// Match both reference cards and inline blocks: inline hydration embeds
|
|
1742
|
+
// the markdown / file source in the same message rawContent, so the parse
|
|
1743
|
+
// below resolves content for inline copy / custom actions after a refresh.
|
|
1744
|
+
const cardEl = fromEl.closest('[data-open-artifact], [data-artifact-inline]');
|
|
1699
1745
|
const msgEl = cardEl?.closest('[data-message-id]');
|
|
1700
1746
|
const msgId = msgEl?.getAttribute('data-message-id');
|
|
1701
1747
|
if (msgId) {
|
|
@@ -1706,23 +1752,159 @@ export const createAgentExperience = (
|
|
|
1706
1752
|
const parsed = JSON.parse(msg.rawContent);
|
|
1707
1753
|
markdown = parsed?.props?.markdown;
|
|
1708
1754
|
title = parsed?.props?.title || title;
|
|
1755
|
+
if (parsed?.props?.file && typeof parsed.props.file === 'object') {
|
|
1756
|
+
file = parsed.props.file as PersonaArtifactFileMeta;
|
|
1757
|
+
}
|
|
1758
|
+
if (!artifact && typeof parsed?.props?.artifactType === 'string') {
|
|
1759
|
+
artifactType = parsed.props.artifactType;
|
|
1760
|
+
}
|
|
1709
1761
|
} catch { /* ignore */ }
|
|
1710
1762
|
}
|
|
1711
1763
|
}
|
|
1712
1764
|
}
|
|
1765
|
+
return { markdown, title, file, artifactType };
|
|
1766
|
+
};
|
|
1767
|
+
|
|
1768
|
+
// Click delegation for artifact download buttons
|
|
1769
|
+
messagesWrapper.addEventListener('click', (event) => {
|
|
1770
|
+
const target = event.target as HTMLElement;
|
|
1771
|
+
const dlBtn = target.closest('[data-download-artifact]') as HTMLElement;
|
|
1772
|
+
if (!dlBtn) return;
|
|
1773
|
+
event.preventDefault();
|
|
1774
|
+
event.stopPropagation();
|
|
1775
|
+
const artifactId = dlBtn.getAttribute('data-download-artifact');
|
|
1776
|
+
if (!artifactId) return;
|
|
1777
|
+
// Let integrator intercept
|
|
1778
|
+
const dlPrevented = config.features?.artifacts?.onArtifactAction?.({ type: 'download', artifactId });
|
|
1779
|
+
if (dlPrevented === true) return;
|
|
1780
|
+
const { markdown, title, file } = resolveCardArtifactContent(dlBtn, artifactId);
|
|
1713
1781
|
if (!markdown) return;
|
|
1714
|
-
|
|
1782
|
+
// File artifacts download the raw unfenced source under their real name/MIME;
|
|
1783
|
+
// non-file markdown artifacts keep the legacy `<title>.md` / text/markdown path.
|
|
1784
|
+
const { filename, mime, content } = downloadInfoFor({ title, markdown, file });
|
|
1785
|
+
const blob = new Blob([content], { type: mime });
|
|
1715
1786
|
const url = URL.createObjectURL(blob);
|
|
1716
1787
|
const a = document.createElement('a');
|
|
1717
1788
|
a.href = url;
|
|
1718
|
-
a.download =
|
|
1789
|
+
a.download = filename;
|
|
1719
1790
|
a.click();
|
|
1720
1791
|
URL.revokeObjectURL(url);
|
|
1721
1792
|
});
|
|
1722
1793
|
|
|
1794
|
+
// Click delegation for integrator-supplied card action buttons. Actions are
|
|
1795
|
+
// looked up by id from fresh config at click time so live config updates
|
|
1796
|
+
// apply. Like the download listener, its stopPropagation() cannot block the
|
|
1797
|
+
// card-open listener on the same element, so that listener skips these too.
|
|
1798
|
+
messagesWrapper.addEventListener('click', (event) => {
|
|
1799
|
+
const target = event.target as HTMLElement;
|
|
1800
|
+
const actionBtn = target.closest('[data-artifact-custom-action]') as HTMLElement;
|
|
1801
|
+
if (!actionBtn) return;
|
|
1802
|
+
event.preventDefault();
|
|
1803
|
+
event.stopPropagation();
|
|
1804
|
+
const actionId = actionBtn.getAttribute('data-artifact-custom-action');
|
|
1805
|
+
if (!actionId) return;
|
|
1806
|
+
// The same attribute serves cards and inline chrome; resolve which surface
|
|
1807
|
+
// the button lives in so the action comes from the matching config list and
|
|
1808
|
+
// the artifact id from the matching container attribute.
|
|
1809
|
+
const inlineEl = actionBtn.closest('[data-artifact-inline]');
|
|
1810
|
+
const cardEl = inlineEl ? null : actionBtn.closest('[data-open-artifact]');
|
|
1811
|
+
const artifactId = inlineEl
|
|
1812
|
+
? inlineEl.getAttribute('data-artifact-inline')
|
|
1813
|
+
: cardEl?.getAttribute('data-open-artifact') ?? null;
|
|
1814
|
+
const actionList = inlineEl
|
|
1815
|
+
? config.features?.artifacts?.inlineActions
|
|
1816
|
+
: config.features?.artifacts?.cardActions;
|
|
1817
|
+
const action = actionList?.find((a) => a.id === actionId);
|
|
1818
|
+
if (!action) return;
|
|
1819
|
+
const { markdown, title, file, artifactType } = resolveCardArtifactContent(actionBtn, artifactId ?? '');
|
|
1820
|
+
const ctx: PersonaArtifactActionContext = { artifactId, title, artifactType, markdown, file };
|
|
1821
|
+
try {
|
|
1822
|
+
void Promise.resolve(action.onClick(ctx)).catch(() => {});
|
|
1823
|
+
} catch {
|
|
1824
|
+
/* ignore */
|
|
1825
|
+
}
|
|
1826
|
+
});
|
|
1827
|
+
|
|
1828
|
+
// Click delegation for the inline chrome Copy button. Like the download /
|
|
1829
|
+
// custom-action listeners, it resolves content from the live registry first,
|
|
1830
|
+
// then falls back to the persisted inline props after a refresh, and its
|
|
1831
|
+
// stopPropagation() cannot block a second listener on the same element (there
|
|
1832
|
+
// is none here, but the pattern is kept consistent).
|
|
1833
|
+
messagesWrapper.addEventListener('click', (event) => {
|
|
1834
|
+
const target = event.target as HTMLElement;
|
|
1835
|
+
const copyBtn = target.closest('[data-copy-artifact]') as HTMLElement;
|
|
1836
|
+
if (!copyBtn) return;
|
|
1837
|
+
event.preventDefault();
|
|
1838
|
+
event.stopPropagation();
|
|
1839
|
+
const artifactId = copyBtn.getAttribute('data-copy-artifact');
|
|
1840
|
+
if (!artifactId) return;
|
|
1841
|
+
// Prefer the live record (covers component JSON); fall back to the persisted
|
|
1842
|
+
// markdown / file source parsed from the inline block's message rawContent.
|
|
1843
|
+
const artifact = session.getArtifactById(artifactId);
|
|
1844
|
+
let text = '';
|
|
1845
|
+
if (artifact) {
|
|
1846
|
+
text = artifactCopyText(artifact);
|
|
1847
|
+
} else {
|
|
1848
|
+
const { markdown, file, artifactType } = resolveCardArtifactContent(copyBtn, artifactId);
|
|
1849
|
+
if (artifactType === 'markdown') {
|
|
1850
|
+
text = artifactCopyText({
|
|
1851
|
+
id: artifactId,
|
|
1852
|
+
artifactType: 'markdown',
|
|
1853
|
+
status: 'complete',
|
|
1854
|
+
markdown: markdown ?? '',
|
|
1855
|
+
...(file ? { file } : {})
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
if (!text) return;
|
|
1860
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
1861
|
+
// Lightweight feedback: swap the copy glyph for a check briefly.
|
|
1862
|
+
const checkIcon = renderLucideIcon('check', 16, 'currentColor', 2);
|
|
1863
|
+
if (checkIcon) {
|
|
1864
|
+
copyBtn.replaceChildren(checkIcon);
|
|
1865
|
+
setTimeout(() => {
|
|
1866
|
+
const copyIcon = renderLucideIcon('copy', 16, 'currentColor', 2);
|
|
1867
|
+
if (copyIcon) copyBtn.replaceChildren(copyIcon);
|
|
1868
|
+
}, 1500);
|
|
1869
|
+
}
|
|
1870
|
+
}).catch(() => { /* ignore */ });
|
|
1871
|
+
});
|
|
1872
|
+
|
|
1873
|
+
// Click delegation for the inline chrome Expand button: open this artifact in
|
|
1874
|
+
// the pane. Fires onArtifactAction({ type: "open" }) first so hosts can
|
|
1875
|
+
// intercept, then mirrors the card-open path — except the pane opens
|
|
1876
|
+
// expanded (fullscreen), never split: the inline block already shows the
|
|
1877
|
+
// full preview at chat width, so a split pane would only duplicate it, and
|
|
1878
|
+
// the click means "expand this file". The pinned flag keeps it expanded
|
|
1879
|
+
// even when layout.showExpandToggle is off; Close is the exit there.
|
|
1880
|
+
messagesWrapper.addEventListener('click', (event) => {
|
|
1881
|
+
const target = event.target as HTMLElement;
|
|
1882
|
+
const expandBtn = target.closest('[data-expand-artifact-inline]') as HTMLElement;
|
|
1883
|
+
if (!expandBtn) return;
|
|
1884
|
+
event.preventDefault();
|
|
1885
|
+
event.stopPropagation();
|
|
1886
|
+
const artifactId = expandBtn.getAttribute('data-expand-artifact-inline');
|
|
1887
|
+
if (!artifactId) return;
|
|
1888
|
+
const openPrevented = config.features?.artifacts?.onArtifactAction?.({ type: 'open', artifactId });
|
|
1889
|
+
if (openPrevented === true) return;
|
|
1890
|
+
artifactsPaneUserHidden = false;
|
|
1891
|
+
// Expand is an explicit open: it overrides the "inline" auto-open
|
|
1892
|
+
// suppression for as long as artifacts exist (same as a card click).
|
|
1893
|
+
artifactsPaneUserOpened = true;
|
|
1894
|
+
artifactPaneExpanded = true;
|
|
1895
|
+
artifactPaneExpandedPinned = true;
|
|
1896
|
+
session.selectArtifact(artifactId);
|
|
1897
|
+
syncArtifactPane();
|
|
1898
|
+
});
|
|
1899
|
+
|
|
1723
1900
|
// Click delegation for artifact reference cards
|
|
1724
1901
|
messagesWrapper.addEventListener('click', (event) => {
|
|
1725
1902
|
const target = event.target as HTMLElement;
|
|
1903
|
+
// Download and custom-action clicks are handled by the listeners above;
|
|
1904
|
+
// their stopPropagation() cannot block a second listener on the same
|
|
1905
|
+
// element, so skip them here explicitly or the card would also open the panel.
|
|
1906
|
+
if (target.closest('[data-download-artifact]')) return;
|
|
1907
|
+
if (target.closest('[data-artifact-custom-action]')) return;
|
|
1726
1908
|
const card = target.closest('[data-open-artifact]') as HTMLElement;
|
|
1727
1909
|
if (!card) return;
|
|
1728
1910
|
const artifactId = card.getAttribute('data-open-artifact');
|
|
@@ -1733,15 +1915,24 @@ export const createAgentExperience = (
|
|
|
1733
1915
|
event.preventDefault();
|
|
1734
1916
|
event.stopPropagation();
|
|
1735
1917
|
artifactsPaneUserHidden = false;
|
|
1918
|
+
// Card click is an explicit open: it overrides the "card"/"inline"
|
|
1919
|
+
// auto-open suppression for as long as artifacts exist.
|
|
1920
|
+
artifactsPaneUserOpened = true;
|
|
1736
1921
|
session.selectArtifact(artifactId);
|
|
1737
1922
|
syncArtifactPane();
|
|
1738
1923
|
});
|
|
1739
1924
|
|
|
1740
|
-
// Keyboard support for artifact cards
|
|
1925
|
+
// Keyboard support for artifact cards and the clip-mode inline expand hitbox
|
|
1926
|
+
// (both expose a data-* attribute on a focusable element; Enter/Space forward
|
|
1927
|
+
// to the element's own click, which the delegated click handlers above own).
|
|
1741
1928
|
messagesWrapper.addEventListener('keydown', (event) => {
|
|
1742
1929
|
if (event.key !== 'Enter' && event.key !== ' ') return;
|
|
1743
1930
|
const target = event.target as HTMLElement;
|
|
1744
|
-
if (
|
|
1931
|
+
if (
|
|
1932
|
+
!target.hasAttribute('data-open-artifact') &&
|
|
1933
|
+
!target.hasAttribute('data-expand-artifact-inline')
|
|
1934
|
+
)
|
|
1935
|
+
return;
|
|
1745
1936
|
event.preventDefault();
|
|
1746
1937
|
target.click();
|
|
1747
1938
|
});
|
|
@@ -2130,6 +2321,12 @@ export const createAgentExperience = (
|
|
|
2130
2321
|
let artifactResizeUnbind: (() => void) | null = null;
|
|
2131
2322
|
let artifactResizeDocEnd: (() => void) | null = null;
|
|
2132
2323
|
let reconcileArtifactResize: () => void = () => {};
|
|
2324
|
+
// Expanded-state transition tracking: stash the resizer's inline width/maxWidth
|
|
2325
|
+
// (which would otherwise beat the expanded class) once per enter, restore once
|
|
2326
|
+
// per leave.
|
|
2327
|
+
let artifactExpandedApplied = false;
|
|
2328
|
+
let artifactStashedWidth = "";
|
|
2329
|
+
let artifactStashedMaxWidth = "";
|
|
2133
2330
|
|
|
2134
2331
|
function stopArtifactResizePointer() {
|
|
2135
2332
|
artifactResizeDocEnd?.();
|
|
@@ -2139,7 +2336,9 @@ export const createAgentExperience = (
|
|
|
2139
2336
|
/** Flush split: overlay handle on the seam so it does not consume flex gap (extension + resizable). */
|
|
2140
2337
|
const positionExtensionArtifactResizeHandle = () => {
|
|
2141
2338
|
if (!artifactSplitRoot || !artifactResizeHandle) return;
|
|
2142
|
-
|
|
2339
|
+
// Welded splits (panel + seamless) have gap 0, so the handle overlays the seam
|
|
2340
|
+
// instead of consuming flex width; detached keeps a real gap and a flex handle.
|
|
2341
|
+
const ext = mount.classList.contains("persona-artifact-welded-split");
|
|
2143
2342
|
const ownerWin = mount.ownerDocument.defaultView ?? window;
|
|
2144
2343
|
const mobile = ownerWin.innerWidth <= 640;
|
|
2145
2344
|
if (!ext || mount.classList.contains("persona-artifact-narrow-host") || mobile) {
|
|
@@ -2159,13 +2358,45 @@ export const createAgentExperience = (
|
|
|
2159
2358
|
artifactResizeHandle.style.bottom = "0";
|
|
2160
2359
|
artifactResizeHandle.style.width = `${hitW}px`;
|
|
2161
2360
|
artifactResizeHandle.style.zIndex = "5";
|
|
2162
|
-
|
|
2361
|
+
// Center the overlay on the seam: with an explicit welded gap the seam sits
|
|
2362
|
+
// half a gap past the chat's right edge (welded default gap is 0).
|
|
2363
|
+
const gapPx = readFlexGapPx(artifactSplitRoot, ownerWin);
|
|
2364
|
+
const left = chat.offsetWidth + gapPx / 2 - hitW / 2;
|
|
2163
2365
|
artifactResizeHandle.style.left = `${Math.max(0, left)}px`;
|
|
2164
2366
|
};
|
|
2165
2367
|
|
|
2166
2368
|
/** No-op until artifact pane is created; replaced below when artifacts are enabled. */
|
|
2167
2369
|
let applyLauncherArtifactPanelWidth: () => void = () => {};
|
|
2168
2370
|
|
|
2371
|
+
// Mobile fullscreen renders a flush, chrome-less panel, so the split chrome
|
|
2372
|
+
// must stand down there. This is the union of the two fullscreen predicates
|
|
2373
|
+
// applyFullHeightStyles/recalcPanelHeight use: launcher fullscreen
|
|
2374
|
+
// (shouldGoFullscreen) and the docked-host variant (dockedHostFullscreen).
|
|
2375
|
+
// Unlike the 641px split breakpoint, it honors the configurable mobileBreakpoint.
|
|
2376
|
+
const isMobileFullscreenActive = (): boolean => {
|
|
2377
|
+
const win = mount.ownerDocument.defaultView ?? window;
|
|
2378
|
+
const mobileFullscreen = config.launcher?.mobileFullscreen ?? true;
|
|
2379
|
+
const mobileBreakpoint = config.launcher?.mobileBreakpoint ?? 640;
|
|
2380
|
+
if (!mobileFullscreen || win.innerWidth > mobileBreakpoint) return false;
|
|
2381
|
+
return launcherEnabled || isDockedMountMode(config);
|
|
2382
|
+
};
|
|
2383
|
+
|
|
2384
|
+
// Desktop side-by-side split chrome mode. 'welded' = panel|seamless (one card,
|
|
2385
|
+
// border+radius wrap both columns); 'detached' = two floating cards; 'none' when
|
|
2386
|
+
// the pane is closed, the narrow-host drawer, or mobile fullscreen.
|
|
2387
|
+
const splitChromeMode = (): "none" | "welded" | "detached" => {
|
|
2388
|
+
if (!artifactsSidebarEnabled(config)) return "none";
|
|
2389
|
+
if (!artifactPaneVisible()) return "none";
|
|
2390
|
+
if (mount.classList.contains("persona-artifact-narrow-host")) return "none";
|
|
2391
|
+
// A fullscreen panel is flush; welded/detached corner chrome would fight it.
|
|
2392
|
+
if (isMobileFullscreenActive()) return "none";
|
|
2393
|
+
const win = mount.ownerDocument.defaultView ?? window;
|
|
2394
|
+
// Must match the artifact split media queries in widget.css (drawer at
|
|
2395
|
+
// max-width:640, split at min-width:641); mobileBreakpoint does not move it.
|
|
2396
|
+
if (win.innerWidth < ARTIFACT_SPLIT_DESKTOP_MIN) return "none";
|
|
2397
|
+
return isArtifactPaneAppearanceDetached(config) ? "detached" : "welded";
|
|
2398
|
+
};
|
|
2399
|
+
|
|
2169
2400
|
const syncArtifactPane = () => {
|
|
2170
2401
|
if (!artifactPaneApi || !artifactsSidebarEnabled(config)) return;
|
|
2171
2402
|
applyArtifactLayoutCssVars(mount, config);
|
|
@@ -2174,16 +2405,79 @@ export const createAgentExperience = (
|
|
|
2174
2405
|
const threshold = config.features?.artifacts?.layout?.narrowHostMaxWidth ?? 520;
|
|
2175
2406
|
const w = panel.getBoundingClientRect().width || 0;
|
|
2176
2407
|
mount.classList.toggle("persona-artifact-narrow-host", w > 0 && w <= threshold);
|
|
2408
|
+
// Thread the single source of truth for "pane surface is shown" into the
|
|
2409
|
+
// pane BEFORE update() so it renders lazily: in inline/card display modes
|
|
2410
|
+
// the pane stays hidden and must not build a second sandboxed artifact
|
|
2411
|
+
// iframe (which would execute artifact scripts twice). The pane records
|
|
2412
|
+
// state while hidden and renders on the next reveal. Set first so an
|
|
2413
|
+
// update() that arrives while hidden is skipped, not rendered-then-hidden.
|
|
2414
|
+
const paneVisible = artifactPaneVisible();
|
|
2415
|
+
artifactPaneApi.setVisible(paneVisible);
|
|
2177
2416
|
artifactPaneApi.update(lastArtifactsState);
|
|
2178
2417
|
if (artifactsPaneUserHidden) {
|
|
2179
2418
|
artifactPaneApi.setMobileOpen(false);
|
|
2180
2419
|
artifactPaneApi.element.classList.add("persona-hidden");
|
|
2181
2420
|
artifactPaneApi.backdrop?.classList.add("persona-hidden");
|
|
2182
|
-
|
|
2421
|
+
artifactPaneExpanded = false;
|
|
2422
|
+
artifactPaneExpandedPinned = false;
|
|
2423
|
+
} else if (lastArtifactsState.artifacts.length > 0 && artifactPaneCanShow()) {
|
|
2183
2424
|
// User chose “show” again (e.g. programmatic showArtifacts): clear dismiss chrome
|
|
2184
2425
|
// and force drawer open so narrow-host / mobile slide-out is not stuck off-screen.
|
|
2426
|
+
// Artifacts whose display mode is "card" or "inline" don't auto-open the
|
|
2427
|
+
// pane (artifactPaneCanShow); it stays hidden until an explicit open or
|
|
2428
|
+
// until a "panel"-mode artifact arrives.
|
|
2185
2429
|
artifactPaneApi.element.classList.remove("persona-hidden");
|
|
2186
2430
|
artifactPaneApi.setMobileOpen(true);
|
|
2431
|
+
} else {
|
|
2432
|
+
// The pane's own update() unhides itself whenever records exist
|
|
2433
|
+
// (applyLayoutVisibility), so re-assert the gate here: card/inline-mode
|
|
2434
|
+
// artifacts keep the pane hidden until an explicit open.
|
|
2435
|
+
artifactPaneApi.setMobileOpen(false);
|
|
2436
|
+
artifactPaneApi.element.classList.add("persona-hidden");
|
|
2437
|
+
artifactPaneApi.backdrop?.classList.add("persona-hidden");
|
|
2438
|
+
artifactPaneExpanded = false;
|
|
2439
|
+
artifactPaneExpandedPinned = false;
|
|
2440
|
+
}
|
|
2441
|
+
// Re-read the toggle config on every sync so a live config.update() can
|
|
2442
|
+
// reveal/remove the button (the pane itself is built once). Disabling the
|
|
2443
|
+
// toggle while expanded also collapses the pane — unless the expansion is
|
|
2444
|
+
// pinned (inline Expand): that fullscreen request stands on its own and
|
|
2445
|
+
// exits via Close.
|
|
2446
|
+
const expandToggleEnabled = config.features?.artifacts?.layout?.showExpandToggle === true;
|
|
2447
|
+
artifactPaneApi.setExpandToggleVisible(expandToggleEnabled);
|
|
2448
|
+
artifactPaneApi.setCopyButtonVisible(
|
|
2449
|
+
config.features?.artifacts?.layout?.showCopyButton === true
|
|
2450
|
+
);
|
|
2451
|
+
artifactPaneApi.setCustomActions(config.features?.artifacts?.toolbarActions ?? []);
|
|
2452
|
+
artifactPaneApi.setTabFade(config.features?.artifacts?.layout?.tabFade);
|
|
2453
|
+
artifactPaneApi.setRenderTabBar(config.features?.artifacts?.renderTabBar);
|
|
2454
|
+
if (!expandToggleEnabled && !artifactPaneExpandedPinned) artifactPaneExpanded = false;
|
|
2455
|
+
// Run the resizer stash/restore once per expanded-state transition: the
|
|
2456
|
+
// resizer's inline width/maxWidth beats the expanded class, so clear it while
|
|
2457
|
+
// expanded and put it back on collapse.
|
|
2458
|
+
if (artifactPaneExpanded !== artifactExpandedApplied) {
|
|
2459
|
+
const paneEl = artifactPaneApi.element;
|
|
2460
|
+
if (artifactPaneExpanded) {
|
|
2461
|
+
artifactStashedWidth = paneEl.style.width;
|
|
2462
|
+
artifactStashedMaxWidth = paneEl.style.maxWidth;
|
|
2463
|
+
paneEl.style.removeProperty("width");
|
|
2464
|
+
paneEl.style.removeProperty("max-width");
|
|
2465
|
+
} else {
|
|
2466
|
+
if (artifactStashedWidth) paneEl.style.width = artifactStashedWidth;
|
|
2467
|
+
if (artifactStashedMaxWidth) paneEl.style.maxWidth = artifactStashedMaxWidth;
|
|
2468
|
+
artifactStashedWidth = "";
|
|
2469
|
+
artifactStashedMaxWidth = "";
|
|
2470
|
+
}
|
|
2471
|
+
artifactExpandedApplied = artifactPaneExpanded;
|
|
2472
|
+
}
|
|
2473
|
+
mount.classList.toggle("persona-artifact-expanded", artifactPaneExpanded);
|
|
2474
|
+
artifactPaneApi.setExpanded(artifactPaneExpanded);
|
|
2475
|
+
// Recompute panel chrome when the split mode changes (pane open/close or
|
|
2476
|
+
// appearance) so the welded border and detached shadow suppression land.
|
|
2477
|
+
const modeNow = splitChromeMode();
|
|
2478
|
+
if (modeNow !== appliedSplitMode) {
|
|
2479
|
+
appliedSplitMode = modeNow;
|
|
2480
|
+
syncPanelChrome();
|
|
2187
2481
|
}
|
|
2188
2482
|
reconcileArtifactResize();
|
|
2189
2483
|
};
|
|
@@ -2204,6 +2498,22 @@ export const createAgentExperience = (
|
|
|
2204
2498
|
onDismiss: () => {
|
|
2205
2499
|
artifactsPaneUserHidden = true;
|
|
2206
2500
|
syncArtifactPane();
|
|
2501
|
+
},
|
|
2502
|
+
onToggleExpand: () => {
|
|
2503
|
+
const next = !artifactPaneExpanded;
|
|
2504
|
+
const artifactId =
|
|
2505
|
+
lastArtifactsState.selectedId ??
|
|
2506
|
+
lastArtifactsState.artifacts[lastArtifactsState.artifacts.length - 1]?.id ??
|
|
2507
|
+
null;
|
|
2508
|
+
const prevented = config.features?.artifacts?.onArtifactAction?.({
|
|
2509
|
+
type: "expand",
|
|
2510
|
+
artifactId,
|
|
2511
|
+
expanded: next,
|
|
2512
|
+
});
|
|
2513
|
+
if (prevented === true) return;
|
|
2514
|
+
artifactPaneExpanded = next;
|
|
2515
|
+
if (!next) artifactPaneExpandedPinned = false;
|
|
2516
|
+
syncArtifactPane();
|
|
2207
2517
|
}
|
|
2208
2518
|
});
|
|
2209
2519
|
artifactPaneApi.element.classList.add("persona-hidden");
|
|
@@ -2246,6 +2556,7 @@ export const createAgentExperience = (
|
|
|
2246
2556
|
const onPointerDown = (e: PointerEvent) => {
|
|
2247
2557
|
if (!artifactPaneApi || e.button !== 0) return;
|
|
2248
2558
|
if (mount.classList.contains("persona-artifact-narrow-host")) return;
|
|
2559
|
+
if (mount.classList.contains("persona-artifact-expanded")) return;
|
|
2249
2560
|
if (win.innerWidth <= 640) return;
|
|
2250
2561
|
e.preventDefault();
|
|
2251
2562
|
stopArtifactResizePointer();
|
|
@@ -2254,9 +2565,13 @@ export const createAgentExperience = (
|
|
|
2254
2565
|
const layout = config.features?.artifacts?.layout;
|
|
2255
2566
|
const onMove = (ev: PointerEvent) => {
|
|
2256
2567
|
const splitW = artifactSplitRoot!.getBoundingClientRect().width;
|
|
2257
|
-
const
|
|
2258
|
-
|
|
2259
|
-
|
|
2568
|
+
const weldedChrome = mount.classList.contains("persona-artifact-welded-split");
|
|
2569
|
+
// Read the effective flex gap either way: welded defaults to 0 but an
|
|
2570
|
+
// explicit splitGap opens a real gap that still consumes row width.
|
|
2571
|
+
const gapPx = readFlexGapPx(artifactSplitRoot!, win);
|
|
2572
|
+
// Welded overlays the handle on the seam (out of flow), so it consumes
|
|
2573
|
+
// no width; detached keeps it as an in-flow flex child.
|
|
2574
|
+
const handleW = weldedChrome ? 0 : handle.getBoundingClientRect().width || 6;
|
|
2260
2575
|
// Handle is left of the artifact: drag left widens artifact, drag right narrows it.
|
|
2261
2576
|
const next = startW - (ev.clientX - startX);
|
|
2262
2577
|
const clamped = resolveArtifactPaneWidthPx(
|
|
@@ -2301,8 +2616,7 @@ export const createAgentExperience = (
|
|
|
2301
2616
|
};
|
|
2302
2617
|
}
|
|
2303
2618
|
if (artifactResizeHandle) {
|
|
2304
|
-
const has =
|
|
2305
|
-
lastArtifactsState.artifacts.length > 0 && !artifactsPaneUserHidden;
|
|
2619
|
+
const has = artifactPaneVisible();
|
|
2306
2620
|
artifactResizeHandle.classList.toggle("persona-hidden", !has);
|
|
2307
2621
|
positionExtensionArtifactResizeHandle();
|
|
2308
2622
|
}
|
|
@@ -2323,8 +2637,7 @@ export const createAgentExperience = (
|
|
|
2323
2637
|
const expanded =
|
|
2324
2638
|
config.features?.artifacts?.layout?.expandedPanelWidth ??
|
|
2325
2639
|
"min(720px, calc(100vw - 24px))";
|
|
2326
|
-
const hasVisible =
|
|
2327
|
-
lastArtifactsState.artifacts.length > 0 && !artifactsPaneUserHidden;
|
|
2640
|
+
const hasVisible = artifactPaneVisible();
|
|
2328
2641
|
if (hasVisible) {
|
|
2329
2642
|
panel.style.width = expanded;
|
|
2330
2643
|
panel.style.maxWidth = expanded;
|
|
@@ -2342,21 +2655,23 @@ export const createAgentExperience = (
|
|
|
2342
2655
|
}
|
|
2343
2656
|
} else {
|
|
2344
2657
|
panel.appendChild(container);
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
// Composer-bar mode: the pill (footer) and peek banner live in a
|
|
2661
|
+
// viewport-fixed sibling of the wrapper (`pillRoot`) so they're
|
|
2662
|
+
// independent of both the wrapper's geometry transitions and the panel's
|
|
2663
|
+
// optional artifact split layout. Critical for modal mode: the wrapper
|
|
2664
|
+
// there has `transform: translate(-50%, -50%)`, which would establish a
|
|
2665
|
+
// containing block trapping any `position: fixed` descendant.
|
|
2666
|
+
//
|
|
2667
|
+
// Order inside pillRoot: peekBanner (slim row above pill) → footer (pill).
|
|
2668
|
+
// pillRoot's `gap` spaces them; the peek is hidden by default until ui.ts
|
|
2669
|
+
// toggles `.persona-pill-peek--visible` based on streaming/hover/open state.
|
|
2670
|
+
if (isComposerBar() && pillRoot) {
|
|
2671
|
+
if (panelElements.peekBanner) {
|
|
2672
|
+
pillRoot.appendChild(panelElements.peekBanner);
|
|
2673
|
+
}
|
|
2674
|
+
pillRoot.appendChild(footer);
|
|
2360
2675
|
}
|
|
2361
2676
|
mount.appendChild(wrapper);
|
|
2362
2677
|
// pillRoot is mounted *after* wrapper so it naturally stacks on top
|
|
@@ -2415,6 +2730,8 @@ export const createAgentExperience = (
|
|
|
2415
2730
|
const fullHeight = dockedMode || sidebarMode || (config.launcher?.fullHeight ?? false);
|
|
2416
2731
|
/** Script-tag / div embed: launcher off, host supplies a sized mount. */
|
|
2417
2732
|
const isInlineEmbed = config.launcher?.enabled === false;
|
|
2733
|
+
/** Detached appearance: inset card over a canvas instead of flush chrome. */
|
|
2734
|
+
const isDetached = config.launcher?.detachedPanel === true;
|
|
2418
2735
|
const panelPartial = config.theme?.components?.panel;
|
|
2419
2736
|
const activeTheme = getActiveTheme(config);
|
|
2420
2737
|
const resolvePanelChrome = (raw: string | undefined, fallback: string): string => {
|
|
@@ -2429,33 +2746,105 @@ export const createAgentExperience = (
|
|
|
2429
2746
|
const mobileBreakpoint = config.launcher?.mobileBreakpoint ?? 640;
|
|
2430
2747
|
const isMobileViewport = ownerWindow.innerWidth <= mobileBreakpoint;
|
|
2431
2748
|
const shouldGoFullscreen = mobileFullscreen && isMobileViewport && launcherEnabled;
|
|
2749
|
+
// Docked host layout forces fullscreen on mobile even when the launcher is
|
|
2750
|
+
// off (host-layout pins the slot fixed and clears its chrome). Suppress the
|
|
2751
|
+
// detached card so it never paints chrome over a flush fullscreen panel.
|
|
2752
|
+
const dockedHostFullscreen = dockedMode && mobileFullscreen && isMobileViewport;
|
|
2432
2753
|
|
|
2433
2754
|
// Determine panel styling based on mode, with theme overrides
|
|
2434
2755
|
const position = config.launcher?.position ?? 'bottom-left';
|
|
2435
2756
|
const isLeftSidebar = position === 'bottom-left' || position === 'top-left';
|
|
2436
2757
|
const overlayZIndex = config.launcher?.zIndex ?? DEFAULT_OVERLAY_Z_INDEX;
|
|
2437
2758
|
|
|
2759
|
+
// Card chrome defaults (floating look): reused to restore detached chrome.
|
|
2760
|
+
// Defaults chain through the aliases themeToCssVariables emits so explicit
|
|
2761
|
+
// theme.components.panel overrides and these defaults never diverge.
|
|
2762
|
+
const cardBorder = 'var(--persona-panel-border, 1px solid var(--persona-border))';
|
|
2763
|
+
const cardShadow = 'var(--persona-panel-shadow, var(--persona-palette-shadows-xl, 0 25px 50px -12px rgba(0, 0, 0, 0.25)))';
|
|
2764
|
+
const cardRadius = 'var(--persona-panel-radius, var(--persona-radius-xl, 0.75rem))';
|
|
2765
|
+
/** Detached restores card chrome except when a host layout goes fullscreen. */
|
|
2766
|
+
const detachedCard = isDetached && !shouldGoFullscreen && !dockedHostFullscreen;
|
|
2767
|
+
// Stamp reflects rendered chrome: cleared when a fullscreen host layout
|
|
2768
|
+
// suppresses the card, so the attribute never lies to the detached CSS.
|
|
2769
|
+
if (detachedCard) {
|
|
2770
|
+
mount.setAttribute("data-persona-panel-detached", "true");
|
|
2771
|
+
} else {
|
|
2772
|
+
mount.removeAttribute("data-persona-panel-detached");
|
|
2773
|
+
}
|
|
2774
|
+
|
|
2438
2775
|
// Default values based on mode
|
|
2439
|
-
let defaultPanelBorder =
|
|
2440
|
-
|
|
2441
|
-
? 'none'
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2776
|
+
let defaultPanelBorder = detachedCard
|
|
2777
|
+
? cardBorder
|
|
2778
|
+
: (sidebarMode || shouldGoFullscreen) ? 'none' : cardBorder;
|
|
2779
|
+
let defaultPanelShadow = detachedCard
|
|
2780
|
+
? cardShadow
|
|
2781
|
+
: shouldGoFullscreen
|
|
2782
|
+
? 'none'
|
|
2783
|
+
: sidebarMode
|
|
2784
|
+
? (isLeftSidebar ? 'var(--persona-palette-shadows-sidebar-left, 2px 0 12px rgba(0, 0, 0, 0.08))' : 'var(--persona-palette-shadows-sidebar-right, -2px 0 12px rgba(0, 0, 0, 0.08))')
|
|
2785
|
+
// Flush inline embeds fill their container: no elevation by default
|
|
2786
|
+
// (detachedPanel or components.panel.shadow opts back in).
|
|
2787
|
+
: isInlineEmbed ? 'none' : cardShadow;
|
|
2788
|
+
|
|
2789
|
+
if (dockedMode && !shouldGoFullscreen && !detachedCard) {
|
|
2447
2790
|
defaultPanelShadow = 'none';
|
|
2448
2791
|
defaultPanelBorder = 'none';
|
|
2449
2792
|
}
|
|
2450
|
-
const defaultPanelBorderRadius =
|
|
2451
|
-
?
|
|
2452
|
-
:
|
|
2793
|
+
const defaultPanelBorderRadius = detachedCard
|
|
2794
|
+
? cardRadius
|
|
2795
|
+
: (sidebarMode || shouldGoFullscreen) ? '0' : cardRadius;
|
|
2453
2796
|
|
|
2454
2797
|
// Apply theme overrides or defaults (components.panel.*)
|
|
2455
2798
|
const panelBorder = resolvePanelChrome(panelPartial?.border, defaultPanelBorder);
|
|
2456
2799
|
const panelShadow = resolvePanelChrome(panelPartial?.shadow, defaultPanelShadow);
|
|
2457
2800
|
const panelBorderRadius = resolvePanelChrome(panelPartial?.borderRadius, defaultPanelBorderRadius);
|
|
2458
2801
|
|
|
2802
|
+
// Split chrome: 'welded' folds the card border onto the outer panel so it
|
|
2803
|
+
// wraps both columns as one card (shadow/radius already on the panel);
|
|
2804
|
+
// 'detached' pulls them into two individually carded columns.
|
|
2805
|
+
const splitMode = splitChromeMode();
|
|
2806
|
+
const detachedSplitActive = splitMode === "detached";
|
|
2807
|
+
const weldedSplitActive = splitMode === "welded";
|
|
2808
|
+
mount.classList.toggle("persona-artifact-detached-split", detachedSplitActive);
|
|
2809
|
+
mount.classList.toggle("persona-artifact-welded-split", weldedSplitActive);
|
|
2810
|
+
const chatCardShadow =
|
|
2811
|
+
'var(--persona-artifact-chat-shadow, var(--persona-artifact-pane-shadow, var(--persona-panel-shadow, var(--persona-palette-shadows-xl, 0 25px 50px -12px rgba(0, 0, 0, 0.25)))))';
|
|
2812
|
+
// Welded moves the chat card border onto the panel, so the chat column goes
|
|
2813
|
+
// borderless; detached cards it individually; otherwise it keeps the border.
|
|
2814
|
+
// 'flush' drops the chat card so the chat is flat flush background and only
|
|
2815
|
+
// the pane floats; 'card' (default) keeps two matched cards. Flush is a
|
|
2816
|
+
// steady state: it applies whether or not a pane is open, so opening or
|
|
2817
|
+
// closing an artifact never flips the chat chrome. Gated to inline embeds
|
|
2818
|
+
// (not docked), matching the card-mode perimeter inset: the flush model only
|
|
2819
|
+
// fits the container-filling embed. Floating/docked/sidebar keep the card look.
|
|
2820
|
+
const rawChatSurface = config.features?.artifacts?.layout?.chatSurface;
|
|
2821
|
+
const resolvedChatSurface = rawChatSurface === "flush" ? "flush" : "card";
|
|
2822
|
+
const chatFlush =
|
|
2823
|
+
resolvedChatSurface === "flush" &&
|
|
2824
|
+
artifactsSidebarEnabled(config) &&
|
|
2825
|
+
isArtifactPaneAppearanceDetached(config) &&
|
|
2826
|
+
isInlineEmbed &&
|
|
2827
|
+
!dockedMode;
|
|
2828
|
+
mount.classList.toggle("persona-artifact-chat-flush", chatFlush);
|
|
2829
|
+
// Flat means flat: flush suppresses the outer panel shadow in every state.
|
|
2830
|
+
const outerPanelShadow = detachedSplitActive || chatFlush ? 'none' : panelShadow;
|
|
2831
|
+
let chatContainerBorder = detachedSplitActive
|
|
2832
|
+
? cardBorder
|
|
2833
|
+
: weldedSplitActive
|
|
2834
|
+
? 'none'
|
|
2835
|
+
: panelBorder;
|
|
2836
|
+
let chatContainerRadius = detachedSplitActive ? cardRadius : panelBorderRadius;
|
|
2837
|
+
if (chatFlush) {
|
|
2838
|
+
chatContainerBorder = 'none';
|
|
2839
|
+
chatContainerRadius = '0';
|
|
2840
|
+
}
|
|
2841
|
+
// Flush fills the container flush, so the outer panel squares off by default;
|
|
2842
|
+
// the pane keeps its own rounded radius. An explicit panel.borderRadius wins.
|
|
2843
|
+
const panelRadiusExplicit =
|
|
2844
|
+
panelPartial?.borderRadius != null && panelPartial.borderRadius !== '';
|
|
2845
|
+
const appliedPanelRadius =
|
|
2846
|
+
chatFlush && !panelRadiusExplicit ? '0' : panelBorderRadius;
|
|
2847
|
+
|
|
2459
2848
|
// Clearing body.style.cssText below wipes the inline `flex: 1 1 0%` /
|
|
2460
2849
|
// `min-height: 0` / `overflow-y: auto` that make the messages area a
|
|
2461
2850
|
// scroll container. Between the reset and the mode-specific reapply,
|
|
@@ -2575,7 +2964,7 @@ export const createAgentExperience = (
|
|
|
2575
2964
|
}
|
|
2576
2965
|
} else if (dockedMode) {
|
|
2577
2966
|
const dockReveal = resolveDockConfig(config).reveal;
|
|
2578
|
-
if (dockReveal === "emerge") {
|
|
2967
|
+
if (dockReveal === "emerge" && !isDetached) {
|
|
2579
2968
|
const dw = resolveDockConfig(config).width;
|
|
2580
2969
|
panel.style.width = dw;
|
|
2581
2970
|
panel.style.maxWidth = dw;
|
|
@@ -2590,12 +2979,38 @@ export const createAgentExperience = (
|
|
|
2590
2979
|
// Box-shadow is applied to panel (parent) instead of container to avoid
|
|
2591
2980
|
// rendering artifacts when container has overflow:hidden + border-radius
|
|
2592
2981
|
// Panel also gets border-radius to make the shadow follow the rounded corners
|
|
2593
|
-
panel.style.boxShadow =
|
|
2594
|
-
panel.style.borderRadius =
|
|
2595
|
-
|
|
2596
|
-
|
|
2982
|
+
panel.style.boxShadow = outerPanelShadow;
|
|
2983
|
+
panel.style.borderRadius = appliedPanelRadius;
|
|
2984
|
+
if (detachedSplitActive) panel.style.border = 'none';
|
|
2985
|
+
else if (weldedSplitActive) panel.style.border = panelBorder;
|
|
2986
|
+
container.style.border = chatContainerBorder;
|
|
2987
|
+
container.style.borderRadius = chatContainerRadius;
|
|
2988
|
+
container.style.boxShadow = detachedSplitActive && !chatFlush ? chatCardShadow : '';
|
|
2989
|
+
// Flush chat has no card surface of its own: override the class-painted
|
|
2990
|
+
// container/body/footer backgrounds so the wrapper's canvas shows through.
|
|
2991
|
+
// The footer's top hairline is chat-card chrome, so it goes too. Element
|
|
2992
|
+
// surfaces (bubbles, cards, composer input) keep their own backgrounds.
|
|
2993
|
+
if (chatFlush) {
|
|
2994
|
+
container.style.background = 'transparent';
|
|
2995
|
+
body.style.background = 'transparent';
|
|
2996
|
+
footer.style.background = 'transparent';
|
|
2997
|
+
footer.style.borderTop = 'none';
|
|
2998
|
+
}
|
|
2999
|
+
|
|
3000
|
+
// Welded outer-right radius: derive the pane's outer corners from the same
|
|
3001
|
+
// resolved value as the chat card's left corners so a custom
|
|
3002
|
+
// components.panel.borderRadius stays symmetric. Explicit
|
|
3003
|
+
// unifiedSplitOuterRadius / paneBorderRadius still win. Applied in
|
|
3004
|
+
// syncPanelChrome after applyArtifactPaneAppearance (which no longer owns it).
|
|
3005
|
+
if (weldedSplitActive) {
|
|
3006
|
+
const wl = config.features?.artifacts?.layout;
|
|
3007
|
+
weldedOuterRadius =
|
|
3008
|
+
wl?.unifiedSplitOuterRadius?.trim() || wl?.paneBorderRadius?.trim() || panelBorderRadius;
|
|
3009
|
+
} else {
|
|
3010
|
+
weldedOuterRadius = null;
|
|
3011
|
+
}
|
|
2597
3012
|
|
|
2598
|
-
if (dockedMode && !shouldGoFullscreen && panelPartial?.border === undefined) {
|
|
3013
|
+
if (dockedMode && !shouldGoFullscreen && !detachedCard && !detachedSplitActive && !weldedSplitActive && panelPartial?.border === undefined) {
|
|
2599
3014
|
container.style.border = 'none';
|
|
2600
3015
|
const dockSide = resolveDockConfig(config).side;
|
|
2601
3016
|
if (dockSide === 'right') {
|
|
@@ -2605,6 +3020,19 @@ export const createAgentExperience = (
|
|
|
2605
3020
|
}
|
|
2606
3021
|
}
|
|
2607
3022
|
|
|
3023
|
+
// Docked + welded: the welded chrome owner is the outer panel, and docked
|
|
3024
|
+
// mode resolves its border to none, so add the dock-facing hairline there so
|
|
3025
|
+
// the split still separates from the host page. Mirrors the flush block's
|
|
3026
|
+
// side choice (right dock => left edge faces the page).
|
|
3027
|
+
if (dockedMode && !shouldGoFullscreen && weldedSplitActive && panelPartial?.border === undefined) {
|
|
3028
|
+
const dockSide = resolveDockConfig(config).side;
|
|
3029
|
+
if (dockSide === 'right') {
|
|
3030
|
+
panel.style.borderLeft = '1px solid var(--persona-border)';
|
|
3031
|
+
} else {
|
|
3032
|
+
panel.style.borderRight = '1px solid var(--persona-border)';
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
|
|
2608
3036
|
if (fullHeight) {
|
|
2609
3037
|
// Mount container
|
|
2610
3038
|
mount.style.display = 'flex';
|
|
@@ -2635,8 +3063,10 @@ export const createAgentExperience = (
|
|
|
2635
3063
|
panel.style.minHeight = '0';
|
|
2636
3064
|
panel.style.maxHeight = '100%';
|
|
2637
3065
|
panel.style.height = '100%';
|
|
2638
|
-
|
|
2639
|
-
|
|
3066
|
+
// Detached split: each card clips its own content and carries its own
|
|
3067
|
+
// shadow, so panel overflow:hidden must not clip the cards' shadows.
|
|
3068
|
+
if (!detachedSplitActive) panel.style.overflow = 'hidden';
|
|
3069
|
+
|
|
2640
3070
|
// Main container
|
|
2641
3071
|
container.style.display = 'flex';
|
|
2642
3072
|
container.style.flexDirection = 'column';
|
|
@@ -2653,7 +3083,19 @@ export const createAgentExperience = (
|
|
|
2653
3083
|
// Footer (composer) - should not shrink
|
|
2654
3084
|
footer.style.flexShrink = '0';
|
|
2655
3085
|
}
|
|
2656
|
-
|
|
3086
|
+
|
|
3087
|
+
// Inline embed: pad the wrapper so the canvas shows around the card when the
|
|
3088
|
+
// panel is a detached card OR a detached split is open (insets the whole split
|
|
3089
|
+
// from the container edges on all sides). Docked owns its inset via
|
|
3090
|
+
// host-layout, so exclude it (no double gap).
|
|
3091
|
+
// Flush chat must be flush to the container, so it skips the perimeter inset;
|
|
3092
|
+
// the wrapper still paints the canvas so one token colors the whole backdrop,
|
|
3093
|
+
// in every state (flush is steady, so idle must match pane-open).
|
|
3094
|
+
if (isInlineEmbed && (detachedCard || detachedSplitActive || chatFlush) && !dockedMode) {
|
|
3095
|
+
if (!chatFlush) wrapper.style.padding = 'var(--persona-panel-inset)';
|
|
3096
|
+
wrapper.style.background = 'var(--persona-panel-canvas-bg)';
|
|
3097
|
+
}
|
|
3098
|
+
|
|
2657
3099
|
// Handle positioning classes based on mode
|
|
2658
3100
|
// First remove all position classes to reset state
|
|
2659
3101
|
wrapper.classList.remove(
|
|
@@ -2670,22 +3112,40 @@ export const createAgentExperience = (
|
|
|
2670
3112
|
// Apply sidebar-specific styles
|
|
2671
3113
|
if (sidebarMode) {
|
|
2672
3114
|
const sidebarWidth = config.launcher?.sidebarWidth ?? '420px';
|
|
2673
|
-
|
|
2674
|
-
// Wrapper - fixed position
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
3115
|
+
|
|
3116
|
+
// Wrapper - fixed position. Detached insets the card off the edges and
|
|
3117
|
+
// shrinks its height by the inset on both ends; flush hugs the edges.
|
|
3118
|
+
if (isDetached) {
|
|
3119
|
+
wrapper.style.cssText = `
|
|
3120
|
+
position: fixed !important;
|
|
3121
|
+
top: var(--persona-panel-inset) !important;
|
|
3122
|
+
bottom: var(--persona-panel-inset) !important;
|
|
3123
|
+
width: ${sidebarWidth} !important;
|
|
3124
|
+
height: calc(100vh - (2 * var(--persona-panel-inset))) !important;
|
|
3125
|
+
max-height: calc(100vh - (2 * var(--persona-panel-inset))) !important;
|
|
3126
|
+
margin: 0 !important;
|
|
3127
|
+
padding: 0 !important;
|
|
3128
|
+
display: flex !important;
|
|
3129
|
+
flex-direction: column !important;
|
|
3130
|
+
z-index: ${overlayZIndex} !important;
|
|
3131
|
+
${isLeftSidebar ? 'left: var(--persona-panel-inset) !important; right: auto !important;' : 'left: auto !important; right: var(--persona-panel-inset) !important;'}
|
|
3132
|
+
`;
|
|
3133
|
+
} else {
|
|
3134
|
+
wrapper.style.cssText = `
|
|
3135
|
+
position: fixed !important;
|
|
3136
|
+
top: 0 !important;
|
|
3137
|
+
bottom: 0 !important;
|
|
3138
|
+
width: ${sidebarWidth} !important;
|
|
3139
|
+
height: 100vh !important;
|
|
3140
|
+
max-height: 100vh !important;
|
|
3141
|
+
margin: 0 !important;
|
|
3142
|
+
padding: 0 !important;
|
|
3143
|
+
display: flex !important;
|
|
3144
|
+
flex-direction: column !important;
|
|
3145
|
+
z-index: ${overlayZIndex} !important;
|
|
3146
|
+
${isLeftSidebar ? 'left: 0 !important; right: auto !important;' : 'left: auto !important; right: 0 !important;'}
|
|
3147
|
+
`;
|
|
3148
|
+
}
|
|
2689
3149
|
|
|
2690
3150
|
// Panel - fill wrapper (override inline width/max-width from panel.ts)
|
|
2691
3151
|
// Box-shadow is on panel to avoid rendering artifacts with container's overflow:hidden
|
|
@@ -2701,15 +3161,16 @@ export const createAgentExperience = (
|
|
|
2701
3161
|
min-height: 0 !important;
|
|
2702
3162
|
margin: 0 !important;
|
|
2703
3163
|
padding: 0 !important;
|
|
2704
|
-
box-shadow: ${
|
|
2705
|
-
border-radius: ${
|
|
3164
|
+
box-shadow: ${outerPanelShadow} !important;
|
|
3165
|
+
border-radius: ${appliedPanelRadius} !important;
|
|
3166
|
+
${detachedSplitActive ? 'border: none !important;' : weldedSplitActive ? `border: ${panelBorder} !important;` : ''}
|
|
2706
3167
|
`;
|
|
2707
3168
|
// Force override any inline width/maxWidth that may be set elsewhere
|
|
2708
3169
|
panel.style.setProperty('width', '100%', 'important');
|
|
2709
3170
|
panel.style.setProperty('max-width', '100%', 'important');
|
|
2710
|
-
|
|
2711
|
-
// Container - apply configurable styles with sidebar layout
|
|
2712
|
-
//
|
|
3171
|
+
|
|
3172
|
+
// Container - apply configurable styles with sidebar layout. Box-shadow is
|
|
3173
|
+
// normally on panel, but a detached split cards the chat column here.
|
|
2713
3174
|
container.style.cssText = `
|
|
2714
3175
|
display: flex !important;
|
|
2715
3176
|
flex-direction: column !important;
|
|
@@ -2719,8 +3180,10 @@ export const createAgentExperience = (
|
|
|
2719
3180
|
min-height: 0 !important;
|
|
2720
3181
|
max-height: 100% !important;
|
|
2721
3182
|
overflow: hidden !important;
|
|
2722
|
-
border-radius: ${
|
|
2723
|
-
border: ${
|
|
3183
|
+
border-radius: ${chatContainerRadius} !important;
|
|
3184
|
+
border: ${chatContainerBorder} !important;
|
|
3185
|
+
${detachedSplitActive && !chatFlush ? `box-shadow: ${chatCardShadow} !important;` : ''}
|
|
3186
|
+
${chatFlush ? 'background: transparent !important;' : ''}
|
|
2724
3187
|
`;
|
|
2725
3188
|
|
|
2726
3189
|
// Remove footer border in sidebar mode
|
|
@@ -2728,6 +3191,7 @@ export const createAgentExperience = (
|
|
|
2728
3191
|
flex-shrink: 0 !important;
|
|
2729
3192
|
border-top: none !important;
|
|
2730
3193
|
padding: 8px 16px 12px 16px !important;
|
|
3194
|
+
${chatFlush ? 'background: transparent !important;' : ''}
|
|
2731
3195
|
`;
|
|
2732
3196
|
}
|
|
2733
3197
|
|
|
@@ -2752,6 +3216,22 @@ export const createAgentExperience = (
|
|
|
2752
3216
|
applyArtifactLayoutCssVars(mount, config);
|
|
2753
3217
|
applyArtifactPaneAppearance(mount, config);
|
|
2754
3218
|
|
|
3219
|
+
// applyFullHeightStyles wipes mount.style.cssText, so re-apply the theme +
|
|
3220
|
+
// artifact layout vars after it, mirroring the init sequence above.
|
|
3221
|
+
syncPanelChrome = () => {
|
|
3222
|
+
applyFullHeightStyles();
|
|
3223
|
+
applyThemeVariables(mount, config);
|
|
3224
|
+
applyArtifactLayoutCssVars(mount, config);
|
|
3225
|
+
applyArtifactPaneAppearance(mount, config);
|
|
3226
|
+
// Owned here so it lands after applyArtifactPaneAppearance and derives from
|
|
3227
|
+
// the same resolved panel radius the chat card uses (see applyFullHeightStyles).
|
|
3228
|
+
if (weldedOuterRadius) {
|
|
3229
|
+
mount.style.setProperty("--persona-artifact-welded-outer-radius", weldedOuterRadius);
|
|
3230
|
+
} else {
|
|
3231
|
+
mount.style.removeProperty("--persona-artifact-welded-outer-radius");
|
|
3232
|
+
}
|
|
3233
|
+
};
|
|
3234
|
+
|
|
2755
3235
|
const destroyCallbacks: Array<() => void> = [];
|
|
2756
3236
|
// Clean up the document-level digit-key shortcut listener registered earlier.
|
|
2757
3237
|
destroyCallbacks.push(() => {
|
|
@@ -4001,15 +4481,57 @@ export const createAgentExperience = (
|
|
|
4001
4481
|
if (directive) {
|
|
4002
4482
|
const lastFp = lastComponentDirectiveFingerprint.get(message.id);
|
|
4003
4483
|
const needsRebuild = lastFp !== fingerprint;
|
|
4004
|
-
|
|
4484
|
+
// Wrap only when the global default allows it AND the component has
|
|
4485
|
+
// not opted out of bubble chrome (e.g. the artifact card carries its
|
|
4486
|
+
// own border, so it renders bare).
|
|
4487
|
+
const wrapChrome =
|
|
4488
|
+
config.wrapComponentDirectiveInBubble !== false &&
|
|
4489
|
+
componentRegistry.getOptions(directive.component)?.bubbleChrome !== false;
|
|
4005
4490
|
let liveBubble: HTMLElement | null = null;
|
|
4006
4491
|
|
|
4007
4492
|
if (needsRebuild) {
|
|
4008
|
-
|
|
4493
|
+
let componentBubble = renderComponentDirective(directive, {
|
|
4009
4494
|
config,
|
|
4010
4495
|
message,
|
|
4011
4496
|
transform
|
|
4012
4497
|
});
|
|
4498
|
+
// Reuse a mounted inline artifact block instead of the fresh
|
|
4499
|
+
// render: replacing it would cut the collapse-to-card animation
|
|
4500
|
+
// short and reload live file-preview iframes.
|
|
4501
|
+
if (
|
|
4502
|
+
componentBubble &&
|
|
4503
|
+
directive.component === "PersonaArtifactInline"
|
|
4504
|
+
) {
|
|
4505
|
+
const fresh = componentBubble.hasAttribute(
|
|
4506
|
+
"data-artifact-inline"
|
|
4507
|
+
)
|
|
4508
|
+
? componentBubble
|
|
4509
|
+
: componentBubble.querySelector<HTMLElement>(
|
|
4510
|
+
"[data-artifact-inline]"
|
|
4511
|
+
);
|
|
4512
|
+
const artifactId =
|
|
4513
|
+
fresh?.getAttribute("data-artifact-inline") ?? "";
|
|
4514
|
+
const escapedId =
|
|
4515
|
+
typeof CSS !== "undefined" && typeof CSS.escape === "function"
|
|
4516
|
+
? CSS.escape(artifactId)
|
|
4517
|
+
: artifactId;
|
|
4518
|
+
const live = artifactId
|
|
4519
|
+
? (container
|
|
4520
|
+
.querySelector<HTMLElement>(`#wrapper-${message.id}`)
|
|
4521
|
+
?.querySelector<HTMLElement>(
|
|
4522
|
+
`[data-artifact-inline="${escapedId}"]`
|
|
4523
|
+
) ?? null)
|
|
4524
|
+
: null;
|
|
4525
|
+
if (
|
|
4526
|
+
fresh &&
|
|
4527
|
+
live &&
|
|
4528
|
+
live !== fresh &&
|
|
4529
|
+
hasLiveInlineArtifactBlock(live)
|
|
4530
|
+
) {
|
|
4531
|
+
if (fresh === componentBubble) componentBubble = live;
|
|
4532
|
+
else fresh.replaceWith(live);
|
|
4533
|
+
}
|
|
4534
|
+
}
|
|
4013
4535
|
if (componentBubble) {
|
|
4014
4536
|
if (wrapChrome) {
|
|
4015
4537
|
const componentWrapper = document.createElement("div");
|
|
@@ -4175,6 +4697,12 @@ export const createAgentExperience = (
|
|
|
4175
4697
|
currentGroup.push(message);
|
|
4176
4698
|
return;
|
|
4177
4699
|
}
|
|
4700
|
+
// A hidden reasoning row does not create a visible break in the
|
|
4701
|
+
// transcript, so it should not split an otherwise contiguous tool
|
|
4702
|
+
// sequence into separate groups.
|
|
4703
|
+
if (message.variant === "reasoning" && !showReasoning) {
|
|
4704
|
+
return;
|
|
4705
|
+
}
|
|
4178
4706
|
if (currentGroup.length > 1) {
|
|
4179
4707
|
toolGroups.push(currentGroup);
|
|
4180
4708
|
}
|
|
@@ -4229,11 +4757,19 @@ export const createAgentExperience = (
|
|
|
4229
4757
|
const stack = document.createElement("div");
|
|
4230
4758
|
stack.className = "persona-tool-group-stack persona-flex persona-flex-col";
|
|
4231
4759
|
|
|
4232
|
-
|
|
4760
|
+
const summaryOnly = config.features?.toolCallDisplay?.groupedMode === "summary";
|
|
4761
|
+
groupContainer.appendChild(summary);
|
|
4762
|
+
if (!summaryOnly) {
|
|
4763
|
+
groupContainer.appendChild(stack);
|
|
4764
|
+
}
|
|
4233
4765
|
groupWrapper.appendChild(groupContainer);
|
|
4234
4766
|
wrappers[0].before(groupWrapper);
|
|
4235
4767
|
|
|
4236
4768
|
wrappers.forEach((wrapper, wrapperIndex) => {
|
|
4769
|
+
if (summaryOnly) {
|
|
4770
|
+
wrapper.remove();
|
|
4771
|
+
return;
|
|
4772
|
+
}
|
|
4237
4773
|
const item = document.createElement("div");
|
|
4238
4774
|
item.className = "persona-tool-group-item persona-relative";
|
|
4239
4775
|
item.setAttribute("data-persona-tool-group-item", "true");
|
|
@@ -5297,6 +5833,12 @@ export const createAgentExperience = (
|
|
|
5297
5833
|
if (lastUserMessage && lastAssistantMessage && activeStreamingTextCandidate) break;
|
|
5298
5834
|
}
|
|
5299
5835
|
renderMessagesWithPlugins(messagesWrapper, messages, postprocess);
|
|
5836
|
+
// Freshly (re)built inline artifact blocks render from their persisted
|
|
5837
|
+
// props; sync them with the live registry so a block created after the
|
|
5838
|
+
// last onArtifactsState emission still shows current content.
|
|
5839
|
+
updateInlineArtifactBlocks(messagesWrapper, lastArtifactsState.artifacts, {
|
|
5840
|
+
suppressTransition: isStreaming,
|
|
5841
|
+
});
|
|
5300
5842
|
ensureToolElapsedTimer();
|
|
5301
5843
|
renderSuggestions(messages);
|
|
5302
5844
|
scheduleAutoScroll(!isStreaming);
|
|
@@ -5467,6 +6009,20 @@ export const createAgentExperience = (
|
|
|
5467
6009
|
},
|
|
5468
6010
|
onArtifactsState(state) {
|
|
5469
6011
|
lastArtifactsState = state;
|
|
6012
|
+
// A cleared registry ends any explicit-open override: the next artifact
|
|
6013
|
+
// decides pane visibility purely from its own display mode.
|
|
6014
|
+
if (state.artifacts.length === 0) {
|
|
6015
|
+
artifactsPaneUserOpened = false;
|
|
6016
|
+
}
|
|
6017
|
+
// Route streaming registry updates (artifact_delta / artifact_complete)
|
|
6018
|
+
// into any inline artifact blocks in the transcript. Suppress the
|
|
6019
|
+
// streaming→complete View Transition while the session is still
|
|
6020
|
+
// streaming: it captures the whole document, and cross-fading a stale
|
|
6021
|
+
// snapshot over still-moving message text reads as ghosting/motion blur
|
|
6022
|
+
// on the transcript.
|
|
6023
|
+
updateInlineArtifactBlocks(messagesWrapper, state.artifacts, {
|
|
6024
|
+
suppressTransition: isStreaming,
|
|
6025
|
+
});
|
|
5470
6026
|
syncArtifactPane();
|
|
5471
6027
|
persistState();
|
|
5472
6028
|
},
|
|
@@ -6396,17 +6952,35 @@ export const createAgentExperience = (
|
|
|
6396
6952
|
|
|
6397
6953
|
try {
|
|
6398
6954
|
if (shouldGoFullscreen) {
|
|
6399
|
-
|
|
6400
|
-
|
|
6955
|
+
// syncPanelChrome re-applies theme + artifact layout vars that the
|
|
6956
|
+
// cssText reset in applyFullHeightStyles wipes; bare re-style flickers.
|
|
6957
|
+
syncPanelChrome();
|
|
6958
|
+
appliedSplitMode = splitChromeMode();
|
|
6401
6959
|
return;
|
|
6402
6960
|
}
|
|
6403
6961
|
|
|
6404
6962
|
// Exiting mobile fullscreen (e.g., orientation change to landscape): reset all styles
|
|
6963
|
+
let chromeResynced = false;
|
|
6405
6964
|
if (wasMobileFullscreen) {
|
|
6406
6965
|
wasMobileFullscreen = false;
|
|
6407
|
-
|
|
6408
|
-
|
|
6966
|
+
syncPanelChrome();
|
|
6967
|
+
chromeResynced = true;
|
|
6968
|
+
}
|
|
6969
|
+
|
|
6970
|
+
// Width-only resize can cross the 640 split boundary without changing
|
|
6971
|
+
// panel geometry (pane ResizeObserver stays silent) or hitting the
|
|
6972
|
+
// fullscreen branches, so resync chrome when the split mode flips.
|
|
6973
|
+
const modeNow = splitChromeMode();
|
|
6974
|
+
if (!chromeResynced && modeNow !== appliedSplitMode) {
|
|
6975
|
+
syncPanelChrome();
|
|
6976
|
+
chromeResynced = true;
|
|
6409
6977
|
}
|
|
6978
|
+
appliedSplitMode = modeNow;
|
|
6979
|
+
// syncPanelChrome does not touch the resize handle, so a width-only flip
|
|
6980
|
+
// between welded (seam overlay) and detached/flex handle would leave it in
|
|
6981
|
+
// the old mode. reconcileArtifactResize re-places it; it never re-enters
|
|
6982
|
+
// syncArtifactPane, so no recursion. No-op when artifacts are disabled.
|
|
6983
|
+
if (chromeResynced) reconcileArtifactResize();
|
|
6410
6984
|
|
|
6411
6985
|
if (!launcherEnabled && !dockedMode) {
|
|
6412
6986
|
panel.style.height = "";
|
|
@@ -8385,6 +8959,7 @@ export const createAgentExperience = (
|
|
|
8385
8959
|
showArtifacts(): void {
|
|
8386
8960
|
if (!artifactsSidebarEnabled(config)) return;
|
|
8387
8961
|
artifactsPaneUserHidden = false;
|
|
8962
|
+
artifactsPaneUserOpened = true;
|
|
8388
8963
|
syncArtifactPane();
|
|
8389
8964
|
artifactPaneApi?.setMobileOpen(true);
|
|
8390
8965
|
},
|
|
@@ -8395,8 +8970,20 @@ export const createAgentExperience = (
|
|
|
8395
8970
|
},
|
|
8396
8971
|
upsertArtifact(manual: PersonaArtifactManualUpsert): PersonaArtifactRecord | null {
|
|
8397
8972
|
if (!artifactsSidebarEnabled(config)) return null;
|
|
8398
|
-
// Programmatic
|
|
8399
|
-
|
|
8973
|
+
// Programmatic upserts match the streamed UX: only "panel"-mode
|
|
8974
|
+
// artifacts auto-open the pane (overriding a previous Close), while
|
|
8975
|
+
// "card"/"inline" stay calm — the injected transcript block is the
|
|
8976
|
+
// affordance. Independent of `transcript: false`: pane-only callers
|
|
8977
|
+
// (e.g. the theme editor preview) rely on the panel-default surfacing;
|
|
8978
|
+
// callers that want the pane in a non-panel mode call showArtifacts().
|
|
8979
|
+
const mode = resolveArtifactDisplayMode(
|
|
8980
|
+
config.features?.artifacts,
|
|
8981
|
+
manual.artifactType
|
|
8982
|
+
);
|
|
8983
|
+
if (mode === "panel") {
|
|
8984
|
+
artifactsPaneUserHidden = false;
|
|
8985
|
+
artifactsPaneUserOpened = true;
|
|
8986
|
+
}
|
|
8400
8987
|
return session.upsertArtifact(manual);
|
|
8401
8988
|
},
|
|
8402
8989
|
selectArtifact(id: string): void {
|
|
@@ -8790,18 +9377,20 @@ export const createAgentExperience = (
|
|
|
8790
9377
|
// bust the message cache and re-render so they pick up real markdown. Bumping
|
|
8791
9378
|
// `configVersion` + clearing the cache is required because the message
|
|
8792
9379
|
// content is unchanged, so the fingerprint cache would otherwise reuse the
|
|
8793
|
-
// stale escaped wrappers.
|
|
9380
|
+
// stale escaped wrappers. `onMarkdownParsersReady` no-ops when the parsers are
|
|
9381
|
+
// already loaded (the ESM build, and the CDN build after the first load), so
|
|
9382
|
+
// the `markdownReadyAtInit` guard is redundant — kept only to skip the
|
|
9383
|
+
// subscription bookkeeping on the common eager path.
|
|
8794
9384
|
if (!markdownReadyAtInit) {
|
|
8795
|
-
|
|
8796
|
-
|
|
8797
|
-
|
|
8798
|
-
|
|
8799
|
-
|
|
8800
|
-
|
|
8801
|
-
|
|
8802
|
-
|
|
8803
|
-
|
|
8804
|
-
});
|
|
9385
|
+
const unsubscribeParsersReady = onMarkdownParsersReady(() => {
|
|
9386
|
+
if (!session) return;
|
|
9387
|
+
configVersion++;
|
|
9388
|
+
messageCache.clear();
|
|
9389
|
+
renderMessagesWithPlugins(messagesWrapper, session.getMessages(), postprocess);
|
|
9390
|
+
});
|
|
9391
|
+
// Drop the subscription on teardown so a late chunk resolution can't clear
|
|
9392
|
+
// the cache and render into a detached `messagesWrapper`.
|
|
9393
|
+
destroyCallbacks.push(unsubscribeParsersReady);
|
|
8805
9394
|
}
|
|
8806
9395
|
|
|
8807
9396
|
return controller;
|