@agent-native/core 0.157.0 → 0.157.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.
Files changed (111) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/server/jobs/analytics-bigquery-backfill.ts +19 -4
  3. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +7 -2
  4. package/corpus/templates/assets/app/global.css +4 -0
  5. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +1 -2
  6. package/corpus/templates/brain/app/routes/sources.tsx +1 -2
  7. package/corpus/templates/calendar/app/hooks/use-bookings.ts +12 -2
  8. package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +64 -2
  9. package/corpus/templates/calendar/server/handlers/bookings.ts +219 -93
  10. package/corpus/templates/calendar/server/plugins/auth.ts +0 -2
  11. package/corpus/templates/clips/actions/create-folder.ts +7 -1
  12. package/corpus/templates/clips/actions/create-meeting.ts +6 -2
  13. package/corpus/templates/clips/actions/create-recording.ts +5 -3
  14. package/corpus/templates/clips/actions/finalize-recording.ts +97 -65
  15. package/corpus/templates/clips/actions/import-loom-recording.ts +5 -3
  16. package/corpus/templates/clips/actions/request-recording-access.ts +272 -0
  17. package/corpus/templates/clips/actions/stitch-recordings.ts +5 -3
  18. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +38 -63
  19. package/corpus/templates/clips/app/components/player/share-dialog.tsx +75 -75
  20. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +3 -21
  21. package/corpus/templates/clips/app/components/sharing/share-ui.tsx +13 -30
  22. package/corpus/templates/clips/app/i18n/en-US.ts +10 -0
  23. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +5 -5
  24. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +9 -16
  25. package/corpus/templates/clips/app/routes/share.$shareId.tsx +135 -18
  26. package/corpus/templates/clips/changelog/2026-08-14-long-resumable-uploads-now-abort-cleanly-and-show-errors.md +6 -0
  27. package/corpus/templates/clips/changelog/2026-08-14-standardized-share-popovers.md +6 -0
  28. package/corpus/templates/clips/chrome-extension/public/manifest.json +11 -3
  29. package/corpus/templates/clips/chrome-extension/src/github-preview-content.ts +37 -8
  30. package/corpus/templates/clips/chrome-extension/src/github-preview.ts +3 -3
  31. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +8 -25
  32. package/corpus/templates/clips/desktop/src/app.tsx +24 -0
  33. package/corpus/templates/clips/desktop/src/lib/media-capture-constraints.ts +42 -13
  34. package/corpus/templates/clips/desktop/src/lib/recorder.ts +11 -18
  35. package/corpus/templates/clips/desktop/src/overlays/onboarding.tsx +1 -0
  36. package/corpus/templates/clips/desktop/src/shared/config.ts +1 -0
  37. package/corpus/templates/clips/desktop/src/styles.css +24 -9
  38. package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +3 -0
  39. package/corpus/templates/clips/desktop/src-tauri/src/config.rs +8 -0
  40. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +154 -176
  41. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +115 -31
  42. package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +7 -0
  43. package/corpus/templates/clips/server/db/index.ts +1 -0
  44. package/corpus/templates/clips/server/db/schema.ts +1 -0
  45. package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +6 -1
  46. package/corpus/templates/clips/server/lib/emails.ts +23 -0
  47. package/corpus/templates/clips/server/lib/public-agent-context.ts +55 -0
  48. package/corpus/templates/clips/server/lib/recordings.ts +2 -1
  49. package/corpus/templates/clips/server/lib/resumable-upload-cleanup.ts +49 -0
  50. package/corpus/templates/clips/server/lib/s3-upload-provider.ts +34 -7
  51. package/corpus/templates/clips/server/lib/upload-interruption.ts +15 -1
  52. package/corpus/templates/clips/server/lib/upload-lease.ts +94 -9
  53. package/corpus/templates/clips/server/routes/api/agent-context.json.get.ts +82 -7
  54. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +1 -0
  55. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +71 -12
  56. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/interrupt.post.ts +4 -3
  57. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +19 -0
  58. package/corpus/templates/clips/server/routes/api/uploads/reap.post.ts +8 -1
  59. package/corpus/templates/clips/shared/recording-link.ts +4 -0
  60. package/corpus/templates/clips/shared/share-loader-response.ts +2 -1
  61. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +7 -16
  62. package/corpus/templates/factory/AGENTS.md +5 -1
  63. package/corpus/templates/factory/README.md +6 -2
  64. package/corpus/templates/factory/actions/view-screen.ts +15 -0
  65. package/corpus/templates/factory/app/components/factory/FactoryAgentsView.tsx +58 -189
  66. package/corpus/templates/factory/app/components/layout/Header.tsx +1 -0
  67. package/corpus/templates/factory/app/components/layout/Layout.tsx +1 -1
  68. package/corpus/templates/factory/app/components/layout/Sidebar.tsx +7 -0
  69. package/corpus/templates/factory/app/hooks/use-navigation-state.ts +3 -0
  70. package/corpus/templates/factory/app/i18n/en-US.ts +3 -1
  71. package/corpus/templates/factory/app/routes/agents.tsx +9 -0
  72. package/corpus/templates/mail/server/plugins/auth.ts +0 -2
  73. package/corpus/templates/plan/app/components/plan/LocalCodebasePicker.tsx +50 -14
  74. package/corpus/templates/plan/app/pages/PlansPage.tsx +9 -19
  75. package/corpus/templates/slides/app/components/editor/HistoryPanel.tsx +7 -1
  76. package/corpus/templates/slides/app/context/DeckContext.tsx +71 -22
  77. package/corpus/templates/slides/app/lib/editor-shortcuts.ts +46 -0
  78. package/corpus/templates/slides/app/pages/DeckEditor.tsx +13 -0
  79. package/dist/client/AgentPanel.d.ts +13 -1
  80. package/dist/client/AgentPanel.js +12 -10
  81. package/dist/client/AssistantChat.js +25 -5
  82. package/dist/client/HighlightedCodeBlock.js +12 -2
  83. package/dist/client/agent-chat/index.d.ts +1 -0
  84. package/dist/client/agent-chat/index.js +1 -0
  85. package/dist/client/chat/markdown-renderer.js +16 -3
  86. package/dist/client/chat-first/apps-rail.js +1 -1
  87. package/dist/client/components/ui/message-scroller.js +1 -1
  88. package/dist/client/settings/AccountSettingsCard.js +48 -19
  89. package/dist/client/sharing/ShareButton.d.ts +1 -2
  90. package/dist/client/sharing/ShareButton.js +5 -41
  91. package/dist/client/sharing/ShareDialog.js +5 -4
  92. package/dist/client/use-chat-threads.js +10 -14
  93. package/dist/collab/struct-routes.d.ts +1 -1
  94. package/dist/localization/default-messages.d.ts +1 -0
  95. package/dist/localization/default-messages.js +1 -0
  96. package/dist/progress/routes.d.ts +1 -1
  97. package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
  98. package/dist/provider-api/actions/provider-api.d.ts +15 -15
  99. package/dist/server/auth-marketing.d.ts +1 -0
  100. package/dist/server/auth-marketing.js +0 -2
  101. package/dist/server/auth.d.ts +1 -0
  102. package/dist/server/auth.js +18 -0
  103. package/dist/server/onboarding-html.d.ts +1 -0
  104. package/dist/server/onboarding-html.js +2 -88
  105. package/dist/server/realtime-token.d.ts +1 -1
  106. package/dist/shared/markdown-block-split.d.ts +16 -0
  107. package/dist/shared/markdown-block-split.js +81 -5
  108. package/dist/styles/agent-native.css +13 -0
  109. package/dist/voice/voice-cleanup-prompt.js +1 -0
  110. package/package.json +2 -2
  111. package/corpus/templates/clips/shared/mic-audio-cleanup.ts +0 -350
