@runtypelabs/persona 4.25.0 → 4.26.1
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.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-Db61aI9J.d.cts → types-BwKTZhg9.d.cts} +14 -0
- package/dist/animations/{types-Db61aI9J.d.ts → types-BwKTZhg9.d.ts} +14 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/animations-extra.d.cts +14 -0
- package/dist/animations-extra.d.ts +14 -0
- package/dist/approval-ui-QAKWNFNM.js +1 -0
- package/dist/approval-ui.cjs +2 -2
- package/dist/approval-ui.d.cts +186 -8
- package/dist/approval-ui.d.ts +186 -8
- package/dist/approval-ui.js +2 -2
- package/dist/{artifacts-ui-IGB5YKLO.js → artifacts-ui-NJGMM27I.js} +1 -1
- package/dist/artifacts-ui.cjs +4 -4
- package/dist/artifacts-ui.d.cts +186 -8
- package/dist/artifacts-ui.d.ts +186 -8
- package/dist/artifacts-ui.js +5 -5
- package/dist/chunk-2ANTRKTV.js +1 -0
- package/dist/chunk-4HPTPUC6.js +1 -0
- package/dist/chunk-6V3MNEKB.js +1 -0
- package/dist/chunk-GKPCIQPB.js +1 -0
- package/dist/chunk-HGUCGM3K.js +9 -0
- package/dist/chunk-IO5VVUKP.js +3 -0
- package/dist/chunk-J4J3Y2P2.js +1 -0
- package/dist/chunk-K6HKIP5K.js +1 -0
- package/dist/chunk-K7NA7G7J.js +1 -0
- package/dist/chunk-MXUTS3LB.js +1 -0
- package/dist/chunk-PDPVORUF.js +1 -0
- package/dist/chunk-QRKGQPU7.js +1 -0
- package/dist/chunk-S6PBWE75.js +1 -0
- package/dist/chunk-T7QM7YMF.js +1 -0
- package/dist/chunk-TR2H475E.js +1 -0
- package/dist/chunk-UPO4GUFC.js +1 -0
- package/dist/chunk-ZC66NAIR.js +1 -0
- package/dist/client-history-GOZWBDA5.js +1 -0
- package/dist/client-history.js +1 -0
- package/dist/client-stream-42FOI3EC.js +4 -0
- package/dist/client-stream.js +5 -0
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/context-mentions-E7HJYWO6.js +161 -0
- package/dist/{context-mentions-inline-I2RDEBAK.js → context-mentions-inline-QVESLW55.js} +1 -1
- package/dist/context-mentions.cjs +7 -7
- package/dist/context-mentions.d.cts +186 -8
- package/dist/context-mentions.d.ts +186 -8
- package/dist/context-mentions.js +8 -8
- package/dist/event-stream-view-FOIJHFQO.js +1 -0
- package/dist/event-stream-view.cjs +1 -1
- package/dist/event-stream-view.d.cts +189 -31
- package/dist/event-stream-view.d.ts +189 -31
- package/dist/event-stream-view.js +1 -1
- package/dist/forms-ui.d.cts +227 -667
- package/dist/forms-ui.d.ts +227 -667
- package/dist/history-shell-QDQ44PSE.js +3 -0
- package/dist/history-shell.js +3 -0
- package/dist/{history-view-BD6OXLNA.js → history-view-NCFBI5GR.js} +2 -2
- package/dist/history-view.cjs +2 -2
- package/dist/history-view.d.cts +35 -1
- package/dist/history-view.d.ts +35 -1
- package/dist/history-view.js +2 -2
- package/dist/index.cjs +80 -74
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +233 -738
- package/dist/index.d.ts +233 -738
- package/dist/index.global.js +3 -215
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +81 -75
- 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 -67
- package/dist/launcher.global.js.map +1 -1
- package/dist/markdown-parsers.js +22 -22
- package/dist/runtype-tts-entry-NFBVWR7N.js +1 -0
- package/dist/runtype-tts.js +1 -1
- package/dist/session-actions-ISSKSI66.js +1 -0
- package/dist/session-actions.js +1 -0
- package/dist/session-reconnect.d.cts +186 -8
- package/dist/session-reconnect.d.ts +186 -8
- package/dist/smart-dom-reader.cjs +13 -13
- package/dist/smart-dom-reader.d.cts +186 -8
- package/dist/smart-dom-reader.d.ts +186 -8
- package/dist/smart-dom-reader.js +11 -11
- package/dist/theme-editor-preview.cjs +116 -98
- package/dist/theme-editor-preview.d.cts +188 -8
- package/dist/theme-editor-preview.d.ts +188 -8
- package/dist/theme-editor-preview.js +49 -54
- package/dist/theme-editor.cjs +2 -2
- package/dist/theme-editor.d.cts +187 -23
- package/dist/theme-editor.d.ts +187 -23
- package/dist/theme-editor.js +2 -2
- package/dist/theme-reference.d.cts +14 -0
- package/dist/theme-reference.d.ts +14 -0
- package/dist/ui-extras-entry-WAYFKVNW.js +3 -0
- package/dist/ui-extras.js +72 -0
- package/dist/voice-runtime-XLU6YK2N.js +17 -0
- package/dist/voice-runtime.cjs +17 -1
- package/dist/voice-runtime.d.cts +11656 -232
- package/dist/voice-runtime.d.ts +11656 -232
- package/dist/voice-runtime.js +17 -1
- package/dist/voice-worklet-player.cjs +19 -4
- package/dist/voice-worklet-player.d.cts +9 -10
- package/dist/voice-worklet-player.d.ts +9 -10
- package/dist/voice-worklet-player.js +19 -4
- package/dist/webmcp-polyfill.js +1 -1
- package/dist/webmcp-runtime-V6ZWW3PI.js +6 -0
- package/dist/webmcp-runtime.cjs +2 -2
- package/dist/webmcp-runtime.d.cts +1 -33
- package/dist/webmcp-runtime.d.ts +1 -33
- package/dist/webmcp-runtime.js +2 -2
- package/dist/widget.css +1 -1
- package/package.json +11 -5
- package/src/approval-paused-tool-settle.test.ts +325 -0
- package/src/client-approve.test.ts +311 -0
- package/src/client-history-eager.ts +9 -0
- package/src/client-history-loader.ts +23 -0
- package/src/client-history.ts +579 -0
- package/src/client-resume.ts +177 -0
- package/src/client-stream-eager.ts +12 -0
- package/src/client-stream-loader.ts +25 -0
- package/src/client-stream.ts +1880 -0
- package/src/client.test.ts +31 -1
- package/src/client.ts +293 -2607
- package/src/components/approval-actions.test.ts +27 -0
- package/src/components/approval-actions.ts +27 -3
- package/src/components/message-bubble.test.ts +19 -0
- package/src/components/message-bubble.ts +2 -0
- package/src/components/registry.ts +6 -6
- package/src/components/tool-bubble.test.ts +9 -0
- package/src/components/tool-bubble.ts +1 -0
- package/src/history-shell-bundle.test.ts +54 -0
- package/src/history-shell-eager.ts +12 -0
- package/src/history-shell-loader.ts +27 -0
- package/src/history-shell.ts +2362 -0
- package/src/history-view-loader.ts +2 -0
- package/src/history-view.ts +3 -0
- package/src/index-core.ts +6 -0
- package/src/index-global.ts +90 -248
- package/src/index.ts +13 -0
- package/src/install.ts +6 -4
- package/src/internal/runtype-history-provider.test.ts +1 -0
- package/src/internal/runtype-history-provider.ts +28 -13
- package/src/plugins/registry.ts +8 -8
- package/src/session-actions-chunk.test.ts +55 -0
- package/src/session-actions-eager.ts +11 -0
- package/src/session-actions-loader.ts +37 -0
- package/src/session-actions.ts +909 -0
- package/src/session-history.test.ts +3 -1
- package/src/session.ts +895 -1538
- package/src/session.voice.test.ts +1171 -5
- package/src/styles/widget.css +58 -0
- package/src/theme-editor/state.ts +91 -91
- package/src/types.ts +204 -5
- package/src/ui-ask-user-sheet.ts +382 -0
- package/src/ui-extras-eager.ts +10 -0
- package/src/ui-extras-entry.ts +9 -0
- package/src/ui-extras-loader.ts +26 -0
- package/src/ui.artifact-rail-resize.test.ts +2 -1
- package/src/ui.ask-user-chunk.test.ts +93 -0
- package/src/ui.history-chunk.test.ts +2 -1
- package/src/ui.history-config-provider.test.ts +2 -1
- package/src/ui.history-confirm-fallback.test.ts +142 -0
- package/src/ui.history-controller.test.ts +2 -1
- package/src/ui.history-lifecycle.test.ts +2 -1
- package/src/ui.history-render-hooks.test.ts +2 -1
- package/src/ui.history-shell.test.ts +2 -1
- package/src/ui.rail-overlay.test.ts +2 -1
- package/src/ui.rail-resize.test.ts +2 -1
- package/src/ui.rail-trigger-pending.test.ts +2 -1
- package/src/ui.ts +340 -2770
- package/src/utils/attachment-manager.ts +67 -67
- package/src/utils/constants.ts +3 -0
- package/src/utils/context-mention-controller.ts +323 -323
- package/src/utils/context-mention-manager.ts +40 -40
- package/src/utils/context-mention-orchestrator.ts +28 -0
- package/src/utils/event-stream-buffer.ts +50 -50
- package/src/utils/event-stream-store.ts +43 -43
- package/src/utils/throughput-tracker.ts +34 -34
- package/src/utils/virtual-scroller.ts +67 -67
- package/src/utils/visitor-store.test.ts +50 -0
- package/src/utils/visitor-store.ts +23 -5
- package/src/voice/__fixtures__/voice-wire/README.md +1 -0
- package/src/voice/__fixtures__/voice-wire/client/cancel.json +3 -0
- package/src/voice/__fixtures__/voice-wire/client/context.json +4 -0
- package/src/voice/__fixtures__/voice-wire/client/delegation_result.json +6 -0
- package/src/voice/__fixtures__/voice-wire/client/delegation_update.json +6 -0
- package/src/voice/__fixtures__/voice-wire/client/ping.json +3 -0
- package/src/voice/__fixtures__/voice-wire/client/playback_progress.json +5 -0
- package/src/voice/__fixtures__/voice-wire/client/start.json +3 -0
- package/src/voice/__fixtures__/voice-wire/server/artifact.json +11 -0
- package/src/voice/__fixtures__/voice-wire/server/attached.json +4 -0
- package/src/voice/__fixtures__/voice-wire/server/audio_clear.json +4 -0
- package/src/voice/__fixtures__/voice-wire/server/audio_end.json +3 -0
- package/src/voice/__fixtures__/voice-wire/server/delegation_cancelled.json +5 -0
- package/src/voice/__fixtures__/voice-wire/server/delegation_completed.json +8 -0
- package/src/voice/__fixtures__/voice-wire/server/delegation_started.json +15 -0
- package/src/voice/__fixtures__/voice-wire/server/error.json +6 -0
- package/src/voice/__fixtures__/voice-wire/server/metrics.json +10 -0
- package/src/voice/__fixtures__/voice-wire/server/pong.json +3 -0
- package/src/voice/__fixtures__/voice-wire/server/session_config.json +16 -0
- package/src/voice/__fixtures__/voice-wire/server/transcript_final.json +7 -0
- package/src/voice/__fixtures__/voice-wire/server/transcript_interim.json +4 -0
- package/src/voice/__fixtures__/voice-wire/server/transcript_partial.json +6 -0
- package/src/voice/__fixtures__/voice-wire/server/transcript_update.json +10 -0
- package/src/voice/__fixtures__/voice-wire/server/warning.json +5 -0
- package/src/voice/audio-playback-manager.test.ts +57 -0
- package/src/voice/audio-playback-manager.ts +120 -98
- package/src/voice/browser-voice-provider.ts +37 -37
- package/src/voice/fallback-speech-engine.ts +22 -16
- package/src/voice/keyed-voice-transcript.ts +314 -0
- package/src/voice/mic-state-styles.ts +182 -0
- package/src/voice/read-aloud-controller.ts +46 -44
- package/src/voice/runtype-speech-engine.ts +35 -33
- package/src/voice/runtype-voice-provider.ts +680 -189
- package/src/voice/session-voice-wiring.ts +266 -0
- package/src/voice/voice-delegation.test.ts +71 -0
- package/src/voice/voice-delegation.ts +364 -0
- package/src/voice/voice-turn-tracker.ts +6 -6
- package/src/voice/voice.test.ts +1060 -8
- package/src/voice/worklet-playback-engine.test.ts +81 -0
- package/src/voice/worklet-playback-engine.ts +21 -3
- package/src/voice-runtime-loader.ts +4 -1
- package/src/voice-runtime.ts +7 -0
- package/src/webmcp-runtime-entry.ts +41 -39
- package/dist/approval-ui-3NMYNXBD.js +0 -1
- package/dist/chunk-5EIIHQLQ.js +0 -1
- package/dist/chunk-7CO4T5KB.js +0 -1
- package/dist/chunk-EFJBUH5F.js +0 -1
- package/dist/chunk-R2TMIMDI.js +0 -3
- package/dist/chunk-UTVLOTWB.js +0 -9
- package/dist/context-mentions-45MODWOY.js +0 -161
- package/dist/event-stream-view-GL3ADRY2.js +0 -1
- package/dist/runtype-tts-entry-UJAEF7NZ.js +0 -1
- package/dist/voice-runtime-7DA7MJH4.js +0 -1
- package/dist/webmcp-runtime-JI7T4QYF.js +0 -6
package/src/ui.ts
CHANGED
|
@@ -6,6 +6,11 @@ import { resolveSanitizer } from "./utils/sanitize";
|
|
|
6
6
|
import { stabilizeStreamingTables } from "./utils/streaming-table";
|
|
7
7
|
import { wrapScrollableTables, refreshTableScrollFades } from "./utils/table-scroll-fade";
|
|
8
8
|
import { onMarkdownParsersReady, getMarkdownParsersSync, loadMarkdownParsers } from "./markdown-parsers-loader";
|
|
9
|
+
import { loadClientStream } from "./client-stream-loader";
|
|
10
|
+
import { HistoryClientError } from "./client";
|
|
11
|
+
import { HistoryProviderError } from "./internal/history-provider";
|
|
12
|
+
import { getUiExtrasSync, loadUiExtras, type UiExtrasModule } from "./ui-extras-loader";
|
|
13
|
+
import type { AskUserSheetContext, AskUserSheetHandlers } from "./ui-ask-user-sheet";
|
|
9
14
|
import {
|
|
10
15
|
AgentWidgetSession,
|
|
11
16
|
AgentWidgetSessionStatus,
|
|
@@ -37,7 +42,6 @@ import {
|
|
|
37
42
|
InjectSystemMessageOptions,
|
|
38
43
|
InjectComponentDirectiveOptions,
|
|
39
44
|
LoadingIndicatorRenderContext,
|
|
40
|
-
AgentWidgetRenderHistoryOpenErrorContext,
|
|
41
45
|
IdleIndicatorRenderContext,
|
|
42
46
|
VoiceStatus,
|
|
43
47
|
ReadAloudState,
|
|
@@ -51,7 +55,6 @@ import {
|
|
|
51
55
|
AgentWidgetSuggestion,
|
|
52
56
|
AgentWidgetSuggestionSource,
|
|
53
57
|
AgentWidgetWelcomeIcon,
|
|
54
|
-
HistoryConversationPatch,
|
|
55
58
|
HistoryConversationSummary,
|
|
56
59
|
HistoryIdentityStatus,
|
|
57
60
|
HistoryReturnSurface,
|
|
@@ -145,10 +148,10 @@ import {
|
|
|
145
148
|
type BeforeSendOutcome,
|
|
146
149
|
type InternalSubmissionSnapshot,
|
|
147
150
|
} from "./utils/composer-submission";
|
|
148
|
-
import {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
} from "./
|
|
151
|
+
import type { ContextMentionOrchestrator } from "./utils/context-mention-orchestrator";
|
|
152
|
+
import { loadContextMentions } from "./context-mentions-loader";
|
|
153
|
+
import { loadContextMentionsInline } from "./context-mentions-inline-loader";
|
|
154
|
+
import { createMentionButton } from "./components/context-mention-button";
|
|
152
155
|
import type { MentionSubmitBundle } from "./utils/context-mention-manager";
|
|
153
156
|
import { createTextPart, ALL_SUPPORTED_MIME_TYPES } from "./utils/content";
|
|
154
157
|
import { applyThemeVariables, createThemeObserver, getActiveTheme, getColorScheme } from "./utils/theme";
|
|
@@ -157,7 +160,7 @@ import { deepMerge } from "./utils/deep-merge";
|
|
|
157
160
|
import { Activity, Check, Copy } from "lucide";
|
|
158
161
|
import { renderLucideIcon, onExtraIconsReady } from "./utils/icons";
|
|
159
162
|
import { renderIconNode } from "./utils/icon-node";
|
|
160
|
-
import { createElement
|
|
163
|
+
import { createElement } from "./utils/dom";
|
|
161
164
|
import { resolveContentMaxWidth } from "./utils/content-width";
|
|
162
165
|
import {
|
|
163
166
|
computeComposerLift,
|
|
@@ -171,16 +174,13 @@ import {
|
|
|
171
174
|
DEFAULT_TOOLTIP_DELAY_MS,
|
|
172
175
|
DEFAULT_TOOLTIP_SKIP_DELAY_MS,
|
|
173
176
|
} from "./utils/tooltip-timing";
|
|
174
|
-
import {
|
|
175
|
-
ariaCombo,
|
|
176
|
-
createShortcutRegistry,
|
|
177
|
-
formatCombo,
|
|
178
|
-
parseCombo,
|
|
179
|
-
} from "./utils/shortcuts";
|
|
177
|
+
import { createShortcutRegistry } from "./utils/shortcuts";
|
|
180
178
|
// The artifact download/copy helpers live in the lazy artifacts-ui chunk;
|
|
181
179
|
// reached via getArtifactsUiSync() in the delegated click handlers (which can
|
|
182
180
|
// only fire on DOM the adopted chunk rendered).
|
|
183
181
|
import { morphMessages } from "./utils/morph";
|
|
182
|
+
import { getVoiceRuntimeSync, loadVoiceRuntime, type VoiceRuntimeModule } from "./voice-runtime-loader";
|
|
183
|
+
import type { MicStateStyles, OriginalMicStyles } from "./voice/mic-state-styles";
|
|
184
184
|
import { normalizeCopiedSelectionText } from "./utils/copy-selection";
|
|
185
185
|
import {
|
|
186
186
|
navigateComposerHistory,
|
|
@@ -202,7 +202,6 @@ import {
|
|
|
202
202
|
import {
|
|
203
203
|
statusCopy,
|
|
204
204
|
DEFAULT_OVERLAY_Z_INDEX,
|
|
205
|
-
PORTALED_OVERLAY_Z_INDEX,
|
|
206
205
|
} from "./utils/constants";
|
|
207
206
|
import {
|
|
208
207
|
applyStreamBuffer,
|
|
@@ -273,18 +272,10 @@ import { createReasoningBubble, updateReasoningBubbleUI } from "./components/rea
|
|
|
273
272
|
import { copyToolDetail } from "./components/tool-details";
|
|
274
273
|
import { createToolBubble, updateToolBubbleUI } from "./components/tool-bubble";
|
|
275
274
|
import {
|
|
276
|
-
buildStructuredAnswers,
|
|
277
275
|
ensureAskUserQuestionSheet,
|
|
278
|
-
getCurrentIndex,
|
|
279
|
-
getQuestionCount,
|
|
280
|
-
getSelectedLabels,
|
|
281
276
|
isAskUserQuestionMessage,
|
|
282
|
-
isGroupedSheet,
|
|
283
|
-
navigateToPage,
|
|
284
277
|
parseAskUserQuestionPayload,
|
|
285
|
-
readAnswersFromSheet,
|
|
286
278
|
removeAskUserQuestionSheet,
|
|
287
|
-
setCurrentAnswer,
|
|
288
279
|
} from "./components/ask-user-question-bubble";
|
|
289
280
|
import {
|
|
290
281
|
isSuggestRepliesMessage,
|
|
@@ -343,27 +334,9 @@ import {
|
|
|
343
334
|
import { createLocalStorageAdapter } from "./utils/storage";
|
|
344
335
|
import { createVisitorStore, type VisitorStore } from "./utils/visitor-store";
|
|
345
336
|
import { loadHistoryView } from "./history-view-loader";
|
|
346
|
-
import type
|
|
347
|
-
|
|
348
|
-
HistoryRailSection,
|
|
349
|
-
HistoryViewHandle,
|
|
350
|
-
HistoryViewOptions,
|
|
351
|
-
} from "./history-view-entry";
|
|
352
|
-
import {
|
|
353
|
-
createHistoryRenderSurface,
|
|
354
|
-
type HistoryRenderSurface,
|
|
355
|
-
} from "./history-render";
|
|
337
|
+
import { getHistoryShellSync, loadHistoryShell, type HistoryShellModule } from "./history-shell-loader";
|
|
338
|
+
import type { HistoryShell, HistoryShellContext } from "./history-shell";
|
|
356
339
|
import { getHistoryProviderFactory } from "./internal/history-provider-registry";
|
|
357
|
-
import type {
|
|
358
|
-
HistoryOperationContext,
|
|
359
|
-
HistoryProvider,
|
|
360
|
-
} from "./internal/history-provider";
|
|
361
|
-
import { createRuntypeHistoryProvider } from "./internal/runtype-history-provider";
|
|
362
|
-
import {
|
|
363
|
-
resolveHistoryShellCopy,
|
|
364
|
-
type ResolvedHistoryShellCopy,
|
|
365
|
-
} from "./components/history-shell-copy";
|
|
366
|
-
import { showHistoryConfirm } from "./components/history-confirm-dialog";
|
|
367
340
|
import { componentRegistry } from "./components/registry";
|
|
368
341
|
import {
|
|
369
342
|
renderComponentDirective,
|
|
@@ -1100,6 +1073,13 @@ export const createAgentExperience = (
|
|
|
1100
1073
|
const historyBootstrapReady = new Promise<void>((resolve) => {
|
|
1101
1074
|
resolveHistoryBootstrap = resolve;
|
|
1102
1075
|
});
|
|
1076
|
+
// With history enabled at mount, history-capable init also waits for the
|
|
1077
|
+
// shell, which installs the provider. Only the IIFE build loads it async
|
|
1078
|
+
// (always resolves, even when the chunk fails).
|
|
1079
|
+
let resolveHistoryShellGate: () => void = () => {};
|
|
1080
|
+
const historyShellGate = new Promise<void>((resolve) => {
|
|
1081
|
+
resolveHistoryShellGate = resolve;
|
|
1082
|
+
});
|
|
1103
1083
|
/** Synchronously loaded draft, replayed into the composer after it mounts. */
|
|
1104
1084
|
let loadedStoredDraft: AgentWidgetStoredDraft | undefined;
|
|
1105
1085
|
|
|
@@ -2335,6 +2315,20 @@ export const createAgentExperience = (
|
|
|
2335
2315
|
*/
|
|
2336
2316
|
const setupMentionOrchestrator = () => {
|
|
2337
2317
|
if (!config.contextMentions?.enabled || !textarea) return;
|
|
2318
|
+
// The orchestrator rides the lazy ui-extras chunk on the CDN: mount it (and
|
|
2319
|
+
// re-collect the composer actions for its affordance buttons) once the
|
|
2320
|
+
// chunk lands, unless a composer rebuild replaced this textarea meanwhile
|
|
2321
|
+
// (the rebuild re-runs this function) or the widget was destroyed.
|
|
2322
|
+
const extras = getUiExtrasSync();
|
|
2323
|
+
if (!extras) {
|
|
2324
|
+
const forTextarea = textarea;
|
|
2325
|
+
loadUiExtras().then(() => {
|
|
2326
|
+
if (mentionOrchestrator || textarea !== forTextarea || !forTextarea.isConnected) return;
|
|
2327
|
+
setupMentionOrchestrator();
|
|
2328
|
+
composerActionRenderer?.resolve();
|
|
2329
|
+
}, () => {});
|
|
2330
|
+
return;
|
|
2331
|
+
}
|
|
2338
2332
|
// Slash-command dispatch (prompt macros write text / submit; client actions
|
|
2339
2333
|
// read/replace the value) and submission are owned by the composer input
|
|
2340
2334
|
// surface itself — the mention runtime builds a textarea (chip) or
|
|
@@ -2346,7 +2340,9 @@ export const createAgentExperience = (
|
|
|
2346
2340
|
const header = composerBindings?.header ?? null;
|
|
2347
2341
|
const chipRow = header ? ensureComposerChipRow(header) : undefined;
|
|
2348
2342
|
|
|
2349
|
-
mentionOrchestrator = createContextMentionOrchestrator({
|
|
2343
|
+
mentionOrchestrator = extras.createContextMentionOrchestrator({
|
|
2344
|
+
// Core's stateful collaborators, not the chunk's copies.
|
|
2345
|
+
deps: { loadContextMentions, loadContextMentionsInline, renderLucideIcon, createMentionButton },
|
|
2350
2346
|
config,
|
|
2351
2347
|
textarea,
|
|
2352
2348
|
anchor: composerForm ?? textarea,
|
|
@@ -3328,345 +3324,63 @@ export const createAgentExperience = (
|
|
|
3328
3324
|
// user message via session.sendMessage so the agent resumes on the next turn.
|
|
3329
3325
|
const askUserOverlay = panelElements.composerOverlay;
|
|
3330
3326
|
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
}
|
|
3373
|
-
};
|
|
3374
|
-
|
|
3375
|
-
/**
|
|
3376
|
-
* Persist in-progress grouped-question answers + page index back to the
|
|
3377
|
-
* source message so a refresh restores the user's spot.
|
|
3378
|
-
*/
|
|
3379
|
-
const persistGroupedProgress = (sheet: HTMLElement): void => {
|
|
3380
|
-
const session = sessionRef.current;
|
|
3381
|
-
if (!session) return;
|
|
3382
|
-
const toolCallId = sheet.getAttribute("data-tool-call-id") ?? "";
|
|
3383
|
-
const sourceMessage = session.getMessages().find((m) => m.toolCall?.id === toolCallId);
|
|
3384
|
-
if (!sourceMessage) return;
|
|
3385
|
-
session.persistAskUserQuestionProgress(sourceMessage, {
|
|
3386
|
-
answers: buildStructuredAnswers(sheet, sourceMessage),
|
|
3387
|
-
currentIndex: getCurrentIndex(sheet),
|
|
3388
|
-
});
|
|
3389
|
-
};
|
|
3390
|
-
|
|
3391
|
-
/**
|
|
3392
|
-
* Build a one-line summary string for the legacy `answer` field on the
|
|
3393
|
-
* answered event when submit-all fires from a grouped sheet.
|
|
3394
|
-
*/
|
|
3395
|
-
const stringifyStructured = (answers: Record<string, string | string[]>): string => {
|
|
3396
|
-
return Object.entries(answers)
|
|
3397
|
-
.map(([q, v]) => `${q}: ${Array.isArray(v) ? v.join(", ") : v}`)
|
|
3398
|
-
.join(" | ");
|
|
3399
|
-
};
|
|
3400
|
-
|
|
3401
|
-
/**
|
|
3402
|
-
* If `groupedAutoAdvance` is enabled (default) and we're not on the final
|
|
3403
|
-
* page, advance one step. The final page never auto-submits: users always
|
|
3404
|
-
* confirm with an explicit Submit-all click so they can review.
|
|
3405
|
-
*/
|
|
3406
|
-
const maybeAutoAdvance = (sheet: HTMLElement): void => {
|
|
3407
|
-
if (config.features?.askUserQuestion?.groupedAutoAdvance === false) return;
|
|
3408
|
-
const idx = getCurrentIndex(sheet);
|
|
3409
|
-
const count = getQuestionCount(sheet);
|
|
3410
|
-
if (idx >= count - 1) return;
|
|
3411
|
-
const sourceMessage = sessionRef.current
|
|
3412
|
-
?.getMessages()
|
|
3413
|
-
.find((m) => m.toolCall?.id === sheet.getAttribute("data-tool-call-id"));
|
|
3414
|
-
if (!sourceMessage) return;
|
|
3415
|
-
navigateToPage(sheet, sourceMessage, config, idx + 1);
|
|
3416
|
-
persistGroupedProgress(sheet);
|
|
3417
|
-
};
|
|
3418
|
-
|
|
3419
|
-
askUserOverlay.addEventListener("click", (event) => {
|
|
3420
|
-
const target = event.target as HTMLElement;
|
|
3421
|
-
const trigger = target.closest<HTMLElement>("[data-ask-user-action]");
|
|
3422
|
-
if (!trigger) return;
|
|
3423
|
-
const sheet = trigger.closest<HTMLElement>("[data-persona-ask-sheet-for]");
|
|
3424
|
-
if (!sheet) return;
|
|
3425
|
-
|
|
3426
|
-
const action = trigger.getAttribute("data-ask-user-action");
|
|
3427
|
-
event.preventDefault();
|
|
3428
|
-
event.stopPropagation();
|
|
3429
|
-
|
|
3430
|
-
if (action === "dismiss") {
|
|
3431
|
-
const toolCallId = sheet.getAttribute("data-tool-call-id") ?? "";
|
|
3432
|
-
mount.dispatchEvent(
|
|
3433
|
-
new CustomEvent("persona:askUserQuestion:dismissed", {
|
|
3434
|
-
detail: { toolUseId: toolCallId },
|
|
3435
|
-
bubbles: true,
|
|
3436
|
-
composed: true,
|
|
3437
|
-
})
|
|
3438
|
-
);
|
|
3439
|
-
removeAskUserQuestionSheet(askUserOverlay, toolCallId);
|
|
3440
|
-
|
|
3441
|
-
// Best-effort: if this sheet corresponds to a LOCAL-awaiting tool,
|
|
3442
|
-
// unblock the paused execution with a sentinel answer so the server
|
|
3443
|
-
// doesn't sit in waiting_for_local forever. Fire-and-forget: errors
|
|
3444
|
-
// are surfaced to the onError callback. Flip the answered flag first
|
|
3445
|
-
// so a racing render pass doesn't re-mount the sheet mid-dismissal.
|
|
3446
|
-
const sourceMessage = sessionRef.current
|
|
3447
|
-
?.getMessages()
|
|
3448
|
-
.find((m) => m.toolCall?.id === toolCallId);
|
|
3449
|
-
if (sourceMessage?.agentMetadata?.awaitingLocalTool) {
|
|
3450
|
-
sessionRef.current?.markAskUserQuestionResolved(sourceMessage);
|
|
3451
|
-
sessionRef.current?.resolveAskUserQuestion(sourceMessage, "(dismissed)");
|
|
3452
|
-
}
|
|
3453
|
-
return;
|
|
3454
|
-
}
|
|
3455
|
-
|
|
3456
|
-
if (action === "pick") {
|
|
3457
|
-
const label = trigger.getAttribute("data-option-label");
|
|
3458
|
-
if (!label) return;
|
|
3459
|
-
const multiSelect = sheet.getAttribute("data-multi-select") === "true";
|
|
3460
|
-
const grouped = isGroupedSheet(sheet);
|
|
3461
|
-
|
|
3462
|
-
if (grouped && multiSelect) {
|
|
3463
|
-
const stored = readAnswersFromSheet(sheet)[getCurrentIndex(sheet)];
|
|
3464
|
-
const set = new Set<string>(Array.isArray(stored) ? stored : []);
|
|
3465
|
-
if (set.has(label)) set.delete(label);
|
|
3466
|
-
else set.add(label);
|
|
3467
|
-
setCurrentAnswer(sheet, Array.from(set));
|
|
3468
|
-
persistGroupedProgress(sheet);
|
|
3469
|
-
return;
|
|
3470
|
-
}
|
|
3471
|
-
|
|
3472
|
-
if (grouped) {
|
|
3473
|
-
setCurrentAnswer(sheet, label);
|
|
3474
|
-
persistGroupedProgress(sheet);
|
|
3475
|
-
maybeAutoAdvance(sheet);
|
|
3476
|
-
return;
|
|
3477
|
-
}
|
|
3478
|
-
|
|
3479
|
-
// 1-question modes: preserve original UX.
|
|
3480
|
-
if (multiSelect) {
|
|
3481
|
-
const pressed = trigger.getAttribute("aria-pressed") === "true";
|
|
3482
|
-
trigger.setAttribute("aria-pressed", pressed ? "false" : "true");
|
|
3483
|
-
trigger.classList.toggle("persona-ask-pill-selected", !pressed);
|
|
3484
|
-
const submitBtn = sheet.querySelector<HTMLButtonElement>(
|
|
3485
|
-
'[data-ask-user-action="submit-multi"]'
|
|
3486
|
-
);
|
|
3487
|
-
if (submitBtn) {
|
|
3488
|
-
submitBtn.disabled = getSelectedLabels(sheet).length === 0;
|
|
3489
|
-
}
|
|
3490
|
-
return;
|
|
3491
|
-
}
|
|
3492
|
-
submitAskUserAnswer(sheet, label, { source: "pick", values: [label] });
|
|
3493
|
-
return;
|
|
3494
|
-
}
|
|
3495
|
-
|
|
3496
|
-
if (action === "submit-multi") {
|
|
3497
|
-
const labels = getSelectedLabels(sheet);
|
|
3498
|
-
if (labels.length === 0) return;
|
|
3499
|
-
submitAskUserAnswer(sheet, labels.join(", "), {
|
|
3500
|
-
source: "multi",
|
|
3501
|
-
values: labels,
|
|
3502
|
-
});
|
|
3503
|
-
return;
|
|
3504
|
-
}
|
|
3505
|
-
|
|
3506
|
-
if (action === "open-free-text") {
|
|
3507
|
-
const row = sheet.querySelector<HTMLElement>('[data-ask-free-text-row="true"]');
|
|
3508
|
-
if (row) {
|
|
3509
|
-
row.classList.remove("persona-hidden");
|
|
3510
|
-
const input = row.querySelector<HTMLInputElement>('[data-ask-free-text-input="true"]');
|
|
3511
|
-
input?.focus();
|
|
3512
|
-
}
|
|
3513
|
-
return;
|
|
3514
|
-
}
|
|
3515
|
-
|
|
3516
|
-
if (action === "focus-free-text") {
|
|
3517
|
-
// Rows-layout Other row: input lives inside the row container itself.
|
|
3518
|
-
// Native click on the input already focuses it; this branch handles
|
|
3519
|
-
// clicks on the badge or row chrome AND digit-shortcut activations.
|
|
3520
|
-
const input = sheet.querySelector<HTMLInputElement>('[data-ask-free-text-input="true"]');
|
|
3521
|
-
input?.focus();
|
|
3522
|
-
return;
|
|
3523
|
-
}
|
|
3524
|
-
|
|
3525
|
-
if (action === "submit-free-text") {
|
|
3526
|
-
const input = sheet.querySelector<HTMLInputElement>('[data-ask-free-text-input="true"]');
|
|
3527
|
-
const text = input?.value ?? "";
|
|
3528
|
-
if (!text.trim()) return;
|
|
3529
|
-
if (isGroupedSheet(sheet)) {
|
|
3530
|
-
setCurrentAnswer(sheet, text.trim());
|
|
3531
|
-
persistGroupedProgress(sheet);
|
|
3532
|
-
maybeAutoAdvance(sheet);
|
|
3327
|
+
// Warm the handler chunk as the sheet mounts so it is ready before any pick.
|
|
3328
|
+
const mountAskUserSheet = (message: AgentWidgetMessage): void => {
|
|
3329
|
+
loadUiExtras().catch(() => {});
|
|
3330
|
+
ensureAskUserQuestionSheet(message, config, askUserOverlay);
|
|
3331
|
+
};
|
|
3332
|
+
|
|
3333
|
+
// Handlers live in the lazy ui-extras chunk (warmed when a sheet mounts).
|
|
3334
|
+
// Only events from inside a mounted sheet need them.
|
|
3335
|
+
let askUserHandlers: AskUserSheetHandlers | null = null;
|
|
3336
|
+
const askUserCtx: AskUserSheetContext = {
|
|
3337
|
+
mount,
|
|
3338
|
+
overlay: askUserOverlay,
|
|
3339
|
+
sessionRef,
|
|
3340
|
+
config: () => config,
|
|
3341
|
+
};
|
|
3342
|
+
// At most one interaction waits on the chunk: later ones (a double click)
|
|
3343
|
+
// are dropped, and the queued one is dropped too if its sheet was removed
|
|
3344
|
+
// meanwhile, so an answer can never be submitted twice.
|
|
3345
|
+
let askUserEventPending = false;
|
|
3346
|
+
const routeAskUserEvent =
|
|
3347
|
+
(kind: keyof AskUserSheetHandlers) =>
|
|
3348
|
+
(event: Event): void => {
|
|
3349
|
+
const target = event.target as HTMLElement | null;
|
|
3350
|
+
const sheet = target?.closest?.("[data-persona-ask-sheet-for]");
|
|
3351
|
+
if (!sheet) return;
|
|
3352
|
+
// Only events the handlers act on (an action control click, Enter in the
|
|
3353
|
+
// free-text input) may claim the pending slot; ordinary typing must not
|
|
3354
|
+
// block the answer that follows it.
|
|
3355
|
+
const actionable =
|
|
3356
|
+
kind === "click"
|
|
3357
|
+
? !!target?.closest("[data-ask-user-action]")
|
|
3358
|
+
: (event as KeyboardEvent).key === "Enter" &&
|
|
3359
|
+
!!target?.matches?.('[data-ask-free-text-input="true"]');
|
|
3360
|
+
if (!actionable) return;
|
|
3361
|
+
const run = (m: UiExtrasModule) => {
|
|
3362
|
+
askUserHandlers ??= m.createAskUserSheetHandlers(askUserCtx);
|
|
3363
|
+
(askUserHandlers[kind] as (e: Event) => void)(event);
|
|
3364
|
+
};
|
|
3365
|
+
const loaded = getUiExtrasSync();
|
|
3366
|
+
if (loaded) {
|
|
3367
|
+
run(loaded);
|
|
3533
3368
|
return;
|
|
3534
3369
|
}
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
.find((m) => m.toolCall?.id === toolCallId);
|
|
3545
|
-
if (!sourceMessage) return;
|
|
3546
|
-
// Flush any unsubmitted free-text input as the current answer.
|
|
3547
|
-
const freeInput = sheet.querySelector<HTMLInputElement>('[data-ask-free-text-input="true"]');
|
|
3548
|
-
const pending = freeInput?.value?.trim() ?? "";
|
|
3549
|
-
if (pending) {
|
|
3550
|
-
const stored = readAnswersFromSheet(sheet)[getCurrentIndex(sheet)];
|
|
3551
|
-
if (typeof stored !== "string" || stored !== pending) {
|
|
3552
|
-
setCurrentAnswer(sheet, pending);
|
|
3553
|
-
}
|
|
3554
|
-
}
|
|
3555
|
-
const direction = action === "next" ? 1 : -1;
|
|
3556
|
-
const nextIdx = getCurrentIndex(sheet) + direction;
|
|
3557
|
-
navigateToPage(sheet, sourceMessage, config, nextIdx);
|
|
3558
|
-
persistGroupedProgress(sheet);
|
|
3559
|
-
return;
|
|
3560
|
-
}
|
|
3561
|
-
|
|
3562
|
-
if (action === "submit-all") {
|
|
3563
|
-
if (!sessionRef.current) return;
|
|
3564
|
-
const toolCallId = sheet.getAttribute("data-tool-call-id") ?? "";
|
|
3565
|
-
const sourceMessage = sessionRef.current
|
|
3566
|
-
.getMessages()
|
|
3567
|
-
.find((m) => m.toolCall?.id === toolCallId);
|
|
3568
|
-
if (!sourceMessage) return;
|
|
3569
|
-
// Flush any pending free-text on the final page first.
|
|
3570
|
-
const freeInput = sheet.querySelector<HTMLInputElement>('[data-ask-free-text-input="true"]');
|
|
3571
|
-
const pending = freeInput?.value?.trim() ?? "";
|
|
3572
|
-
if (pending) setCurrentAnswer(sheet, pending);
|
|
3573
|
-
|
|
3574
|
-
const structured = buildStructuredAnswers(sheet, sourceMessage);
|
|
3575
|
-
// Persist final answers to message metadata BEFORE resolving so the
|
|
3576
|
-
// answered-state review card (which reads `agentMetadata
|
|
3577
|
-
// .askUserQuestionAnswers`) shows the user's actual picks instead of
|
|
3578
|
-
// "(skipped)" placeholders. Without this, any answer set only via the
|
|
3579
|
-
// pending-flush above (or via paths that bypassed the per-pick persist
|
|
3580
|
-
// hook) would be missing from the transcript review even though it
|
|
3581
|
-
// landed in the structured payload sent to the agent.
|
|
3582
|
-
sessionRef.current.persistAskUserQuestionProgress(sourceMessage, {
|
|
3583
|
-
answers: structured,
|
|
3584
|
-
currentIndex: getCurrentIndex(sheet),
|
|
3585
|
-
});
|
|
3586
|
-
const summary = stringifyStructured(structured);
|
|
3587
|
-
submitAskUserAnswer(sheet, summary || "(submitted)", {
|
|
3588
|
-
source: "submit-all",
|
|
3589
|
-
structured,
|
|
3590
|
-
});
|
|
3591
|
-
return;
|
|
3592
|
-
}
|
|
3593
|
-
|
|
3594
|
-
if (action === "skip") {
|
|
3595
|
-
if (!sessionRef.current) return;
|
|
3596
|
-
const toolCallId = sheet.getAttribute("data-tool-call-id") ?? "";
|
|
3597
|
-
const sourceMessage = sessionRef.current
|
|
3598
|
-
.getMessages()
|
|
3599
|
-
.find((m) => m.toolCall?.id === toolCallId);
|
|
3600
|
-
if (!sourceMessage) return;
|
|
3601
|
-
|
|
3602
|
-
const grouped = isGroupedSheet(sheet);
|
|
3603
|
-
const idx = getCurrentIndex(sheet);
|
|
3604
|
-
const count = getQuestionCount(sheet);
|
|
3605
|
-
const isFinal = idx >= count - 1;
|
|
3606
|
-
|
|
3607
|
-
// Single-question payloads behave like dismiss.
|
|
3608
|
-
if (!grouped) {
|
|
3609
|
-
mount.dispatchEvent(
|
|
3610
|
-
new CustomEvent("persona:askUserQuestion:dismissed", {
|
|
3611
|
-
detail: { toolUseId: toolCallId },
|
|
3612
|
-
bubbles: true,
|
|
3613
|
-
composed: true,
|
|
3614
|
-
})
|
|
3615
|
-
);
|
|
3616
|
-
removeAskUserQuestionSheet(askUserOverlay, toolCallId);
|
|
3617
|
-
if (sourceMessage.agentMetadata?.awaitingLocalTool) {
|
|
3618
|
-
sessionRef.current.markAskUserQuestionResolved(sourceMessage);
|
|
3619
|
-
sessionRef.current.resolveAskUserQuestion(sourceMessage, "(dismissed)");
|
|
3370
|
+
if (askUserEventPending) return;
|
|
3371
|
+
askUserEventPending = true;
|
|
3372
|
+
loadUiExtras().then(
|
|
3373
|
+
(m) => {
|
|
3374
|
+
askUserEventPending = false;
|
|
3375
|
+
if (sheet.isConnected) run(m);
|
|
3376
|
+
},
|
|
3377
|
+
() => {
|
|
3378
|
+
askUserEventPending = false;
|
|
3620
3379
|
}
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
// resolved Record. setCurrentAnswer with an empty string deletes the
|
|
3626
|
-
// index from the in-memory map.
|
|
3627
|
-
setCurrentAnswer(sheet, "");
|
|
3628
|
-
// Also clear any unsubmitted free-text on this page.
|
|
3629
|
-
const freeInput = sheet.querySelector<HTMLInputElement>('[data-ask-free-text-input="true"]');
|
|
3630
|
-
if (freeInput) freeInput.value = "";
|
|
3631
|
-
|
|
3632
|
-
if (isFinal) {
|
|
3633
|
-
// Submit with whatever has been recorded so far.
|
|
3634
|
-
const structured = buildStructuredAnswers(sheet, sourceMessage);
|
|
3635
|
-
const summary = stringifyStructured(structured);
|
|
3636
|
-
submitAskUserAnswer(sheet, summary || "(skipped)", {
|
|
3637
|
-
source: "submit-all",
|
|
3638
|
-
structured,
|
|
3639
|
-
});
|
|
3640
|
-
return;
|
|
3641
|
-
}
|
|
3642
|
-
|
|
3643
|
-
// Intermediate page: advance one step without recording.
|
|
3644
|
-
navigateToPage(sheet, sourceMessage, config, idx + 1);
|
|
3645
|
-
persistGroupedProgress(sheet);
|
|
3646
|
-
return;
|
|
3647
|
-
}
|
|
3648
|
-
});
|
|
3649
|
-
|
|
3650
|
-
// Enter on the free-text input → submit. Stays on the overlay because the
|
|
3651
|
-
// event target IS the input, which lives inside the overlay subtree.
|
|
3652
|
-
askUserOverlay.addEventListener("keydown", (event) => {
|
|
3653
|
-
if (event.key !== "Enter") return;
|
|
3654
|
-
const target = event.target as HTMLElement;
|
|
3655
|
-
const input = target as HTMLInputElement;
|
|
3656
|
-
if (!input.matches?.('[data-ask-free-text-input="true"]')) return;
|
|
3657
|
-
const sheet = input.closest<HTMLElement>("[data-persona-ask-sheet-for]");
|
|
3658
|
-
if (!sheet) return;
|
|
3659
|
-
event.preventDefault();
|
|
3660
|
-
const text = input.value;
|
|
3661
|
-
if (!text.trim()) return;
|
|
3662
|
-
if (isGroupedSheet(sheet)) {
|
|
3663
|
-
setCurrentAnswer(sheet, text.trim());
|
|
3664
|
-
persistGroupedProgress(sheet);
|
|
3665
|
-
maybeAutoAdvance(sheet);
|
|
3666
|
-
return;
|
|
3667
|
-
}
|
|
3668
|
-
submitAskUserAnswer(sheet, text, { source: "free-text" });
|
|
3669
|
-
});
|
|
3380
|
+
);
|
|
3381
|
+
};
|
|
3382
|
+
askUserOverlay.addEventListener("click", routeAskUserEvent("click"));
|
|
3383
|
+
askUserOverlay.addEventListener("keydown", routeAskUserEvent("keydown"));
|
|
3670
3384
|
|
|
3671
3385
|
// Digit 1–9 → pick option N on the current rows-layout single-select page.
|
|
3672
3386
|
// Listens on `document` so the shortcut fires regardless of where focus
|
|
@@ -3726,7 +3440,8 @@ export const createAgentExperience = (
|
|
|
3726
3440
|
* to cover expanded, collapsed, and dragged widths alike.
|
|
3727
3441
|
*/
|
|
3728
3442
|
const railedChatMinPx = (): number => {
|
|
3729
|
-
const
|
|
3443
|
+
const railHost = historyShell?.railShell ? historyShell.railHost : null;
|
|
3444
|
+
const host = railHost?.isConnected ? railHost : null;
|
|
3730
3445
|
const railPx = host ? host.getBoundingClientRect().width : 0;
|
|
3731
3446
|
if (railPx <= 0) return ARTIFACT_RESIZE_CHAT_MIN_PX;
|
|
3732
3447
|
return railPx + ARTIFACT_RESIZE_RAILED_TRANSCRIPT_MIN_PX;
|
|
@@ -5354,6 +5069,10 @@ export const createAgentExperience = (
|
|
|
5354
5069
|
string,
|
|
5355
5070
|
{ streaming?: boolean; role: AgentWidgetMessage["role"] }
|
|
5356
5071
|
>();
|
|
5072
|
+
// The live call already showed its AI-disclosure notice; in the composer bar
|
|
5073
|
+
// (no status line) it holds the input placeholder, restored at hang-up.
|
|
5074
|
+
let voiceDisclosed = false;
|
|
5075
|
+
let placeholderBeforeDisclosure: string | null = null;
|
|
5357
5076
|
const voiceState = {
|
|
5358
5077
|
active: false,
|
|
5359
5078
|
manuallyDeactivated: false,
|
|
@@ -6581,7 +6300,7 @@ export const createAgentExperience = (
|
|
|
6581
6300
|
!message.agentMetadata?.askUserQuestionAnswered
|
|
6582
6301
|
) {
|
|
6583
6302
|
liveAskToolIds.add(message.toolCall.id);
|
|
6584
|
-
|
|
6303
|
+
mountAskUserSheet(message);
|
|
6585
6304
|
}
|
|
6586
6305
|
return;
|
|
6587
6306
|
}
|
|
@@ -6694,7 +6413,7 @@ export const createAgentExperience = (
|
|
|
6694
6413
|
!message.agentMetadata?.askUserQuestionAnswered
|
|
6695
6414
|
) {
|
|
6696
6415
|
liveAskToolIds.add(message.toolCall!.id);
|
|
6697
|
-
|
|
6416
|
+
mountAskUserSheet(message);
|
|
6698
6417
|
}
|
|
6699
6418
|
return;
|
|
6700
6419
|
}
|
|
@@ -6717,7 +6436,7 @@ export const createAgentExperience = (
|
|
|
6717
6436
|
!message.agentMetadata?.askUserQuestionAnswered
|
|
6718
6437
|
) {
|
|
6719
6438
|
liveAskToolIds.add(message.toolCall!.id);
|
|
6720
|
-
|
|
6439
|
+
mountAskUserSheet(message);
|
|
6721
6440
|
}
|
|
6722
6441
|
return;
|
|
6723
6442
|
}
|
|
@@ -7479,6 +7198,8 @@ export const createAgentExperience = (
|
|
|
7479
7198
|
const warmMarkdownParsers = () => {
|
|
7480
7199
|
if (markdownParsersWarmed) return;
|
|
7481
7200
|
markdownParsersWarmed = true;
|
|
7201
|
+
// The stream processor is needed for the first reply; warm it too.
|
|
7202
|
+
loadClientStream().catch(() => {});
|
|
7482
7203
|
loadMarkdownParsers().catch(() => {
|
|
7483
7204
|
// Failed fetch (ad blocker, offline): allow the next visibility change
|
|
7484
7205
|
// or render to retry; the chunk loader resets its cached promise on
|
|
@@ -8699,7 +8420,10 @@ export const createAgentExperience = (
|
|
|
8699
8420
|
};
|
|
8700
8421
|
let historyInternals: WidgetHistoryInternals = {
|
|
8701
8422
|
...(visitorStore ? { visitorStore } : {}),
|
|
8702
|
-
historyBootstrapReady
|
|
8423
|
+
historyBootstrapReady:
|
|
8424
|
+
config.features?.history?.enabled === true && !getHistoryShellSync()
|
|
8425
|
+
? Promise.all([historyBootstrapReady, historyShellGate]).then(() => {})
|
|
8426
|
+
: historyBootstrapReady,
|
|
8703
8427
|
getStoredMessageCursor: () => readMetadataString('historyMessageCursor'),
|
|
8704
8428
|
setStoredMessageCursor: (cursor: string | null) => {
|
|
8705
8429
|
if (cursor === null) {
|
|
@@ -9082,6 +8806,20 @@ export const createAgentExperience = (
|
|
|
9082
8806
|
|
|
9083
8807
|
switch (status) {
|
|
9084
8808
|
case 'listening':
|
|
8809
|
+
// A speech-to-speech call discloses that it's an AI, once per call.
|
|
8810
|
+
if (!voiceDisclosed) {
|
|
8811
|
+
const notice = session.getVoiceDisclosure();
|
|
8812
|
+
if (notice) {
|
|
8813
|
+
voiceDisclosed = true;
|
|
8814
|
+
if (isComposerBar()) {
|
|
8815
|
+
placeholderBeforeDisclosure = textarea.placeholder;
|
|
8816
|
+
textarea.placeholder = notice;
|
|
8817
|
+
announce(notice);
|
|
8818
|
+
} else {
|
|
8819
|
+
showComposerNotice(notice, 10_000);
|
|
8820
|
+
}
|
|
8821
|
+
}
|
|
8822
|
+
}
|
|
9085
8823
|
// A continuous realtime call re-enters `listening` after every spoken
|
|
9086
8824
|
// reply, so reassert the recording styles here (they were replaced by
|
|
9087
8825
|
// the `processing`/`speaking` states during the turn). The initial
|
|
@@ -9107,6 +8845,11 @@ export const createAgentExperience = (
|
|
|
9107
8845
|
micButton?.setAttribute("aria-label", "End voice session");
|
|
9108
8846
|
} else {
|
|
9109
8847
|
voiceState.active = false;
|
|
8848
|
+
voiceDisclosed = false;
|
|
8849
|
+
if (placeholderBeforeDisclosure !== null) {
|
|
8850
|
+
textarea.placeholder = placeholderBeforeDisclosure;
|
|
8851
|
+
placeholderBeforeDisclosure = null;
|
|
8852
|
+
}
|
|
9110
8853
|
removeRuntypeMicStateStyles();
|
|
9111
8854
|
emitVoiceState("system");
|
|
9112
8855
|
persistVoiceMetadata();
|
|
@@ -9222,71 +8965,13 @@ export const createAgentExperience = (
|
|
|
9222
8965
|
// mutation. The lazily loaded Messages view owns the list itself.
|
|
9223
8966
|
// ==========================================================================
|
|
9224
8967
|
|
|
9225
|
-
|
|
9226
|
-
|
|
9227
|
-
|
|
9228
|
-
const HISTORY_EXIT_TIMEOUT_MS = 250;
|
|
9229
|
-
|
|
9230
|
-
let historyShellCopy: ResolvedHistoryShellCopy = resolveHistoryShellCopy(
|
|
9231
|
-
config.features?.history?.copy
|
|
9232
|
-
);
|
|
9233
|
-
let historyProvider: HistoryProvider | null = null;
|
|
9234
|
-
let historyUnavailable = false;
|
|
9235
|
-
/** Default view + plugin render-hook arbitration. Null while closed. */
|
|
9236
|
-
let historySurface: HistoryRenderSurface | null = null;
|
|
9237
|
-
let historyOperationContext: HistoryOperationContext | null = null;
|
|
9238
|
-
let historyReturnSurface: HistoryReturnSurface = "conversation";
|
|
9239
|
-
let historyInvoker: HTMLElement | null = null;
|
|
9240
|
-
let historyButton: HTMLButtonElement | null = null;
|
|
9241
|
-
/** The inserted node: removing the button alone would orphan its wrapper. */
|
|
9242
|
-
let historyButtonWrapper: HTMLElement | null = null;
|
|
8968
|
+
let historyShell: HistoryShell | null = null;
|
|
8969
|
+
let historyShellLoad: Promise<HistoryShell | null> | null = null;
|
|
8970
|
+
let historyShellTornDown = false;
|
|
9243
8971
|
let historySessionState: SessionHistoryState = session.getHistoryState();
|
|
9244
|
-
let historyIdentityKey: string | null = null;
|
|
9245
|
-
let historyOpenToken = 0;
|
|
9246
|
-
let clearChatDefaultLabel: string | null = null;
|
|
9247
|
-
let unsubscribeHistoryAvailability: (() => void) | null = null;
|
|
9248
|
-
let unsubscribeHistoryIdentity: (() => void) | null = null;
|
|
9249
|
-
const historyRegionId = `persona-history-${Math.random().toString(36).slice(2, 8)}`;
|
|
9250
8972
|
|
|
9251
8973
|
const historyFeatureEnabled = (): boolean =>
|
|
9252
8974
|
config.features?.history?.enabled === true;
|
|
9253
|
-
const historyAvailable = (): boolean =>
|
|
9254
|
-
historyFeatureEnabled() && !!historyProvider && !historyUnavailable;
|
|
9255
|
-
/** Switching conversations mid-turn would abandon a live answer. */
|
|
9256
|
-
const historyTurnBusy = (): boolean => {
|
|
9257
|
-
const status = session.getStatus();
|
|
9258
|
-
return isStreaming || status === "paused" || status === "resuming";
|
|
9259
|
-
};
|
|
9260
|
-
const historyScope = (): HistoryScope => {
|
|
9261
|
-
const configured = config.features?.history?.scope;
|
|
9262
|
-
if (configured) return configured;
|
|
9263
|
-
// Derived, not requested: narrow to what the provider advertises rather
|
|
9264
|
-
// than asking it for a scope it will reject.
|
|
9265
|
-
const derived: HistoryScope = config.getIdentityProof
|
|
9266
|
-
? "verified-user"
|
|
9267
|
-
: "browser";
|
|
9268
|
-
const scopes = historyProvider?.capabilities.scopes;
|
|
9269
|
-
if (!scopes || scopes.includes(derived)) return derived;
|
|
9270
|
-
return scopes[0] ?? derived;
|
|
9271
|
-
};
|
|
9272
|
-
const historyOperationScope = (): HistoryScope =>
|
|
9273
|
-
historyOperationContext?.scope ?? historyScope();
|
|
9274
|
-
const activeHistoryTargetId = (): string | null =>
|
|
9275
|
-
session.getClientSession()?.targetId ?? null;
|
|
9276
|
-
|
|
9277
|
-
const identityStatusKey = (status: HistoryIdentityStatus): string =>
|
|
9278
|
-
`${status.state}:${"reason" in status ? status.reason : ""}`;
|
|
9279
|
-
|
|
9280
|
-
/** Instance-scoped only: authentication state is never broadcast page-wide. */
|
|
9281
|
-
const emitHistoryIdentityStatus = (status: HistoryIdentityStatus): void => {
|
|
9282
|
-
const key = identityStatusKey(status);
|
|
9283
|
-
if (key === historyIdentityKey) return;
|
|
9284
|
-
historyIdentityKey = key;
|
|
9285
|
-
eventBus.emit("history:identityStatusChanged", {
|
|
9286
|
-
status,
|
|
9287
|
-
timestamp: Date.now(),
|
|
9288
|
-
});
|
|
9289
|
-
};
|
|
9290
8975
|
|
|
9291
8976
|
/** Unconditional: `announce()` is gated on an unrelated scroll opt-in. */
|
|
9292
8977
|
const announceHistory = (message: string): void => {
|
|
@@ -9295,529 +8980,6 @@ export const createAgentExperience = (
|
|
|
9295
8980
|
liveRegion.textContent = message;
|
|
9296
8981
|
};
|
|
9297
8982
|
|
|
9298
|
-
// --- provider ------------------------------------------------------------
|
|
9299
|
-
|
|
9300
|
-
/** One provider build per widget instance; a new factory identity rebuilds. */
|
|
9301
|
-
let configuredProviderFactory: (() => HistoryProvider) | null = null;
|
|
9302
|
-
let configuredProviderInstance: HistoryProvider | null = null;
|
|
9303
|
-
|
|
9304
|
-
const buildHistoryProvider = (): HistoryProvider | null => {
|
|
9305
|
-
if (!historyFeatureEnabled()) return null;
|
|
9306
|
-
// Demo/test override first, then the host's own provider; production
|
|
9307
|
-
// without one builds the Runtype provider from client-token config.
|
|
9308
|
-
const override = getHistoryProviderFactory();
|
|
9309
|
-
if (override) return override();
|
|
9310
|
-
const configured = config.features?.history?.provider;
|
|
9311
|
-
if (configured) {
|
|
9312
|
-
if (typeof configured !== "function") return configured;
|
|
9313
|
-
if (configuredProviderFactory !== configured) {
|
|
9314
|
-
configuredProviderFactory = configured;
|
|
9315
|
-
configuredProviderInstance = configured();
|
|
9316
|
-
}
|
|
9317
|
-
return configuredProviderInstance;
|
|
9318
|
-
}
|
|
9319
|
-
if (!session.isClientTokenMode()) return null;
|
|
9320
|
-
return createRuntypeHistoryProvider({
|
|
9321
|
-
client: session.getClient(),
|
|
9322
|
-
getIdentityProofConfigured: () =>
|
|
9323
|
-
typeof config.getIdentityProof === "function",
|
|
9324
|
-
onActivationCommitted: (clientSession) =>
|
|
9325
|
-
session.bindActivatedSession(clientSession),
|
|
9326
|
-
// Connection-config rebuilds replace the client under the provider.
|
|
9327
|
-
getClient: () => session.getClient(),
|
|
9328
|
-
});
|
|
9329
|
-
};
|
|
9330
|
-
|
|
9331
|
-
const installHistoryProvider = (): void => {
|
|
9332
|
-
unsubscribeHistoryAvailability?.();
|
|
9333
|
-
unsubscribeHistoryAvailability = null;
|
|
9334
|
-
unsubscribeHistoryIdentity?.();
|
|
9335
|
-
unsubscribeHistoryIdentity = null;
|
|
9336
|
-
historyUnavailable = false;
|
|
9337
|
-
historyProvider = buildHistoryProvider();
|
|
9338
|
-
const next: WidgetHistoryInternals = { ...historyInternals };
|
|
9339
|
-
if (historyProvider) next.historyProvider = historyProvider;
|
|
9340
|
-
else delete next.historyProvider;
|
|
9341
|
-
historyInternals = next;
|
|
9342
|
-
session.setHistoryInternals(historyInternals);
|
|
9343
|
-
if (!historyProvider) return;
|
|
9344
|
-
unsubscribeHistoryAvailability =
|
|
9345
|
-
historyProvider.subscribeAvailability?.((available) => {
|
|
9346
|
-
historyUnavailable = !available;
|
|
9347
|
-
// A degrade must remove an ALREADY-rendered surface, not just a flag.
|
|
9348
|
-
if (!available) closeHistory({ restoreFocus: false });
|
|
9349
|
-
syncHistoryChromeImpl();
|
|
9350
|
-
}) ?? null;
|
|
9351
|
-
unsubscribeHistoryIdentity = historyProvider.subscribeIdentityStatus(
|
|
9352
|
-
(status) => emitHistoryIdentityStatus(status)
|
|
9353
|
-
);
|
|
9354
|
-
historyIdentityKey = identityStatusKey(historyProvider.getIdentityStatus());
|
|
9355
|
-
};
|
|
9356
|
-
|
|
9357
|
-
// --- presentation hosts --------------------------------------------------
|
|
9358
|
-
|
|
9359
|
-
/**
|
|
9360
|
-
* Resolved against the history HOST width, never the viewport. With the
|
|
9361
|
-
* artifact split mounted the split root is that host, not the container:
|
|
9362
|
-
* width the artifact pane borrows from the chat column is still the widget's,
|
|
9363
|
-
* so a drag on the split must never read as a narrow host and re-mode the
|
|
9364
|
-
* rail. A real window/host narrowing shrinks the split root too, and still
|
|
9365
|
-
* flips.
|
|
9366
|
-
*/
|
|
9367
|
-
const resolveHistoryPresentation = (): ResolvedHistoryPresentation => {
|
|
9368
|
-
const configured = config.features?.history?.presentation ?? "panel";
|
|
9369
|
-
if (configured === "panel") return "panel";
|
|
9370
|
-
if (configured === "auto") {
|
|
9371
|
-
// Floating launchers stay panel-based at every width.
|
|
9372
|
-
const inlineOrDocked = !launcherEnabled || isDockedMountMode(config);
|
|
9373
|
-
if (!inlineOrDocked) return "panel";
|
|
9374
|
-
}
|
|
9375
|
-
const host = artifactSplitRoot ?? container;
|
|
9376
|
-
const width = host.getBoundingClientRect().width || host.clientWidth;
|
|
9377
|
-
return width >= RAIL_MIN_CONTAINER_WIDTH ? "rail" : "panel";
|
|
9378
|
-
};
|
|
9379
|
-
|
|
9380
|
-
const setHistoryHostInert = (element: HTMLElement, inert: boolean): void => {
|
|
9381
|
-
if (inert) {
|
|
9382
|
-
element.setAttribute("aria-hidden", "true");
|
|
9383
|
-
element.setAttribute("inert", "");
|
|
9384
|
-
} else {
|
|
9385
|
-
element.removeAttribute("aria-hidden");
|
|
9386
|
-
element.removeAttribute("inert");
|
|
9387
|
-
}
|
|
9388
|
-
};
|
|
9389
|
-
|
|
9390
|
-
/** Suppression of the shell header's own children while Messages owns the bar. */
|
|
9391
|
-
const HISTORY_SUPPRESSED_ATTR = "data-persona-history-suppressed";
|
|
9392
|
-
/** Shell-owned wrapper for the view's bar contents. Null while not hosting. */
|
|
9393
|
-
let historyHeaderHost: HTMLElement | null = null;
|
|
9394
|
-
/** Header carrying the swap-time min-height pin. Null while not hosting. */
|
|
9395
|
-
let historyPinnedHeader: HTMLElement | null = null;
|
|
9396
|
-
/** Handle + arbitrated element, tracked from before `historySurface` is assigned. */
|
|
9397
|
-
let historyViewHandle: HistoryViewHandle | null = null;
|
|
9398
|
-
let historyMountedElement: HTMLElement | null = null;
|
|
9399
|
-
let capturedPanelHeader: HTMLElement | null = null;
|
|
9400
|
-
let capturedPanelHeaderDisplay = "";
|
|
9401
|
-
|
|
9402
|
-
/** The display the shell header returns to; "none" means there is no bar. */
|
|
9403
|
-
const shownHeaderDisplay = (): string =>
|
|
9404
|
-
header === capturedPanelHeader
|
|
9405
|
-
? capturedPanelHeaderDisplay
|
|
9406
|
-
: config.layout?.showHeader === false
|
|
9407
|
-
? "none"
|
|
9408
|
-
: "";
|
|
9409
|
-
|
|
9410
|
-
/**
|
|
9411
|
-
* One persistent bar: the shell header hosts the view's bar contents whenever
|
|
9412
|
-
* the default view owns a panel and there is a visible header to host them in.
|
|
9413
|
-
* A plugin's full custom surface owns everything, so it falls back to hiding.
|
|
9414
|
-
*/
|
|
9415
|
-
const historyHeaderExternal = (): boolean =>
|
|
9416
|
-
historyPresentation === "panel" &&
|
|
9417
|
-
!!historyViewHandle &&
|
|
9418
|
-
historyMountedElement === historyViewHandle.element &&
|
|
9419
|
-
config.layout?.showHeader !== false &&
|
|
9420
|
-
shownHeaderDisplay() !== "none";
|
|
9421
|
-
|
|
9422
|
-
const suppressHeaderChildren = (): void => {
|
|
9423
|
-
for (const child of Array.from(header.children)) {
|
|
9424
|
-
if (child === historyHeaderHost) continue;
|
|
9425
|
-
child.setAttribute(HISTORY_SUPPRESSED_ATTR, "");
|
|
9426
|
-
}
|
|
9427
|
-
};
|
|
9428
|
-
|
|
9429
|
-
/** By attribute, not by captured list: the header may have been rebuilt. */
|
|
9430
|
-
const unsuppressHeaderChildren = (): void => {
|
|
9431
|
-
for (const node of Array.from(
|
|
9432
|
-
container.querySelectorAll(`[${HISTORY_SUPPRESSED_ATTR}]`)
|
|
9433
|
-
)) {
|
|
9434
|
-
node.removeAttribute(HISTORY_SUPPRESSED_ATTR);
|
|
9435
|
-
}
|
|
9436
|
-
};
|
|
9437
|
-
|
|
9438
|
-
const hostHistoryHeaderContent = (): void => {
|
|
9439
|
-
const view = historyViewHandle;
|
|
9440
|
-
if (!view) return;
|
|
9441
|
-
// Pin the pre-swap height so the contents swap never moves the chrome: the
|
|
9442
|
-
// hosted bar is usually shorter than the title cluster it replaces, and
|
|
9443
|
-
// constant-height chrome is what keeps the switch from reading as layout
|
|
9444
|
-
// shift. Measurable only while the original contents are still visible; a
|
|
9445
|
-
// re-entry on an already-hosted header keeps the standing pin, and a
|
|
9446
|
-
// rebuilt header re-measures before its own suppression below.
|
|
9447
|
-
const originalsVisible = Array.from(header.children).some(
|
|
9448
|
-
(child) =>
|
|
9449
|
-
child !== historyHeaderHost &&
|
|
9450
|
-
!child.hasAttribute(HISTORY_SUPPRESSED_ATTR)
|
|
9451
|
-
);
|
|
9452
|
-
if (originalsVisible) {
|
|
9453
|
-
if (historyPinnedHeader && historyPinnedHeader !== header) {
|
|
9454
|
-
historyPinnedHeader.style.removeProperty("min-height");
|
|
9455
|
-
historyPinnedHeader = null;
|
|
9456
|
-
}
|
|
9457
|
-
const measured = header.offsetHeight;
|
|
9458
|
-
if (measured > 0) {
|
|
9459
|
-
header.style.minHeight = `${measured}px`;
|
|
9460
|
-
historyPinnedHeader = header;
|
|
9461
|
-
}
|
|
9462
|
-
}
|
|
9463
|
-
if (!historyHeaderHost) {
|
|
9464
|
-
historyHeaderHost = createElement("div", "persona-history-header-host");
|
|
9465
|
-
}
|
|
9466
|
-
view.setHeaderPlacement("external");
|
|
9467
|
-
const content = view.getHeaderElement();
|
|
9468
|
-
if (content.parentNode !== historyHeaderHost) {
|
|
9469
|
-
historyHeaderHost.replaceChildren(content);
|
|
9470
|
-
}
|
|
9471
|
-
// The wrapper follows a rebuilt header binding; focus inside it survives.
|
|
9472
|
-
if (historyHeaderHost.parentNode !== header) {
|
|
9473
|
-
header.appendChild(historyHeaderHost);
|
|
9474
|
-
}
|
|
9475
|
-
suppressHeaderChildren();
|
|
9476
|
-
};
|
|
9477
|
-
|
|
9478
|
-
const releaseHistoryHeaderContent = (): void => {
|
|
9479
|
-
if (historyPinnedHeader) {
|
|
9480
|
-
historyPinnedHeader.style.removeProperty("min-height");
|
|
9481
|
-
historyPinnedHeader = null;
|
|
9482
|
-
}
|
|
9483
|
-
if (!historyHeaderHost) return;
|
|
9484
|
-
// The view re-adopts its bar; the wrapper never owns the content's lifetime.
|
|
9485
|
-
historyViewHandle?.setHeaderPlacement("inline");
|
|
9486
|
-
historyHeaderHost.remove();
|
|
9487
|
-
historyHeaderHost = null;
|
|
9488
|
-
unsuppressHeaderChildren();
|
|
9489
|
-
};
|
|
9490
|
-
|
|
9491
|
-
/**
|
|
9492
|
-
* Panel presentation obscures the conversation, so the transcript AND the
|
|
9493
|
-
* composer must be unreachable: a visitor must never send into a conversation
|
|
9494
|
-
* they cannot see. The header bar itself stays: only its contents swap, which
|
|
9495
|
-
* is also why it is never inert here (rail changes nothing at all: the
|
|
9496
|
-
* conversation stays primary).
|
|
9497
|
-
*
|
|
9498
|
-
* The widget's close (×) usually lives inside that header, but no trap
|
|
9499
|
-
* results: the view's back control is the initial focus target, Escape exits,
|
|
9500
|
-
* and both restore the header contents before focus lands. `top-right` close
|
|
9501
|
-
* placement parents the × to the container, so it stays reachable either way.
|
|
9502
|
-
*
|
|
9503
|
-
* Restores exactly what it captured.
|
|
9504
|
-
*/
|
|
9505
|
-
let restorePanelHost: (() => void) | null = null;
|
|
9506
|
-
const enforcePanelHost = (): void => {
|
|
9507
|
-
if (!restorePanelHost) return;
|
|
9508
|
-
body.style.display = "none";
|
|
9509
|
-
setHistoryHostInert(body, true);
|
|
9510
|
-
// Live `footer` / `header` bindings: a composer-plugin rebuild or a
|
|
9511
|
-
// header-layout rebuild swaps them and re-enters here for the replacement.
|
|
9512
|
-
footer.hidden = true;
|
|
9513
|
-
setHistoryHostInert(footer, true);
|
|
9514
|
-
if (historyHeaderExternal()) {
|
|
9515
|
-
hostHistoryHeaderContent();
|
|
9516
|
-
header.style.display = shownHeaderDisplay();
|
|
9517
|
-
setHistoryHostInert(header, false);
|
|
9518
|
-
return;
|
|
9519
|
-
}
|
|
9520
|
-
// No bar to host in: hide the header the way the surface used to.
|
|
9521
|
-
releaseHistoryHeaderContent();
|
|
9522
|
-
header.style.display = "none";
|
|
9523
|
-
setHistoryHostInert(header, true);
|
|
9524
|
-
};
|
|
9525
|
-
reapplyHistoryHostChrome = enforcePanelHost;
|
|
9526
|
-
|
|
9527
|
-
const mountPanelHost = (element: HTMLElement): void => {
|
|
9528
|
-
const previousDisplay = body.style.display;
|
|
9529
|
-
const previousFooterHidden = footer.hidden;
|
|
9530
|
-
const capturedFooter = footer;
|
|
9531
|
-
const capturedHeader = header;
|
|
9532
|
-
const previousHeaderDisplay = header.style.display;
|
|
9533
|
-
capturedPanelHeader = capturedHeader;
|
|
9534
|
-
capturedPanelHeaderDisplay = previousHeaderDisplay;
|
|
9535
|
-
restorePanelHost = () => {
|
|
9536
|
-
restorePanelHost = null;
|
|
9537
|
-
releaseHistoryHeaderContent();
|
|
9538
|
-
capturedPanelHeader = null;
|
|
9539
|
-
body.style.display = previousDisplay;
|
|
9540
|
-
setHistoryHostInert(body, false);
|
|
9541
|
-
capturedFooter.hidden = previousFooterHidden;
|
|
9542
|
-
setHistoryHostInert(capturedFooter, false);
|
|
9543
|
-
if (footer !== capturedFooter) {
|
|
9544
|
-
footer.hidden = false;
|
|
9545
|
-
setHistoryHostInert(footer, false);
|
|
9546
|
-
}
|
|
9547
|
-
capturedHeader.style.display = previousHeaderDisplay;
|
|
9548
|
-
setHistoryHostInert(capturedHeader, false);
|
|
9549
|
-
if (header !== capturedHeader) {
|
|
9550
|
-
// A replacement carries no captured state; only config decides it.
|
|
9551
|
-
header.style.display = config.layout?.showHeader === false ? "none" : "";
|
|
9552
|
-
setHistoryHostInert(header, false);
|
|
9553
|
-
}
|
|
9554
|
-
};
|
|
9555
|
-
footer.parentNode?.insertBefore(element, footer);
|
|
9556
|
-
enforcePanelHost();
|
|
9557
|
-
};
|
|
9558
|
-
|
|
9559
|
-
let railShell: HTMLElement | null = null;
|
|
9560
|
-
let railHost: HTMLElement | null = null;
|
|
9561
|
-
let railColumn: HTMLElement | null = null;
|
|
9562
|
-
/** Stand-in element holding the docked column while the chunk loads. */
|
|
9563
|
-
let railPlaceholder: HTMLElement | null = null;
|
|
9564
|
-
/** Container order the rail borrows from and hands back, header first. */
|
|
9565
|
-
const railBorrowed = (): Array<HTMLElement | null> => [
|
|
9566
|
-
header,
|
|
9567
|
-
panelElements.closeButtonWrapper,
|
|
9568
|
-
panelElements.clearChatButtonWrapper,
|
|
9569
|
-
body,
|
|
9570
|
-
// Composer-bar mode keeps the footer outside the container; the parent
|
|
9571
|
-
// checks below then never adopt it.
|
|
9572
|
-
footer,
|
|
9573
|
-
];
|
|
9574
|
-
|
|
9575
|
-
/** Collapsed icon rail, measured from the reference sidebar. */
|
|
9576
|
-
const RAIL_COLLAPSED_WIDTH = 52;
|
|
9577
|
-
const railCollapsible = (): boolean =>
|
|
9578
|
-
config.features?.history?.rail?.collapsible !== false;
|
|
9579
|
-
const railSide = (): "left" | "right" =>
|
|
9580
|
-
config.features?.history?.rail?.side === "right" ? "right" : "left";
|
|
9581
|
-
/** The band a rail width may take, by config or by drag. */
|
|
9582
|
-
const RAIL_MIN_WIDTH = 200;
|
|
9583
|
-
const RAIL_MAX_WIDTH = 400;
|
|
9584
|
-
const clampRailWidth = (value: number): number =>
|
|
9585
|
-
Math.min(RAIL_MAX_WIDTH, Math.max(RAIL_MIN_WIDTH, Math.round(value)));
|
|
9586
|
-
|
|
9587
|
-
const railResizable = (): boolean =>
|
|
9588
|
-
config.features?.history?.rail?.resizable === true;
|
|
9589
|
-
/** Resolved once per widget: storage, else nothing. */
|
|
9590
|
-
let railWidthChoice: number | null | undefined;
|
|
9591
|
-
|
|
9592
|
-
// Same teaser pattern as the collapsed state: blocked storage throws rather
|
|
9593
|
-
// than fails, so the resolved value is also the in-memory fallback.
|
|
9594
|
-
const storedRailWidth = (): number | null => {
|
|
9595
|
-
if (railWidthChoice === undefined) {
|
|
9596
|
-
railWidthChoice = null;
|
|
9597
|
-
if (config.persistState !== false) {
|
|
9598
|
-
try {
|
|
9599
|
-
const stored = Number(
|
|
9600
|
-
window.localStorage.getItem(`${currentKeyPrefix()}rail-width`)
|
|
9601
|
-
);
|
|
9602
|
-
if (stored) railWidthChoice = stored;
|
|
9603
|
-
} catch {
|
|
9604
|
-
/* blocked storage: the config width stands */
|
|
9605
|
-
}
|
|
9606
|
-
}
|
|
9607
|
-
}
|
|
9608
|
-
return railWidthChoice;
|
|
9609
|
-
};
|
|
9610
|
-
|
|
9611
|
-
/**
|
|
9612
|
-
* Expanded rail width, shared by the column and the floating overlay. A
|
|
9613
|
-
* dragged width outranks config, the way the collapsed state does: a live
|
|
9614
|
-
* `update()` must not undo what the visitor chose.
|
|
9615
|
-
*/
|
|
9616
|
-
const railWidth = (): number =>
|
|
9617
|
-
clampRailWidth(
|
|
9618
|
-
storedRailWidth() ?? config.features?.history?.rail?.width ?? 260
|
|
9619
|
-
);
|
|
9620
|
-
|
|
9621
|
-
const setStoredRailWidth = (next: number): void => {
|
|
9622
|
-
railWidthChoice = next;
|
|
9623
|
-
if (config.persistState === false) return;
|
|
9624
|
-
try {
|
|
9625
|
-
window.localStorage.setItem(`${currentKeyPrefix()}rail-width`, String(next));
|
|
9626
|
-
} catch {
|
|
9627
|
-
/* blocked storage: the in-memory value above is the fallback */
|
|
9628
|
-
}
|
|
9629
|
-
};
|
|
9630
|
-
/**
|
|
9631
|
-
* Overlay mode: the collapsed rail is a trigger in the conversation header
|
|
9632
|
-
* plus a floating host, so there is no icon column and no mounted view at
|
|
9633
|
-
* rest. `collapsed` then means "not pinned".
|
|
9634
|
-
*/
|
|
9635
|
-
const railOverlayMode = (): boolean =>
|
|
9636
|
-
railCollapsible() &&
|
|
9637
|
-
config.features?.history?.rail?.collapsedBehavior === "overlay";
|
|
9638
|
-
/** True while the view is mounted in the floating host, not a rail column. */
|
|
9639
|
-
let railOverlayOpen = false;
|
|
9640
|
-
|
|
9641
|
-
/** Decorative image for a config-supplied rail icon or brand URL. */
|
|
9642
|
-
const railIconImage = (src: string): HTMLImageElement => {
|
|
9643
|
-
const image = createElement("img");
|
|
9644
|
-
image.src = src;
|
|
9645
|
-
image.alt = "";
|
|
9646
|
-
image.setAttribute("aria-hidden", "true");
|
|
9647
|
-
return image;
|
|
9648
|
-
};
|
|
9649
|
-
|
|
9650
|
-
/**
|
|
9651
|
-
* One brand declaration, resolved here where the lucide registry lives, into
|
|
9652
|
-
* the callback both rail placements call: the expanded heading and the
|
|
9653
|
-
* collapsed toggle's rest face. Precedence render > iconUrl > icon; the
|
|
9654
|
-
* icon cases resolve once (so an unknown name warns once) and each caller
|
|
9655
|
-
* gets its own copy, since both faces can be in the DOM at once.
|
|
9656
|
-
*/
|
|
9657
|
-
const railBrandNode = ():
|
|
9658
|
-
| ((collapsed: boolean) => Element | null)
|
|
9659
|
-
| undefined => {
|
|
9660
|
-
const brand = config.features?.history?.rail?.brand;
|
|
9661
|
-
if (!brand) return undefined;
|
|
9662
|
-
let warned = false;
|
|
9663
|
-
let mark: Element | null | undefined;
|
|
9664
|
-
return (collapsed) => {
|
|
9665
|
-
if (brand.render) {
|
|
9666
|
-
try {
|
|
9667
|
-
return brand.render({ collapsed }) ?? null;
|
|
9668
|
-
} catch (error) {
|
|
9669
|
-
if (!warned) {
|
|
9670
|
-
warned = true;
|
|
9671
|
-
console.warn("[persona] history rail brand threw", error);
|
|
9672
|
-
}
|
|
9673
|
-
return null;
|
|
9674
|
-
}
|
|
9675
|
-
}
|
|
9676
|
-
if (mark === undefined) {
|
|
9677
|
-
if (brand.iconUrl) mark = railIconImage(brand.iconUrl);
|
|
9678
|
-
else mark = brand.icon ? renderLucideIcon(brand.icon, 20) : null;
|
|
9679
|
-
}
|
|
9680
|
-
return mark ? (mark.cloneNode(true) as Element) : null;
|
|
9681
|
-
};
|
|
9682
|
-
};
|
|
9683
|
-
|
|
9684
|
-
/**
|
|
9685
|
-
* Config nav sections, normalized for the size-capped chunk: icon precedence
|
|
9686
|
-
* (renderIcon > iconUrl > icon) collapses to one memoized thunk resolved
|
|
9687
|
-
* here, where the lucide registry already lives, and every host callback gets
|
|
9688
|
-
* a warn-once-per-section guard.
|
|
9689
|
-
*/
|
|
9690
|
-
const configNavSections = (): HistoryRailSection[] =>
|
|
9691
|
-
config.features?.history?.rail?.sections?.map((section) => {
|
|
9692
|
-
let warned = false;
|
|
9693
|
-
const warn = (error: unknown): void => {
|
|
9694
|
-
if (warned) return;
|
|
9695
|
-
warned = true;
|
|
9696
|
-
console.warn("[persona] history rail section threw", section.id, error);
|
|
9697
|
-
};
|
|
9698
|
-
return {
|
|
9699
|
-
id: section.id,
|
|
9700
|
-
title: section.title,
|
|
9701
|
-
placement: section.placement ?? "above-conversations",
|
|
9702
|
-
items: section.items.map((item) => {
|
|
9703
|
-
// Built on first paint and cached: an unknown lucide name warns once,
|
|
9704
|
-
// and a presentation flip reuses the node it made.
|
|
9705
|
-
let node: Element | null | undefined;
|
|
9706
|
-
return {
|
|
9707
|
-
id: item.id,
|
|
9708
|
-
label: item.label,
|
|
9709
|
-
badge: item.badge,
|
|
9710
|
-
iconNode: (): Element | null => {
|
|
9711
|
-
if (node !== undefined) return node;
|
|
9712
|
-
try {
|
|
9713
|
-
if (item.renderIcon) node = item.renderIcon() ?? null;
|
|
9714
|
-
else if (item.iconUrl) node = railIconImage(item.iconUrl);
|
|
9715
|
-
else node = item.icon ? renderLucideIcon(item.icon, 20) : null;
|
|
9716
|
-
} catch (error) {
|
|
9717
|
-
warn(error);
|
|
9718
|
-
node = null;
|
|
9719
|
-
}
|
|
9720
|
-
return node;
|
|
9721
|
-
},
|
|
9722
|
-
onSelect: () => {
|
|
9723
|
-
try {
|
|
9724
|
-
item.onSelect();
|
|
9725
|
-
} catch (error) {
|
|
9726
|
-
warn(error);
|
|
9727
|
-
}
|
|
9728
|
-
},
|
|
9729
|
-
};
|
|
9730
|
-
}),
|
|
9731
|
-
};
|
|
9732
|
-
}) ?? [];
|
|
9733
|
-
|
|
9734
|
-
/** One array for the chunk: config sections first in each placement bucket. */
|
|
9735
|
-
const railNavSections = (
|
|
9736
|
-
pluginSections: HistoryRailSection[]
|
|
9737
|
-
): HistoryRailSection[] | undefined => {
|
|
9738
|
-
const sections = configNavSections();
|
|
9739
|
-
for (const section of pluginSections) {
|
|
9740
|
-
// Config owns the id space; a colliding plugin section is dropped.
|
|
9741
|
-
if (sections.some((existing) => existing.id === section.id)) {
|
|
9742
|
-
console.warn("[persona] duplicate history rail section id", section.id);
|
|
9743
|
-
} else sections.push(section);
|
|
9744
|
-
}
|
|
9745
|
-
return sections.length ? sections : undefined;
|
|
9746
|
-
};
|
|
9747
|
-
|
|
9748
|
-
/** Resolved once per widget: storage, else the configured default. */
|
|
9749
|
-
let railCollapsed: boolean | null = null;
|
|
9750
|
-
|
|
9751
|
-
const railCollapseKey = (): string => `${currentKeyPrefix()}rail-collapsed`;
|
|
9752
|
-
|
|
9753
|
-
// localStorage access throws (not just fails) in Safari private mode and
|
|
9754
|
-
// partitioned iframes, so the resolved value is also the in-memory fallback.
|
|
9755
|
-
const isRailCollapsed = (): boolean => {
|
|
9756
|
-
if (railCollapsed === null) {
|
|
9757
|
-
railCollapsed = config.features?.history?.rail?.defaultCollapsed === true;
|
|
9758
|
-
if (config.persistState !== false) {
|
|
9759
|
-
try {
|
|
9760
|
-
const stored = window.localStorage.getItem(railCollapseKey());
|
|
9761
|
-
if (stored) railCollapsed = stored === "1";
|
|
9762
|
-
} catch {
|
|
9763
|
-
/* blocked storage: the resolved default stands */
|
|
9764
|
-
}
|
|
9765
|
-
}
|
|
9766
|
-
}
|
|
9767
|
-
return railCollapsed;
|
|
9768
|
-
};
|
|
9769
|
-
|
|
9770
|
-
const setRailCollapsed = (next: boolean): void => {
|
|
9771
|
-
railCollapsed = next;
|
|
9772
|
-
if (config.persistState === false) return;
|
|
9773
|
-
try {
|
|
9774
|
-
window.localStorage.setItem(railCollapseKey(), next ? "1" : "0");
|
|
9775
|
-
} catch {
|
|
9776
|
-
/* blocked storage: the in-memory value above is the fallback */
|
|
9777
|
-
}
|
|
9778
|
-
};
|
|
9779
|
-
|
|
9780
|
-
/**
|
|
9781
|
-
* Collapsed only ever applies to a collapsible rail presentation, and never
|
|
9782
|
-
* in overlay mode, where a mounted rail is always the expanded one.
|
|
9783
|
-
*/
|
|
9784
|
-
const railShowsCollapsed = (): boolean =>
|
|
9785
|
-
historyPresentation === "rail" &&
|
|
9786
|
-
railCollapsible() &&
|
|
9787
|
-
!railOverlayMode() &&
|
|
9788
|
-
isRailCollapsed();
|
|
9789
|
-
|
|
9790
|
-
/** Overlay-mode counterpart, assigned with the overlay controller below. */
|
|
9791
|
-
let toggleRailPinned: () => void = () => {};
|
|
9792
|
-
|
|
9793
|
-
const toggleRailCollapsed = (): void => {
|
|
9794
|
-
// The rail's own toggle sits where the trigger does, so in overlay mode it
|
|
9795
|
-
// is that control: it pins a floating rail and unpins a docked one.
|
|
9796
|
-
if (railOverlayMode()) {
|
|
9797
|
-
toggleRailPinned();
|
|
9798
|
-
return;
|
|
9799
|
-
}
|
|
9800
|
-
setRailCollapsed(!isRailCollapsed());
|
|
9801
|
-
historySurface?.view.setCollapsed(railShowsCollapsed());
|
|
9802
|
-
applyRailChrome();
|
|
9803
|
-
// The transcript column resizes beside the anchor; a clamp would bounce it.
|
|
9804
|
-
repinAnchoredMessage();
|
|
9805
|
-
};
|
|
9806
|
-
|
|
9807
|
-
/**
|
|
9808
|
-
* One declaration, three artifacts: the binding below, the toggle's tooltip
|
|
9809
|
-
* hint chip, and its `aria-keyshortcuts`. Null when unset or unparseable.
|
|
9810
|
-
*/
|
|
9811
|
-
const railCollapseShortcut = (): {
|
|
9812
|
-
combo: string;
|
|
9813
|
-
hint: string;
|
|
9814
|
-
aria: string;
|
|
9815
|
-
} | null => {
|
|
9816
|
-
const combo = config.features?.history?.rail?.collapseShortcut;
|
|
9817
|
-
if (!combo || !parseCombo(combo)) return null;
|
|
9818
|
-
return { combo, hint: formatCombo(combo), aria: ariaCombo(combo) };
|
|
9819
|
-
};
|
|
9820
|
-
|
|
9821
8983
|
/**
|
|
9822
8984
|
* Every binding this instance owns, re-derived so a live `update()` of the
|
|
9823
8985
|
* config or the plugin list lands. Config registers before plugins, so a
|
|
@@ -9826,8 +8988,10 @@ export const createAgentExperience = (
|
|
|
9826
8988
|
const syncShortcuts = (): void => {
|
|
9827
8989
|
releaseShortcuts.forEach((release) => release());
|
|
9828
8990
|
releaseShortcuts = [];
|
|
9829
|
-
|
|
9830
|
-
|
|
8991
|
+
// The rail collapse combo belongs to the (lazy) history shell.
|
|
8992
|
+
const shell = historyShell;
|
|
8993
|
+
const shortcut = shell?.railCollapseShortcut();
|
|
8994
|
+
if (shell && shortcut) {
|
|
9831
8995
|
releaseShortcuts.push(
|
|
9832
8996
|
shortcuts.register({
|
|
9833
8997
|
id: "history-rail-collapse",
|
|
@@ -9841,10 +9005,12 @@ export const createAgentExperience = (
|
|
|
9841
9005
|
when: () =>
|
|
9842
9006
|
(historyVisible &&
|
|
9843
9007
|
historyPresentation === "rail" &&
|
|
9844
|
-
railCollapsible()) ||
|
|
9845
|
-
railTriggerApplies(),
|
|
9008
|
+
shell.railCollapsible()) ||
|
|
9009
|
+
shell.railTriggerApplies(),
|
|
9846
9010
|
run: () =>
|
|
9847
|
-
railOverlayMode()
|
|
9011
|
+
shell.railOverlayMode()
|
|
9012
|
+
? shell.toggleRailPinned()
|
|
9013
|
+
: shell.toggleRailCollapsed(),
|
|
9848
9014
|
})
|
|
9849
9015
|
);
|
|
9850
9016
|
}
|
|
@@ -9876,946 +9042,6 @@ export const createAgentExperience = (
|
|
|
9876
9042
|
};
|
|
9877
9043
|
syncShortcuts();
|
|
9878
9044
|
|
|
9879
|
-
// --- collapsed rail as a floating overlay --------------------------------
|
|
9880
|
-
//
|
|
9881
|
-
// Rest state is a trigger at the leading edge of the conversation header and
|
|
9882
|
-
// nothing else: no column, no mounted view, and the history chunk unloaded
|
|
9883
|
-
// until a hover warms it. The pointer entering floats the expanded rail over
|
|
9884
|
-
// the conversation; clicking pins it back into the full-height column.
|
|
9885
|
-
|
|
9886
|
-
/** Grace before a pointer that left both surfaces dismisses the rail. */
|
|
9887
|
-
const RAIL_OVERLAY_GRACE_MS = 300;
|
|
9888
|
-
/**
|
|
9889
|
-
* Gap from the trigger, the docked edge and the bottom. A var reference, not
|
|
9890
|
-
* a number, so a live theme update reaches an open overlay unaided.
|
|
9891
|
-
*/
|
|
9892
|
-
const RAIL_OVERLAY_MARGIN = "var(--persona-history-overlay-margin,8px)";
|
|
9893
|
-
|
|
9894
|
-
let railTriggerButton: HTMLButtonElement | null = null;
|
|
9895
|
-
let railTriggerWrapper: HTMLElement | null = null;
|
|
9896
|
-
/** Docking opens awaiting the history chunk; the trigger hides for them. */
|
|
9897
|
-
let railPinPendingOpens = 0;
|
|
9898
|
-
let railOverlayHost: HTMLElement | null = null;
|
|
9899
|
-
let railGraceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
9900
|
-
/** Pointer is over the trigger or over the floating rail. */
|
|
9901
|
-
let railPointerInside = false;
|
|
9902
|
-
/**
|
|
9903
|
-
* Uncovering the trigger re-enters it with no pointer movement at all, which
|
|
9904
|
-
* would undo the dismissal that just uncovered it. That synthetic enter
|
|
9905
|
-
* arrives within a frame or two of the unmount.
|
|
9906
|
-
*/
|
|
9907
|
-
let railUncoveredUntil = 0;
|
|
9908
|
-
|
|
9909
|
-
/** Touch has no hover to open with, so it taps the overlay open instead. */
|
|
9910
|
-
const coarsePointer = (): boolean =>
|
|
9911
|
-
window.matchMedia?.("(pointer: coarse)").matches === true;
|
|
9912
|
-
|
|
9913
|
-
/** The trigger stands in for the collapsed rail, so it needs a rail width. */
|
|
9914
|
-
const railTriggerApplies = (): boolean =>
|
|
9915
|
-
historyAvailable() &&
|
|
9916
|
-
railOverlayMode() &&
|
|
9917
|
-
(historyPresentation ?? resolveHistoryPresentation()) === "rail";
|
|
9918
|
-
|
|
9919
|
-
/** Docked in its own column rather than floating over the conversation. */
|
|
9920
|
-
const railPinned = (): boolean => historyVisible && !railOverlayOpen;
|
|
9921
|
-
|
|
9922
|
-
const cancelRailGrace = (): void => {
|
|
9923
|
-
if (railGraceTimer !== null) clearTimeout(railGraceTimer);
|
|
9924
|
-
railGraceTimer = null;
|
|
9925
|
-
};
|
|
9926
|
-
|
|
9927
|
-
/** Keyboard focus warms without opening; Enter and Space commit. */
|
|
9928
|
-
const warmHistoryChunk = (): void => {
|
|
9929
|
-
// A failed load just leaves the overlay closed; the loader retries later.
|
|
9930
|
-
void loadHistoryView().catch(() => {});
|
|
9931
|
-
};
|
|
9932
|
-
|
|
9933
|
-
/** Leaving both surfaces dismisses, but only after a grace to come back in. */
|
|
9934
|
-
const scheduleRailOverlayClose = (): void => {
|
|
9935
|
-
cancelRailGrace();
|
|
9936
|
-
if (!railOverlayOpen) return;
|
|
9937
|
-
railGraceTimer = setTimeout(() => {
|
|
9938
|
-
railGraceTimer = null;
|
|
9939
|
-
if (railPointerInside || !railOverlayOpen) return;
|
|
9940
|
-
// A keyboard visitor inside the floating rail never loses it to a stray
|
|
9941
|
-
// pointer leaving the widget.
|
|
9942
|
-
if (railOverlayHost?.contains(document.activeElement)) return;
|
|
9943
|
-
// A pointer dismissal moves focus no more than a pointer open does.
|
|
9944
|
-
closeHistory({ restoreFocus: false });
|
|
9945
|
-
}, RAIL_OVERLAY_GRACE_MS);
|
|
9946
|
-
};
|
|
9947
|
-
|
|
9948
|
-
/**
|
|
9949
|
-
* Hover keep-alive is geometric, not element-based: a pointer travelling
|
|
9950
|
-
* from the trigger to the rail crosses the conversation header, which is
|
|
9951
|
-
* neither. The safe zone is the trigger, the rail, and the bridge between
|
|
9952
|
-
* them: the rail's own horizontal extent, from the trigger's row down to the
|
|
9953
|
-
* rail's top edge. Derived from the live rects, so a right-docked rail needs
|
|
9954
|
-
* no separate case.
|
|
9955
|
-
*/
|
|
9956
|
-
const inRailSafeZone = (x: number, y: number): boolean => {
|
|
9957
|
-
const rail = railOverlayHost?.getBoundingClientRect();
|
|
9958
|
-
if (!rail) return false;
|
|
9959
|
-
const row = railTriggerButton?.getBoundingClientRect();
|
|
9960
|
-
// The rail and the bridge are one band: the rail's own width, from the
|
|
9961
|
-
// trigger's row down to the rail's bottom. The trigger is its own rect.
|
|
9962
|
-
const top = row ? Math.min(row.top, rail.top) : rail.top;
|
|
9963
|
-
return (
|
|
9964
|
-
(x >= rail.left && x <= rail.right && y >= top && y <= rail.bottom) ||
|
|
9965
|
-
(!!row && x >= row.left && x <= row.right && y >= row.top && y <= row.bottom)
|
|
9966
|
-
);
|
|
9967
|
-
};
|
|
9968
|
-
|
|
9969
|
-
const handleRailPointerMove = (event: PointerEvent): void => {
|
|
9970
|
-
if (!railOverlayOpen) return;
|
|
9971
|
-
railPointerInside = inRailSafeZone(event.clientX, event.clientY);
|
|
9972
|
-
if (railPointerInside) cancelRailGrace();
|
|
9973
|
-
// Re-arming on every outside move would restart the countdown forever.
|
|
9974
|
-
else if (railGraceTimer === null) scheduleRailOverlayClose();
|
|
9975
|
-
};
|
|
9976
|
-
|
|
9977
|
-
/**
|
|
9978
|
-
* No dwell: the rail answers the pointer the moment it arrives, as fast as
|
|
9979
|
-
* the chunk allows (already loaded, that is the same frame). The 300ms leave
|
|
9980
|
-
* grace is what an accidental pass over the trigger costs.
|
|
9981
|
-
*/
|
|
9982
|
-
const openRailOverlay = (opts?: { keyboard?: boolean }): void => {
|
|
9983
|
-
if (historyVisible || !railTriggerApplies()) return;
|
|
9984
|
-
railOverlayOpen = true;
|
|
9985
|
-
void openHistory({
|
|
9986
|
-
invoker: railTriggerButton,
|
|
9987
|
-
keyboard: opts?.keyboard === true,
|
|
9988
|
-
}).then(() => {
|
|
9989
|
-
// The chunk can resolve after the pointer left, or not resolve at all.
|
|
9990
|
-
if (!historyVisible) railOverlayOpen = false;
|
|
9991
|
-
else if (!railPointerInside && document.activeElement !== railTriggerButton) {
|
|
9992
|
-
closeHistory();
|
|
9993
|
-
}
|
|
9994
|
-
});
|
|
9995
|
-
};
|
|
9996
|
-
|
|
9997
|
-
/**
|
|
9998
|
-
* Floating, the rail's own toggle pins instead of collapsing, so it wears
|
|
9999
|
-
* the expand label; docked, it says collapse again.
|
|
10000
|
-
*/
|
|
10001
|
-
const syncRailToggleLabel = (): void => {
|
|
10002
|
-
// The mounted element, not the surface: the first mount happens inside the
|
|
10003
|
-
// surface constructor, before `historySurface` is assigned.
|
|
10004
|
-
const toggle = historyMountedElement?.querySelector(
|
|
10005
|
-
'[data-persona-history-focus="collapse"]'
|
|
10006
|
-
);
|
|
10007
|
-
if (!toggle) return;
|
|
10008
|
-
toggle.setAttribute(
|
|
10009
|
-
"aria-label",
|
|
10010
|
-
railOverlayOpen
|
|
10011
|
-
? historyShellCopy.expandLabel
|
|
10012
|
-
: historyShellCopy.collapseLabel
|
|
10013
|
-
);
|
|
10014
|
-
toggle.setAttribute("aria-expanded", railOverlayOpen ? "false" : "true");
|
|
10015
|
-
};
|
|
10016
|
-
|
|
10017
|
-
/**
|
|
10018
|
-
* Pin: the floating rail gives way to the full-height column, moving the
|
|
10019
|
-
* SAME view element when one is already open.
|
|
10020
|
-
*/
|
|
10021
|
-
const pinRail = (): void => {
|
|
10022
|
-
setRailCollapsed(false);
|
|
10023
|
-
const surface = historySurface;
|
|
10024
|
-
if (!railOverlayOpen || !surface) {
|
|
10025
|
-
void openHistory({ invoker: railTriggerButton });
|
|
10026
|
-
return;
|
|
10027
|
-
}
|
|
10028
|
-
const refocus = document.activeElement === railTriggerButton;
|
|
10029
|
-
railOverlayOpen = false;
|
|
10030
|
-
unmountHistoryHosts();
|
|
10031
|
-
// Detach before re-hosting, exactly as the panel/rail move does.
|
|
10032
|
-
surface.element.remove();
|
|
10033
|
-
mountHistoryElement(surface.element);
|
|
10034
|
-
syncRailToggleLabel();
|
|
10035
|
-
// The trigger stands down beside the rail's own toggle, so keyboard focus
|
|
10036
|
-
// has to follow the control there.
|
|
10037
|
-
if (refocus) focusHistoryEntry();
|
|
10038
|
-
repinAnchoredMessage();
|
|
10039
|
-
syncHistoryChromeImpl();
|
|
10040
|
-
};
|
|
10041
|
-
|
|
10042
|
-
/** Unpin: the column closes and the trigger takes the control back. */
|
|
10043
|
-
const unpinRail = (): void => {
|
|
10044
|
-
setRailCollapsed(true);
|
|
10045
|
-
closeHistory();
|
|
10046
|
-
};
|
|
10047
|
-
|
|
10048
|
-
toggleRailPinned = (): void => {
|
|
10049
|
-
if (!historyVisible || railOverlayOpen) pinRail();
|
|
10050
|
-
else unpinRail();
|
|
10051
|
-
};
|
|
10052
|
-
|
|
10053
|
-
/**
|
|
10054
|
-
* The sidebar glyph, plainly. `rail.brand` belongs to the icon column, which
|
|
10055
|
-
* has no other identity, and to the rail's own header; this control sits in
|
|
10056
|
-
* a conversation header that already carries the agent's.
|
|
10057
|
-
*/
|
|
10058
|
-
const buildRailTrigger = (): void => {
|
|
10059
|
-
const shortcut = railCollapseShortcut();
|
|
10060
|
-
const parts = createHeaderIconButton({
|
|
10061
|
-
ariaLabel: historyShellCopy.expandLabel,
|
|
10062
|
-
iconName: "panel-left",
|
|
10063
|
-
wrapperClassName:
|
|
10064
|
-
"persona-relative persona-inline-flex persona-items-center persona-justify-center",
|
|
10065
|
-
extraClassName: "persona-rail-trigger",
|
|
10066
|
-
// Hovering answers with the rail itself; a bubble would race the flyover.
|
|
10067
|
-
// The combo stays discoverable on the floating rail's own toggle.
|
|
10068
|
-
tooltip: false,
|
|
10069
|
-
attrs: {
|
|
10070
|
-
"data-persona-rail-trigger": "",
|
|
10071
|
-
"aria-controls": historyRegionId,
|
|
10072
|
-
...(shortcut ? { "aria-keyshortcuts": shortcut.aria } : {}),
|
|
10073
|
-
},
|
|
10074
|
-
});
|
|
10075
|
-
const button = parts.button;
|
|
10076
|
-
button.addEventListener("mouseenter", () => {
|
|
10077
|
-
railPointerInside = true;
|
|
10078
|
-
cancelRailGrace();
|
|
10079
|
-
if (coarsePointer() || Date.now() < railUncoveredUntil) return;
|
|
10080
|
-
openRailOverlay();
|
|
10081
|
-
});
|
|
10082
|
-
button.addEventListener("mouseleave", () => {
|
|
10083
|
-
railPointerInside = false;
|
|
10084
|
-
// A real departure ends the hold-off: the next enter is intent.
|
|
10085
|
-
railUncoveredUntil = 0;
|
|
10086
|
-
scheduleRailOverlayClose();
|
|
10087
|
-
});
|
|
10088
|
-
// Focus only warms: Enter and Space are how a keyboard visitor commits.
|
|
10089
|
-
button.addEventListener("focus", () => {
|
|
10090
|
-
if (!historyVisible) warmHistoryChunk();
|
|
10091
|
-
});
|
|
10092
|
-
button.addEventListener("click", (event) => {
|
|
10093
|
-
if (!historyAvailable()) return;
|
|
10094
|
-
// Touch taps the overlay open first and pins on a second tap. A pointer
|
|
10095
|
-
// that can hover is already looking at the rail, and Enter/Space (a click
|
|
10096
|
-
// with detail 0) is a commitment either way, so both pin outright.
|
|
10097
|
-
if (event.detail !== 0 && coarsePointer() && !historyVisible) {
|
|
10098
|
-
openRailOverlay();
|
|
10099
|
-
} else pinRail();
|
|
10100
|
-
});
|
|
10101
|
-
railTriggerButton = button;
|
|
10102
|
-
railTriggerWrapper = parts.wrapper;
|
|
10103
|
-
};
|
|
10104
|
-
|
|
10105
|
-
/**
|
|
10106
|
-
* The trigger is chrome, not surface state: it exists whenever a collapsed
|
|
10107
|
-
* overlay rail could be opened, and stands down only while the rail is
|
|
10108
|
-
* docked, where the rail's own header toggle is the same control.
|
|
10109
|
-
*/
|
|
10110
|
-
const syncRailOverlayTrigger = (): void => {
|
|
10111
|
-
// A header rebuild detaches it; a stale ref must not block recreation.
|
|
10112
|
-
if (railTriggerWrapper && !railTriggerWrapper.isConnected) {
|
|
10113
|
-
railTriggerWrapper = null;
|
|
10114
|
-
railTriggerButton = null;
|
|
10115
|
-
}
|
|
10116
|
-
if (!railTriggerApplies()) {
|
|
10117
|
-
railTriggerWrapper?.remove();
|
|
10118
|
-
railTriggerWrapper = null;
|
|
10119
|
-
railTriggerButton = null;
|
|
10120
|
-
return;
|
|
10121
|
-
}
|
|
10122
|
-
if (!railTriggerButton) buildRailTrigger();
|
|
10123
|
-
const wrapper = railTriggerWrapper;
|
|
10124
|
-
const button = railTriggerButton;
|
|
10125
|
-
if (!wrapper || !button) return;
|
|
10126
|
-
// Leading edge of the conversation header, mirrored for a right rail.
|
|
10127
|
-
const lead = railSide() !== "right" ? header.firstChild : null;
|
|
10128
|
-
if ((lead ?? header.lastChild) !== wrapper) header.insertBefore(wrapper, lead);
|
|
10129
|
-
wrapper.style.display =
|
|
10130
|
-
railPinned() || railPinPendingOpens > 0 ? "none" : "";
|
|
10131
|
-
button.setAttribute("aria-label", historyShellCopy.expandLabel);
|
|
10132
|
-
button.setAttribute("aria-expanded", historyVisible ? "true" : "false");
|
|
10133
|
-
// The floating rail hangs from this control, so a rebuild or a resize that
|
|
10134
|
-
// moved it re-anchors what is already open.
|
|
10135
|
-
if (railOverlayHost) applyRailChrome();
|
|
10136
|
-
};
|
|
10137
|
-
|
|
10138
|
-
/**
|
|
10139
|
-
* Click outside dismisses the floating rail. Portaled surfaces it opened
|
|
10140
|
-
* itself (row menus, confirmations) are not "outside" it.
|
|
10141
|
-
*/
|
|
10142
|
-
const handleRailOverlayPointerDown = (event: Event): void => {
|
|
10143
|
-
if (!railOverlayOpen) return;
|
|
10144
|
-
const target = event.target;
|
|
10145
|
-
if (!(target instanceof Node)) return;
|
|
10146
|
-
if (railOverlayHost?.contains(target) || railTriggerWrapper?.contains(target)) {
|
|
10147
|
-
return;
|
|
10148
|
-
}
|
|
10149
|
-
if (
|
|
10150
|
-
target instanceof Element &&
|
|
10151
|
-
target.closest('.persona-dropdown-menu,[role="alertdialog"]')
|
|
10152
|
-
) {
|
|
10153
|
-
return;
|
|
10154
|
-
}
|
|
10155
|
-
closeHistory({ restoreFocus: false });
|
|
10156
|
-
};
|
|
10157
|
-
document.addEventListener("pointerdown", handleRailOverlayPointerDown, true);
|
|
10158
|
-
destroyCallbacks.push(() => {
|
|
10159
|
-
document.removeEventListener("pointerdown", handleRailOverlayPointerDown, true);
|
|
10160
|
-
// Attached only while the rail floats; removing an unattached one is free.
|
|
10161
|
-
document.removeEventListener("pointermove", handleRailPointerMove);
|
|
10162
|
-
railResizeRelease?.();
|
|
10163
|
-
cancelRailGrace();
|
|
10164
|
-
});
|
|
10165
|
-
|
|
10166
|
-
// --- drag-resize of the docked rail --------------------------------------
|
|
10167
|
-
|
|
10168
|
-
let railResizeHandle: HTMLElement | null = null;
|
|
10169
|
-
/** Ends an in-flight drag: its listeners are on the document, not the handle. */
|
|
10170
|
-
let railResizeRelease: (() => void) | null = null;
|
|
10171
|
-
/** Arrow-key step, matching the reference sidebar's coarse nudge. */
|
|
10172
|
-
const RAIL_RESIZE_STEP = 16;
|
|
10173
|
-
|
|
10174
|
-
const commitRailWidth = (next: number): void => {
|
|
10175
|
-
setStoredRailWidth(clampRailWidth(next));
|
|
10176
|
-
applyRailChrome();
|
|
10177
|
-
// The transcript resized beside the anchor; a clamp would bounce it.
|
|
10178
|
-
repinAnchoredMessage();
|
|
10179
|
-
};
|
|
10180
|
-
|
|
10181
|
-
/** Mirrors the artifact split handle: pointer capture, document-level drag. */
|
|
10182
|
-
const buildRailResizeHandle = (): HTMLElement => {
|
|
10183
|
-
const handle = createElement("div", "persona-rail-resizer");
|
|
10184
|
-
handle.tabIndex = 0;
|
|
10185
|
-
handle.setAttribute("role", "separator");
|
|
10186
|
-
handle.setAttribute("aria-orientation", "vertical");
|
|
10187
|
-
handle.setAttribute("aria-valuemin", String(RAIL_MIN_WIDTH));
|
|
10188
|
-
handle.setAttribute("aria-valuemax", String(RAIL_MAX_WIDTH));
|
|
10189
|
-
|
|
10190
|
-
handle.addEventListener("pointerdown", (event) => {
|
|
10191
|
-
const host = railHost;
|
|
10192
|
-
if (!host || event.button !== 0) return;
|
|
10193
|
-
event.preventDefault();
|
|
10194
|
-
railResizeRelease?.();
|
|
10195
|
-
const startX = event.clientX;
|
|
10196
|
-
const startWidth = host.getBoundingClientRect().width || railWidth();
|
|
10197
|
-
// A leading rail widens as the pointer travels right; a trailing one
|
|
10198
|
-
// mirrors that.
|
|
10199
|
-
const direction = railSide() === "right" ? -1 : 1;
|
|
10200
|
-
let width = startWidth;
|
|
10201
|
-
const doc = mount.ownerDocument;
|
|
10202
|
-
// The collapse animation is a transition on this very basis; left on, it
|
|
10203
|
-
// would trail the pointer for the whole drag.
|
|
10204
|
-
host.style.transition = "none";
|
|
10205
|
-
const onMove = (move: PointerEvent): void => {
|
|
10206
|
-
width = clampRailWidth(startWidth + direction * (move.clientX - startX));
|
|
10207
|
-
// Straight onto the basis: a chrome pass per pointer move is waste.
|
|
10208
|
-
host.style.flex = `0 0 ${width}px`;
|
|
10209
|
-
handle.setAttribute("aria-valuenow", String(width));
|
|
10210
|
-
};
|
|
10211
|
-
const onUp = (): void => {
|
|
10212
|
-
railResizeRelease = null;
|
|
10213
|
-
doc.removeEventListener("pointermove", onMove);
|
|
10214
|
-
doc.removeEventListener("pointerup", onUp);
|
|
10215
|
-
doc.removeEventListener("pointercancel", onUp);
|
|
10216
|
-
host.style.removeProperty("transition");
|
|
10217
|
-
try {
|
|
10218
|
-
handle.releasePointerCapture(event.pointerId);
|
|
10219
|
-
} catch {
|
|
10220
|
-
/* the capture may already be gone */
|
|
10221
|
-
}
|
|
10222
|
-
commitRailWidth(width);
|
|
10223
|
-
};
|
|
10224
|
-
railResizeRelease = onUp;
|
|
10225
|
-
doc.addEventListener("pointermove", onMove);
|
|
10226
|
-
doc.addEventListener("pointerup", onUp);
|
|
10227
|
-
doc.addEventListener("pointercancel", onUp);
|
|
10228
|
-
try {
|
|
10229
|
-
handle.setPointerCapture(event.pointerId);
|
|
10230
|
-
} catch {
|
|
10231
|
-
/* pointer capture is an enhancement, not the mechanism */
|
|
10232
|
-
}
|
|
10233
|
-
});
|
|
10234
|
-
|
|
10235
|
-
handle.addEventListener("keydown", (event) => {
|
|
10236
|
-
// Arrows track the visual direction, so a trailing rail inverts them.
|
|
10237
|
-
const step = railSide() === "right" ? -RAIL_RESIZE_STEP : RAIL_RESIZE_STEP;
|
|
10238
|
-
const width = railWidth();
|
|
10239
|
-
const next =
|
|
10240
|
-
event.key === "ArrowRight"
|
|
10241
|
-
? width + step
|
|
10242
|
-
: event.key === "ArrowLeft"
|
|
10243
|
-
? width - step
|
|
10244
|
-
: event.key === "Home"
|
|
10245
|
-
? RAIL_MIN_WIDTH
|
|
10246
|
-
: event.key === "End"
|
|
10247
|
-
? RAIL_MAX_WIDTH
|
|
10248
|
-
: null;
|
|
10249
|
-
if (next === null) return;
|
|
10250
|
-
event.preventDefault();
|
|
10251
|
-
commitRailWidth(next);
|
|
10252
|
-
});
|
|
10253
|
-
return handle;
|
|
10254
|
-
};
|
|
10255
|
-
|
|
10256
|
-
/** Docked and expanded only: the floating rail and the icon column never resize. */
|
|
10257
|
-
const syncRailResizeHandle = (
|
|
10258
|
-
shell: HTMLElement,
|
|
10259
|
-
before: HTMLElement
|
|
10260
|
-
): void => {
|
|
10261
|
-
if (!railResizable() || railShowsCollapsed()) {
|
|
10262
|
-
railResizeRelease?.();
|
|
10263
|
-
railResizeHandle?.remove();
|
|
10264
|
-
return;
|
|
10265
|
-
}
|
|
10266
|
-
const handle = railResizeHandle ?? buildRailResizeHandle();
|
|
10267
|
-
railResizeHandle = handle;
|
|
10268
|
-
handle.setAttribute("aria-label", historyShellCopy.resizeLabel);
|
|
10269
|
-
handle.setAttribute("aria-valuenow", String(railWidth()));
|
|
10270
|
-
// Between the two, on the edge the divider already faces.
|
|
10271
|
-
if (handle.nextElementSibling !== before) shell.insertBefore(handle, before);
|
|
10272
|
-
};
|
|
10273
|
-
|
|
10274
|
-
/**
|
|
10275
|
-
* Rail geometry is config-derived, so it must be re-derivable: a live
|
|
10276
|
-
* `update()` of `rail.width` / `rail.side` lands here, not only at mount.
|
|
10277
|
-
*/
|
|
10278
|
-
applyRailChrome = (): void => {
|
|
10279
|
-
// The bar mirrors the docked edge, so the view hears about a side flip even
|
|
10280
|
-
// while it is presenting as a panel.
|
|
10281
|
-
historySurface?.view.setRailSide(railSide());
|
|
10282
|
-
const trailing = railSide() === "right";
|
|
10283
|
-
const overlay = railOverlayHost;
|
|
10284
|
-
if (overlay) {
|
|
10285
|
-
// Hangs from the trigger's row rather than the widget's top edge, so the
|
|
10286
|
-
// trigger stays visible and clickable above it. Measured, since a header
|
|
10287
|
-
// rebuild or a resize can move the trigger.
|
|
10288
|
-
const below = railTriggerButton
|
|
10289
|
-
? railTriggerButton.getBoundingClientRect().bottom -
|
|
10290
|
-
container.getBoundingClientRect().top
|
|
10291
|
-
: 0;
|
|
10292
|
-
overlay.style.top = `calc(${Math.max(0, Math.round(below))}px + ${RAIL_OVERLAY_MARGIN})`;
|
|
10293
|
-
overlay.style.width = `${railWidth()}px`;
|
|
10294
|
-
overlay.style.left = trailing ? "" : RAIL_OVERLAY_MARGIN;
|
|
10295
|
-
overlay.style.right = trailing ? RAIL_OVERLAY_MARGIN : "";
|
|
10296
|
-
}
|
|
10297
|
-
const host = railHost;
|
|
10298
|
-
const shell = railShell;
|
|
10299
|
-
const column = railColumn;
|
|
10300
|
-
if (!host || !shell || !column) return;
|
|
10301
|
-
host.style.flex = `0 0 ${
|
|
10302
|
-
railShowsCollapsed() ? RAIL_COLLAPSED_WIDTH : railWidth()
|
|
10303
|
-
}px`;
|
|
10304
|
-
// The divider always faces the conversation, whichever edge the rail took.
|
|
10305
|
-
const divider = "1px solid var(--persona-divider,#e5e7eb)";
|
|
10306
|
-
host.style.borderRight = trailing ? "" : divider;
|
|
10307
|
-
host.style.borderLeft = trailing ? divider : "";
|
|
10308
|
-
const leading = trailing ? column : host;
|
|
10309
|
-
// Reorder only on an actual side flip: re-parenting blurs what it moves.
|
|
10310
|
-
if (shell.firstElementChild !== leading) {
|
|
10311
|
-
shell.append(leading, trailing ? host : column);
|
|
10312
|
-
}
|
|
10313
|
-
syncRailResizeHandle(shell, trailing ? host : column);
|
|
10314
|
-
};
|
|
10315
|
-
|
|
10316
|
-
/**
|
|
10317
|
-
* Rail is a shell-owned navigation column running the FULL widget height
|
|
10318
|
-
* beside a still-operable conversation: the shell header, `body`, the
|
|
10319
|
-
* in-container composer and any top-right action wrappers move into a column
|
|
10320
|
-
* next to the host, and the shell takes the header's old container slot.
|
|
10321
|
-
*/
|
|
10322
|
-
const mountRailHost = (element: HTMLElement): void => {
|
|
10323
|
-
// A pending docking open already built the shell around a placeholder;
|
|
10324
|
-
// the arriving view takes its slot with no reflow around it.
|
|
10325
|
-
if (railPlaceholder && railHost) {
|
|
10326
|
-
railPlaceholder.replaceWith(element);
|
|
10327
|
-
railPlaceholder = null;
|
|
10328
|
-
applyRailChrome();
|
|
10329
|
-
return;
|
|
10330
|
-
}
|
|
10331
|
-
const shell = createElement("div", "persona-history-rail-shell");
|
|
10332
|
-
shell.style.cssText = "display:flex;flex-direction:row;flex:1 1 auto;min-height:0";
|
|
10333
|
-
const column = createElement("div", "persona-history-rail-conversation");
|
|
10334
|
-
// position: top-right close/clear wrappers anchor here instead of the
|
|
10335
|
-
// container, or they would float over the rail.
|
|
10336
|
-
column.style.cssText =
|
|
10337
|
-
"display:flex;flex-direction:column;flex:1 1 auto;min-width:0;min-height:0;position:relative";
|
|
10338
|
-
// The collapse transition is a rule in the history chunk's stylesheet: an
|
|
10339
|
-
// inline one could not carry the reduced-motion query.
|
|
10340
|
-
const host = createElement("div", "persona-history-rail-host");
|
|
10341
|
-
host.style.cssText = "display:flex;min-height:0;overflow:hidden";
|
|
10342
|
-
|
|
10343
|
-
container.insertBefore(shell, header.parentNode === container ? header : body);
|
|
10344
|
-
for (const node of railBorrowed()) {
|
|
10345
|
-
if (node?.parentNode === container) column.appendChild(node);
|
|
10346
|
-
}
|
|
10347
|
-
shell.append(host, column);
|
|
10348
|
-
host.appendChild(element);
|
|
10349
|
-
railShell = shell;
|
|
10350
|
-
railHost = host;
|
|
10351
|
-
railColumn = column;
|
|
10352
|
-
applyRailChrome();
|
|
10353
|
-
};
|
|
10354
|
-
|
|
10355
|
-
/**
|
|
10356
|
-
* Space reservation for a docking open that still awaits the chunk: the full
|
|
10357
|
-
* rail shell mounts around an empty stand-in at the final width, so the
|
|
10358
|
-
* conversation and its header paint at their docked geometry from the first
|
|
10359
|
-
* frame instead of being pushed over when the view lands.
|
|
10360
|
-
*/
|
|
10361
|
-
const mountRailPlaceholder = (): void => {
|
|
10362
|
-
if (railShell) return;
|
|
10363
|
-
const placeholder = createElement(
|
|
10364
|
-
"div",
|
|
10365
|
-
"persona-history-rail-placeholder"
|
|
10366
|
-
);
|
|
10367
|
-
placeholder.style.cssText = "flex:1 1 auto;min-height:0";
|
|
10368
|
-
railPlaceholder = placeholder;
|
|
10369
|
-
mountRailHost(placeholder);
|
|
10370
|
-
repinAnchoredMessage();
|
|
10371
|
-
};
|
|
10372
|
-
|
|
10373
|
-
/** Hands the reserved column back (failed or re-presented open). */
|
|
10374
|
-
const clearRailPlaceholder = (): void => {
|
|
10375
|
-
if (!railPlaceholder) return;
|
|
10376
|
-
unmountHistoryHosts();
|
|
10377
|
-
repinAnchoredMessage();
|
|
10378
|
-
};
|
|
10379
|
-
|
|
10380
|
-
/**
|
|
10381
|
-
* Floating host for the collapsed overlay rail: the expanded view elevated
|
|
10382
|
-
* over a conversation that keeps its own layout, so nothing is borrowed and
|
|
10383
|
-
* nothing reflows around it.
|
|
10384
|
-
*/
|
|
10385
|
-
const mountRailOverlayHost = (element: HTMLElement): void => {
|
|
10386
|
-
const host = createElement("div", "persona-history-rail-overlay");
|
|
10387
|
-
// Every themeable value is a var() reference with its default in the
|
|
10388
|
-
// fallback, so an unset token costs nothing and a live one lands at once.
|
|
10389
|
-
host.style.cssText =
|
|
10390
|
-
`position:absolute;bottom:${RAIL_OVERLAY_MARGIN};` +
|
|
10391
|
-
`display:flex;overflow:hidden;z-index:${PORTALED_OVERLAY_Z_INDEX - 1};` +
|
|
10392
|
-
"border-radius:var(--persona-history-overlay-radius,16px);" +
|
|
10393
|
-
"background:var(--persona-history-overlay-bg,var(--persona-container,#f7f7f8));" +
|
|
10394
|
-
"box-shadow:var(--persona-history-overlay-shadow,0 12px 40px rgba(0,0,0,0.25))";
|
|
10395
|
-
// The conversation column already relies on a positioned container.
|
|
10396
|
-
container.style.position = "relative";
|
|
10397
|
-
container.appendChild(host);
|
|
10398
|
-
host.appendChild(element);
|
|
10399
|
-
// The safe zone spans nodes the rail does not own, so hover is tracked by
|
|
10400
|
-
// position for as long as it is open.
|
|
10401
|
-
document.addEventListener("pointermove", handleRailPointerMove);
|
|
10402
|
-
railOverlayHost = host;
|
|
10403
|
-
applyRailChrome();
|
|
10404
|
-
syncRailToggleLabel();
|
|
10405
|
-
};
|
|
10406
|
-
|
|
10407
|
-
const unmountHistoryHosts = (): void => {
|
|
10408
|
-
restorePanelHost?.();
|
|
10409
|
-
if (railOverlayHost) {
|
|
10410
|
-
// Dismissed under the pointer: hold the hover off until the synthetic
|
|
10411
|
-
// enter from uncovering the trigger has passed.
|
|
10412
|
-
if (railPointerInside) railUncoveredUntil = Date.now() + 150;
|
|
10413
|
-
document.removeEventListener("pointermove", handleRailPointerMove);
|
|
10414
|
-
railOverlayHost.remove();
|
|
10415
|
-
railOverlayHost = null;
|
|
10416
|
-
}
|
|
10417
|
-
const shell = railShell;
|
|
10418
|
-
if (shell) {
|
|
10419
|
-
// A drag in flight owns document listeners the host is about to lose.
|
|
10420
|
-
railResizeRelease?.();
|
|
10421
|
-
// Live bindings, in panel order, before the slot the shell took. A header
|
|
10422
|
-
// restored first also takes its inline action wrappers back with it, so
|
|
10423
|
-
// the `contains` check skips them.
|
|
10424
|
-
for (const node of railBorrowed()) {
|
|
10425
|
-
if (node && shell.contains(node)) container.insertBefore(node, shell);
|
|
10426
|
-
}
|
|
10427
|
-
shell.remove();
|
|
10428
|
-
railShell = null;
|
|
10429
|
-
railHost = null;
|
|
10430
|
-
railColumn = null;
|
|
10431
|
-
railPlaceholder = null;
|
|
10432
|
-
}
|
|
10433
|
-
};
|
|
10434
|
-
|
|
10435
|
-
/** Shell-owned chrome applied to whatever element arbitration produced. */
|
|
10436
|
-
const prepareHistoryElement = (element: HTMLElement): void => {
|
|
10437
|
-
historyMountedElement = element;
|
|
10438
|
-
element.id = historyRegionId;
|
|
10439
|
-
// Host-side flex sizing: the chunk sizes itself to 100%, the shell decides
|
|
10440
|
-
// how it participates in the column/row it was just dropped into.
|
|
10441
|
-
element.style.flex = "1 1 auto";
|
|
10442
|
-
element.style.minHeight = "0";
|
|
10443
|
-
};
|
|
10444
|
-
|
|
10445
|
-
/** The chunk owns its own classes/labels; the shell only picks the host. */
|
|
10446
|
-
const mountHistoryElement = (element: HTMLElement): void => {
|
|
10447
|
-
prepareHistoryElement(element);
|
|
10448
|
-
// The open that reserved the column can resolve to another host (a width
|
|
10449
|
-
// change mid-load re-presents as panel); the stand-in must not outlive it.
|
|
10450
|
-
if (historyPresentation !== "rail" || railOverlayOpen) {
|
|
10451
|
-
clearRailPlaceholder();
|
|
10452
|
-
}
|
|
10453
|
-
if (historyPresentation !== "rail") mountPanelHost(element);
|
|
10454
|
-
else if (railOverlayOpen) mountRailOverlayHost(element);
|
|
10455
|
-
else mountRailHost(element);
|
|
10456
|
-
};
|
|
10457
|
-
|
|
10458
|
-
/**
|
|
10459
|
-
* Live rail <-> panel transition. ONE view instance survives the move, so the
|
|
10460
|
-
* list, its fixed operation context, and any pending work are preserved. A
|
|
10461
|
-
* custom full view is re-invoked with the new presentation value.
|
|
10462
|
-
*/
|
|
10463
|
-
const syncHistoryPresentation = (): void => {
|
|
10464
|
-
// The trigger belongs to a rail-capable width, so it resolves here too, and
|
|
10465
|
-
// the header toggle stands down beside whatever it resolved to.
|
|
10466
|
-
syncRailOverlayTrigger();
|
|
10467
|
-
syncHistoryButton();
|
|
10468
|
-
if (!historyVisible || !historySurface) return;
|
|
10469
|
-
const next = resolveHistoryPresentation();
|
|
10470
|
-
if (next === historyPresentation) return;
|
|
10471
|
-
const focusKey = document.activeElement;
|
|
10472
|
-
// The bar's contents may be focused inside the shell header, not the view.
|
|
10473
|
-
const refocus =
|
|
10474
|
-
focusKey instanceof HTMLElement &&
|
|
10475
|
-
(historySurface.element.contains(focusKey) ||
|
|
10476
|
-
historyHeaderHost?.contains(focusKey) === true)
|
|
10477
|
-
? focusKey
|
|
10478
|
-
: null;
|
|
10479
|
-
unmountHistoryHosts();
|
|
10480
|
-
// Detach before re-hosting so a mid-move re-arbitration cannot re-insert
|
|
10481
|
-
// the surface into the host it is being moved out of.
|
|
10482
|
-
historySurface.element.remove();
|
|
10483
|
-
// Rail is always inline; the panel host re-externalizes on mount.
|
|
10484
|
-
historySurface.view.setHeaderPlacement("inline");
|
|
10485
|
-
historyPresentation = next;
|
|
10486
|
-
historySurface.view.setPresentation(next);
|
|
10487
|
-
// Panel always shows the whole list; returning to rail restores the state.
|
|
10488
|
-
historySurface.view.setCollapsed(railShowsCollapsed());
|
|
10489
|
-
historySurface.requestRender();
|
|
10490
|
-
if (!historySurface.element.isConnected) {
|
|
10491
|
-
mountHistoryElement(historySurface.element);
|
|
10492
|
-
}
|
|
10493
|
-
if (refocus?.isConnected) refocus.focus();
|
|
10494
|
-
else focusHistoryEntry();
|
|
10495
|
-
syncScrollToBottomButton();
|
|
10496
|
-
// A one-frame layout removal above the anchored message clamps scrollTop.
|
|
10497
|
-
repinAnchoredMessage();
|
|
10498
|
-
syncHistoryChromeImpl();
|
|
10499
|
-
};
|
|
10500
|
-
|
|
10501
|
-
// --- open / close --------------------------------------------------------
|
|
10502
|
-
|
|
10503
|
-
/** The bar lives in the shell header while it is hosted there. */
|
|
10504
|
-
const queryHistoryOwned = <T extends HTMLElement>(
|
|
10505
|
-
element: HTMLElement,
|
|
10506
|
-
selector: string
|
|
10507
|
-
): T | null =>
|
|
10508
|
-
historyHeaderHost?.querySelector<T>(selector) ??
|
|
10509
|
-
element.querySelector<T>(selector);
|
|
10510
|
-
|
|
10511
|
-
const focusHistoryEntry = (): void => {
|
|
10512
|
-
const element = historySurface?.element ?? historyMountedElement;
|
|
10513
|
-
if (!element) return;
|
|
10514
|
-
const close = queryHistoryOwned(
|
|
10515
|
-
element,
|
|
10516
|
-
// The rail's leading control is a collapse toggle, not a close.
|
|
10517
|
-
'[data-persona-history-focus="close"],[data-persona-history-focus="collapse"]'
|
|
10518
|
-
);
|
|
10519
|
-
if (close) {
|
|
10520
|
-
close.focus();
|
|
10521
|
-
return;
|
|
10522
|
-
}
|
|
10523
|
-
const heading =
|
|
10524
|
-
queryHistoryOwned<HTMLElement>(element, ".persona-history-title") ??
|
|
10525
|
-
// Custom contents may expose neither: the region itself is the fallback.
|
|
10526
|
-
element;
|
|
10527
|
-
heading.tabIndex = -1;
|
|
10528
|
-
heading.focus();
|
|
10529
|
-
};
|
|
10530
|
-
|
|
10531
|
-
const openHistory = async (opts?: {
|
|
10532
|
-
returnSurface?: HistoryReturnSurface;
|
|
10533
|
-
invoker?: HTMLElement | null;
|
|
10534
|
-
/** Rail only: an open with no keyboard behind it must not move focus. */
|
|
10535
|
-
keyboard?: boolean;
|
|
10536
|
-
/** `false` skips the entry focus entirely (programmatic/preview opens). */
|
|
10537
|
-
focus?: boolean;
|
|
10538
|
-
}): Promise<void> => {
|
|
10539
|
-
if (!historyAvailable() || historyVisible) return;
|
|
10540
|
-
const provider = historyProvider;
|
|
10541
|
-
if (!provider) return;
|
|
10542
|
-
// An unpinned overlay rail IS its trigger, so restoring that state must
|
|
10543
|
-
// render chrome only: no surface, and no chunk fetched for it.
|
|
10544
|
-
if (!railOverlayOpen && railTriggerApplies() && isRailCollapsed()) {
|
|
10545
|
-
syncRailOverlayTrigger();
|
|
10546
|
-
return;
|
|
10547
|
-
}
|
|
10548
|
-
// A reopen mid-exit finishes the outgoing teardown first: never two
|
|
10549
|
-
// surfaces, never a restore that lands after this one mounts.
|
|
10550
|
-
settleHistoryExit();
|
|
10551
|
-
const token = ++historyOpenToken;
|
|
10552
|
-
historyReturnSurface = opts?.returnSurface ?? "conversation";
|
|
10553
|
-
historyInvoker = opts?.invoker ?? historyButton;
|
|
10554
|
-
|
|
10555
|
-
// An open that will dock the rail takes its final geometry for the chunk
|
|
10556
|
-
// load: the trigger hides (painted, the glyph flashes at the header's
|
|
10557
|
-
// leading edge and then jumps to the mounted rail's own toggle) and the
|
|
10558
|
-
// column is reserved (unreserved, the conversation header paints at the
|
|
10559
|
-
// widget edge and is pushed over when the rail lands).
|
|
10560
|
-
const pinPending = railTriggerApplies() && !railOverlayOpen;
|
|
10561
|
-
if (pinPending) {
|
|
10562
|
-
railPinPendingOpens += 1;
|
|
10563
|
-
syncRailOverlayTrigger();
|
|
10564
|
-
mountRailPlaceholder();
|
|
10565
|
-
}
|
|
10566
|
-
let module: Awaited<ReturnType<typeof loadHistoryView>>;
|
|
10567
|
-
try {
|
|
10568
|
-
module = await loadHistoryView();
|
|
10569
|
-
} catch {
|
|
10570
|
-
// Lazy-chunk failure keeps the invoking surface interactive and retryable.
|
|
10571
|
-
announceHistory(historyShellCopy.openHistoryLabel);
|
|
10572
|
-
if (pinPending) {
|
|
10573
|
-
railPinPendingOpens -= 1;
|
|
10574
|
-
// A superseded open leaves the shared chrome to its successor.
|
|
10575
|
-
if (token === historyOpenToken) {
|
|
10576
|
-
clearRailPlaceholder();
|
|
10577
|
-
syncRailOverlayTrigger();
|
|
10578
|
-
}
|
|
10579
|
-
}
|
|
10580
|
-
return;
|
|
10581
|
-
}
|
|
10582
|
-
if (pinPending) railPinPendingOpens -= 1;
|
|
10583
|
-
if (token !== historyOpenToken || historyVisible) return;
|
|
10584
|
-
|
|
10585
|
-
// One scope for the whole opened view; every operation reuses it.
|
|
10586
|
-
historyOperationContext = { scope: historyScope() };
|
|
10587
|
-
historyPresentation = resolveHistoryPresentation();
|
|
10588
|
-
historyVisible = true;
|
|
10589
|
-
|
|
10590
|
-
// Built externally when there is a shell header to host it in; the panel
|
|
10591
|
-
// host re-decides on mount, so this only avoids a needless first insert.
|
|
10592
|
-
const initialHeaderPlacement: HistoryHeaderPlacement =
|
|
10593
|
-
historyPresentation === "panel" &&
|
|
10594
|
-
config.layout?.showHeader !== false &&
|
|
10595
|
-
header.style.display !== "none"
|
|
10596
|
-
? "external"
|
|
10597
|
-
: "inline";
|
|
10598
|
-
|
|
10599
|
-
const rowAvatar = config.features?.history?.rowAvatar;
|
|
10600
|
-
const shortcut = railCollapseShortcut();
|
|
10601
|
-
const collapseShortcutStrings = shortcut
|
|
10602
|
-
? { hint: shortcut.hint, aria: shortcut.aria }
|
|
10603
|
-
: null;
|
|
10604
|
-
|
|
10605
|
-
const baseViewOptions: HistoryViewOptions = {
|
|
10606
|
-
provider,
|
|
10607
|
-
context: historyOperationContext,
|
|
10608
|
-
targetId: activeHistoryTargetId(),
|
|
10609
|
-
presentation: historyPresentation,
|
|
10610
|
-
collapsible: railCollapsible(),
|
|
10611
|
-
collapsed: railShowsCollapsed(),
|
|
10612
|
-
railSide: railSide(),
|
|
10613
|
-
renderRailHeader: config.features?.history?.rail?.renderHeader,
|
|
10614
|
-
railBrand: railBrandNode(),
|
|
10615
|
-
onToggleCollapse: toggleRailCollapsed,
|
|
10616
|
-
// Formatted strings only: the size-capped chunk never imports shortcuts.
|
|
10617
|
-
...(collapseShortcutStrings
|
|
10618
|
-
? { collapseShortcut: collapseShortcutStrings }
|
|
10619
|
-
: {}),
|
|
10620
|
-
headerPlacement: initialHeaderPlacement,
|
|
10621
|
-
showScopeStatus: config.features?.history?.showScopeStatus !== false,
|
|
10622
|
-
showDelete: config.features?.history?.showDelete !== false,
|
|
10623
|
-
showDeleteAll: config.features?.history?.showDeleteAll !== false,
|
|
10624
|
-
...(config.features?.history?.listActions?.length
|
|
10625
|
-
? { listActions: config.features.history.listActions }
|
|
10626
|
-
: {}),
|
|
10627
|
-
// Rows borrow the launcher's IMAGE mark only: agentIconText carries the
|
|
10628
|
-
// merged 💬 default, which would put a placeholder glyph on every row.
|
|
10629
|
-
// No mark at all means text-only rows, the assistant-list default.
|
|
10630
|
-
rowAvatar:
|
|
10631
|
-
rowAvatar === false
|
|
10632
|
-
? false
|
|
10633
|
-
: typeof rowAvatar === "string"
|
|
10634
|
-
? rowAvatar
|
|
10635
|
-
: config.launcher?.iconUrl,
|
|
10636
|
-
activeConversationId: session.getActiveConversationId(),
|
|
10637
|
-
...(config.features?.history?.grouping
|
|
10638
|
-
? { grouping: config.features.history.grouping }
|
|
10639
|
-
: {}),
|
|
10640
|
-
...(config.features?.history?.copy
|
|
10641
|
-
? { copy: config.features.history.copy }
|
|
10642
|
-
: {}),
|
|
10643
|
-
...(config.features?.history?.pageSize !== undefined
|
|
10644
|
-
? { pageSize: config.features.history.pageSize }
|
|
10645
|
-
: {}),
|
|
10646
|
-
// The pending/error surface owns failures (optimistic open), so the
|
|
10647
|
-
// row must not double-report them.
|
|
10648
|
-
onSelect: (conversationId) =>
|
|
10649
|
-
openHistoryConversation(conversationId, { focusComposer: true }).catch(
|
|
10650
|
-
() => {}
|
|
10651
|
-
),
|
|
10652
|
-
onActiveConversationChange: (summary) =>
|
|
10653
|
-
setActiveConversationSummary(summary),
|
|
10654
|
-
// The chunk is size-capped, so it borrows the shell's tooltip module.
|
|
10655
|
-
attachTooltip,
|
|
10656
|
-
onStartNew: () => startNewConversation({ focusComposer: true }),
|
|
10657
|
-
onClose: () => closeHistory(),
|
|
10658
|
-
onRequestDeleteConversation: (conversationId) =>
|
|
10659
|
-
requestDeleteConversation(conversationId),
|
|
10660
|
-
onRequestClearHistory: () => requestClearConversationHistory(),
|
|
10661
|
-
...(provider.resetDevice
|
|
10662
|
-
? { onRequestResetIdentity: () => requestResetHistoryIdentity() }
|
|
10663
|
-
: {}),
|
|
10664
|
-
};
|
|
10665
|
-
|
|
10666
|
-
// Plugin hooks arbitrate around the default view; the shell keeps placement,
|
|
10667
|
-
// open/close, Escape, confirmations, announcements, and focus.
|
|
10668
|
-
historySurface = createHistoryRenderSurface({
|
|
10669
|
-
plugins,
|
|
10670
|
-
config,
|
|
10671
|
-
getPresentation: () => historyPresentation ?? "panel",
|
|
10672
|
-
getReturnSurface: () => historyReturnSurface,
|
|
10673
|
-
close: () => closeHistory(),
|
|
10674
|
-
createView: ({ slots, renderDom, onModelChange, railSections }) => {
|
|
10675
|
-
// Tracked before `historySurface` is assigned: the first mount happens
|
|
10676
|
-
// inside this constructor and already needs the handle.
|
|
10677
|
-
historyViewHandle = module.createHistoryView({
|
|
10678
|
-
...baseViewOptions,
|
|
10679
|
-
railSections: railNavSections(railSections),
|
|
10680
|
-
slots,
|
|
10681
|
-
renderDom,
|
|
10682
|
-
onModelChange,
|
|
10683
|
-
onAnnounce: announceHistory,
|
|
10684
|
-
});
|
|
10685
|
-
return historyViewHandle;
|
|
10686
|
-
},
|
|
10687
|
-
onElementChanged: (next, previous) => {
|
|
10688
|
-
if (!previous?.isConnected) return mountHistoryElement(next);
|
|
10689
|
-
prepareHistoryElement(next);
|
|
10690
|
-
previous.replaceWith(next);
|
|
10691
|
-
// Default <-> custom re-arbitration changes who owns the bar.
|
|
10692
|
-
enforcePanelHost();
|
|
10693
|
-
},
|
|
10694
|
-
});
|
|
10695
|
-
historySurface.view.setNewConversationRequired(
|
|
10696
|
-
historySessionState.recovery === "new_conversation_required"
|
|
10697
|
-
);
|
|
10698
|
-
// The panel makes the conversation inert behind it, so it always takes
|
|
10699
|
-
// focus. The rail leaves it operable: a pointer or programmatic open would
|
|
10700
|
-
// only leave a keyboard ring on the toggle, so it keeps focus where it is.
|
|
10701
|
-
// `focus: false` opts out entirely: the caller owns focus (preview replay).
|
|
10702
|
-
if (
|
|
10703
|
-
opts?.focus !== false &&
|
|
10704
|
-
(historyPresentation !== "rail" || opts?.keyboard === true)
|
|
10705
|
-
) {
|
|
10706
|
-
focusHistoryEntry();
|
|
10707
|
-
}
|
|
10708
|
-
syncScrollToBottomButton();
|
|
10709
|
-
repinAnchoredMessage();
|
|
10710
|
-
syncHistoryChromeImpl();
|
|
10711
|
-
eventBus.emit("history:opened", {
|
|
10712
|
-
presentation: historyPresentation,
|
|
10713
|
-
returnSurface: historyReturnSurface,
|
|
10714
|
-
timestamp: Date.now(),
|
|
10715
|
-
});
|
|
10716
|
-
};
|
|
10717
|
-
|
|
10718
|
-
/**
|
|
10719
|
-
* Close is: exit animation -> unmount and restore the hidden chrome ->
|
|
10720
|
-
* restore focus. The teardown half is deferred behind the view's `playExit()`
|
|
10721
|
-
* promise, so it is exposed here for whoever preempts it (a reopen, a widget
|
|
10722
|
-
* teardown). Idempotent, and a no-op while nothing is leaving.
|
|
10723
|
-
*/
|
|
10724
|
-
let settleHistoryExit: () => void = () => {};
|
|
10725
|
-
|
|
10726
|
-
const closeHistory = (opts?: { restoreFocus?: boolean }): void => {
|
|
10727
|
-
if (!historyVisible) return;
|
|
10728
|
-
historyOpenToken += 1;
|
|
10729
|
-
const returnSurface = historyReturnSurface;
|
|
10730
|
-
const invoker = historyInvoker;
|
|
10731
|
-
const surface = historySurface;
|
|
10732
|
-
const restoreInvokerFocus = opts?.restoreFocus !== false;
|
|
10733
|
-
// Flipped before the animation: a second close is a no-op and a reopen
|
|
10734
|
-
// mounts fresh rather than re-entering the surface that is leaving.
|
|
10735
|
-
historyVisible = false;
|
|
10736
|
-
|
|
10737
|
-
let done = false;
|
|
10738
|
-
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
10739
|
-
const finish = (): void => {
|
|
10740
|
-
if (done) return;
|
|
10741
|
-
done = true;
|
|
10742
|
-
settleHistoryExit = () => {};
|
|
10743
|
-
if (timer !== null) clearTimeout(timer);
|
|
10744
|
-
// The floating rail is torn down with the surface it hosted.
|
|
10745
|
-
railOverlayOpen = false;
|
|
10746
|
-
cancelRailGrace();
|
|
10747
|
-
// Before focus restoration below: the invoker lives in the shell header,
|
|
10748
|
-
// which is inert until this restores it.
|
|
10749
|
-
unmountHistoryHosts();
|
|
10750
|
-
// Dispose before destroy: cleanups belong to the render being torn down.
|
|
10751
|
-
surface?.dispose();
|
|
10752
|
-
surface?.view.destroy();
|
|
10753
|
-
surface?.element.remove();
|
|
10754
|
-
// A reopen that preempted this exit already owns the shell state.
|
|
10755
|
-
if (historySurface === surface) {
|
|
10756
|
-
historySurface = null;
|
|
10757
|
-
historyViewHandle = null;
|
|
10758
|
-
historyMountedElement = null;
|
|
10759
|
-
historyPresentation = null;
|
|
10760
|
-
historyOperationContext = null;
|
|
10761
|
-
historyInvoker = null;
|
|
10762
|
-
}
|
|
10763
|
-
syncScrollToBottomButton();
|
|
10764
|
-
// Removing a full-height host above the anchored message clamps scrollTop.
|
|
10765
|
-
repinAnchoredMessage();
|
|
10766
|
-
syncHistoryChromeImpl();
|
|
10767
|
-
if (restoreInvokerFocus) {
|
|
10768
|
-
const target = invoker ?? historyButton;
|
|
10769
|
-
if (target?.isConnected) target.focus();
|
|
10770
|
-
else maybeFocusInput();
|
|
10771
|
-
}
|
|
10772
|
-
eventBus.emit("history:closed", { returnSurface, timestamp: Date.now() });
|
|
10773
|
-
};
|
|
10774
|
-
|
|
10775
|
-
// Only the arbitrated default view animates: a plugin full view owns its
|
|
10776
|
-
// own element and never got an entrance either.
|
|
10777
|
-
const exit =
|
|
10778
|
-
surface && surface.element === surface.view.element
|
|
10779
|
-
? surface.view.playExit()
|
|
10780
|
-
: null;
|
|
10781
|
-
if (!exit) {
|
|
10782
|
-
finish();
|
|
10783
|
-
return;
|
|
10784
|
-
}
|
|
10785
|
-
settleHistoryExit = finish;
|
|
10786
|
-
// A cancelled or never-settling animation must never wedge the close.
|
|
10787
|
-
timer = setTimeout(finish, HISTORY_EXIT_TIMEOUT_MS);
|
|
10788
|
-
void exit.then(finish);
|
|
10789
|
-
};
|
|
10790
|
-
|
|
10791
|
-
/** Escape returns to the recorded invoking surface, like the back control. */
|
|
10792
|
-
const handleHistoryKeydown = (event: KeyboardEvent): void => {
|
|
10793
|
-
if (event.key !== "Escape" || !historyVisible || !historySurface) return;
|
|
10794
|
-
// Rail leaves the conversation operable: only its own focus closes it, and
|
|
10795
|
-
// the floating rail's scope includes the trigger it hangs from.
|
|
10796
|
-
if (historyPresentation === "rail") {
|
|
10797
|
-
const target = event.target;
|
|
10798
|
-
if (
|
|
10799
|
-
!(target instanceof Node) ||
|
|
10800
|
-
!(
|
|
10801
|
-
historySurface.element.contains(target) ||
|
|
10802
|
-
(railOverlayOpen && railTriggerWrapper?.contains(target) === true)
|
|
10803
|
-
)
|
|
10804
|
-
) {
|
|
10805
|
-
return;
|
|
10806
|
-
}
|
|
10807
|
-
}
|
|
10808
|
-
event.preventDefault();
|
|
10809
|
-
event.stopPropagation();
|
|
10810
|
-
closeHistory();
|
|
10811
|
-
};
|
|
10812
|
-
container.addEventListener("keydown", handleHistoryKeydown);
|
|
10813
|
-
destroyCallbacks.push(() =>
|
|
10814
|
-
container.removeEventListener("keydown", handleHistoryKeydown)
|
|
10815
|
-
);
|
|
10816
|
-
|
|
10817
|
-
// --- session operations --------------------------------------------------
|
|
10818
|
-
|
|
10819
9045
|
// ---- header title binding (layout.header.titleSource: "conversation") ----
|
|
10820
9046
|
// The history view reports the active conversation's list summary through
|
|
10821
9047
|
// onActiveConversationChange; the shell owns the fallback and the write, so
|
|
@@ -10874,696 +9100,26 @@ export const createAgentExperience = (
|
|
|
10874
9100
|
const activeId = session.getActiveConversationId();
|
|
10875
9101
|
if (!activeId) return;
|
|
10876
9102
|
if (actionId === "delete") {
|
|
10877
|
-
void requestDeleteConversation(activeId);
|
|
9103
|
+
void withHistoryShell((shell) => shell.requestDeleteConversation(activeId));
|
|
10878
9104
|
} else if (actionId === "star" && session.canUpdateConversations()) {
|
|
10879
|
-
void
|
|
10880
|
-
|
|
10881
|
-
|
|
10882
|
-
|
|
10883
|
-
|
|
10884
|
-
|
|
10885
|
-
// --- optimistic conversation open ---------------------------------------
|
|
10886
|
-
// Selection acknowledges within a frame: the composer gates immediately, the
|
|
10887
|
-
// stand-in takes the surface only if the fetch outlasts the show delay, and
|
|
10888
|
-
// the transcript hydrates when the fetch lands. The token invalidates a stale
|
|
10889
|
-
// resolution after a newer open, a new conversation, or a failure the visitor
|
|
10890
|
-
// navigated away from.
|
|
10891
|
-
let conversationOpenToken = 0;
|
|
10892
|
-
// Show delay: opens that land inside it swap straight from the welcome or the
|
|
10893
|
-
// previous conversation to the loaded transcript, so no skeleton ever flashes.
|
|
10894
|
-
const CONVERSATION_OPEN_TAKEOVER_DELAY_MS = 250;
|
|
10895
|
-
let conversationOpenTakeoverTimer: ReturnType<typeof setTimeout> | null = null;
|
|
10896
|
-
|
|
10897
|
-
const cancelConversationOpenTakeover = (): void => {
|
|
10898
|
-
if (conversationOpenTakeoverTimer) clearTimeout(conversationOpenTakeoverTimer);
|
|
10899
|
-
conversationOpenTakeoverTimer = null;
|
|
10900
|
-
};
|
|
10901
|
-
destroyCallbacks.push(cancelConversationOpenTakeover);
|
|
10902
|
-
|
|
10903
|
-
/** Hands the surface to the mounted stand-in. Idempotent by re-application. */
|
|
10904
|
-
const engageConversationOpenTakeover = (): void => {
|
|
10905
|
-
cancelConversationOpenTakeover();
|
|
10906
|
-
const element = conversationOpenPendingEl;
|
|
10907
|
-
if (!element) return;
|
|
10908
|
-
conversationOpenTakeover = true;
|
|
10909
|
-
element.hidden = false;
|
|
10910
|
-
messagesWrapper.style.display = "none";
|
|
10911
|
-
// The welcome is a sibling of the transcript, so hiding the wrapper alone
|
|
10912
|
-
// would leave it painted above the stand-in.
|
|
10913
|
-
updateWelcome();
|
|
10914
|
-
};
|
|
10915
|
-
|
|
10916
|
-
const clearConversationOpenPending = (): void => {
|
|
10917
|
-
conversationOpenToken += 1;
|
|
10918
|
-
cancelConversationOpenTakeover();
|
|
10919
|
-
if (!conversationOpenPendingEl) return;
|
|
10920
|
-
conversationOpenPendingEl.remove();
|
|
10921
|
-
conversationOpenPendingEl = null;
|
|
10922
|
-
conversationOpenTakeover = false;
|
|
10923
|
-
messagesWrapper.style.removeProperty("display");
|
|
10924
|
-
setHistoryHostInert(footer, false);
|
|
10925
|
-
// Restores the welcome when navigation lands back on an empty transcript.
|
|
10926
|
-
updateWelcome();
|
|
10927
|
-
};
|
|
10928
|
-
|
|
10929
|
-
/**
|
|
10930
|
-
* Commit fade for the reopened transcript. WAAPI, not a CSS transition: the
|
|
10931
|
-
* wrapper is the morph target, and post-render inline state is stripped by
|
|
10932
|
-
* every render inside the completion window.
|
|
10933
|
-
*/
|
|
10934
|
-
const fadeInTranscript = (): void => {
|
|
10935
|
-
try {
|
|
10936
|
-
if (window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches) {
|
|
10937
|
-
return;
|
|
10938
|
-
}
|
|
10939
|
-
messagesWrapper.animate([{ opacity: 0 }, { opacity: 1 }], {
|
|
10940
|
-
duration: 150,
|
|
10941
|
-
easing: "ease-out",
|
|
10942
|
-
});
|
|
10943
|
-
} catch {
|
|
10944
|
-
/* environments without WAAPI land the transcript without motion */
|
|
10945
|
-
}
|
|
10946
|
-
};
|
|
10947
|
-
|
|
10948
|
-
// The stand-in surfaces get the transcript's centered column from their
|
|
10949
|
-
// container classes, so the hydrated messages land exactly where they stood.
|
|
10950
|
-
const skeletonBubble = (width: string, trailing: boolean): HTMLElement =>
|
|
10951
|
-
createNode("div", {
|
|
10952
|
-
className: cx(
|
|
10953
|
-
"persona-conversation-loading-bubble",
|
|
10954
|
-
trailing && "persona-conversation-loading-bubble--trailing"
|
|
10955
|
-
),
|
|
10956
|
-
attrs: { "aria-hidden": "true" },
|
|
10957
|
-
style: { width },
|
|
10958
|
-
});
|
|
10959
|
-
|
|
10960
|
-
const buildConversationOpenSkeleton = (): HTMLElement =>
|
|
10961
|
-
createNode(
|
|
10962
|
-
"div",
|
|
10963
|
-
{ className: "persona-conversation-loading-body" },
|
|
10964
|
-
skeletonBubble("58%", false),
|
|
10965
|
-
skeletonBubble("72%", true),
|
|
10966
|
-
skeletonBubble("44%", false)
|
|
10967
|
-
);
|
|
10968
|
-
|
|
10969
|
-
const mountConversationOpenState = (element: HTMLElement): void => {
|
|
10970
|
-
conversationOpenPendingEl?.remove();
|
|
10971
|
-
conversationOpenPendingEl = element;
|
|
10972
|
-
// The takeover is deferred, so the stand-in mounts hidden; one that replaces
|
|
10973
|
-
// an already-engaged stand-in keeps the surface instead of blanking it.
|
|
10974
|
-
element.hidden = !conversationOpenTakeover;
|
|
10975
|
-
body.insertBefore(element, messagesWrapper);
|
|
10976
|
-
cancelConversationOpenTakeover();
|
|
10977
|
-
if (!conversationOpenTakeover) {
|
|
10978
|
-
conversationOpenTakeoverTimer = setTimeout(
|
|
10979
|
-
engageConversationOpenTakeover,
|
|
10980
|
-
CONVERSATION_OPEN_TAKEOVER_DELAY_MS
|
|
10981
|
-
);
|
|
10982
|
-
}
|
|
10983
|
-
// The composer must not send into the conversation being replaced. The
|
|
10984
|
-
// panel exit restores the footer after its animation, so the close path
|
|
10985
|
-
// re-asserts this gate (see the history:closed re-assert below).
|
|
10986
|
-
setHistoryHostInert(footer, true);
|
|
10987
|
-
};
|
|
10988
|
-
|
|
10989
|
-
const showConversationOpenPending = (): void => {
|
|
10990
|
-
// Same plugin -> config -> default chain as the streaming indicator. The
|
|
10991
|
-
// hook replaces the skeleton only; the container keeps the centered
|
|
10992
|
-
// column, the status role, and the composer gate.
|
|
10993
|
-
const context: LoadingIndicatorRenderContext = {
|
|
10994
|
-
config,
|
|
10995
|
-
streaming: false,
|
|
10996
|
-
location: "conversation-open",
|
|
10997
|
-
defaultRenderer: buildConversationOpenSkeleton,
|
|
10998
|
-
};
|
|
10999
|
-
let content: HTMLElement | null = null;
|
|
11000
|
-
const loadingPlugin = plugins.find((p) => p.renderLoadingIndicator);
|
|
11001
|
-
if (loadingPlugin?.renderLoadingIndicator) {
|
|
11002
|
-
content = loadingPlugin.renderLoadingIndicator(context);
|
|
11003
|
-
}
|
|
11004
|
-
if (content === null && config.loadingIndicator?.render) {
|
|
11005
|
-
content = config.loadingIndicator.render(context);
|
|
11006
|
-
}
|
|
11007
|
-
mountConversationOpenState(
|
|
11008
|
-
createNode(
|
|
11009
|
-
"div",
|
|
11010
|
-
{
|
|
11011
|
-
className: "persona-conversation-loading",
|
|
11012
|
-
attrs: {
|
|
11013
|
-
role: "status",
|
|
11014
|
-
"aria-label": historyShellCopy.openConversationLoadingLabel,
|
|
11015
|
-
},
|
|
11016
|
-
},
|
|
11017
|
-
content ?? buildConversationOpenSkeleton()
|
|
11018
|
-
)
|
|
11019
|
-
);
|
|
11020
|
-
};
|
|
11021
|
-
|
|
11022
|
-
const showConversationOpenError = (conversationId: string): void => {
|
|
11023
|
-
const retry = (): void => {
|
|
11024
|
-
void openHistoryConversation(conversationId, {
|
|
11025
|
-
focusComposer: true,
|
|
11026
|
-
}).catch(() => {});
|
|
11027
|
-
};
|
|
11028
|
-
const back = historyAvailable()
|
|
11029
|
-
? (): void => {
|
|
11030
|
-
clearConversationOpenPending();
|
|
11031
|
-
void openHistory();
|
|
11032
|
-
}
|
|
11033
|
-
: undefined;
|
|
11034
|
-
const actionButton = (label: string, onClick: () => void): HTMLElement => {
|
|
11035
|
-
const button = createNode("button", {
|
|
11036
|
-
className: "persona-conversation-loading-error-action",
|
|
11037
|
-
attrs: { type: "button" },
|
|
11038
|
-
text: label,
|
|
11039
|
-
});
|
|
11040
|
-
button.addEventListener("click", onClick);
|
|
11041
|
-
return button;
|
|
11042
|
-
};
|
|
11043
|
-
const buildDefaultBlock = (): HTMLElement =>
|
|
11044
|
-
createNode(
|
|
11045
|
-
"div",
|
|
11046
|
-
{ className: "persona-conversation-loading-error-body" },
|
|
11047
|
-
createNode("p", {
|
|
11048
|
-
className: "persona-conversation-loading-error-title",
|
|
11049
|
-
text: historyShellCopy.openConversationErrorTitle,
|
|
11050
|
-
}),
|
|
11051
|
-
createNode(
|
|
11052
|
-
"div",
|
|
11053
|
-
{ className: "persona-conversation-loading-error-actions" },
|
|
11054
|
-
actionButton(historyShellCopy.openConversationRetryLabel, retry),
|
|
11055
|
-
back
|
|
11056
|
-
? actionButton(historyShellCopy.openConversationBackLabel, back)
|
|
11057
|
-
: null
|
|
11058
|
-
)
|
|
11059
|
-
);
|
|
11060
|
-
// First non-null plugin hook wins (plugins are priority-sorted); a throw
|
|
11061
|
-
// is reported and skipped. The container keeps the centered column, the
|
|
11062
|
-
// alert role, and the composer gate around whatever the hook returns.
|
|
11063
|
-
const context: AgentWidgetRenderHistoryOpenErrorContext = Object.freeze({
|
|
11064
|
-
conversationId,
|
|
11065
|
-
config,
|
|
11066
|
-
copy: Object.freeze({
|
|
11067
|
-
title: historyShellCopy.openConversationErrorTitle,
|
|
11068
|
-
retryLabel: historyShellCopy.openConversationRetryLabel,
|
|
11069
|
-
backLabel: historyShellCopy.openConversationBackLabel,
|
|
11070
|
-
}),
|
|
11071
|
-
retry,
|
|
11072
|
-
...(back ? { back } : {}),
|
|
11073
|
-
defaultRenderer: buildDefaultBlock,
|
|
11074
|
-
});
|
|
11075
|
-
let content: HTMLElement | null = null;
|
|
11076
|
-
for (const plugin of plugins) {
|
|
11077
|
-
if (!plugin.renderHistoryOpenError) continue;
|
|
11078
|
-
try {
|
|
11079
|
-
content = plugin.renderHistoryOpenError(context);
|
|
11080
|
-
} catch (error) {
|
|
11081
|
-
console.warn("[persona] renderHistoryOpenError threw", error);
|
|
11082
|
-
content = null;
|
|
11083
|
-
}
|
|
11084
|
-
if (content) break;
|
|
11085
|
-
}
|
|
11086
|
-
mountConversationOpenState(
|
|
11087
|
-
createNode(
|
|
11088
|
-
"div",
|
|
11089
|
-
{
|
|
11090
|
-
className: "persona-conversation-loading-error",
|
|
11091
|
-
attrs: { role: "alert" },
|
|
11092
|
-
},
|
|
11093
|
-
content ?? buildDefaultBlock()
|
|
11094
|
-
)
|
|
11095
|
-
);
|
|
11096
|
-
// A failure must never sit hidden behind the surface it replaces, so the
|
|
11097
|
-
// error skips the stand-in's show delay.
|
|
11098
|
-
engageConversationOpenTakeover();
|
|
11099
|
-
};
|
|
11100
|
-
|
|
11101
|
-
// The panel exit's deferred teardown restores the footer it captured, which
|
|
11102
|
-
// would lift the composer gate mid-fetch; re-assert it while an open is
|
|
11103
|
-
// still pending.
|
|
11104
|
-
eventBus.on("history:closed", () => {
|
|
11105
|
-
if (conversationOpenPendingEl) setHistoryHostInert(footer, true);
|
|
11106
|
-
});
|
|
11107
|
-
|
|
11108
|
-
/**
|
|
11109
|
-
* Optimistic reopen: navigate on the click, hydrate on the fetch. The
|
|
11110
|
-
* returned promise keeps the transactional contract (resolves once the
|
|
11111
|
-
* transcript is live, rejects on failure) for `controller.openConversation`.
|
|
11112
|
-
* `suppressScrollSend` covers the hydration: otherwise the last restored
|
|
11113
|
-
* user message triggers the anchor-top send scroll.
|
|
11114
|
-
*/
|
|
11115
|
-
const openHistoryConversation = async (
|
|
11116
|
-
conversationId: string,
|
|
11117
|
-
{ focusComposer = false }: { focusComposer?: boolean } = {}
|
|
11118
|
-
): Promise<void> => {
|
|
11119
|
-
const scope = historyOperationScope();
|
|
11120
|
-
// Seeds the header title binding from the list row before the list leaves.
|
|
11121
|
-
// The surface is usually gone by hydration time (the exit outruns the
|
|
11122
|
-
// fetch), so this is the only moment the title can be read from the list.
|
|
11123
|
-
const seededFromList = !!historySurface;
|
|
11124
|
-
historySurface?.view.setActiveConversationId(conversationId);
|
|
11125
|
-
clearConversationOpenPending();
|
|
11126
|
-
const token = conversationOpenToken;
|
|
11127
|
-
showConversationOpenPending();
|
|
11128
|
-
if (historyVisible && (historyPresentation === "panel" || railOverlayOpen)) {
|
|
11129
|
-
closeHistory({ restoreFocus: false });
|
|
11130
|
-
}
|
|
11131
|
-
suppressScrollSend = true;
|
|
11132
|
-
try {
|
|
11133
|
-
await session.openConversation(conversationId, { scope });
|
|
11134
|
-
} catch (error) {
|
|
11135
|
-
if (token === conversationOpenToken) {
|
|
11136
|
-
showConversationOpenError(conversationId);
|
|
11137
|
-
}
|
|
11138
|
-
throw error;
|
|
11139
|
-
} finally {
|
|
11140
|
-
suppressScrollSend = false;
|
|
11141
|
-
}
|
|
11142
|
-
if (token !== conversationOpenToken) return;
|
|
11143
|
-
clearConversationOpenPending();
|
|
11144
|
-
messageCache.clear();
|
|
11145
|
-
resetAnchorState();
|
|
11146
|
-
resumeAutoScroll();
|
|
11147
|
-
if (!restoreScrollPosition()) jumpToBottomInstant();
|
|
11148
|
-
// Fades only after the scroll settles, so the motion covers the final frame.
|
|
11149
|
-
fadeInTranscript();
|
|
11150
|
-
syncEarlierMessagesPill();
|
|
11151
|
-
// Without a list at selection time there was no title to seed.
|
|
11152
|
-
if (!seededFromList) setActiveConversationSummary(null);
|
|
11153
|
-
eventBus.emit("history:conversationOpened", {
|
|
11154
|
-
conversationId,
|
|
11155
|
-
title: activeConversationTitle,
|
|
11156
|
-
scope,
|
|
11157
|
-
timestamp: Date.now(),
|
|
11158
|
-
});
|
|
11159
|
-
// Only interaction paths focus: focusing a programmatic open scrolls the
|
|
11160
|
-
// host page to the widget (same-origin iframes included).
|
|
11161
|
-
if (focusComposer) maybeFocusInput();
|
|
11162
|
-
};
|
|
11163
|
-
|
|
11164
|
-
/**
|
|
11165
|
-
* The single commit path behind the header action, the view's `onStartNew`,
|
|
11166
|
-
* and `controller.startNewConversation()`, so one emit covers all three.
|
|
11167
|
-
*/
|
|
11168
|
-
const startNewConversation = async ({
|
|
11169
|
-
focusComposer = false,
|
|
11170
|
-
}: { focusComposer?: boolean } = {}): Promise<void> => {
|
|
11171
|
-
// A new conversation supersedes any transcript fetch still in flight.
|
|
11172
|
-
clearConversationOpenPending();
|
|
11173
|
-
await session.startNewConversation({ scope: historyOperationScope() });
|
|
11174
|
-
setActiveConversationSummary(null);
|
|
11175
|
-
messageCache.clear();
|
|
11176
|
-
resetAnchorState();
|
|
11177
|
-
resumeAutoScroll();
|
|
11178
|
-
jumpToBottomInstant();
|
|
11179
|
-
syncEarlierMessagesPill();
|
|
11180
|
-
const conversationId = session.getActiveConversationId();
|
|
11181
|
-
historySurface?.view.setActiveConversationId(conversationId);
|
|
11182
|
-
// Emitted before the close so a host can tell a commit from a plain close.
|
|
11183
|
-
eventBus.emit("history:conversationStarted", {
|
|
11184
|
-
conversationId,
|
|
11185
|
-
timestamp: Date.now(),
|
|
11186
|
-
});
|
|
11187
|
-
if (historyVisible && (historyPresentation === "panel" || railOverlayOpen)) {
|
|
11188
|
-
closeHistory({ restoreFocus: false });
|
|
11189
|
-
}
|
|
11190
|
-
if (focusComposer) maybeFocusInput();
|
|
11191
|
-
};
|
|
11192
|
-
|
|
11193
|
-
/** Shared rename/star path: provider update, then view + header binding. */
|
|
11194
|
-
const updateHistoryConversation = async (
|
|
11195
|
-
conversationId: string,
|
|
11196
|
-
patch: HistoryConversationPatch
|
|
11197
|
-
): Promise<HistoryConversationSummary> => {
|
|
11198
|
-
const summary = await session.updateConversation(conversationId, {
|
|
11199
|
-
...patch,
|
|
11200
|
-
...(patch.title !== undefined ? { title: patch.title.trim() } : {}),
|
|
11201
|
-
});
|
|
11202
|
-
// The view re-renders and reports the active title through the reporter;
|
|
11203
|
-
// without a mounted view, keep the header binding fresh directly.
|
|
11204
|
-
historySurface?.view.applyConversationSummary(summary);
|
|
11205
|
-
if (!historySurface && conversationId === session.getActiveConversationId()) {
|
|
11206
|
-
setActiveConversationSummary(summary);
|
|
11207
|
-
}
|
|
11208
|
-
return summary;
|
|
11209
|
-
};
|
|
11210
|
-
|
|
11211
|
-
const deleteHistoryConversation = async (
|
|
11212
|
-
conversationId: string
|
|
11213
|
-
): Promise<void> => {
|
|
11214
|
-
const scope = historyOperationScope();
|
|
11215
|
-
const wasActive = session.getActiveConversationId() === conversationId;
|
|
11216
|
-
await session.deleteConversation(conversationId, { scope });
|
|
11217
|
-
// Prune the open list too: the view's own delete flow removes its row
|
|
11218
|
-
// after this resolves, but built-in/headless deletes have no view caller.
|
|
11219
|
-
historySurface?.view.removeConversationSummary(conversationId);
|
|
11220
|
-
if (wasActive) {
|
|
11221
|
-
setActiveConversationSummary(null);
|
|
11222
|
-
messageCache.clear();
|
|
11223
|
-
resetAnchorState();
|
|
11224
|
-
resumeAutoScroll();
|
|
11225
|
-
syncEarlierMessagesPill();
|
|
11226
|
-
}
|
|
11227
|
-
eventBus.emit("history:conversationDeleted", {
|
|
11228
|
-
conversationId,
|
|
11229
|
-
scope,
|
|
11230
|
-
wasActive,
|
|
11231
|
-
timestamp: Date.now(),
|
|
11232
|
-
});
|
|
11233
|
-
};
|
|
11234
|
-
|
|
11235
|
-
const clearConversationHistory = async (opts?: {
|
|
11236
|
-
targetId?: string;
|
|
11237
|
-
/** Deliberate headless opt-in to the whole authorized client-token scope. */
|
|
11238
|
-
allTargets?: boolean;
|
|
11239
|
-
scope?: HistoryScope;
|
|
11240
|
-
}): Promise<{ deleted: number }> => {
|
|
11241
|
-
const scope = opts?.scope ?? historyOperationScope();
|
|
11242
|
-
// Default to the same filter the visible list uses; only an explicit
|
|
11243
|
-
// headless opt-in deletes conversations the visitor was never shown.
|
|
11244
|
-
const targetId = opts?.allTargets
|
|
11245
|
-
? null
|
|
11246
|
-
: (opts?.targetId ?? activeHistoryTargetId());
|
|
11247
|
-
const result = await session.clearConversationHistory({
|
|
11248
|
-
...(targetId ? { targetId } : {}),
|
|
11249
|
-
scope,
|
|
11250
|
-
});
|
|
11251
|
-
setActiveConversationSummary(null);
|
|
11252
|
-
messageCache.clear();
|
|
11253
|
-
resetAnchorState();
|
|
11254
|
-
resumeAutoScroll();
|
|
11255
|
-
syncEarlierMessagesPill();
|
|
11256
|
-
eventBus.emit("history:cleared", {
|
|
11257
|
-
deleted: result.deleted,
|
|
11258
|
-
scope,
|
|
11259
|
-
targetId,
|
|
11260
|
-
timestamp: Date.now(),
|
|
11261
|
-
});
|
|
11262
|
-
return result;
|
|
11263
|
-
};
|
|
11264
|
-
|
|
11265
|
-
/**
|
|
11266
|
-
* D6: the local wipe is UNCONDITIONAL. Records survive on the server; this
|
|
11267
|
-
* browser is detached from them whether or not revocation was confirmed.
|
|
11268
|
-
*/
|
|
11269
|
-
const resetHistoryIdentity = async (): Promise<{
|
|
11270
|
-
remoteRevocationConfirmed: boolean;
|
|
11271
|
-
}> => {
|
|
11272
|
-
// Rejection is reserved for misuse; remote failure resolves false.
|
|
11273
|
-
if (!historyProvider?.resetDevice) {
|
|
11274
|
-
throw new Error(
|
|
11275
|
-
"[Persona] resetHistoryIdentity() requires a history provider that can reset this device"
|
|
11276
|
-
);
|
|
11277
|
-
}
|
|
11278
|
-
let remoteRevocationConfirmed = false;
|
|
11279
|
-
try {
|
|
11280
|
-
const result = await session.resetHistoryDevice();
|
|
11281
|
-
remoteRevocationConfirmed = result.remoteRevocationConfirmed;
|
|
11282
|
-
} finally {
|
|
11283
|
-
setActiveConversationSummary(null);
|
|
11284
|
-
session.clearArtifacts();
|
|
11285
|
-
messageCache.clear();
|
|
11286
|
-
lastAppliedMessages = null;
|
|
11287
|
-
config.clearStoredSessionId?.();
|
|
11288
|
-
config.clearStoredConversationId?.();
|
|
11289
|
-
persistentMetadata = {};
|
|
11290
|
-
actionManager.syncFromMetadata();
|
|
11291
|
-
if (storageAdapter?.clear) {
|
|
11292
|
-
runStorageMutation(
|
|
11293
|
-
() => storageAdapter.clear!(),
|
|
11294
|
-
"[AgentWidget] Failed to clear storage adapter:"
|
|
11295
|
-
);
|
|
11296
|
-
}
|
|
11297
|
-
resetAnchorState();
|
|
11298
|
-
resumeAutoScroll();
|
|
11299
|
-
syncEarlierMessagesPill();
|
|
11300
|
-
closeHistory({ restoreFocus: false });
|
|
11301
|
-
maybeFocusInput();
|
|
11302
|
-
if (!open) launcherSurfaceInstance?.launcher.element.focus();
|
|
11303
|
-
}
|
|
11304
|
-
announceHistory(
|
|
11305
|
-
remoteRevocationConfirmed
|
|
11306
|
-
? historyShellCopy.identityResetNotice
|
|
11307
|
-
: historyShellCopy.identityResetUnconfirmedNotice
|
|
11308
|
-
);
|
|
11309
|
-
eventBus.emit("history:identityReset", {
|
|
11310
|
-
remoteRevocationConfirmed,
|
|
11311
|
-
timestamp: Date.now(),
|
|
11312
|
-
});
|
|
11313
|
-
return { remoteRevocationConfirmed };
|
|
11314
|
-
};
|
|
11315
|
-
|
|
11316
|
-
// --- confirmations (shell-owned alert dialogs) ---------------------------
|
|
11317
|
-
|
|
11318
|
-
// With the artifact split, `container` is only the chat column, so an
|
|
11319
|
-
// inset:0 overlay there would leave the artifact pane undimmed. The panel
|
|
11320
|
-
// wraps the whole split and is already position:relative on that path;
|
|
11321
|
-
// forcing `relative` onto it elsewhere would not survive the chrome
|
|
11322
|
-
// passes' cssText resets, so plain layouts keep the container host.
|
|
11323
|
-
const historyConfirmHost = (): HTMLElement =>
|
|
11324
|
-
artifactSplitRoot ? panel : container;
|
|
11325
|
-
|
|
11326
|
-
const requestDeleteConversation = async (
|
|
11327
|
-
conversationId: string
|
|
11328
|
-
): Promise<"deleted" | "cancelled"> => {
|
|
11329
|
-
const confirmed = await showHistoryConfirm({
|
|
11330
|
-
host: historyConfirmHost(),
|
|
11331
|
-
title: historyShellCopy.deleteConversationConfirmTitle,
|
|
11332
|
-
description: historyShellCopy.deleteConversationConfirm,
|
|
11333
|
-
confirmLabel: historyShellCopy.deleteConversationConfirmLabel,
|
|
11334
|
-
cancelLabel: historyShellCopy.confirmCancelLabel,
|
|
11335
|
-
});
|
|
11336
|
-
if (!confirmed) return "cancelled";
|
|
11337
|
-
await deleteHistoryConversation(conversationId);
|
|
11338
|
-
return "deleted";
|
|
11339
|
-
};
|
|
11340
|
-
|
|
11341
|
-
const requestClearConversationHistory = async (): Promise<
|
|
11342
|
-
"cleared" | "cancelled"
|
|
11343
|
-
> => {
|
|
11344
|
-
// Scope-aware copy: never imply the delete is limited to the rendered page.
|
|
11345
|
-
const verified = historyOperationScope() === "verified-user";
|
|
11346
|
-
const confirmed = await showHistoryConfirm({
|
|
11347
|
-
host: historyConfirmHost(),
|
|
11348
|
-
title: historyShellCopy.clearHistoryConfirmTitle,
|
|
11349
|
-
description: verified
|
|
11350
|
-
? historyShellCopy.clearHistoryVerifiedConfirm
|
|
11351
|
-
: historyShellCopy.clearHistoryConfirm,
|
|
11352
|
-
confirmLabel: historyShellCopy.clearHistoryConfirmLabel,
|
|
11353
|
-
cancelLabel: historyShellCopy.confirmCancelLabel,
|
|
11354
|
-
});
|
|
11355
|
-
if (!confirmed) return "cancelled";
|
|
11356
|
-
await clearConversationHistory();
|
|
11357
|
-
return "cleared";
|
|
11358
|
-
};
|
|
11359
|
-
|
|
11360
|
-
const requestResetHistoryIdentity = async (): Promise<
|
|
11361
|
-
{ outcome: "cancelled" } | { outcome: "reset"; remoteRevocationConfirmed: boolean }
|
|
11362
|
-
> => {
|
|
11363
|
-
const confirmed = await showHistoryConfirm({
|
|
11364
|
-
host: historyConfirmHost(),
|
|
11365
|
-
title: historyShellCopy.resetIdentityConfirmTitle,
|
|
11366
|
-
description: historyShellCopy.resetIdentityConfirm,
|
|
11367
|
-
confirmLabel: historyShellCopy.resetIdentityConfirmLabel,
|
|
11368
|
-
cancelLabel: historyShellCopy.confirmCancelLabel,
|
|
11369
|
-
});
|
|
11370
|
-
if (!confirmed) return { outcome: "cancelled" };
|
|
11371
|
-
const { remoteRevocationConfirmed } = await resetHistoryIdentity();
|
|
11372
|
-
return { outcome: "reset", remoteRevocationConfirmed };
|
|
11373
|
-
};
|
|
11374
|
-
|
|
11375
|
-
// --- "show earlier messages" prepend -------------------------------------
|
|
11376
|
-
|
|
11377
|
-
const earlierMessagesButton = createElement(
|
|
11378
|
-
"button",
|
|
11379
|
-
"persona-history-earlier"
|
|
11380
|
-
) as HTMLButtonElement;
|
|
11381
|
-
earlierMessagesButton.type = "button";
|
|
11382
|
-
earlierMessagesButton.textContent = historyShellCopy.showEarlierMessagesLabel;
|
|
11383
|
-
earlierMessagesButton.setAttribute("data-persona-history-earlier", "");
|
|
11384
|
-
Object.assign(earlierMessagesButton.style, {
|
|
11385
|
-
alignSelf: "center",
|
|
11386
|
-
minHeight: "44px",
|
|
11387
|
-
padding: "0 16px",
|
|
11388
|
-
borderRadius: "999px",
|
|
11389
|
-
border: "1px solid var(--persona-border, rgba(0,0,0,0.12))",
|
|
11390
|
-
background: "transparent",
|
|
11391
|
-
color: "inherit",
|
|
11392
|
-
font: "inherit",
|
|
11393
|
-
cursor: "pointer",
|
|
11394
|
-
flexShrink: "0",
|
|
11395
|
-
} satisfies Partial<CSSStyleDeclaration>);
|
|
11396
|
-
|
|
11397
|
-
const syncEarlierMessagesPill = (): void => {
|
|
11398
|
-
const show =
|
|
11399
|
-
historyAvailable() &&
|
|
11400
|
-
!!historySessionState.nextMessageCursor &&
|
|
11401
|
-
!!session.getActiveConversationId();
|
|
11402
|
-
if (!show) {
|
|
11403
|
-
earlierMessagesButton.remove();
|
|
11404
|
-
return;
|
|
11405
|
-
}
|
|
11406
|
-
earlierMessagesButton.textContent = historyShellCopy.showEarlierMessagesLabel;
|
|
11407
|
-
if (earlierMessagesButton.parentNode !== body) {
|
|
11408
|
-
body.insertBefore(earlierMessagesButton, body.firstChild);
|
|
9105
|
+
void withHistoryShell((shell) =>
|
|
9106
|
+
shell.updateHistoryConversation(activeId, {
|
|
9107
|
+
starred: !activeConversationStarred,
|
|
9108
|
+
})
|
|
9109
|
+
).catch(() => {});
|
|
11409
9110
|
}
|
|
11410
|
-
};
|
|
11411
|
-
|
|
11412
|
-
earlierMessagesButton.addEventListener("click", () => {
|
|
11413
|
-
const conversationId = session.getActiveConversationId();
|
|
11414
|
-
const cursor = historySessionState.nextMessageCursor;
|
|
11415
|
-
if (!conversationId || !cursor || earlierMessagesButton.disabled) return;
|
|
11416
|
-
earlierMessagesButton.disabled = true;
|
|
11417
|
-
// Capture before the prepend so the reader's viewport stays put.
|
|
11418
|
-
const previousHeight = body.scrollHeight;
|
|
11419
|
-
const previousTop = body.scrollTop;
|
|
11420
|
-
void session
|
|
11421
|
-
.loadOlderMessages(conversationId, cursor, { scope: historyOperationScope() })
|
|
11422
|
-
.then(() => {
|
|
11423
|
-
body.scrollTop = previousTop + (body.scrollHeight - previousHeight);
|
|
11424
|
-
// Prepending shifts everything below the anchor.
|
|
11425
|
-
repinAnchoredMessage();
|
|
11426
|
-
})
|
|
11427
|
-
.catch(() => {})
|
|
11428
|
-
.finally(() => {
|
|
11429
|
-
earlierMessagesButton.disabled = false;
|
|
11430
|
-
syncEarlierMessagesPill();
|
|
11431
|
-
});
|
|
11432
9111
|
});
|
|
11433
9112
|
|
|
11434
|
-
// --- header chrome -------------------------------------------------------
|
|
11435
|
-
|
|
11436
|
-
// Shares the header control chrome (box + glyph from the header tokens) with
|
|
11437
|
-
// the close and clear-chat buttons beside it.
|
|
11438
|
-
const buildHistoryButton = (): {
|
|
11439
|
-
button: HTMLButtonElement;
|
|
11440
|
-
wrapper: HTMLElement;
|
|
11441
|
-
} => {
|
|
11442
|
-
const parts = createHeaderIconButton({
|
|
11443
|
-
ariaLabel: historyShellCopy.openHistoryLabel,
|
|
11444
|
-
iconName: "history",
|
|
11445
|
-
wrapperClassName:
|
|
11446
|
-
"persona-relative persona-inline-flex persona-items-center persona-justify-center",
|
|
11447
|
-
extraClassName: "persona-history-toggle",
|
|
11448
|
-
attrs: { "data-persona-history-toggle": "" },
|
|
11449
|
-
});
|
|
11450
|
-
parts.button.addEventListener("click", (event) => {
|
|
11451
|
-
if (!historyAvailable() || historyTurnBusy()) return;
|
|
11452
|
-
if (historyVisible) {
|
|
11453
|
-
closeHistory();
|
|
11454
|
-
return;
|
|
11455
|
-
}
|
|
11456
|
-
// Enter/Space synthesize a click with detail 0; a pointer press reports > 0.
|
|
11457
|
-
void openHistory({ invoker: parts.button, keyboard: event.detail === 0 });
|
|
11458
|
-
});
|
|
11459
|
-
return parts;
|
|
11460
|
-
};
|
|
11461
|
-
|
|
11462
|
-
const syncHistoryButton = (): void => {
|
|
11463
|
-
if (!historyAvailable()) {
|
|
11464
|
-
historyButtonWrapper?.remove();
|
|
11465
|
-
historyButton = null;
|
|
11466
|
-
historyButtonWrapper = null;
|
|
11467
|
-
return;
|
|
11468
|
-
}
|
|
11469
|
-
// A header rebuild detaches the old button; a stale ref must not block
|
|
11470
|
-
// recreation into the replacement header.
|
|
11471
|
-
if (historyButtonWrapper && !historyButtonWrapper.isConnected) {
|
|
11472
|
-
historyButton = null;
|
|
11473
|
-
historyButtonWrapper = null;
|
|
11474
|
-
}
|
|
11475
|
-
if (!historyButton && header) {
|
|
11476
|
-
const parts = buildHistoryButton();
|
|
11477
|
-
historyButton = parts.button;
|
|
11478
|
-
historyButtonWrapper = parts.wrapper;
|
|
11479
|
-
const insertBefore =
|
|
11480
|
-
panelElements.clearChatButtonWrapper || panelElements.closeButtonWrapper;
|
|
11481
|
-
// Layouts may parent these wrappers in a trailing cluster rather than
|
|
11482
|
-
// the header itself; insert wherever they live so close stays outermost.
|
|
11483
|
-
if (insertBefore?.parentNode && header.contains(insertBefore)) {
|
|
11484
|
-
insertBefore.parentNode.insertBefore(historyButtonWrapper, insertBefore);
|
|
11485
|
-
} else {
|
|
11486
|
-
header.appendChild(historyButtonWrapper);
|
|
11487
|
-
}
|
|
11488
|
-
}
|
|
11489
|
-
if (!historyButton || !historyButtonWrapper) return;
|
|
11490
|
-
// Every rail surface carries its own toggle, and so does the trigger that
|
|
11491
|
-
// summons one: the header keeps a control only when neither is on screen.
|
|
11492
|
-
const railed =
|
|
11493
|
-
railTriggerApplies() || (historyVisible && historyPresentation === "rail");
|
|
11494
|
-
historyButtonWrapper.style.display = railed ? "none" : "";
|
|
11495
|
-
if (railed) {
|
|
11496
|
-
// Hiding the control under focus would drop it to the body; the rail's
|
|
11497
|
-
// own toggle is the same control, so focus follows it there.
|
|
11498
|
-
if (document.activeElement === historyButton) focusHistoryEntry();
|
|
11499
|
-
return;
|
|
11500
|
-
}
|
|
11501
|
-
const busy = historyTurnBusy();
|
|
11502
|
-
historyButton.disabled = busy;
|
|
11503
|
-
historyButton.setAttribute("aria-disabled", busy ? "true" : "false");
|
|
11504
|
-
const label = busy
|
|
11505
|
-
? historyShellCopy.openHistoryBusyLabel
|
|
11506
|
-
: historyShellCopy.openHistoryLabel;
|
|
11507
|
-
// The factory tooltip reads the live aria-label; a title would double it.
|
|
11508
|
-
historyButton.setAttribute("aria-label", label);
|
|
11509
|
-
// Rail/auto toggles a navigation region; panel navigates to a surface.
|
|
11510
|
-
if ((config.features?.history?.presentation ?? "panel") === "panel") {
|
|
11511
|
-
historyButton.removeAttribute("aria-expanded");
|
|
11512
|
-
historyButton.removeAttribute("aria-controls");
|
|
11513
|
-
} else {
|
|
11514
|
-
historyButton.setAttribute("aria-expanded", historyVisible ? "true" : "false");
|
|
11515
|
-
historyButton.setAttribute("aria-controls", historyRegionId);
|
|
11516
|
-
}
|
|
11517
|
-
};
|
|
11518
|
-
|
|
11519
|
-
/**
|
|
11520
|
-
* With history available the visible start-over affordance becomes
|
|
11521
|
-
* "New conversation" (`clearChat()` stays programmatic-only).
|
|
11522
|
-
*/
|
|
11523
|
-
let clearChatRelabelled = false;
|
|
11524
|
-
const syncClearChatAffordance = (): void => {
|
|
11525
|
-
const button = panelElements.clearChatButton;
|
|
11526
|
-
if (!button) return;
|
|
11527
|
-
if (historyAvailable()) {
|
|
11528
|
-
if (!clearChatRelabelled) {
|
|
11529
|
-
clearChatDefaultLabel = button.getAttribute("aria-label") ?? "";
|
|
11530
|
-
clearChatRelabelled = true;
|
|
11531
|
-
}
|
|
11532
|
-
// aria-label only: the styled tooltip reads it live, and a title would
|
|
11533
|
-
// render a second native tooltip on top.
|
|
11534
|
-
button.setAttribute("aria-label", historyShellCopy.newConversationLabel);
|
|
11535
|
-
button.removeAttribute("title");
|
|
11536
|
-
// No touch-target pin: the shared header control class owns the box, and
|
|
11537
|
-
// a 44px floor here would desync this control from its neighbours.
|
|
11538
|
-
return;
|
|
11539
|
-
}
|
|
11540
|
-
// Only undo a relabel we performed; never clobber host-configured copy.
|
|
11541
|
-
if (!clearChatRelabelled) return;
|
|
11542
|
-
clearChatRelabelled = false;
|
|
11543
|
-
if (clearChatDefaultLabel) {
|
|
11544
|
-
button.setAttribute("aria-label", clearChatDefaultLabel);
|
|
11545
|
-
}
|
|
11546
|
-
};
|
|
11547
|
-
|
|
11548
|
-
const syncHistoryChromeImpl = (): void => {
|
|
11549
|
-
historyShellCopy = resolveHistoryShellCopy(config.features?.history?.copy);
|
|
11550
|
-
syncHistoryButton();
|
|
11551
|
-
syncRailOverlayTrigger();
|
|
11552
|
-
syncClearChatAffordance();
|
|
11553
|
-
syncEarlierMessagesPill();
|
|
11554
|
-
};
|
|
11555
|
-
historyChromeSync = syncHistoryChromeImpl;
|
|
11556
|
-
|
|
11557
9113
|
// --- session-driven state ------------------------------------------------
|
|
11558
9114
|
|
|
11559
9115
|
historyStateHandler = (state) => {
|
|
11560
9116
|
historySessionState = state;
|
|
11561
|
-
historySurface?.view.setNewConversationRequired(
|
|
9117
|
+
historyShell?.historySurface?.view.setNewConversationRequired(
|
|
11562
9118
|
state.recovery === "new_conversation_required"
|
|
11563
9119
|
);
|
|
11564
9120
|
// Sending stays blocked through a destructive/continuity transition.
|
|
11565
9121
|
setComposerDisabled(state.sendBlocked || isStreaming);
|
|
11566
|
-
syncEarlierMessagesPill();
|
|
9122
|
+
historyShell?.syncEarlierMessagesPill();
|
|
11567
9123
|
};
|
|
11568
9124
|
|
|
11569
9125
|
historyNoticeHandler = (notice) => {
|
|
@@ -11576,22 +9132,124 @@ export const createAgentExperience = (
|
|
|
11576
9132
|
resumeAutoScroll();
|
|
11577
9133
|
}
|
|
11578
9134
|
announceHistory(notice.message);
|
|
11579
|
-
historySurface?.view.refresh();
|
|
9135
|
+
historyShell?.historySurface?.view.refresh();
|
|
9136
|
+
};
|
|
9137
|
+
|
|
9138
|
+
// --- lazy shell ----------------------------------------------------------
|
|
9139
|
+
// Placement, the rail, open/close and the conversation operations live in
|
|
9140
|
+
// `history-shell.ts`: inlined for npm builds, the lazy `history-shell.js`
|
|
9141
|
+
// chunk in the IIFE/CDN bundle. The shell is created when
|
|
9142
|
+
// `features.history` is enabled (at mount or by a later `update()`), or on
|
|
9143
|
+
// the first history API call. Until then history is simply unavailable.
|
|
9144
|
+
const historyShellContext: HistoryShellContext = {
|
|
9145
|
+
get actionManager() { return actionManager; },
|
|
9146
|
+
get activeConversationTitle() { return activeConversationTitle; },
|
|
9147
|
+
announceHistory,
|
|
9148
|
+
get applyRailChrome() { return applyRailChrome; },
|
|
9149
|
+
set applyRailChrome(value) { applyRailChrome = value; },
|
|
9150
|
+
get artifactSplitRoot() { return artifactSplitRoot; },
|
|
9151
|
+
get body() { return body; },
|
|
9152
|
+
get config() { return config; },
|
|
9153
|
+
get container() { return container; },
|
|
9154
|
+
get conversationOpenPendingEl() { return conversationOpenPendingEl; },
|
|
9155
|
+
set conversationOpenPendingEl(value) { conversationOpenPendingEl = value; },
|
|
9156
|
+
get conversationOpenTakeover() { return conversationOpenTakeover; },
|
|
9157
|
+
set conversationOpenTakeover(value) { conversationOpenTakeover = value; },
|
|
9158
|
+
currentKeyPrefix,
|
|
9159
|
+
destroyCallbacks,
|
|
9160
|
+
eventBus,
|
|
9161
|
+
get footer() { return footer; },
|
|
9162
|
+
get header() { return header; },
|
|
9163
|
+
get historyChromeSync() { return historyChromeSync; },
|
|
9164
|
+
set historyChromeSync(value) { historyChromeSync = value; },
|
|
9165
|
+
historyFeatureEnabled,
|
|
9166
|
+
get historyInternals() { return historyInternals; },
|
|
9167
|
+
set historyInternals(value) { historyInternals = value; },
|
|
9168
|
+
get historyPresentation() { return historyPresentation; },
|
|
9169
|
+
set historyPresentation(value) { historyPresentation = value; },
|
|
9170
|
+
get historySessionState() { return historySessionState; },
|
|
9171
|
+
get historyVisible() { return historyVisible; },
|
|
9172
|
+
set historyVisible(value) { historyVisible = value; },
|
|
9173
|
+
get isStreaming() { return isStreaming; },
|
|
9174
|
+
jumpToBottomInstant,
|
|
9175
|
+
get lastAppliedMessages() { return lastAppliedMessages; },
|
|
9176
|
+
set lastAppliedMessages(value) { lastAppliedMessages = value; },
|
|
9177
|
+
get launcherEnabled() { return launcherEnabled; },
|
|
9178
|
+
get launcherSurfaceInstance() { return launcherSurfaceInstance; },
|
|
9179
|
+
maybeFocusInput,
|
|
9180
|
+
messageCache,
|
|
9181
|
+
get messagesWrapper() { return messagesWrapper; },
|
|
9182
|
+
mount,
|
|
9183
|
+
get open() { return open; },
|
|
9184
|
+
panel,
|
|
9185
|
+
panelElements,
|
|
9186
|
+
get persistentMetadata() { return persistentMetadata; },
|
|
9187
|
+
set persistentMetadata(value) { persistentMetadata = value; },
|
|
9188
|
+
plugins,
|
|
9189
|
+
get reapplyHistoryHostChrome() { return reapplyHistoryHostChrome; },
|
|
9190
|
+
set reapplyHistoryHostChrome(value) { reapplyHistoryHostChrome = value; },
|
|
9191
|
+
repinAnchoredMessage,
|
|
9192
|
+
resetAnchorState,
|
|
9193
|
+
restoreScrollPosition,
|
|
9194
|
+
resumeAutoScroll,
|
|
9195
|
+
runStorageMutation,
|
|
9196
|
+
get session() { return session; },
|
|
9197
|
+
setActiveConversationSummary,
|
|
9198
|
+
storageAdapter,
|
|
9199
|
+
get suppressScrollSend() { return suppressScrollSend; },
|
|
9200
|
+
set suppressScrollSend(value) { suppressScrollSend = value; },
|
|
9201
|
+
syncScrollToBottomButton,
|
|
9202
|
+
updateWelcome,
|
|
9203
|
+
getHistoryProviderFactory,
|
|
9204
|
+
historyErrors: { HistoryClientError, HistoryProviderError },
|
|
9205
|
+
isDockedMountMode,
|
|
9206
|
+
renderLucideIcon,
|
|
9207
|
+
loadHistoryView,
|
|
9208
|
+
createHeaderIconButton,
|
|
9209
|
+
attachTooltip,
|
|
9210
|
+
};
|
|
9211
|
+
|
|
9212
|
+
const startHistoryShell = (module: HistoryShellModule): HistoryShell | null => {
|
|
9213
|
+
if (!historyShell && !historyShellTornDown) {
|
|
9214
|
+
historyShell = module.createHistoryShell(historyShellContext);
|
|
9215
|
+
// The rail collapse combo registers through the shell that owns the rail.
|
|
9216
|
+
if (historyShell.railCollapseShortcut()) syncShortcuts();
|
|
9217
|
+
}
|
|
9218
|
+
return historyShell;
|
|
9219
|
+
};
|
|
9220
|
+
const ensureHistoryShell = (): Promise<HistoryShell | null> => {
|
|
9221
|
+
if (historyShell) return Promise.resolve(historyShell);
|
|
9222
|
+
const ready = getHistoryShellSync();
|
|
9223
|
+
if (ready) return Promise.resolve(startHistoryShell(ready));
|
|
9224
|
+
historyShellLoad ??= loadHistoryShell().then(startHistoryShell, (error) => {
|
|
9225
|
+
historyShellLoad = null;
|
|
9226
|
+
throw error;
|
|
9227
|
+
});
|
|
9228
|
+
return historyShellLoad;
|
|
9229
|
+
};
|
|
9230
|
+
/** Runs a history API call, synchronously once the shell is available. */
|
|
9231
|
+
const withHistoryShell = <T>(
|
|
9232
|
+
run: (shell: HistoryShell) => Promise<T>
|
|
9233
|
+
): Promise<T> => {
|
|
9234
|
+
const ready = getHistoryShellSync();
|
|
9235
|
+
const shell = historyShell ?? (ready ? startHistoryShell(ready) : null);
|
|
9236
|
+
if (shell) return run(shell);
|
|
9237
|
+
return ensureHistoryShell().then((loaded) => {
|
|
9238
|
+
if (!loaded) throw new Error("[AgentWidget] The widget was destroyed");
|
|
9239
|
+
return run(loaded);
|
|
9240
|
+
});
|
|
9241
|
+
};
|
|
9242
|
+
const syncHistoryPresentation = (): void => {
|
|
9243
|
+
historyShell?.syncHistoryPresentation();
|
|
11580
9244
|
};
|
|
11581
|
-
|
|
11582
|
-
installHistoryProvider();
|
|
11583
|
-
syncHistoryChromeImpl();
|
|
11584
9245
|
destroyCallbacks.push(() => {
|
|
11585
|
-
|
|
11586
|
-
unsubscribeHistoryIdentity?.();
|
|
11587
|
-
// An open in flight (a hover this teardown raced) must not mount after it.
|
|
11588
|
-
historyOpenToken += 1;
|
|
11589
|
-
// A pending exit still owns a mounted surface and a live timer.
|
|
11590
|
-
settleHistoryExit();
|
|
11591
|
-
historySurface?.dispose();
|
|
11592
|
-
historySurface?.view.destroy();
|
|
11593
|
-
historySurface = null;
|
|
9246
|
+
historyShellTornDown = true;
|
|
11594
9247
|
});
|
|
9248
|
+
if (historyFeatureEnabled()) {
|
|
9249
|
+
void ensureHistoryShell()
|
|
9250
|
+
.catch(() => {})
|
|
9251
|
+
.finally(resolveHistoryShellGate);
|
|
9252
|
+
}
|
|
11595
9253
|
|
|
11596
9254
|
// Client-token init on mount is reserved for widgets that need a session at
|
|
11597
9255
|
// boot: visitor history (boot reconciliation) and `sessionInit: 'mount'`.
|
|
@@ -11607,11 +9265,12 @@ export const createAgentExperience = (
|
|
|
11607
9265
|
.then(() => {
|
|
11608
9266
|
// Boot resume is authoritative only after reconciliation; the client
|
|
11609
9267
|
// already gated its init on `historyBootstrapReady`.
|
|
11610
|
-
|
|
11611
|
-
|
|
9268
|
+
const shell = historyShell;
|
|
9269
|
+
if (!shell?.historyAvailable()) return undefined;
|
|
9270
|
+
return session.reconcileBootConversation({ scope: shell.historyScope() });
|
|
11612
9271
|
})
|
|
11613
9272
|
.then(() => {
|
|
11614
|
-
syncEarlierMessagesPill();
|
|
9273
|
+
historyShell?.syncEarlierMessagesPill();
|
|
11615
9274
|
})
|
|
11616
9275
|
.catch((err) => {
|
|
11617
9276
|
if (config.debug) {
|
|
@@ -11852,7 +9511,7 @@ export const createAgentExperience = (
|
|
|
11852
9511
|
let composerNoticeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
11853
9512
|
|
|
11854
9513
|
/** Transient message in the status region; a lock reason always outranks it. */
|
|
11855
|
-
function showComposerNotice(text: string): void {
|
|
9514
|
+
function showComposerNotice(text: string, ms = 4000): void {
|
|
11856
9515
|
announce(text);
|
|
11857
9516
|
if (isComposerBar() || !statusText) return;
|
|
11858
9517
|
if (statusText.hasAttribute(COMPOSER_REASON_ATTR)) return;
|
|
@@ -11877,7 +9536,7 @@ export const createAgentExperience = (
|
|
|
11877
9536
|
status
|
|
11878
9537
|
);
|
|
11879
9538
|
}
|
|
11880
|
-
},
|
|
9539
|
+
}, ms);
|
|
11881
9540
|
}
|
|
11882
9541
|
|
|
11883
9542
|
/** Put the deferred submission back in the editable draft and drop the card. */
|
|
@@ -12470,13 +10129,7 @@ export const createAgentExperience = (
|
|
|
12470
10129
|
let speechRecognition: any = null;
|
|
12471
10130
|
let isRecording = false;
|
|
12472
10131
|
let pauseTimer: number | null = null;
|
|
12473
|
-
let originalMicStyles:
|
|
12474
|
-
backgroundColor: string;
|
|
12475
|
-
color: string;
|
|
12476
|
-
borderColor: string;
|
|
12477
|
-
iconName: string;
|
|
12478
|
-
iconSize: number;
|
|
12479
|
-
} | null = null;
|
|
10132
|
+
let originalMicStyles: OriginalMicStyles | null = null;
|
|
12480
10133
|
|
|
12481
10134
|
/**
|
|
12482
10135
|
* `voiceRecognition.completionBehavior` (roadmap section 14). Default stays
|
|
@@ -12773,142 +10426,39 @@ export const createAgentExperience = (
|
|
|
12773
10426
|
};
|
|
12774
10427
|
|
|
12775
10428
|
|
|
12776
|
-
// ---
|
|
12777
|
-
|
|
12778
|
-
|
|
12779
|
-
|
|
12780
|
-
|
|
12781
|
-
|
|
12782
|
-
|
|
12783
|
-
|
|
12784
|
-
|
|
12785
|
-
|
|
12786
|
-
|
|
10429
|
+
// --- Session-voice (Runtype / custom) mic state styling ---
|
|
10430
|
+
// The per-state styling ships in the lazy voice-runtime chunk
|
|
10431
|
+
// (`voice/mic-state-styles.ts`). These states only arise from a provider,
|
|
10432
|
+
// which setupVoice builds from that same chunk, so it is already loaded here;
|
|
10433
|
+
// the async fallback only covers a call that races the first load.
|
|
10434
|
+
let micStateStyles: MicStateStyles | null = null;
|
|
10435
|
+
const withMicStateStyles = (run: (styles: MicStateStyles) => void): void => {
|
|
10436
|
+
const apply = (mod: VoiceRuntimeModule) => {
|
|
10437
|
+
micStateStyles ??= mod.createMicStateStyles({
|
|
10438
|
+
mic: () => micButton,
|
|
10439
|
+
config: () => config,
|
|
10440
|
+
session: () => session,
|
|
10441
|
+
get original() {
|
|
10442
|
+
return originalMicStyles;
|
|
10443
|
+
},
|
|
10444
|
+
set original(next) {
|
|
10445
|
+
originalMicStyles = next;
|
|
10446
|
+
},
|
|
10447
|
+
setMicState,
|
|
10448
|
+
renderIcon: renderLucideIcon,
|
|
10449
|
+
iconFallbackPx: COMPOSER_CONTROL_ICON_FALLBACK_PX,
|
|
10450
|
+
});
|
|
10451
|
+
run(micStateStyles);
|
|
12787
10452
|
};
|
|
10453
|
+
const mod = getVoiceRuntimeSync();
|
|
10454
|
+
if (mod) apply(mod);
|
|
10455
|
+
else loadVoiceRuntime().then(apply, () => {});
|
|
12788
10456
|
};
|
|
12789
|
-
|
|
12790
|
-
|
|
12791
|
-
const
|
|
12792
|
-
if (!micButton) return;
|
|
12793
|
-
const existingSvg = micButton.querySelector("svg");
|
|
12794
|
-
if (existingSvg) existingSvg.remove();
|
|
12795
|
-
const size = originalMicStyles?.iconSize ?? (parseFloat(config.voiceRecognition?.iconSize ?? "") || COMPOSER_CONTROL_ICON_FALLBACK_PX);
|
|
12796
|
-
const newSvg = renderLucideIcon(iconName, size, color, 1.5);
|
|
12797
|
-
if (newSvg) micButton.appendChild(newSvg);
|
|
12798
|
-
};
|
|
12799
|
-
|
|
12800
|
-
/** Remove all voice state CSS classes */
|
|
12801
|
-
const removeAllVoiceStateClasses = () => {
|
|
12802
|
-
if (!micButton) return;
|
|
12803
|
-
micButton.classList.remove("persona-voice-recording", "persona-voice-processing", "persona-voice-speaking");
|
|
12804
|
-
};
|
|
12805
|
-
|
|
12806
|
-
// --- Per-state style application ---
|
|
12807
|
-
|
|
12808
|
-
const applyRuntypeMicRecordingStyles = () => {
|
|
12809
|
-
if (!micButton) return;
|
|
12810
|
-
storeOriginalMicStyles();
|
|
12811
|
-
const voiceConfig = config.voiceRecognition ?? {};
|
|
12812
|
-
const recordingBackgroundColor = voiceConfig.recordingBackgroundColor;
|
|
12813
|
-
const recordingIconColor = voiceConfig.recordingIconColor;
|
|
12814
|
-
const recordingBorderColor = voiceConfig.recordingBorderColor;
|
|
12815
|
-
removeAllVoiceStateClasses();
|
|
12816
|
-
micButton.classList.add("persona-voice-recording");
|
|
12817
|
-
setMicState("recording");
|
|
12818
|
-
micButton.style.backgroundColor = recordingBackgroundColor ?? "var(--persona-voice-recording-bg, #ef4444)";
|
|
12819
|
-
micButton.style.color = recordingIconColor ?? "var(--persona-voice-recording-indicator, #ffffff)";
|
|
12820
|
-
if (recordingIconColor) {
|
|
12821
|
-
const svg = micButton.querySelector("svg");
|
|
12822
|
-
if (svg) svg.setAttribute("stroke", recordingIconColor);
|
|
12823
|
-
}
|
|
12824
|
-
if (recordingBorderColor) micButton.style.borderColor = recordingBorderColor;
|
|
12825
|
-
micButton.setAttribute("aria-label", "Stop voice recognition");
|
|
12826
|
-
};
|
|
12827
|
-
|
|
12828
|
-
const applyRuntypeMicProcessingStyles = () => {
|
|
12829
|
-
if (!micButton) return;
|
|
12830
|
-
storeOriginalMicStyles();
|
|
12831
|
-
const voiceConfig = config.voiceRecognition ?? {};
|
|
12832
|
-
const interruptionMode = session.getVoiceInterruptionMode();
|
|
12833
|
-
const iconName = voiceConfig.processingIconName ?? "loader";
|
|
12834
|
-
const iconColor = voiceConfig.processingIconColor ?? originalMicStyles?.color ?? "";
|
|
12835
|
-
const bgColor = voiceConfig.processingBackgroundColor ?? originalMicStyles?.backgroundColor ?? "";
|
|
12836
|
-
const borderColor = voiceConfig.processingBorderColor ?? originalMicStyles?.borderColor ?? "";
|
|
12837
|
-
|
|
12838
|
-
removeAllVoiceStateClasses();
|
|
12839
|
-
micButton.classList.add("persona-voice-processing");
|
|
12840
|
-
setMicState("processing");
|
|
12841
|
-
micButton.style.backgroundColor = bgColor;
|
|
12842
|
-
micButton.style.borderColor = borderColor;
|
|
12843
|
-
const resolvedColor = iconColor || "currentColor";
|
|
12844
|
-
micButton.style.color = resolvedColor;
|
|
12845
|
-
swapMicIcon(iconName, resolvedColor);
|
|
12846
|
-
micButton.setAttribute("aria-label", "Processing voice input");
|
|
12847
|
-
// In "none" mode the button is not actionable during processing
|
|
12848
|
-
if (interruptionMode === "none") {
|
|
12849
|
-
micButton.style.cursor = "default";
|
|
12850
|
-
}
|
|
12851
|
-
};
|
|
12852
|
-
|
|
12853
|
-
const applyRuntypeMicSpeakingStyles = () => {
|
|
12854
|
-
if (!micButton) return;
|
|
12855
|
-
storeOriginalMicStyles();
|
|
12856
|
-
const voiceConfig = config.voiceRecognition ?? {};
|
|
12857
|
-
const interruptionMode = session.getVoiceInterruptionMode();
|
|
12858
|
-
// Default icon depends on interruption mode:
|
|
12859
|
-
// "square" for cancel, "mic" for barge-in (hot mic), "volume-2" otherwise
|
|
12860
|
-
const defaultSpeakingIcon = interruptionMode === "cancel" ? "square"
|
|
12861
|
-
: interruptionMode === "barge-in" ? "mic"
|
|
12862
|
-
: "volume-2";
|
|
12863
|
-
const iconName = voiceConfig.speakingIconName ?? defaultSpeakingIcon;
|
|
12864
|
-
const iconColor = voiceConfig.speakingIconColor
|
|
12865
|
-
?? (interruptionMode === "barge-in" ? (voiceConfig.recordingIconColor ?? originalMicStyles?.color ?? "") : (originalMicStyles?.color ?? ""));
|
|
12866
|
-
const bgColor = voiceConfig.speakingBackgroundColor
|
|
12867
|
-
?? (interruptionMode === "barge-in" ? (voiceConfig.recordingBackgroundColor ?? "var(--persona-voice-recording-bg, #ef4444)") : (originalMicStyles?.backgroundColor ?? ""));
|
|
12868
|
-
const borderColor = voiceConfig.speakingBorderColor
|
|
12869
|
-
?? (interruptionMode === "barge-in" ? (voiceConfig.recordingBorderColor ?? "") : (originalMicStyles?.borderColor ?? ""));
|
|
12870
|
-
|
|
12871
|
-
removeAllVoiceStateClasses();
|
|
12872
|
-
micButton.classList.add("persona-voice-speaking");
|
|
12873
|
-
setMicState("speaking");
|
|
12874
|
-
micButton.style.backgroundColor = bgColor;
|
|
12875
|
-
micButton.style.borderColor = borderColor;
|
|
12876
|
-
const resolvedColor = iconColor || "currentColor";
|
|
12877
|
-
micButton.style.color = resolvedColor;
|
|
12878
|
-
swapMicIcon(iconName, resolvedColor);
|
|
12879
|
-
|
|
12880
|
-
// aria-label varies by interruption mode
|
|
12881
|
-
const ariaLabel = interruptionMode === "cancel"
|
|
12882
|
-
? "Stop playback and re-record"
|
|
12883
|
-
: interruptionMode === "barge-in"
|
|
12884
|
-
? "Speak to interrupt"
|
|
12885
|
-
: "Agent is speaking";
|
|
12886
|
-
micButton.setAttribute("aria-label", ariaLabel);
|
|
12887
|
-
// In "none" mode the button is not actionable during speaking
|
|
12888
|
-
if (interruptionMode === "none") {
|
|
12889
|
-
micButton.style.cursor = "default";
|
|
12890
|
-
}
|
|
12891
|
-
// In "barge-in" mode, add recording class to show mic is hot
|
|
12892
|
-
if (interruptionMode === "barge-in") {
|
|
12893
|
-
micButton.classList.add("persona-voice-recording");
|
|
12894
|
-
}
|
|
12895
|
-
};
|
|
12896
|
-
|
|
10457
|
+
const applyRuntypeMicRecordingStyles = () => withMicStateStyles((s) => s.recording());
|
|
10458
|
+
const applyRuntypeMicProcessingStyles = () => withMicStateStyles((s) => s.processing());
|
|
10459
|
+
const applyRuntypeMicSpeakingStyles = () => withMicStateStyles((s) => s.speaking());
|
|
12897
10460
|
/** Restore mic button to idle state (icon, colors, aria-label, cursor) */
|
|
12898
|
-
const removeRuntypeMicStateStyles = () =>
|
|
12899
|
-
if (!micButton) return;
|
|
12900
|
-
removeAllVoiceStateClasses();
|
|
12901
|
-
setMicState("idle");
|
|
12902
|
-
if (originalMicStyles) {
|
|
12903
|
-
micButton.style.backgroundColor = originalMicStyles.backgroundColor ?? "";
|
|
12904
|
-
micButton.style.color = originalMicStyles.color ?? "";
|
|
12905
|
-
micButton.style.borderColor = originalMicStyles.borderColor ?? "";
|
|
12906
|
-
swapMicIcon(originalMicStyles.iconName, originalMicStyles.color || "currentColor");
|
|
12907
|
-
originalMicStyles = null;
|
|
12908
|
-
}
|
|
12909
|
-
micButton.style.cursor = "";
|
|
12910
|
-
micButton.setAttribute("aria-label", "Start voice recognition");
|
|
12911
|
-
};
|
|
10461
|
+
const removeRuntypeMicStateStyles = () => withMicStateStyles((s) => s.reset());
|
|
12912
10462
|
|
|
12913
10463
|
// Wire up mic button click handler
|
|
12914
10464
|
const handleMicButtonClick = () => {
|
|
@@ -13510,8 +11060,9 @@ export const createAgentExperience = (
|
|
|
13510
11060
|
clearChatButton.addEventListener("click", () => {
|
|
13511
11061
|
// With history available this affordance is "New conversation": a local
|
|
13512
11062
|
// view clear would leave the server record open and unreachable.
|
|
13513
|
-
|
|
13514
|
-
|
|
11063
|
+
const shell = historyShell;
|
|
11064
|
+
if (shell?.historyAvailable()) {
|
|
11065
|
+
void shell.startNewConversation({ focusComposer: true }).catch((error) => {
|
|
13515
11066
|
if (config.debug) {
|
|
13516
11067
|
// eslint-disable-next-line no-console
|
|
13517
11068
|
console.warn("[AgentWidget] New conversation failed:", error);
|
|
@@ -14353,7 +11904,7 @@ export const createAgentExperience = (
|
|
|
14353
11904
|
closeButtonWrapper.className = "persona-absolute persona-top-4 persona-right-4 persona-z-50";
|
|
14354
11905
|
// A mounted rail owns the conversation column; anchoring to the
|
|
14355
11906
|
// container would float the button over the rail.
|
|
14356
|
-
const anchor = railColumn ?? container;
|
|
11907
|
+
const anchor = historyShell?.railColumn ?? container;
|
|
14357
11908
|
anchor.style.position = "relative";
|
|
14358
11909
|
anchor.appendChild(closeButtonWrapper);
|
|
14359
11910
|
} else {
|
|
@@ -14492,7 +12043,7 @@ export const createAgentExperience = (
|
|
|
14492
12043
|
// Position to the left of the close button (which is at right: 1rem/16px)
|
|
14493
12044
|
// Close button is ~32px wide, plus small gap = 48px from right
|
|
14494
12045
|
clearChatButtonWrapper.style.right = "48px";
|
|
14495
|
-
const anchor = railColumn ?? container;
|
|
12046
|
+
const anchor = historyShell?.railColumn ?? container;
|
|
14496
12047
|
anchor.style.position = "relative";
|
|
14497
12048
|
anchor.appendChild(clearChatButtonWrapper);
|
|
14498
12049
|
} else {
|
|
@@ -14642,13 +12193,17 @@ export const createAgentExperience = (
|
|
|
14642
12193
|
config.getIdentityProof !== previousIdentityProof ||
|
|
14643
12194
|
config.clientToken !== previousClientToken
|
|
14644
12195
|
) {
|
|
14645
|
-
closeHistory({ restoreFocus: false });
|
|
14646
|
-
installHistoryProvider();
|
|
12196
|
+
historyShell?.closeHistory({ restoreFocus: false });
|
|
12197
|
+
historyShell?.installHistoryProvider();
|
|
14647
12198
|
}
|
|
14648
|
-
syncHistoryChromeImpl();
|
|
12199
|
+
historyShell?.syncHistoryChromeImpl();
|
|
14649
12200
|
// Presentation may have flipped between panel and rail; the open view
|
|
14650
12201
|
// instance moves hosts rather than being recreated.
|
|
14651
12202
|
syncHistoryPresentation();
|
|
12203
|
+
// Turning history on loads the shell, which installs and syncs itself.
|
|
12204
|
+
if (historyFeatureEnabled() && !historyShell) {
|
|
12205
|
+
void ensureHistoryShell().catch(() => {});
|
|
12206
|
+
}
|
|
14652
12207
|
renderMessagesWithPlugins(
|
|
14653
12208
|
messagesWrapper,
|
|
14654
12209
|
session.getMessages(),
|
|
@@ -15541,30 +13096,40 @@ export const createAgentExperience = (
|
|
|
15541
13096
|
conversationId: string,
|
|
15542
13097
|
opts?: { focus?: boolean }
|
|
15543
13098
|
): Promise<void> {
|
|
15544
|
-
return
|
|
15545
|
-
|
|
15546
|
-
|
|
13099
|
+
return withHistoryShell((shell) =>
|
|
13100
|
+
shell.openHistoryConversation(conversationId, {
|
|
13101
|
+
focusComposer: opts?.focus === true,
|
|
13102
|
+
})
|
|
13103
|
+
);
|
|
15547
13104
|
},
|
|
15548
13105
|
startNewConversation(opts?: { focus?: boolean }): Promise<void> {
|
|
15549
|
-
return
|
|
13106
|
+
return withHistoryShell((shell) =>
|
|
13107
|
+
shell.startNewConversation({ focusComposer: opts?.focus === true })
|
|
13108
|
+
);
|
|
15550
13109
|
},
|
|
15551
13110
|
warmSession(): void {
|
|
15552
13111
|
warmSession();
|
|
15553
13112
|
},
|
|
15554
13113
|
deleteConversation(conversationId: string): Promise<void> {
|
|
15555
|
-
return
|
|
13114
|
+
return withHistoryShell((shell) =>
|
|
13115
|
+
shell.deleteHistoryConversation(conversationId)
|
|
13116
|
+
);
|
|
15556
13117
|
},
|
|
15557
13118
|
renameConversation(conversationId, title) {
|
|
15558
|
-
return
|
|
13119
|
+
return withHistoryShell((shell) =>
|
|
13120
|
+
shell.updateHistoryConversation(conversationId, { title })
|
|
13121
|
+
);
|
|
15559
13122
|
},
|
|
15560
13123
|
setConversationStarred(conversationId, starred) {
|
|
15561
|
-
return
|
|
13124
|
+
return withHistoryShell((shell) =>
|
|
13125
|
+
shell.updateHistoryConversation(conversationId, { starred })
|
|
13126
|
+
);
|
|
15562
13127
|
},
|
|
15563
13128
|
clearConversationHistory(opts) {
|
|
15564
|
-
return clearConversationHistory(opts);
|
|
13129
|
+
return withHistoryShell((shell) => shell.clearConversationHistory(opts));
|
|
15565
13130
|
},
|
|
15566
13131
|
resetHistoryIdentity(): Promise<{ remoteRevocationConfirmed: boolean }> {
|
|
15567
|
-
return resetHistoryIdentity();
|
|
13132
|
+
return withHistoryShell((shell) => shell.resetHistoryIdentity());
|
|
15568
13133
|
},
|
|
15569
13134
|
async getVisitorToken(): Promise<string | null> {
|
|
15570
13135
|
const store = visitorStore;
|
|
@@ -15573,6 +13138,7 @@ export const createAgentExperience = (
|
|
|
15573
13138
|
return store === visitorStore ? token : null;
|
|
15574
13139
|
},
|
|
15575
13140
|
getHistoryIdentityStatus(): HistoryIdentityStatus {
|
|
13141
|
+
const historyProvider = historyShell?.historyProvider;
|
|
15576
13142
|
if (!historyProvider) {
|
|
15577
13143
|
return historyFeatureEnabled()
|
|
15578
13144
|
? { state: "unavailable", reason: "ineligible_mode" }
|
|
@@ -15581,13 +13147,17 @@ export const createAgentExperience = (
|
|
|
15581
13147
|
return historyProvider.getIdentityStatus();
|
|
15582
13148
|
},
|
|
15583
13149
|
showHistory(opts): Promise<void> {
|
|
15584
|
-
|
|
15585
|
-
|
|
15586
|
-
|
|
15587
|
-
|
|
13150
|
+
// Disabled history never loads the shell just to report unavailable.
|
|
13151
|
+
if (!historyFeatureEnabled()) return Promise.resolve();
|
|
13152
|
+
return withHistoryShell((shell) =>
|
|
13153
|
+
shell.openHistory({
|
|
13154
|
+
...(opts?.returnSurface ? { returnSurface: opts.returnSurface } : {}),
|
|
13155
|
+
...(opts?.focus !== undefined ? { focus: opts.focus } : {}),
|
|
13156
|
+
})
|
|
13157
|
+
);
|
|
15588
13158
|
},
|
|
15589
13159
|
hideHistory(opts): void {
|
|
15590
|
-
closeHistory(
|
|
13160
|
+
historyShell?.closeHistory(
|
|
15591
13161
|
opts?.restoreFocus !== undefined
|
|
15592
13162
|
? { restoreFocus: opts.restoreFocus }
|
|
15593
13163
|
: undefined
|