@agent-native/toolkit 0.227.0 → 0.228.0-nightly-20261110212400

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.
@@ -73,7 +73,7 @@ import React, {
73
73
  lazy,
74
74
  Suspense,
75
75
  } from "react";
76
- import { flushSync } from "react-dom";
76
+ import { createPortal, flushSync } from "react-dom";
77
77
 
78
78
  import { setComposerReferenceEventTarget } from "../../composer/runtime-adapters.js";
79
79
  import { Skeleton } from "../../ui/skeleton.js";
@@ -296,6 +296,12 @@ export interface AgentSidebarProps {
296
296
  forceOverlay?: boolean;
297
297
  defaultOpen?: boolean;
298
298
  disableChatShortcut?: boolean;
299
+ /**
300
+ * Draws the panel inside this element instead of beside the page, for an app
301
+ * whose own sidebar holds the agent. The app then owns the panel's width,
302
+ * motion and close button, and the key that toggles the sidebar.
303
+ */
304
+ panelHost?: HTMLElement | null;
299
305
  showCollapseButton?: boolean;
300
306
  animateMobile?: boolean;
301
307
  animateDesktop?: boolean;
@@ -376,6 +382,7 @@ export function AgentSidebar({
376
382
  forceOverlay = false,
377
383
  defaultOpen,
378
384
  disableChatShortcut = false,
385
+ panelHost,
379
386
  showCollapseButton = true,
380
387
  animateMobile = true,
381
388
  animateDesktop = true,
@@ -644,9 +651,18 @@ export function AgentSidebar({
644
651
  !isPerAppChatHosted &&
645
652
  !presentationMode &&
646
653
  (!frameCodeMode || !shouldParentFrameOwnAgentPanel());
654
+ const hostedPanel = ownsPanel && panelHost ? panelHost : null;
655
+ // A host switches between the agent and its other sections, so the chat
656
+ // stays mounted behind them once it has opened.
657
+ const [hostedPanelOpened, setHostedPanelOpened] = useState(false);
658
+ useEffect(() => {
659
+ if (!hostedPanel) setHostedPanelOpened(false);
660
+ else if (open) setHostedPanelOpened(true);
661
+ }, [hostedPanel, open]);
647
662
  const shouldMountPanel =
648
663
  ownsPanel &&
649
664
  (open ||
665
+ hostedPanelOpened ||
650
666
  backgroundPanelActive ||
651
667
  runningTabIds.size > 0 ||
652
668
  hasPendingPanelEvents);
@@ -978,10 +994,12 @@ export function AgentSidebar({
978
994
  : !presentationMode && (frameOwned ? frameSidebarOpen : open)),
979
995
  source: frameOwned ? "frame" : "app",
980
996
  mode: frameOwned ? "code" : "app",
997
+ hosted: Boolean(hostedPanel),
981
998
  });
982
999
  }, [
983
1000
  frameCodeMode,
984
1001
  frameSidebarOpen,
1002
+ hostedPanel,
985
1003
  open,
986
1004
  presentationMode,
987
1005
  hasFrameSidebarState,
@@ -1468,14 +1486,22 @@ export function AgentSidebar({
1468
1486
  useEffect(() => {
1469
1487
  if (!enabled) return;
1470
1488
  const handleKeyDown = (e: KeyboardEvent) => {
1471
- if (isAgentSidebarToggleShortcut(e)) {
1489
+ // A host's element that isn't on the page yet, such as one a loading
1490
+ // page's skeleton holds, has no panel to answer ⌘\, so the shell does.
1491
+ if (!panelHost?.isConnected && isAgentSidebarToggleShortcut(e)) {
1472
1492
  e.preventDefault();
1473
1493
  window.dispatchEvent(
1474
1494
  new CustomEvent("agent-panel:toggle", { detail: { focus: true } }),
1475
1495
  );
1476
1496
  return;
1477
1497
  }
1478
- if (!disableChatShortcut && isAgentPanelChatShortcut(e)) {
1498
+ // A Mac reports ⇧⌘I with a lowercase key, and a hosting page's panel
1499
+ // owns that key.
1500
+ if (
1501
+ !disableChatShortcut &&
1502
+ isAgentPanelChatShortcut(e) &&
1503
+ !(panelHost && e.shiftKey)
1504
+ ) {
1479
1505
  e.preventDefault();
1480
1506
  let selectionText = "";
1481
1507
  try {
@@ -1500,7 +1526,7 @@ export function AgentSidebar({
1500
1526
  };
1501
1527
  document.addEventListener("keydown", handleKeyDown);
1502
1528
  return () => document.removeEventListener("keydown", handleKeyDown);
1503
- }, [disableChatShortcut, enabled]);
1529
+ }, [disableChatShortcut, enabled, panelHost]);
1504
1530
 
1505
1531
  useEffect(() => {
1506
1532
  const handler = (event: MessageEvent) => {
@@ -1602,12 +1628,12 @@ export function AgentSidebar({
1602
1628
  }, [persistWidth]);
1603
1629
 
1604
1630
  const isLeft = effectivePosition === "left";
1605
- const isOverlay = isMobile || forceOverlay;
1606
- const wideDrawerEnabled = isWideDrawer && !isOverlay;
1631
+ const isOverlay = !hostedPanel && (isMobile || forceOverlay);
1632
+ const wideDrawerEnabled = isWideDrawer && !isOverlay && !hostedPanel;
1607
1633
  const mobileAnimationEnabled =
1608
1634
  !presentationMode && isOverlay && animateMobile;
1609
1635
  const desktopAnimationEnabled =
1610
- !presentationMode && !isOverlay && effectiveAnimateDesktop;
1636
+ !presentationMode && !isOverlay && !hostedPanel && effectiveAnimateDesktop;
1611
1637
  const sidebarAnimationEnabled =
1612
1638
  mobileAnimationEnabled || desktopAnimationEnabled;
1613
1639
  const [renderAnimatedPanel, setRenderAnimatedPanel] =
@@ -1713,7 +1739,119 @@ export function AgentSidebar({
1713
1739
  };
1714
1740
  }
1715
1741
 
1716
- const sidebar = shouldRenderPanel ? (
1742
+ const panelBody = (
1743
+ <LazyChunkErrorBoundary
1744
+ fallback={<LazyChunkRetryFallback />}
1745
+ shouldHandleError={isPanelImportError}
1746
+ onError={onPanelLoadFailed}
1747
+ >
1748
+ <Suspense
1749
+ fallback={
1750
+ <div
1751
+ aria-hidden="true"
1752
+ className="flex min-h-0 flex-1 flex-col gap-3 p-3"
1753
+ >
1754
+ <Skeleton className="h-8 w-full" />
1755
+ <div className="flex-1" />
1756
+ <Skeleton className="h-28 w-full rounded-xl" />
1757
+ </div>
1758
+ }
1759
+ >
1760
+ <AgentSidebarPanel
1761
+ onReadyChange={onPanelReadyChange}
1762
+ onReferenceTargetChange={onReferenceTargetChange}
1763
+ onNavigationChange={onNavigationChange}
1764
+ onTabsClosing={onTabsClosing}
1765
+ emptyStateText={emptyStateText}
1766
+ mcpSuggestionExcludedIntegrationIds={
1767
+ mcpSuggestionExcludedIntegrationIds
1768
+ }
1769
+ suggestions={suggestions}
1770
+ dynamicSuggestions={dynamicSuggestions}
1771
+ suggestionPlacement="context-chips"
1772
+ composerToolbarSlot={composerToolbarSlot}
1773
+ composerSlot={composerSlot}
1774
+ // A host's tab opens the chat without taking focus from the tab.
1775
+ {...(hostedPanel ? { composerAutoFocus: false } : {})}
1776
+ composerContextProvider={composerContextProvider}
1777
+ onComposerTextChange={onComposerTextChange}
1778
+ imageModelMenu={imageModelMenu}
1779
+ availableAgents={effectiveAvailableAgents}
1780
+ availableModels={availableModels}
1781
+ modelListLoading={modelListLoading}
1782
+ selectedAgent={effectiveSelectedAgent}
1783
+ onAgentChange={effectiveOnAgentChange}
1784
+ hostedHarness={hostedHarnessEnabled}
1785
+ onConnectProvider={onConnectProvider}
1786
+ builderConnectTransport={builderConnectTransport}
1787
+ onConnectLocalRuntime={onConnectLocalRuntime}
1788
+ runtime={runtime}
1789
+ adapterReloadKey={adapterReloadKey}
1790
+ threadFooterSlot={threadFooterSlot}
1791
+ apiUrl={apiUrl}
1792
+ agentChatSurface={agentChatSurface}
1793
+ desktopIdentityUnauthenticated={desktopIdentityUnauthenticated}
1794
+ desktopIdentityAuthenticated={desktopIdentityAuthenticated}
1795
+ showTabBar={effectiveShowTabBar}
1796
+ suppressInlineOpenApp={suppressInlineOpenApp}
1797
+ composerPlaceholder={composerPlaceholder}
1798
+ showMissingApiKeySetup={showMissingApiKeySetup}
1799
+ setupCardOwner={setupCardOwner}
1800
+ showGuidedQuestions={showGuidedQuestions}
1801
+ missingApiKeySetupLayout="sidebar"
1802
+ defaultMode={defaultMode}
1803
+ onCollapse={() => setOpenPersisted(false)}
1804
+ showCollapseButton={!hostedPanel && showCollapseButton}
1805
+ onSnapTo75Percent={
1806
+ isOverlay || hostedPanel ? undefined : snapTo75Percent
1807
+ }
1808
+ isWideDrawer={wideDrawerEnabled}
1809
+ onExitWideDrawer={
1810
+ isOverlay || hostedPanel ? undefined : exitWideDrawer
1811
+ }
1812
+ onFullViewRequest={onFullscreenRequest}
1813
+ onOpenSettings={onOpenSettings}
1814
+ onNewCliTab={onNewCliTab}
1815
+ onNewUiTab={onNewUiTab}
1816
+ renderCliTab={renderCliTab}
1817
+ newTabMode={newTabMode}
1818
+ newCliTabLabel={newCliTabLabel}
1819
+ newUiTabLabel={newUiTabLabel}
1820
+ storageKey={storageKey}
1821
+ restoreActiveThread={restoreActiveThread}
1822
+ scope={scope}
1823
+ {...(chatHistory ? { chatHistory } : {})}
1824
+ isolateHistoryByScope={isolateHistoryByScope}
1825
+ showScopeBadge={showScopeBadge}
1826
+ browserTabId={resolvedBrowserTabId}
1827
+ threadUrlSync={threadUrlSync}
1828
+ agentPageHref={agentPageHref}
1829
+ thinkingDisplay={thinkingDisplay}
1830
+ showModelSelector={showModelSelector}
1831
+ chatOnly={chatOnly}
1832
+ />
1833
+ </Suspense>
1834
+ </LazyChunkErrorBoundary>
1835
+ );
1836
+
1837
+ const sidebar = !shouldRenderPanel ? null : hostedPanel ? (
1838
+ createPortal(
1839
+ <div
1840
+ className="agent-sidebar-panel agent-kit-density flex min-h-0 flex-1 flex-col overflow-hidden"
1841
+ data-agent-sidebar-layout="hosted"
1842
+ data-agent-sidebar-state={panelOpen ? "open" : "closed"}
1843
+ inert={!panelOpen || undefined}
1844
+ >
1845
+ <div
1846
+ ref={panelElementRef}
1847
+ className="agent-sidebar-panel-inner relative flex min-h-0 flex-1 flex-col"
1848
+ >
1849
+ {panelBody}
1850
+ </div>
1851
+ </div>,
1852
+ hostedPanel,
1853
+ )
1854
+ ) : (
1717
1855
  <>
1718
1856
  {showResizeHandle && !isLeft && (
1719
1857
  <ResizeHandle
@@ -1766,92 +1904,7 @@ export function AgentSidebar({
1766
1904
  ref={panelElementRef}
1767
1905
  className="agent-sidebar-panel-inner relative flex min-h-0 flex-1 flex-col"
1768
1906
  >
1769
- <LazyChunkErrorBoundary
1770
- fallback={<LazyChunkRetryFallback />}
1771
- shouldHandleError={isPanelImportError}
1772
- onError={onPanelLoadFailed}
1773
- >
1774
- <Suspense
1775
- fallback={
1776
- <div
1777
- aria-hidden="true"
1778
- className="flex min-h-0 flex-1 flex-col gap-3 p-3"
1779
- >
1780
- <Skeleton className="h-8 w-full" />
1781
- <div className="flex-1" />
1782
- <Skeleton className="h-28 w-full rounded-xl" />
1783
- </div>
1784
- }
1785
- >
1786
- <AgentSidebarPanel
1787
- onReadyChange={onPanelReadyChange}
1788
- onReferenceTargetChange={onReferenceTargetChange}
1789
- onNavigationChange={onNavigationChange}
1790
- onTabsClosing={onTabsClosing}
1791
- emptyStateText={emptyStateText}
1792
- mcpSuggestionExcludedIntegrationIds={
1793
- mcpSuggestionExcludedIntegrationIds
1794
- }
1795
- suggestions={suggestions}
1796
- dynamicSuggestions={dynamicSuggestions}
1797
- suggestionPlacement="context-chips"
1798
- composerToolbarSlot={composerToolbarSlot}
1799
- composerSlot={composerSlot}
1800
- composerContextProvider={composerContextProvider}
1801
- onComposerTextChange={onComposerTextChange}
1802
- imageModelMenu={imageModelMenu}
1803
- availableAgents={effectiveAvailableAgents}
1804
- availableModels={availableModels}
1805
- modelListLoading={modelListLoading}
1806
- selectedAgent={effectiveSelectedAgent}
1807
- onAgentChange={effectiveOnAgentChange}
1808
- hostedHarness={hostedHarnessEnabled}
1809
- onConnectProvider={onConnectProvider}
1810
- builderConnectTransport={builderConnectTransport}
1811
- onConnectLocalRuntime={onConnectLocalRuntime}
1812
- runtime={runtime}
1813
- adapterReloadKey={adapterReloadKey}
1814
- threadFooterSlot={threadFooterSlot}
1815
- apiUrl={apiUrl}
1816
- agentChatSurface={agentChatSurface}
1817
- desktopIdentityUnauthenticated={desktopIdentityUnauthenticated}
1818
- desktopIdentityAuthenticated={desktopIdentityAuthenticated}
1819
- showTabBar={effectiveShowTabBar}
1820
- suppressInlineOpenApp={suppressInlineOpenApp}
1821
- composerPlaceholder={composerPlaceholder}
1822
- showMissingApiKeySetup={showMissingApiKeySetup}
1823
- setupCardOwner={setupCardOwner}
1824
- showGuidedQuestions={showGuidedQuestions}
1825
- missingApiKeySetupLayout="sidebar"
1826
- defaultMode={defaultMode}
1827
- onCollapse={() => setOpenPersisted(false)}
1828
- showCollapseButton={showCollapseButton}
1829
- onSnapTo75Percent={isOverlay ? undefined : snapTo75Percent}
1830
- isWideDrawer={isOverlay ? false : isWideDrawer}
1831
- onExitWideDrawer={isOverlay ? undefined : exitWideDrawer}
1832
- onFullViewRequest={onFullscreenRequest}
1833
- onOpenSettings={onOpenSettings}
1834
- onNewCliTab={onNewCliTab}
1835
- onNewUiTab={onNewUiTab}
1836
- renderCliTab={renderCliTab}
1837
- newTabMode={newTabMode}
1838
- newCliTabLabel={newCliTabLabel}
1839
- newUiTabLabel={newUiTabLabel}
1840
- storageKey={storageKey}
1841
- restoreActiveThread={restoreActiveThread}
1842
- scope={scope}
1843
- chatHistory={chatHistory}
1844
- isolateHistoryByScope={isolateHistoryByScope}
1845
- showScopeBadge={showScopeBadge}
1846
- browserTabId={resolvedBrowserTabId}
1847
- threadUrlSync={threadUrlSync}
1848
- agentPageHref={agentPageHref}
1849
- thinkingDisplay={thinkingDisplay}
1850
- showModelSelector={showModelSelector}
1851
- chatOnly={chatOnly}
1852
- />
1853
- </Suspense>
1854
- </LazyChunkErrorBoundary>
1907
+ {panelBody}
1855
1908
  </div>
1856
1909
  </div>
1857
1910
  {showResizeHandle && isLeft && (
@@ -1866,7 +1919,7 @@ export function AgentSidebar({
1866
1919
  />
1867
1920
  )}
1868
1921
  </>
1869
- ) : null;
1922
+ );
1870
1923
 
1871
1924
  const drawerPlaceholder =
1872
1925
  wideDrawerEnabled && !presentationMode && panelOpen ? (
@@ -1928,7 +1981,9 @@ export function AgentSidebar({
1928
1981
  className="agent-sidebar-main-surface flex flex-1 flex-col overflow-auto min-w-0"
1929
1982
  data-agent-sidebar-main-position={effectivePosition}
1930
1983
  data-agent-sidebar-main-state={
1931
- !isOverlay && !presentationMode && panelOpen ? "open" : "closed"
1984
+ !isOverlay && !hostedPanel && !presentationMode && panelOpen
1985
+ ? "open"
1986
+ : "closed"
1932
1987
  }
1933
1988
  data-agent-sidebar-resizing={isResizing ? "true" : undefined}
1934
1989
  >
@@ -195,7 +195,7 @@ export interface AssistantChatAdapterContext {
195
195
 
196
196
  export interface AssistantChatProps {
197
197
  /** API endpoint URL. Default: "/_agent-native/agent-chat" */
198
- apiUrl?: string;
198
+ apiUrl?: string | undefined;
199
199
  /** Optional Nitro response-streaming endpoint, usually supplied by VITE_AGENT_NATIVE_AGENT_CHAT_STREAM_URL. */
200
200
  streamingUrl?: string;
201
201
  /** Stable tab identifier passed to the adapter for event correlation */
@@ -218,21 +218,21 @@ export interface AssistantChatProps {
218
218
  * Identifies which surface hosts this chat. Defaults to "app", which keeps
219
219
  * dev filesystem/bash code-editing tools out of in-product sidebars.
220
220
  */
221
- agentChatSurface?: AgentChatSurfaceKind;
221
+ agentChatSurface?: AgentChatSurfaceKind | undefined;
222
222
  /** Whether the desktop host is currently showing its unauthenticated identity gate. */
223
- desktopIdentityUnauthenticated?: boolean;
223
+ desktopIdentityUnauthenticated?: boolean | undefined;
224
224
  /** Whether the desktop host has just established its authenticated identity session. */
225
- desktopIdentityAuthenticated?: boolean;
225
+ desktopIdentityAuthenticated?: boolean | undefined;
226
226
  /** Route completed first-party open_app calls through the host app pane. */
227
- suppressInlineOpenApp?: boolean;
227
+ suppressInlineOpenApp?: boolean | undefined;
228
228
  /** Placeholder text for empty state */
229
- emptyStateText?: string;
229
+ emptyStateText?: string | undefined;
230
230
  /** Initial empty-thread starters. Completed turns use the runtime's model-authored suggestions. */
231
- suggestions?: AgentSuggestionInput[];
231
+ suggestions?: AgentSuggestionInput[] | undefined;
232
232
  /** Initial empty-thread suggestion behavior; never disables or replaces runtime follow-ups. */
233
- dynamicSuggestions?: AgentDynamicSuggestionsOption;
233
+ dynamicSuggestions?: AgentDynamicSuggestionsOption | undefined;
234
234
  /** Exclude providers whose connection flow is handled by the host app. */
235
- mcpSuggestionExcludedIntegrationIds?: readonly string[];
235
+ mcpSuggestionExcludedIntegrationIds?: readonly string[] | undefined;
236
236
  /** Where suggestions appear. Context chips use a next-action bar at the thread base. */
237
237
  suggestionPlacement?:
238
238
  | "empty-state"
@@ -298,7 +298,9 @@ export interface AssistantChatProps {
298
298
  /** Optional ordering policy for assistant message text and attached widgets. */
299
299
  resolveMessageContentOrder?: AgentMessageContentOrderResolver;
300
300
  /** App-owned context controller, mounted separately for each chat thread. */
301
- composerContextProvider?: React.ComponentType<AssistantChatComposerContextProviderProps>;
301
+ composerContextProvider?:
302
+ | React.ComponentType<AssistantChatComposerContextProviderProps>
303
+ | undefined;
302
304
  /** Optional home content rendered above the composer on an empty chat. */
303
305
  homeIntroSlot?: React.ReactNode;
304
306
  /** Optional content rendered below the composer on an empty chat. */
@@ -309,17 +311,17 @@ export interface AssistantChatProps {
309
311
  * Host apps can use this to render contextual, non-destructive affordances
310
312
  * beside the shared composer without replacing the composer stack.
311
313
  */
312
- onComposerTextChange?: (text: string) => void;
314
+ onComposerTextChange?: ((text: string) => void) | undefined;
313
315
  /** Class applied to the shared composer area for host-specific sizing/skin. */
314
316
  composerAreaClassName?: string;
315
317
  /** Placeholder for the shared composer in its normal idle state. */
316
- composerPlaceholder?: string;
318
+ composerPlaceholder?: string | undefined;
317
319
  /** Controls the compactness of the provider setup panel attached above the composer. */
318
320
  missingApiKeySetupLayout?: BuilderSetupCardLayout;
319
321
  /** Hide the provider setup panel when another host surface owns that prompt. */
320
- showMissingApiKeySetup?: boolean;
322
+ showMissingApiKeySetup?: boolean | undefined;
321
323
  /** Keep chat-owned recovery cards hidden when the host renders the setup card. */
322
- setupCardOwner?: "chat" | "host";
324
+ setupCardOwner?: "chat" | "host" | undefined;
323
325
  /** Visual density for the shared composer shell. */
324
326
  composerLayoutVariant?: AgentComposerLayoutVariant;
325
327
  /** Center the composer on a fresh empty chat instead of pinning it low. */
@@ -331,7 +333,7 @@ export interface AssistantChatProps {
331
333
  /** Optional action rendered beside the voice/send controls. */
332
334
  composerExtraActionButton?: React.ReactNode;
333
335
  /** Show the framework model picker in the shared composer. Defaults to true. */
334
- showModelSelector?: boolean;
336
+ showModelSelector?: boolean | undefined;
335
337
  /** Disable the composer for capability-gated surfaces while still showing history. */
336
338
  composerDisabled?: boolean;
337
339
  /** Block sending while keeping the composer editable. */
@@ -357,14 +359,16 @@ export interface AssistantChatProps {
357
359
  /** Selected effort override for this conversation */
358
360
  selectedEffort?: ReasoningEffort;
359
361
  /** Available engine/model list for the model picker */
360
- availableModels?: Array<{
361
- engine: string;
362
- label: string;
363
- models: string[];
364
- configured: boolean;
365
- }>;
362
+ availableModels?:
363
+ | Array<{
364
+ engine: string;
365
+ label: string;
366
+ models: string[];
367
+ configured: boolean;
368
+ }>
369
+ | undefined;
366
370
  /** Whether the model list is still being resolved. */
367
- modelListLoading?: boolean;
371
+ modelListLoading?: boolean | undefined;
368
372
  /** Whether the model list failed to load. */
369
373
  modelListError?: boolean;
370
374
  /** Retry loading a host-managed model list. */
@@ -374,24 +378,24 @@ export interface AssistantChatProps {
374
378
  /** Callback when user picks an effort from the picker */
375
379
  onEffortChange?: (effort: ReasoningEffort) => void;
376
380
  /** Local or hosted agent runtimes shown above the model list. */
377
- availableAgents?: ComposerAgentOption[];
381
+ availableAgents?: ComposerAgentOption[] | undefined;
378
382
  /** Selected agent runtime identifier. */
379
- selectedAgent?: string;
383
+ selectedAgent?: string | undefined;
380
384
  /** Mark the selected runtime as the hosted tools-only harness mode. */
381
385
  hostedHarness?: boolean;
382
386
  /** Callback when the user picks an agent runtime. */
383
- onAgentChange?: (agent: string) => void;
387
+ onAgentChange?: ((agent: string) => void) | undefined;
384
388
  /**
385
389
  * Optional secondary model menu (e.g. an image-generation model) shown inside
386
390
  * the composer's model picker. Opt-in; chat-only apps omit it.
387
391
  */
388
- imageModelMenu?: ComposerImageModelMenu;
392
+ imageModelMenu?: ComposerImageModelMenu | undefined;
389
393
  /** Callback when user clicks "Fork Chat" in the message actions menu */
390
394
  onForkChat?: () => void | boolean | Promise<void | boolean>;
391
395
  /** Override Builder/provider connect routing for embedded hosts. */
392
- onConnectProvider?: () => void;
396
+ onConnectProvider?: (() => void) | undefined;
393
397
  /** Route local runtime setup through the host's native bridge. */
394
- onConnectLocalRuntime?: (engine: string) => void;
398
+ onConnectLocalRuntime?: ((engine: string) => void) | undefined;
395
399
  /**
396
400
  * Controls the shared composer + menu. Sidebar keeps the full menu by default;
397
401
  * hosts without the sidebar provider stack can use upload-only.
@@ -400,13 +404,13 @@ export interface AssistantChatProps {
400
404
  /** Replace the built-in transport with an AgentKit-native BYO transport. */
401
405
  createTransport?: (context: AssistantChatAdapterContext) => AgentTransport;
402
406
  /** Hide the default guided-question card when another host owns its flow. */
403
- showGuidedQuestions?: boolean;
407
+ showGuidedQuestions?: boolean | undefined;
404
408
  /**
405
409
  * Bring-your-own agent runtime. When supplied, AssistantChat keeps the
406
410
  * standard composer/transcript/tool rendering shell but sends turns through
407
411
  * this runtime instead of the built-in Agent-Native SSE endpoint.
408
412
  */
409
- runtime?: AgentChatRuntime;
413
+ runtime?: AgentChatRuntime | undefined;
410
414
  /**
411
415
  * Explicitly recreate an injected adapter or runtime when its identity
412
416
  * changes. Omit for the production sidebar so parent rerenders do not reset
@@ -446,5 +450,5 @@ export interface AssistantChatProps {
446
450
  * let the reader's own preference apply, which is what surfaces the in-chat
447
451
  * control — a pinned mode hides it rather than leaving a dead menu item.
448
452
  */
449
- thinkingDisplay?: ThinkingDisplay;
453
+ thinkingDisplay?: ThinkingDisplay | undefined;
450
454
  }
@@ -130,6 +130,8 @@ export interface ShareButtonProps {
130
130
  copiedLabel: string;
131
131
  onCopy: () => Promise<boolean | void> | boolean | void;
132
132
  size?: "default" | "sm";
133
+ /** `plain` draws Share as text, set apart from Copy link. */
134
+ appearance?: "joined" | "plain";
133
135
  className?: string;
134
136
  };
135
137
  /** Tooltip for the Copy link button: which link it copies. */
@@ -413,10 +415,11 @@ export function ShareButton(props: ShareButtonProps) {
413
415
  controller.handleShareTabChange,
414
416
  ]);
415
417
  const triggerLabel = t("agentChat.share.share", { defaultValue: "Share" });
418
+ const plainQuickCopy = props.quickCopy?.appearance === "plain";
416
419
  const trigger = (
417
420
  <ShareTrigger
418
421
  label={
419
- props.quickCopy ? (
422
+ props.quickCopy && !plainQuickCopy ? (
420
423
  <span className="flex items-center gap-2">
421
424
  <IconUserPlus aria-hidden="true" />
422
425
  <span>{props.triggerContent ?? triggerLabel}</span>
@@ -428,8 +431,10 @@ export function ShareButton(props: ShareButtonProps) {
428
431
  className={props.triggerClassName}
429
432
  aria-label={triggerLabel}
430
433
  title={triggerLabel}
431
- intent={props.quickCopy ? "primary" : undefined}
432
- emphasis={props.quickCopy ? "solid" : undefined}
434
+ intent={props.quickCopy && !plainQuickCopy ? "primary" : undefined}
435
+ emphasis={
436
+ plainQuickCopy ? "ghost" : props.quickCopy ? "solid" : undefined
437
+ }
433
438
  />
434
439
  );
435
440
  if (isSmallScreen) {
@@ -442,6 +447,7 @@ export function ShareButton(props: ShareButtonProps) {
442
447
  copiedLabel={props.quickCopy.copiedLabel}
443
448
  onCopy={props.quickCopy.onCopy}
444
449
  size={props.quickCopy.size}
450
+ appearance={props.quickCopy.appearance}
445
451
  className={props.quickCopy.className}
446
452
  />
447
453
  ) : (
@@ -482,6 +488,7 @@ export function ShareButton(props: ShareButtonProps) {
482
488
  copiedLabel={props.quickCopy.copiedLabel}
483
489
  onCopy={props.quickCopy.onCopy}
484
490
  size={props.quickCopy.size}
491
+ appearance={props.quickCopy.appearance}
485
492
  className={props.quickCopy.className}
486
493
  />
487
494
  </PopoverAnchor>
@@ -4824,6 +4824,8 @@ function TiptapComposerSurface({
4824
4824
  (e.metaKey || e.ctrlKey) &&
4825
4825
  !e.altKey;
4826
4826
  if (isToggleCombo) {
4827
+ // A page that has already used the key keeps it.
4828
+ if (e.defaultPrevented) return;
4827
4829
  e.preventDefault();
4828
4830
  if (voiceChatActiveRef.current) return;
4829
4831
  if (voice.state === "recording" || voice.state === "starting") {
@@ -559,6 +559,22 @@ describe("TiptapComposer voice chat", () => {
559
559
  expect(voice.start).not.toHaveBeenCalled();
560
560
  });
561
561
 
562
+ it("leaves the dictation key to a page that has already used it", async () => {
563
+ await act(async () => root.render(<Harness />));
564
+
565
+ const event = new KeyboardEvent("keydown", {
566
+ key: "m",
567
+ metaKey: true,
568
+ shiftKey: true,
569
+ cancelable: true,
570
+ });
571
+ event.preventDefault();
572
+ act(() => {
573
+ window.dispatchEvent(event);
574
+ });
575
+ expect(voice.start).not.toHaveBeenCalled();
576
+ });
577
+
562
578
  it("shows a failed session as an amber composer notice with a retry", async () => {
563
579
  activate("error");
564
580
  realtime.errorMessage = "The realtime voice connection failed.";
@@ -13,6 +13,8 @@ export interface JoinedShareControlProps {
13
13
  copiedLabel: string;
14
14
  onCopy: () => Promise<boolean | void> | boolean | void;
15
15
  size?: "default" | "sm";
16
+ /** `plain` sets the two apart, as quiet controls in a toolbar. */
17
+ appearance?: "joined" | "plain";
16
18
  disabled?: boolean;
17
19
  blocked?: boolean;
18
20
  className?: string;
@@ -29,6 +31,7 @@ export const JoinedShareControl = forwardRef<
29
31
  copiedLabel,
30
32
  onCopy,
31
33
  size = "default",
34
+ appearance = "joined",
32
35
  disabled,
33
36
  blocked,
34
37
  className,
@@ -55,6 +58,46 @@ export const JoinedShareControl = forwardRef<
55
58
  };
56
59
 
57
60
  const label = copied ? copiedLabel : copyLabel;
61
+ const plain = appearance === "plain";
62
+ const copyButton = (
63
+ // A click closes a tooltip, so the confirmation holds it open.
64
+ <Tooltip {...(copied ? { open: true } : {})}>
65
+ <TooltipTrigger asChild>
66
+ <Button
67
+ type="button"
68
+ size="sm"
69
+ variant={plain ? "ghost" : "default"}
70
+ className={cn(
71
+ "w-8 shrink-0 px-0 shadow-none",
72
+ plain && "text-muted-foreground hover:text-foreground",
73
+ blocked && "opacity-50",
74
+ )}
75
+ aria-label={label}
76
+ disabled={disabled}
77
+ data-blocked={blocked ? "" : undefined}
78
+ onClick={() => void copy()}
79
+ >
80
+ {copied ? (
81
+ <IconCheck aria-hidden="true" />
82
+ ) : (
83
+ <IconLink aria-hidden="true" />
84
+ )}
85
+ </Button>
86
+ </TooltipTrigger>
87
+ <TooltipContent side="bottom">{label}</TooltipContent>
88
+ </Tooltip>
89
+ );
90
+ if (plain) {
91
+ return (
92
+ <div
93
+ ref={ref}
94
+ className={cn("flex shrink-0 items-center gap-0.5", className)}
95
+ >
96
+ {trigger}
97
+ {copyButton}
98
+ </div>
99
+ );
100
+ }
58
101
  const segmentHeightClass =
59
102
  size === "sm"
60
103
  ? "[&>*:not([data-slot=button-group-separator]):not([data-button-group-ignore]):not(select[aria-hidden=true])]:!h-8"
@@ -69,30 +112,7 @@ export const JoinedShareControl = forwardRef<
69
112
  )}
70
113
  >
71
114
  {trigger}
72
- <Tooltip>
73
- <TooltipTrigger asChild>
74
- <Button
75
- type="button"
76
- size="sm"
77
- variant="default"
78
- className={cn(
79
- "w-8 shrink-0 px-0 shadow-none",
80
- blocked && "opacity-50",
81
- )}
82
- aria-label={label}
83
- disabled={disabled}
84
- data-blocked={blocked ? "" : undefined}
85
- onClick={() => void copy()}
86
- >
87
- {copied ? (
88
- <IconCheck aria-hidden="true" />
89
- ) : (
90
- <IconLink aria-hidden="true" />
91
- )}
92
- </Button>
93
- </TooltipTrigger>
94
- <TooltipContent side="bottom">{label}</TooltipContent>
95
- </Tooltip>
115
+ {copyButton}
96
116
  </ButtonGroup>
97
117
  );
98
118
  });