@@ -217,7 +217,10 @@ interface DeckContextType {
217
217
  ) => void;
218
218
  reloadDecks: () => Promise<void>;
219
219
  reloadDecksWithStatus: () => Promise<DeckReloadStatus>;
220
- refreshOpenDeck: (deckId: string) => Promise<Deck | null>;
220
+ refreshOpenDeck: (
221
+ deckId: string,
222
+ options?: { clearPendingWrites?: boolean },
223
+ ) => Promise<Deck | null>;
221
224
  getDeck: (id: string) => Deck | undefined;
222
225
  addSlide: (
223
226
  deckId: string,
@@ -312,6 +315,8 @@ function normalizeActionDeck(value: unknown): Deck | null {
312
315
  const pendingSaves = new Map<string, ReturnType<typeof setTimeout>>();
313
316
  const inFlightSaves = new Set<string>();
314
317
  const inFlightSaveChains = new Map<string, Promise<void>>();
318
+ const inFlightSaveControllers = new Map<string, AbortController>();
319
+ const deckSaveGenerations = new Map<string, number>();
315
320
  const immediateFlushRequests = new Set<string>();
316
321
  const deckSaveRetryAttempts = new Map<string, number>();
317
322
  const failedSaveDecks = new Set<string>();
@@ -403,7 +408,11 @@ function deckPayload(deck: Deck): Record<string, unknown> {
403
408
  * the `save-deck` action is called first, then any trailing granular ops are
404
409
  * sent through `patch-deck`.
405
410
  */
406
- async function persistDeckOps(deckId: string, ops: GranularOp[]) {
411
+ async function persistDeckOps(
412
+ deckId: string,
413
+ ops: GranularOp[],
414
+ signal?: AbortSignal,
415
+ ) {
407
416
  if (ops[0].op === "full-replace") {
408
417
  // Legacy full-deck write — used by undo/redo and setDeckSlides.
409
418
  // `callAction` bounds it so a stalled save can't wedge `inFlightSaves`
@@ -413,20 +422,28 @@ async function persistDeckOps(deckId: string, ops: GranularOp[]) {
413
422
  await callAction(
414
423
  "save-deck",
415
424
  { deckId, deck: deckPayload(deck) },
416
- { method: "PUT" },
425
+ { method: "PUT", signal },
417
426
  );
418
427
  const trailingOps = ops.slice(1) as PatchDeckOp[];
419
428
  if (trailingOps.length > 0) {
420
- await callAction("patch-deck", {
421
- deckId,
422
- operations: trailingOps,
423
- });
429
+ await callAction(
430
+ "patch-deck",
431
+ {
432
+ deckId,
433
+ operations: trailingOps,
434
+ },
435
+ { signal },
436
+ );
424
437
  }
425
438
  } else {
426
- await callAction("patch-deck", {
427
- deckId,
428
- operations: ops as PatchDeckOp[],
429
- });
439
+ await callAction(
440
+ "patch-deck",
441
+ {
442
+ deckId,
443
+ operations: ops as PatchDeckOp[],
444
+ },
445
+ { signal },
446
+ );
430
447
  }
431
448
  }
432
449
 
@@ -455,15 +472,23 @@ function drainPendingDeckOps(deckId: string): Promise<void> {
455
472
  return Promise.resolve();
456
473
  }
457
474
 
475
+ const generation = deckSaveGenerations.get(deckId) ?? 0;
476
+ const controller =
477
+ typeof AbortController === "undefined" ? null : new AbortController();
478
+ if (controller) inFlightSaveControllers.set(deckId, controller);
458
479
  inFlightSaves.add(deckId);
459
- let succeeded = false;
460
- const next = persistDeckOps(deckId, ops)
480
+ const isCurrentGeneration = () =>
481
+ (deckSaveGenerations.get(deckId) ?? 0) === generation;
482
+ const next = persistDeckOps(deckId, ops, controller?.signal)
461
483
  .then(() => {
462
- succeeded = true;
484
+ if (!isCurrentGeneration()) return;
463
485
  deckSaveRetryAttempts.delete(deckId);
464
486
  failedSaveDecks.delete(deckId);
465
487
  })
466
488
  .catch((err) => {
489
+ // A restore or delete invalidated this request. Its result must not
490
+ // resurrect the old queue or schedule a retry after the boundary.
491
+ if (!isCurrentGeneration()) return;
467
492
  console.error(`Failed to save deck ${deckId}:`, err);
468
493
  const pending = pendingOpsQueue.get(deckId) ?? [];
469
494
  pendingOpsQueue.set(deckId, [...ops, ...pending]);
@@ -488,10 +513,13 @@ function drainPendingDeckOps(deckId: string): Promise<void> {
488
513
  .finally(() => {
489
514
  if (inFlightSaveChains.get(deckId) === next) {
490
515
  inFlightSaveChains.delete(deckId);
516
+ if (controller && inFlightSaveControllers.get(deckId) === controller) {
517
+ inFlightSaveControllers.delete(deckId);
518
+ }
491
519
  inFlightSaves.delete(deckId);
492
520
  const flushImmediately = immediateFlushRequests.delete(deckId);
493
521
  notifySaveListeners();
494
- if (succeeded && flushImmediately) {
522
+ if (flushImmediately) {
495
523
  void drainPendingDeckOps(deckId);
496
524
  }
497
525
  }
@@ -506,13 +534,14 @@ function drainPendingDeckOps(deckId: string): Promise<void> {
506
534
  * follow-up drain chained by immediateFlushRequests for ops queued while a
507
535
  * save was already running, and any requeued retry after a failed attempt.
508
536
  * Used when a caller must not proceed (e.g. firing an agent request against
509
- * a slide) until an "immediate" persistence op has actually reached the
510
- * server. Throws if the save ultimately fails after retries exhaust, since
511
- * `drainPendingDeckOps` swallows save errors internally to drive its own
512
- * retry loop and its promise always resolves regardless of outcome.
537
+ * a slide or restoring a saved version) until every write issued before that
538
+ * boundary has reached the server. Throws if the save ultimately fails after
539
+ * retries exhaust, since `drainPendingDeckOps` swallows save errors internally
540
+ * to drive its own retry loop and its promise always resolves regardless of
541
+ * outcome.
513
542
  */
514
543
  async function flushDeckSave(deckId: string): Promise<void> {
515
- for (let i = 0; i < 40; i++) {
544
+ while (true) {
516
545
  const active = inFlightSaveChains.get(deckId);
517
546
  if (active) {
518
547
  await active;
@@ -647,6 +676,8 @@ function flushPendingSaves() {
647
676
  }
648
677
 
649
678
  function discardPendingDeckOps(deckId: string) {
679
+ deckSaveGenerations.set(deckId, (deckSaveGenerations.get(deckId) ?? 0) + 1);
680
+ inFlightSaveControllers.get(deckId)?.abort();
650
681
  const timer = pendingSaves.get(deckId);
651
682
  if (timer) clearTimeout(timer);
652
683
  pendingSaves.delete(deckId);
@@ -1354,7 +1385,15 @@ export function DeckProvider({ children }: { children: ReactNode }) {
1354
1385
  * this, chat-driven edits land in the editor with Undo disabled.
1355
1386
  */
1356
1387
  const applyRemoteDeckUpdate = useCallback(
1357
- (updated: Deck, label = "Agent edit") => {
1388
+ (
1389
+ updated: Deck,
1390
+ label = "Agent edit",
1391
+ options?: { clearPendingWrites?: boolean },
1392
+ ) => {
1393
+ if (options?.clearPendingWrites) {
1394
+ discardPendingDeckOps(updated.id);
1395
+ dirtyDeckIdsRef.current.delete(updated.id);
1396
+ }
1358
1397
  const before = decksRef.current.find((d) => d.id === updated.id);
1359
1398
  if (
1360
1399
  before &&
@@ -1447,7 +1486,10 @@ export function DeckProvider({ children }: { children: ReactNode }) {
1447
1486
  // - Dirty deck / unsaved local create → additive merge only: surface
1448
1487
  // agent-added slides without ever overwriting or dropping local slides.
1449
1488
  const refetchOpenDeckIfChanged = useCallback(
1450
- async (currentOpenId: string): Promise<Deck | null> => {
1489
+ async (
1490
+ currentOpenId: string,
1491
+ options?: { clearPendingWrites?: boolean },
1492
+ ): Promise<Deck | null> => {
1451
1493
  const requestId =
1452
1494
  (openDeckRequestIdByDeckRef.current.get(currentOpenId) ?? 0) + 1;
1453
1495
  openDeckRequestIdByDeckRef.current.set(currentOpenId, requestId);
@@ -1459,6 +1501,13 @@ export function DeckProvider({ children }: { children: ReactNode }) {
1459
1501
  // still-pending create — nothing authoritative to reconcile.
1460
1502
  if (!serverDeck) return null;
1461
1503
  const clientDeck = decksRef.current.find((d) => d.id === currentOpenId);
1504
+ if (options?.clearPendingWrites) {
1505
+ lastExternalUpdateRef.current = Date.now();
1506
+ applyRemoteDeckUpdate(serverDeck, "Deck restored", {
1507
+ clearPendingWrites: true,
1508
+ });
1509
+ return serverDeck;
1510
+ }
1462
1511
 
1463
1512
  const hasLocalEdits =
1464
1513
  pendingCreateIdsRef.current.has(currentOpenId) ||
@@ -0,0 +1,46 @@
1
+ interface EditorShortcutEvent {
2
+ key: string;
3
+ altKey: boolean;
4
+ ctrlKey: boolean;
5
+ metaKey: boolean;
6
+ shiftKey: boolean;
7
+ repeat: boolean;
8
+ isComposing: boolean;
9
+ target: EventTarget | null;
10
+ }
11
+
12
+ function isEditableSurface(target: EventTarget | null): boolean {
13
+ return (
14
+ target instanceof HTMLElement &&
15
+ (target.closest(
16
+ "input, textarea, select, [contenteditable='true'], [role='textbox']",
17
+ ) !== null ||
18
+ target.isContentEditable)
19
+ );
20
+ }
21
+
22
+ export function shouldStopSlidesItalicShortcut(event: EditorShortcutEvent) {
23
+ if (
24
+ event.repeat ||
25
+ event.isComposing ||
26
+ event.key.toLowerCase() !== "i" ||
27
+ event.altKey ||
28
+ event.shiftKey ||
29
+ !(event.ctrlKey || event.metaKey)
30
+ ) {
31
+ return false;
32
+ }
33
+
34
+ return true;
35
+ }
36
+
37
+ export function isSlidesItalicEditableTarget(event: EditorShortcutEvent) {
38
+ return isEditableSurface(event.target);
39
+ }
40
+
41
+ export function shouldSuppressSlidesItalicShortcut(event: EditorShortcutEvent) {
42
+ return (
43
+ shouldStopSlidesItalicShortcut(event) &&
44
+ !isSlidesItalicEditableTarget(event)
45
+ );
46
+ }
@@ -76,6 +76,7 @@ import {
76
76
  shouldShowDeckEditorSkeleton,
77
77
  } from "@/lib/deck-editor-loading";
78
78
  import { getPreset } from "@/lib/design-systems";
79
+ import { shouldSuppressSlidesItalicShortcut } from "@/lib/editor-shortcuts";
79
80
  import { exportDeckToGoogleSlides } from "@/lib/export-google-slides-client";
80
81
  import { exportDeckAsPdf } from "@/lib/export-pdf-client";
81
82
  import { exportDeckAsPptx } from "@/lib/export-pptx-client";
@@ -784,6 +785,18 @@ export default function DeckEditor() {
784
785
  document.removeEventListener("keydown", handleTextToolShortcut);
785
786
  }, [canEdit]);
786
787
 
788
+ useEffect(() => {
789
+ const handleItalicShortcut = (event: KeyboardEvent) => {
790
+ if (!shouldSuppressSlidesItalicShortcut(event)) return;
791
+ event.preventDefault();
792
+ event.stopImmediatePropagation();
793
+ };
794
+
795
+ document.addEventListener("keydown", handleItalicShortcut, true);
796
+ return () =>
797
+ document.removeEventListener("keydown", handleItalicShortcut, true);
798
+ }, []);
799
+
787
800
  // Delete key deletes the current slide
788
801
  useEffect(() => {
789
802
  const handleKeyDown = (e: KeyboardEvent) => {
@@ -201,6 +201,18 @@ export interface AgentSidebarProps {
201
201
  chatViewTransitionHandoff?: boolean;
202
202
  /** Namespace for persisted chat state. Use the same key as AgentChatHome. */
203
203
  storageKey?: string;
204
+ /** Namespace for the persisted open/closed preference. Defaults to storageKey. */
205
+ openStorageKey?: string;
206
+ /** API base URL used by the chat surface. */
207
+ apiUrl?: string;
208
+ /** Runtime surface identity used for server-side chat capabilities. */
209
+ agentChatSurface?: AgentChatSurfaceKind;
210
+ /** Show the chat thread tab row. Default: true. */
211
+ showTabBar?: MultiTabAssistantChatProps["showTabBar"];
212
+ /** Keep inline app-opening results inside the current app chat. */
213
+ suppressInlineOpenApp?: AssistantChatProps["suppressInlineOpenApp"];
214
+ /** Placeholder shown in the chat composer. */
215
+ composerPlaceholder?: AssistantChatProps["composerPlaceholder"];
204
216
  /** Open the sidebar when a chat run is active or reconnects. */
205
217
  openOnChatRunning?: boolean;
206
218
  /** Called when the user selects the full-view action from the chat sidebar. */
@@ -222,7 +234,7 @@ export interface AgentSidebarProps {
222
234
  * Wraps app content with a toggleable agent sidebar.
223
235
  * Use AgentToggleButton in your header to open/close it.
224
236
  */
225
- export declare function AgentSidebar({ children, emptyStateText, suggestions, dynamicSuggestions, composerToolbarSlot, composerSlot, onComposerTextChange, imageModelMenu, threadFooterSlot, defaultSidebarWidth, sidebarWidth, position, defaultOpen, animateMobile, animateDesktop, chatViewTransition, chatViewTransitionHandoff, storageKey, openOnChatRunning, onFullscreenRequest, scope, showScopeBadge, browserTabId, threadUrlSync, agentPageHref, suppressFirstRunOnboarding, }: AgentSidebarProps): React.JSX.Element;
237
+ export declare function AgentSidebar({ children, emptyStateText, suggestions, dynamicSuggestions, composerToolbarSlot, composerSlot, onComposerTextChange, imageModelMenu, threadFooterSlot, defaultSidebarWidth, sidebarWidth, position, defaultOpen, animateMobile, animateDesktop, chatViewTransition, chatViewTransitionHandoff, storageKey, openStorageKey, apiUrl, agentChatSurface, showTabBar, suppressInlineOpenApp, composerPlaceholder, openOnChatRunning, onFullscreenRequest, scope, showScopeBadge, browserTabId, threadUrlSync, agentPageHref, suppressFirstRunOnboarding, }: AgentSidebarProps): React.JSX.Element;
226
238
  /**
227
239
  * Focus the agent chat composer input.
228
240
  * Opens the sidebar if closed, then focuses the text input.
@@ -722,7 +722,7 @@ function AgentPanelInner({ defaultMode = "chat", className, style, apiUrl, empty
722
722
  (activeTabMessageCount <= 0 && activeTab.status === "idle")) {
723
723
  return null;
724
724
  }
725
- return (_jsx(ShareButton, { resourceType: "chat_thread", resourceId: activeTab.id, allowedRoles: ["viewer", "editor", "admin"], resourceTitle: activeTab.label || t("agentPanel.chat"), shareUrl: getChatThreadShareUrl(activeTab.id), trigger: "icon", triggerClassName: "h-7 w-7", defaultOpen: onCollapse && shareFromMenuOpen, onOpenChange: onCollapse ? setShareFromMenuOpen : undefined }));
725
+ return (_jsx(ShareButton, { resourceType: "chat_thread", resourceId: activeTab.id, allowedRoles: ["viewer", "editor", "admin"], resourceTitle: activeTab.label || t("agentPanel.chat"), shareUrl: getChatThreadShareUrl(activeTab.id), triggerClassName: "h-7 px-2", defaultOpen: onCollapse && shareFromMenuOpen, onOpenChange: onCollapse ? setShareFromMenuOpen : undefined }));
726
726
  })(), feedbackEnabled ? (_jsx(FeedbackButton, { variant: "icon", side: "bottom", align: "end", chatSessionId: activeChatSessionId, chatStorageKey: storageKey, open: feedbackOpen, onOpenChange: setFeedbackOpen, trigger: _jsx("button", { type: "button", tabIndex: -1, "aria-hidden": "true", className: "pointer-events-none absolute end-0 top-full h-px w-px opacity-0" }) })) : null, mode === "chat" && (_jsx(IconTooltip, { content: t("agentPanel.newChat"), children: _jsx("button", { onClick: addTab, "aria-label": t("agentPanel.newChat"), className: "flex h-6 w-6 items-center justify-center rounded text-muted-foreground hover:text-foreground hover:bg-accent/50", children: _jsx(IconPlus, { size: 14 }) }) })), !onCollapse && mode === "cli" && canUseCodeTools && (_jsx(IconTooltip, { content: t("agentPanel.newTerminal"), children: _jsx("button", { onClick: addCliTab, "aria-label": t("agentPanel.newTerminal"), className: "flex h-6 w-6 items-center justify-center rounded text-muted-foreground hover:text-foreground hover:bg-accent/50", children: _jsx(IconPlus, { size: 14 }) }) })), _jsxs(DropdownMenu, { open: headerMenuOpen, onOpenChange: setHeaderMenuOpen, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx("button", { className: cn("flex h-6 w-6 items-center justify-center rounded text-muted-foreground hover:text-foreground hover:bg-accent/50", (headerMenuOpen || mode === "settings") &&
727
727
  "bg-accent text-foreground"), "aria-label": t("agentPanel.panelOptions"), children: _jsx(IconDotsVertical, { size: 14 }) }) }), _jsxs(DropdownMenuContent, { align: "end", sideOffset: 6, className: "w-48", onCloseAutoFocus: (event) => {
728
728
  // A sibling overlay owns focus next; restoring it to the menu
@@ -786,7 +786,7 @@ function AgentPanelInner({ defaultMode = "chat", className, style, apiUrl, empty
786
786
  ? tabs.find((tab) => tab.id === activeTabId)
787
787
  : undefined;
788
788
  const canShareActiveTab = activeTab && (activeTabMessageCount > 0 || activeTab.status !== "idle");
789
- return (_jsxs(_Fragment, { children: [_jsx("div", { "aria-hidden": "true", "data-agent-page-chat-fade": "", className: "pointer-events-none absolute inset-x-0 top-0 z-50 h-16 bg-gradient-to-b from-background via-background/90 to-transparent opacity-0 transition-opacity duration-150" }), _jsx("div", { className: "pointer-events-none absolute inset-x-0 top-3 z-[60] flex justify-end px-3 sm:top-4 sm:px-4", children: _jsxs("div", { className: "pointer-events-auto flex items-center gap-1", children: [canShareActiveTab ? (_jsx(ShareButton, { resourceType: "chat_thread", resourceId: activeTab.id, allowedRoles: ["viewer", "editor", "admin"], resourceTitle: activeTab.label || t("agentPanel.chat"), shareUrl: getChatThreadShareUrl(activeTab.id), trigger: "icon", triggerClassName: "h-8 w-8 border border-border bg-background/95 shadow-sm backdrop-blur hover:bg-accent" })) : null, _jsxs("button", { type: "button", "data-agent-page-new-chat": "", "aria-label": t("agentPanel.newChat"), onClick: () => {
789
+ return (_jsxs(_Fragment, { children: [_jsx("div", { "aria-hidden": "true", "data-agent-page-chat-fade": "", className: "pointer-events-none absolute inset-x-0 top-0 z-50 h-16 bg-gradient-to-b from-background via-background/90 to-transparent opacity-0 transition-opacity duration-150" }), _jsx("div", { className: "pointer-events-none absolute inset-x-0 top-3 z-[60] flex justify-end px-3 sm:top-4 sm:px-4", children: _jsxs("div", { className: "pointer-events-auto flex items-center gap-1", children: [canShareActiveTab ? (_jsx(ShareButton, { resourceType: "chat_thread", resourceId: activeTab.id, allowedRoles: ["viewer", "editor", "admin"], resourceTitle: activeTab.label || t("agentPanel.chat"), shareUrl: getChatThreadShareUrl(activeTab.id), triggerClassName: "h-8 px-2 border border-border bg-background/95 shadow-sm backdrop-blur hover:bg-accent" })) : null, _jsxs("button", { type: "button", "data-agent-page-new-chat": "", "aria-label": t("agentPanel.newChat"), onClick: () => {
790
790
  addTab();
791
791
  }, className: "inline-flex h-8 items-center gap-1.5 rounded-md border border-border bg-background/95 px-2.5 text-xs font-medium text-foreground shadow-sm backdrop-blur transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: [_jsx(IconPlus, { size: 14 }), _jsx("span", { children: t("agentPanel.newChat") })] })] }) })] }));
792
792
  }, [getChatThreadShareUrl, t]);
@@ -1407,14 +1407,16 @@ export function AgentChatSurface({ mode = "panel", className, defaultMode = "cha
1407
1407
  * Wraps app content with a toggleable agent sidebar.
1408
1408
  * Use AgentToggleButton in your header to open/close it.
1409
1409
  */
1410
- export function AgentSidebar({ children, emptyStateText = "How can I help you?", suggestions, dynamicSuggestions, composerToolbarSlot, composerSlot, onComposerTextChange, imageModelMenu, threadFooterSlot, defaultSidebarWidth, sidebarWidth, position = "right", defaultOpen = false, animateMobile = true, animateDesktop = true, chatViewTransition = false, chatViewTransitionHandoff = false, storageKey, openOnChatRunning = false, onFullscreenRequest, scope, showScopeBadge, browserTabId, threadUrlSync, agentPageHref, suppressFirstRunOnboarding = false, }) {
1410
+ export function AgentSidebar({ children, emptyStateText = "How can I help you?", suggestions, dynamicSuggestions, composerToolbarSlot, composerSlot, onComposerTextChange, imageModelMenu, threadFooterSlot, defaultSidebarWidth, sidebarWidth, position = "right", defaultOpen = false, animateMobile = true, animateDesktop = true, chatViewTransition = false, chatViewTransitionHandoff = false, storageKey, openStorageKey, apiUrl, agentChatSurface, showTabBar = true, suppressInlineOpenApp, composerPlaceholder, openOnChatRunning = false, onFullscreenRequest, scope, showScopeBadge, browserTabId, threadUrlSync, agentPageHref, suppressFirstRunOnboarding = false, }) {
1411
+ const sidebarOpenStorageKey = openStorageKey ?? storageKey;
1411
1412
  const onboardingPreviewMode = useOnboardingPreviewMode();
1412
1413
  const firstRunOnboardingGateOwnsSurface = useFirstRunOnboardingGateOwnsSurface();
1413
1414
  const showFirstRunOnboarding = !firstRunOnboardingGateOwnsSurface &&
1414
1415
  !suppressFirstRunOnboarding &&
1415
1416
  (SHOW_FIRST_RUN_ONBOARDING || onboardingPreviewMode);
1416
1417
  const initialWidth = defaultSidebarWidth ?? sidebarWidth ?? 380;
1417
- const [open, setOpen] = useState(() => openOnChatRunning || getInitialAgentSidebarOpen(defaultOpen, storageKey));
1418
+ const [open, setOpen] = useState(() => openOnChatRunning ||
1419
+ getInitialAgentSidebarOpen(defaultOpen, sidebarOpenStorageKey));
1418
1420
  const [presentationMode, setPresentationMode] = useState(false);
1419
1421
  const [width, setWidth] = useState(initialWidth);
1420
1422
  const [isWideDrawer, setIsWideDrawer] = useState(() => {
@@ -1471,15 +1473,15 @@ export function AgentSidebar({ children, emptyStateText = "How can I help you?",
1471
1473
  const setOpenPersisted = useCallback((next) => {
1472
1474
  setOpen((prev) => {
1473
1475
  const value = typeof next === "function" ? next(prev) : next;
1474
- setAgentSidebarOpenPreference(value, storageKey);
1476
+ setAgentSidebarOpenPreference(value, sidebarOpenStorageKey);
1475
1477
  return value;
1476
1478
  });
1477
- }, [storageKey]);
1479
+ }, [sidebarOpenStorageKey]);
1478
1480
  const applyUrlOpenOverride = useCallback(() => {
1479
- const override = consumeAgentSidebarUrlOpenOverride(storageKey);
1481
+ const override = consumeAgentSidebarUrlOpenOverride(sidebarOpenStorageKey);
1480
1482
  if (override !== null)
1481
1483
  setOpenPersisted(override);
1482
- }, [setOpenPersisted, storageKey]);
1484
+ }, [setOpenPersisted, sidebarOpenStorageKey]);
1483
1485
  useEffect(() => {
1484
1486
  applyUrlOpenOverride();
1485
1487
  return subscribeAgentSidebarUrlChanges(applyUrlOpenOverride);
@@ -1924,11 +1926,11 @@ export function AgentSidebar({ children, emptyStateText = "How can I help you?",
1924
1926
  ? "desktop"
1925
1927
  : undefined, "data-agent-sidebar-layout": panelLayout, "data-agent-sidebar-position": position, "data-agent-sidebar-state": panelOpen ? "open" : "closed", "data-agent-sidebar-resizing": isResizing ? "true" : undefined, "data-agent-sidebar-chat-handoff": chatViewTransitionHandoff ? "true" : undefined, style: chatViewTransition
1926
1928
  ? getAgentChatViewTransitionStyle(panelStyle)
1927
- : panelStyle, inert: sidebarAnimationEnabled && !panelOpen ? true : undefined, "aria-hidden": sidebarAnimationEnabled && !panelOpen ? true : undefined, children: _jsx("div", { className: "agent-sidebar-panel-inner flex min-h-0 flex-1 flex-col", children: _jsx(AgentPanel, { emptyStateText: emptyStateText, suggestions: suggestions, dynamicSuggestions: dynamicSuggestions, composerToolbarSlot: composerToolbarSlot, composerSlot: composerSlot, onComposerTextChange: onComposerTextChange, imageModelMenu: imageModelMenu, threadFooterSlot: threadFooterSlot, missingApiKeySetupLayout: "sidebar", onCollapse: () => setOpenPersisted(false), onSnapTo75Percent: isMobile ? undefined : snapTo75Percent, isWideDrawer: isMobile ? false : isWideDrawer, onExitWideDrawer: isMobile ? undefined : exitWideDrawer, onFullViewRequest: onFullscreenRequest, storageKey: storageKey, scope: scope, showScopeBadge: showScopeBadge, browserTabId: browserTabId, threadUrlSync: threadUrlSync, agentPageHref: agentPageHref, allowSettingsMode: false, chatOnly: true }) }) }), showResizeHandle && isLeft && (_jsx(ResizeHandle, { position: position, onDrag: handleDrag, onResizeStart: handleResizeStart, onResizeEnd: handleResizeEnd }))] })) : null;
1929
+ : panelStyle, inert: sidebarAnimationEnabled && !panelOpen ? true : undefined, "aria-hidden": sidebarAnimationEnabled && !panelOpen ? true : undefined, children: _jsx("div", { className: "agent-sidebar-panel-inner flex min-h-0 flex-1 flex-col", children: _jsx(AgentPanel, { emptyStateText: emptyStateText, suggestions: suggestions, dynamicSuggestions: dynamicSuggestions, composerToolbarSlot: composerToolbarSlot, composerSlot: composerSlot, onComposerTextChange: onComposerTextChange, imageModelMenu: imageModelMenu, threadFooterSlot: threadFooterSlot, apiUrl: apiUrl, agentChatSurface: agentChatSurface, showTabBar: showTabBar, suppressInlineOpenApp: suppressInlineOpenApp, composerPlaceholder: composerPlaceholder, missingApiKeySetupLayout: "sidebar", onCollapse: () => setOpenPersisted(false), onSnapTo75Percent: isMobile ? undefined : snapTo75Percent, isWideDrawer: isMobile ? false : isWideDrawer, onExitWideDrawer: isMobile ? undefined : exitWideDrawer, onFullViewRequest: onFullscreenRequest, storageKey: storageKey, scope: scope, showScopeBadge: showScopeBadge, browserTabId: browserTabId, threadUrlSync: threadUrlSync, agentPageHref: agentPageHref, allowSettingsMode: false, chatOnly: true }) }) }), showResizeHandle && isLeft && (_jsx(ResizeHandle, { position: position, onDrag: handleDrag, onResizeStart: handleResizeStart, onResizeEnd: handleResizeEnd }))] })) : null;
1928
1930
  const drawerPlaceholder = wideDrawerEnabled && !presentationMode && panelOpen ? (_jsx("div", { "aria-hidden": "true", className: "agent-sidebar-drawer-placeholder shrink-0", "data-agent-sidebar-placeholder": "true", style: { width: drawerPlaceholderWidth + 1 } })) : null;
1929
1931
  return (_jsx(AgentSidebarOnboardingContext.Provider, { value: true, children: _jsxs(RealtimeVoiceModeProvider, { browserTabId: browserTabId, children: [showFirstRunOnboarding && (_jsx(Suspense, { fallback: null, children: _jsx(FirstRunOnboarding, {}) })), _jsxs("div", { className: "agent-sidebar-shell flex min-w-0 flex-1 h-screen overflow-hidden", "data-agent-sidebar-position": position, "data-agent-sidebar-resizing": isResizing ? "true" : undefined, children: [isMobile &&
1930
1932
  !presentationMode &&
1931
- (mobileAnimationEnabled ? shouldRenderPanel : open) && (_jsx("div", { className: cn("agent-sidebar-backdrop fixed inset-0 bg-foreground/40", mobileAnimationEnabled && !panelOpen && "pointer-events-none"), "data-agent-sidebar-animation": mobileAnimationEnabled ? "mobile" : undefined, "data-agent-sidebar-state": panelOpen ? "open" : "closed", style: { zIndex: SIDEBAR_OVERLAY_Z_INDEX - 1 }, onClick: () => setOpenPersisted(false) })), shouldMountPanel ? _jsx(URLSync, { browserTabId: browserTabId }) : null, isLeft && !presentationMode ? sidebar : null, isLeft && !presentationMode ? drawerPlaceholder : null, _jsx("div", { className: "agent-sidebar-main-surface flex flex-1 flex-col overflow-auto min-w-0", "data-agent-sidebar-main-position": position, "data-agent-sidebar-main-state": !isMobile && !presentationMode && panelOpen ? "open" : "closed", "data-agent-sidebar-resizing": isResizing ? "true" : undefined, children: _jsx(ScreenRefreshBoundary, { children: children }) }), !isLeft && !presentationMode ? drawerPlaceholder : null, !isLeft && !presentationMode ? sidebar : null] })] }) }));
1933
+ (mobileAnimationEnabled ? shouldRenderPanel : open) && (_jsx("div", { className: cn("agent-sidebar-backdrop fixed inset-0 bg-foreground/40", mobileAnimationEnabled && !panelOpen && "pointer-events-none"), "data-agent-sidebar-animation": mobileAnimationEnabled ? "mobile" : undefined, "data-agent-sidebar-state": panelOpen ? "open" : "closed", style: { zIndex: SIDEBAR_OVERLAY_Z_INDEX - 1 }, onClick: () => setOpenPersisted(false) })), shouldMountPanel ? _jsx(URLSync, { browserTabId: browserTabId }) : null, isResizing ? (_jsx("div", { "aria-hidden": "true", className: "agent-sidebar-resize-overlay" })) : null, isLeft && !presentationMode ? sidebar : null, isLeft && !presentationMode ? drawerPlaceholder : null, _jsx("div", { className: "agent-sidebar-main-surface flex flex-1 flex-col overflow-auto min-w-0", "data-agent-sidebar-main-position": position, "data-agent-sidebar-main-state": !isMobile && !presentationMode && panelOpen ? "open" : "closed", "data-agent-sidebar-resizing": isResizing ? "true" : undefined, children: _jsx(ScreenRefreshBoundary, { children: children }) }), !isLeft && !presentationMode ? drawerPlaceholder : null, !isLeft && !presentationMode ? sidebar : null] })] }) }));
1932
1934
  }
1933
1935
  /**
1934
1936
  * Focus the agent chat composer input.
@@ -4171,13 +4171,33 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
4171
4171
  }
4172
4172
  void addToQueue(suggestion);
4173
4173
  }, className: "w-full px-2 py-1 text-center text-[13px] text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", children: suggestion }, suggestion))) })) : null, showInlineEmptyThreadFooterSlot ? (_jsx("div", { className: "agent-thread-footer-slot agent-thread-footer-slot--empty", children: resolvedThreadFooterSlot })) : null] })) : (_jsxs(MessageScrollerContent, { className: "agent-thread-content gap-4 px-4 py-4", children: [_jsx(AssistantMessageListErrorBoundary, { resetKey: messageListResetKey, children: _jsx(UserStoppedRunContext.Provider, { value: wasRecentlyStoppedRun, children: _jsx(ThreadPrimitive.Messages
4174
- // assistant-ui indexes message parts through tap resources.
4175
- // Keep this key tied to that shape so clear/add transitions remount stale
4176
- // lookups without remounting on every streamed text update.
4177
- , { components: {
4174
+ // Deliberately NOT keyed on part structure. Doing that
4175
+ // remounted the whole transcript every time a tool call
4176
+ // started or a placeholder id was rewritten — a flash and a
4177
+ // lost scroll position in the middle of an answer. The
4178
+ // error boundary above is the mechanism for assistant-ui's
4179
+ // stale tap-resource errors: it catches them, clears, and
4180
+ // retries, and its retry signature includes the reset key so
4181
+ // a genuinely new structure always gets a fresh budget.
4182
+ // `assistant-ui-part-churn.spec.tsx` drives append, mutate,
4183
+ // rename and splice through both the import and streaming
4184
+ // paths and records that no such error occurs.
4185
+ , {
4186
+ // Deliberately NOT keyed on part structure. Doing that
4187
+ // remounted the whole transcript every time a tool call
4188
+ // started or a placeholder id was rewritten — a flash and a
4189
+ // lost scroll position in the middle of an answer. The
4190
+ // error boundary above is the mechanism for assistant-ui's
4191
+ // stale tap-resource errors: it catches them, clears, and
4192
+ // retries, and its retry signature includes the reset key so
4193
+ // a genuinely new structure always gets a fresh budget.
4194
+ // `assistant-ui-part-churn.spec.tsx` drives append, mutate,
4195
+ // rename and splice through both the import and streaming
4196
+ // paths and records that no such error occurs.
4197
+ components: {
4178
4198
  UserMessage: AssistantChatUserMessageItem,
4179
4199
  AssistantMessage: AssistantChatAssistantMessageItem,
4180
- } }, messageListResetKey) }) }), visibleLoopLimit && !showRunningInUI && (_jsx(MessageScrollerItem, { children: _jsx(LoopLimitContinueCard, { info: visibleLoopLimit, onContinue: () => {
4200
+ } }) }) }), visibleLoopLimit && !showRunningInUI && (_jsx(MessageScrollerItem, { children: _jsx(LoopLimitContinueCard, { info: visibleLoopLimit, onContinue: () => {
4181
4201
  setShowContinue(false);
4182
4202
  setLoopLimitInfo(null);
4183
4203
  addToQueue("Continue from where you left off.", undefined, undefined, undefined, undefined, "queued", "continue");
@@ -44,6 +44,13 @@ export function HighlightedCodeBlock({ code, lang, containerClass, streaming = f
44
44
  const renderedHashRef = useRef(null);
45
45
  const debounceTimerRef = useRef(null);
46
46
  const cancelledRef = useRef(false);
47
+ // Whether this block has already put code on screen. Reserving space
48
+ // invisibly is right on FIRST paint — it avoids an unhighlighted→highlighted
49
+ // flash — but once code is visible, hiding it again to wait for Shiki blanks
50
+ // a code box the user was reading. That is what happened whenever the block
51
+ // lost its html (a re-highlight after the content changed): readable code
52
+ // went blank mid-answer.
53
+ const hasPaintedRef = useRef(false);
47
54
  useEffect(() => {
48
55
  cancelledRef.current = false;
49
56
  return () => {
@@ -108,11 +115,14 @@ export function HighlightedCodeBlock({ code, lang, containerClass, streaming = f
108
115
  }
109
116
  }, [code, lang, streaming, loadHighlighter]);
110
117
  if (html) {
118
+ hasPaintedRef.current = true;
111
119
  return (_jsx("div", { className: containerClass, dangerouslySetInnerHTML: { __html: html } }));
112
120
  }
113
121
  // Streaming: show plain growing text (previous html already kept above when
114
122
  // available). Non-streaming first paint: reserve space invisibly so we never
115
- // flash unhighlighted → highlighted.
116
- const showPlain = streaming || failed;
123
+ // flash unhighlighted → highlighted. Once painted, never hide again.
124
+ const showPlain = streaming || failed || hasPaintedRef.current;
125
+ if (showPlain)
126
+ hasPaintedRef.current = true;
117
127
  return (_jsx("div", { className: containerClass, "aria-busy": !showPlain && !failed, children: _jsx("pre", { children: _jsx("code", { className: lang ? `language-${lang}` : undefined, style: showPlain ? undefined : { visibility: "hidden" }, children: code }) }) }));
118
128
  }
@@ -1,3 +1,4 @@
1
+ export { MemoryRouter as AgentChatMemoryRouter } from "react-router";
1
2
  export { AgentAskPopover, type AgentAskPopoverProps, } from "../AgentAskPopover.js";
2
3
  export { addContextToAgentChat, appendAgentChatContextToMessage, clearAgentChatContext, formatAgentChatContextItemsForPrompt, insertAgentComposerReference, listAgentChatContext, normalizeAgentComposerReference, refreshAgentChatContext, removeAgentChatContextItem, requestAgentChatThreadOpen, requestAgentTaskOpen, sendToAgentChat, sendToAgentChatAndConfirm, reportAgentChatSubmitResult, AGENT_CHAT_SUBMIT_RESULT_EVENT, parseSubmitChatMessage, setAgentChatContextItem, setContextToAgentChat, generateTabId, type ParsedSubmitChat, type AgentChatOpenTaskRequest, type AgentChatOpenThreadRequest, type AgentChatContextItem, type AgentChatContextMessage, type AgentChatContextMutationOptions, type AgentChatContextRemoveOptions, type AgentChatContextSetOptions, type AgentChatContextState, type AgentChatMessage, type AgentChatSubmitResult, type SendToAgentChatAndConfirmResult, type AgentComposerReference, type AgentComposerReferenceInsertOptions, type AgentComposerReferenceInsertPayload, } from "../agent-chat.js";
3
4
  export { saveAgentEngineApiKey, saveAgentEngineProviderSettings, type AgentEngineProvider, type SaveAgentEngineApiKeyOptions, type SaveAgentEngineProviderSettingsOptions, } from "../agent-engine-key.js";
@@ -1,3 +1,4 @@
1
+ export { MemoryRouter as AgentChatMemoryRouter } from "react-router";
1
2
  export { AgentAskPopover, } from "../AgentAskPopover.js";
2
3
  export { addContextToAgentChat, appendAgentChatContextToMessage, clearAgentChatContext, formatAgentChatContextItemsForPrompt, insertAgentComposerReference, listAgentChatContext, normalizeAgentComposerReference, refreshAgentChatContext, removeAgentChatContextItem, requestAgentChatThreadOpen, requestAgentTaskOpen, sendToAgentChat, sendToAgentChatAndConfirm, reportAgentChatSubmitResult, AGENT_CHAT_SUBMIT_RESULT_EVENT, parseSubmitChatMessage, setAgentChatContextItem, setContextToAgentChat, generateTabId, } from "../agent-chat.js";
3
4
  export { saveAgentEngineApiKey, saveAgentEngineProviderSettings, } from "../agent-engine-key.js";
@@ -1,4 +1,4 @@
1
- import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // Owns: lazy react-markdown/shiki loaders, SmoothMarkdownText, MarkdownText,
3
3
  // HighlightedCodeBlock wrapper, and the markdownComponents/markdownUrlTransform
4
4
  // used by every markdown render path in AssistantChat.
@@ -476,13 +476,26 @@ export function SmoothMarkdownText({ text, streaming, resetKey, statusType = "co
476
476
  const visibleText = useSmoothStreamingText(text, shouldAnimate, resetKey);
477
477
  const ReactMarkdown = markdownModule?.default;
478
478
  const gfm = remarkGfmFn;
479
- const markdownBlocks = useMemo(() => (streaming ? splitMarkdownBlocks(visibleText) : null), [streaming, visibleText]);
479
+ // Split whether or not the turn is still streaming. Rendering the finished
480
+ // message as one whole-document ReactMarkdown instead swapped the element
481
+ // tree the instant `streaming` went false, so React unmounted every block and
482
+ // rebuilt it: code blocks re-highlighted, heights collapsed for a frame, and
483
+ // the scroll jumped — the "flash at the end" report. The split is faithful to
484
+ // a whole-document parse (see markdown-block-split.ts), so one tree serves
485
+ // both phases and nothing is rebuilt when streaming stops.
486
+ const markdownBlocks = useMemo(() => splitMarkdownBlocks(visibleText), [visibleText]);
487
+ // The tail renders through the same component as completed blocks, so when it
488
+ // is promoted to a completed block React matches the same type at the same
489
+ // key and keeps its DOM instead of remounting that paragraph.
490
+ const renderedBlocks = useMemo(() => markdownBlocks.tail
491
+ ? [...markdownBlocks.completedBlocks, markdownBlocks.tail]
492
+ : markdownBlocks.completedBlocks, [markdownBlocks]);
480
493
  useEffect(() => {
481
494
  if (!onRevealComplete || !shouldAnimate || visibleText !== text)
482
495
  return;
483
496
  onRevealComplete();
484
497
  }, [onRevealComplete, shouldAnimate, text, visibleText]);
485
- return (_jsx("div", { className: "agent-markdown break-words", "data-status": statusType, "data-streaming": streaming ? "true" : undefined, children: mdReady && ReactMarkdown && gfm ? (markdownBlocks ? (_jsxs(_Fragment, { children: [markdownBlocks.completedBlocks.map((blockText, index) => (_jsx(MemoizedMarkdownBlock, { blockText: blockText }, index))), markdownBlocks.tail ? (_jsx(ReactMarkdown, { remarkPlugins: [gfm], components: markdownComponents, urlTransform: markdownUrlTransform, children: wrapLegacyChartShorthandLines(markdownBlocks.tail) })) : null] })) : (_jsx(ReactMarkdown, { remarkPlugins: [gfm], components: markdownComponents, urlTransform: markdownUrlTransform, children: wrapLegacyChartShorthandLines(visibleText) }))) : (_jsx("span", { style: { whiteSpace: "pre-wrap" }, children: visibleText })) }));
498
+ return (_jsx("div", { className: "agent-markdown break-words", "data-status": statusType, "data-streaming": streaming ? "true" : undefined, children: mdReady && ReactMarkdown && gfm ? (renderedBlocks.map((blockText, index) => (_jsx(MemoizedMarkdownBlock, { blockText: blockText }, index)))) : (_jsx("span", { style: { whiteSpace: "pre-wrap" }, children: visibleText })) }));
486
499
  }
487
500
  // ─── MarkdownText ──────────────────────────────────────────────────────────────
488
501
  export function MarkdownText() {
@@ -98,7 +98,7 @@ export const ChatFirstAppsRail = memo(function ChatFirstAppsRail({ apps, activeA
98
98
  persistLayout({ ...layout, orderedIds: nextOrder });
99
99
  }
100
100
  if (collapsed) {
101
- return (_jsxs("section", { "data-chat-first-apps-rail": true, className: "flex flex-col items-center gap-1 px-1.5 pt-2", "aria-label": copy("workspaceApps"), children: [createTrigger, loading && apps.length === 0
101
+ return (_jsxs("section", { "data-chat-first-apps-rail": true, className: "flex flex-col items-center gap-1 px-1.5 pt-2", "aria-label": copy("workspaceApps"), children: [loading && apps.length === 0
102
102
  ? [0, 1, 2].map((index) => (_jsx(Skeleton, { className: "size-9 rounded-md" }, index)))
103
103
  : visibleApps.map((app) => (_jsx("button", { type: "button", "data-chat-first-app": true, "data-app-id": app.id, className: cn("flex size-9 items-center justify-center rounded-md", activeAppId === app.id
104
104
  ? "text-sidebar-foreground"
@@ -27,6 +27,6 @@ function MessageScrollerItem(props) {
27
27
  function MessageScrollerButton({ className, children, render, ...props }) {
28
28
  const t = useT();
29
29
  return (_jsx(ShadcnMessageScroller.Button, { render: render ??
30
- ((buttonProps) => (_jsx("div", { className: "shrink-0 flex justify-center -mb-1", children: _jsx("button", { ...buttonProps, className: cn("flex h-7 w-7 items-center justify-center rounded-full border border-border bg-background shadow-sm hover:bg-accent", "data-[active=false]:hidden", className, buttonProps.className), "aria-label": t("agentChat.composer.scrollToBottom"), children: children ?? (_jsx(IconChevronDown, { className: "h-3.5 w-3.5 text-muted-foreground" })) }) }))), ...props }));
30
+ ((buttonProps) => (_jsx("div", { className: "pointer-events-none absolute inset-x-0 bottom-2 z-10 flex justify-center", children: _jsx("button", { ...buttonProps, className: cn("pointer-events-auto flex h-7 w-7 items-center justify-center rounded-full border border-border bg-background shadow-sm hover:bg-accent", "data-[active=false]:hidden", className, buttonProps.className), "aria-label": t("agentChat.composer.scrollToBottom"), children: children ?? (_jsx(IconChevronDown, { className: "h-3.5 w-3.5 text-muted-foreground" })) }) }))), ...props }));
31
31
  }
32
32
  export { MessageScroller, MessageScrollerButton, MessageScrollerContent, MessageScrollerItem, MessageScrollerProvider, MessageScrollerViewport, useMessageScroller, useMessageScrollerScrollable, useMessageScrollerVisibility, };