@agent-native/core 0.0.0-beta-20260820222620 → 0.0.0-beta-20260820224008

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.
@@ -26,6 +26,13 @@ export interface ChatThreadScope {
26
26
  id: string;
27
27
  label?: string;
28
28
  }
29
+ export declare function isAppOwnedChatScope(scope?: ChatThreadScope | null): boolean;
30
+ /**
31
+ * A scoped rail may claim a legacy unscoped thread on its first write, but
32
+ * once a thread has a scope, a non-null incoming scope must match it. App-
33
+ * owned threads additionally require a scope on every subsequent write.
34
+ */
35
+ export declare function threadScopeMismatch(existing?: ChatThreadScope | null, incoming?: ChatThreadScope | null): boolean;
29
36
  export interface ChatThreadSource {
30
37
  platform?: string | null;
31
38
  appId?: string | null;
@@ -241,6 +241,21 @@ export async function repairLegacyChatThreadMessageCounts(options = {}) {
241
241
  function generateId() {
242
242
  return `thread-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
243
243
  }
244
+ export function isAppOwnedChatScope(scope) {
245
+ return scope?.type === "workspace-app" || scope?.type === "desktop-app";
246
+ }
247
+ /**
248
+ * A scoped rail may claim a legacy unscoped thread on its first write, but
249
+ * once a thread has a scope, a non-null incoming scope must match it. App-
250
+ * owned threads additionally require a scope on every subsequent write.
251
+ */
252
+ export function threadScopeMismatch(existing, incoming) {
253
+ if (!existing)
254
+ return false;
255
+ if (!incoming)
256
+ return isAppOwnedChatScope(existing);
257
+ return existing.type !== incoming.type || existing.id !== incoming.id;
258
+ }
244
259
  function readScope(r) {
245
260
  const type = r.scope_type;
246
261
  const id = r.scope_id;
@@ -107,6 +107,8 @@ export interface AgentPanelProps extends Omit<AssistantChatProps, "onSwitchToCli
107
107
  restoreActiveThread?: boolean;
108
108
  /** Ambient resource context rendered as a composer chip. */
109
109
  scope?: import("./use-chat-threads.js").ChatThreadScope | null;
110
+ /** Keep app-owned chat history isolated to the supplied scope. */
111
+ isolateHistoryByScope?: boolean;
110
112
  /** @deprecated Scope context now appears inside the composer. */
111
113
  showScopeBadge?: MultiTabAssistantChatProps["showScopeBadge"];
112
114
  /** Stable browser tab id used for tab-scoped app-state context. */
@@ -239,6 +241,8 @@ export interface AgentSidebarProps {
239
241
  onFullscreenRequest?: () => void;
240
242
  /** Ambient resource context rendered as a composer chip. */
241
243
  scope?: import("./use-chat-threads.js").ChatThreadScope | null;
244
+ /** Keep app-owned chat history isolated to the supplied scope. */
245
+ isolateHistoryByScope?: boolean;
242
246
  /** @deprecated Scope context now appears inside the composer. */
243
247
  showScopeBadge?: MultiTabAssistantChatProps["showScopeBadge"];
244
248
  /** Stable browser tab id used for tab-scoped app-state context. */
@@ -254,7 +258,7 @@ export interface AgentSidebarProps {
254
258
  * Wraps app content with a toggleable agent sidebar.
255
259
  * Use AgentToggleButton in your header to open/close it.
256
260
  */
257
- export declare function AgentSidebar({ children, emptyStateText, suggestions, dynamicSuggestions, composerToolbarSlot, composerSlot, onComposerTextChange, imageModelMenu, availableAgents, availableModels, modelListLoading, selectedAgent, onAgentChange, onConnectLocalRuntime, runtime, adapterReloadKey, threadFooterSlot, defaultSidebarWidth, sidebarWidth, position, defaultOpen, animateMobile, animateDesktop, chatViewTransition, chatViewTransitionHandoff, storageKey, openStorageKey, apiUrl, agentChatSurface, desktopIdentityUnauthenticated, desktopIdentityAuthenticated, showTabBar, suppressInlineOpenApp, composerPlaceholder, openOnChatRunning, onFullscreenRequest, scope, showScopeBadge, browserTabId, threadUrlSync, agentPageHref, suppressFirstRunOnboarding, }: AgentSidebarProps): React.JSX.Element;
261
+ export declare function AgentSidebar({ children, emptyStateText, suggestions, dynamicSuggestions, composerToolbarSlot, composerSlot, onComposerTextChange, imageModelMenu, availableAgents, availableModels, modelListLoading, selectedAgent, onAgentChange, onConnectLocalRuntime, runtime, adapterReloadKey, threadFooterSlot, defaultSidebarWidth, sidebarWidth, position, defaultOpen, animateMobile, animateDesktop, chatViewTransition, chatViewTransitionHandoff, storageKey, openStorageKey, apiUrl, agentChatSurface, desktopIdentityUnauthenticated, desktopIdentityAuthenticated, showTabBar, suppressInlineOpenApp, composerPlaceholder, openOnChatRunning, onFullscreenRequest, scope, isolateHistoryByScope, showScopeBadge, browserTabId, threadUrlSync, agentPageHref, suppressFirstRunOnboarding, }: AgentSidebarProps): React.JSX.Element;
258
262
  /**
259
263
  * Focus the agent chat composer input.
260
264
  * Opens the sidebar if closed, then focuses the text input.
@@ -436,7 +436,7 @@ function CodeAccessUnavailablePanel({ title, description, ctaLabel, ctaHref, sec
436
436
  });
437
437
  }, className: "inline-flex items-center gap-1.5 rounded-md border border-border bg-background px-3 py-1.5 text-xs font-medium text-foreground hover:bg-accent", children: secondaryCtaLabel })] })] }));
438
438
  }
439
- function AgentPanelInner({ defaultMode = "chat", className, style, apiUrl, emptyStateText, emptyStateAddon, suggestions, dynamicSuggestions, showHeader = true, onCollapse, isFullscreen, onToggleFullscreen, onFullViewRequest, onSnapTo75Percent, isWideDrawer, onExitWideDrawer, devAppUrl, storageKey, restoreActiveThread = true, scope, showScopeBadge, browserTabId, threadUrlSync, chatNotice, showTabBar = true, showPageNewChatButton = false, allowSettingsMode = true, chatOnly = false, agentPageHref, codeAccess, ...assistantChatProps }) {
439
+ function AgentPanelInner({ defaultMode = "chat", className, style, apiUrl, emptyStateText, emptyStateAddon, suggestions, dynamicSuggestions, showHeader = true, onCollapse, isFullscreen, onToggleFullscreen, onFullViewRequest, onSnapTo75Percent, isWideDrawer, onExitWideDrawer, devAppUrl, storageKey, restoreActiveThread = true, scope, isolateHistoryByScope = false, showScopeBadge, browserTabId, threadUrlSync, chatNotice, showTabBar = true, showPageNewChatButton = false, allowSettingsMode = true, chatOnly = false, agentPageHref, codeAccess, ...assistantChatProps }) {
440
440
  const t = useT();
441
441
  const navigate = useNavigate();
442
442
  const location = useLocation();
@@ -838,7 +838,7 @@ function AgentPanelInner({ defaultMode = "chat", className, style, apiUrl, empty
838
838
  const isActive = tab.id === activeTabId ||
839
839
  (tab.id === focusParentId &&
840
840
  activeTab?.parentThreadId === tab.id);
841
- return (_jsxs("div", { role: "button", tabIndex: 0, ref: isActive ? activeTabRefCb : undefined, onClick: () => setActiveTabId(tab.id), onKeyDown: activateOnKeyDown(() => setActiveTabId(tab.id)), className: cn("agent-tab relative flex shrink-0 items-center gap-1 rounded-md px-2.5 py-1.5 text-[11px] font-medium cursor-pointer max-w-[150px]", isActive
841
+ return (_jsxs("div", { role: "button", tabIndex: 0, ref: isActive ? activeTabRefCb : undefined, onClick: () => setActiveTabId(tab.id), onKeyDown: activateOnKeyDown(() => setActiveTabId(tab.id)), className: cn("agent-tab relative flex shrink-0 items-center gap-1 rounded-md px-2.5 py-1.5 text-[11px] font-medium cursor-pointer min-w-[56px] max-w-[150px]", isActive
842
842
  ? "bg-accent text-foreground"
843
843
  : "text-muted-foreground hover:bg-accent hover:text-foreground"), children: [_jsx("span", { className: "truncate pe-1", children: tab.label }), tab.status === "running" && (_jsx("span", { className: "h-1.5 w-1.5 shrink-0 rounded-full bg-muted-foreground/50 animate-pulse" })), _jsx("button", { type: "button", "aria-label": t("agentPanel.closeTab"), onClick: (e) => {
844
844
  e.stopPropagation();
@@ -881,7 +881,7 @@ function AgentPanelInner({ defaultMode = "chat", className, style, apiUrl, empty
881
881
  const isActive = tab.id === activeTabId ||
882
882
  (tab.id === focusParentId &&
883
883
  activeTab?.parentThreadId === tab.id);
884
- return (_jsxs("div", { role: "button", tabIndex: 0, ref: isActive ? activeTabRefCb : undefined, onClick: () => setActiveTabId(tab.id), onKeyDown: activateOnKeyDown(() => setActiveTabId(tab.id)), className: cn("agent-tab relative flex shrink-0 items-center gap-1 rounded-md px-2.5 py-1.5 text-[11px] font-medium cursor-pointer max-w-[150px]", isActive
884
+ return (_jsxs("div", { role: "button", tabIndex: 0, ref: isActive ? activeTabRefCb : undefined, onClick: () => setActiveTabId(tab.id), onKeyDown: activateOnKeyDown(() => setActiveTabId(tab.id)), className: cn("agent-tab relative flex shrink-0 items-center gap-1 rounded-md px-2.5 py-1.5 text-[11px] font-medium cursor-pointer min-w-[56px] max-w-[150px]", isActive
885
885
  ? "bg-accent text-foreground"
886
886
  : "text-muted-foreground hover:bg-accent hover:text-foreground"), children: [_jsx("span", { className: "truncate pe-1", children: tab.label }), tab.status === "running" && (_jsx("span", { className: "h-1.5 w-1.5 shrink-0 rounded-full bg-muted-foreground/50 animate-pulse" })), _jsx("button", { type: "button", "aria-label": t("agentPanel.closeTab"), onClick: (e) => {
887
887
  e.stopPropagation();
@@ -899,7 +899,7 @@ function AgentPanelInner({ defaultMode = "chat", className, style, apiUrl, empty
899
899
  })
900
900
  : cliTabs.map((id, i) => (_jsxs("div", { role: "button", tabIndex: 0, ref: id === activeCliTab
901
901
  ? activeTabRefCb
902
- : undefined, onClick: () => setActiveCliTab(id), onKeyDown: activateOnKeyDown(() => setActiveCliTab(id)), className: cn("agent-tab relative flex shrink-0 items-center gap-1 rounded-md px-2.5 py-1.5 text-[11px] font-medium cursor-pointer", id === activeCliTab
902
+ : undefined, onClick: () => setActiveCliTab(id), onKeyDown: activateOnKeyDown(() => setActiveCliTab(id)), className: cn("agent-tab relative flex shrink-0 items-center gap-1 rounded-md px-2.5 py-1.5 text-[11px] font-medium cursor-pointer min-w-[56px]", id === activeCliTab
903
903
  ? "bg-accent text-foreground"
904
904
  : "text-muted-foreground hover:bg-accent hover:text-foreground"), children: [_jsxs("span", { children: ["Terminal ", i + 1] }), _jsx("button", { type: "button", "aria-label": t("agentPanel.closeTab"), onClick: (e) => {
905
905
  e.stopPropagation();
@@ -917,7 +917,7 @@ function AgentPanelInner({ defaultMode = "chat", className, style, apiUrl, empty
917
917
  ? "bg-accent text-foreground"
918
918
  : "text-muted-foreground hover:bg-accent hover:text-foreground"), children: "Main" }), childTabs.map((tab) => (_jsxs("div", { role: "button", tabIndex: 0, ref: tab.id === activeTabId
919
919
  ? activeTabRefCb
920
- : undefined, onClick: () => setActiveTabId(tab.id), onKeyDown: activateOnKeyDown(() => setActiveTabId(tab.id)), className: cn("agent-tab relative flex shrink-0 items-center gap-1 rounded-md px-2 py-1 text-[10px] font-medium cursor-pointer max-w-[140px]", tab.id === activeTabId
920
+ : undefined, onClick: () => setActiveTabId(tab.id), onKeyDown: activateOnKeyDown(() => setActiveTabId(tab.id)), className: cn("agent-tab relative flex shrink-0 items-center gap-1 rounded-md px-2 py-1 text-[10px] font-medium cursor-pointer min-w-[48px] max-w-[140px]", tab.id === activeTabId
921
921
  ? "bg-accent text-foreground"
922
922
  : "text-muted-foreground hover:bg-accent hover:text-foreground"), children: [_jsx("span", { className: "truncate pe-1", children: tab.subAgentName || tab.label }), tab.status === "running" && (_jsx("span", { className: "h-1 w-1 shrink-0 rounded-full bg-muted-foreground/50 animate-pulse" })), _jsx("button", { type: "button", "aria-label": t("agentPanel.closeTab"), onClick: (e) => {
923
923
  e.stopPropagation();
@@ -981,7 +981,7 @@ function AgentPanelInner({ defaultMode = "chat", className, style, apiUrl, empty
981
981
  `margin-left:auto;margin-right:auto;width:100%;}`,
982
982
  } }), SHOW_ONBOARDING && mounted && (_jsx(Suspense, { fallback: null, children: _jsx(OnboardingPanel, {}) })), showFirstRunOnboarding && mounted && !insideAgentSidebar && (_jsx(Suspense, { fallback: null, children: _jsx(FirstRunOnboarding, {}) })), _jsx("div", { className: cn("flex flex-col min-h-0", mode === "chat" ? "flex-1" : "shrink-0"), children: mounted && (_jsx(Suspense, { fallback: _jsx(ChatLoadingSkeleton, { renderHeader: showHeader ? renderChatHeader : undefined, centerComposerWhenEmpty: assistantChatProps.centerComposerWhenEmpty, composerSlot: assistantChatProps.composerSlot, composerAreaClassName: assistantChatProps.composerAreaClassName, composerLayoutVariant: assistantChatProps.composerLayoutVariant }), children: _jsx(MultiTabAssistantChatLazy, { ...assistantChatProps, agentChatSurface: effectiveAgentChatSurface, apiUrl: apiUrl, showHeader: false, renderHeader: showHeader ? renderChatHeader : undefined, showTabBar: showTabBar, renderOverlay: showPageNewChatButton && !showHeader
983
983
  ? renderPageChatOverlay
984
- : undefined, contentHidden: mode !== "chat", emptyStateText: emptyStateText, emptyStateAddon: emptyStateAddon, suggestions: suggestions, dynamicSuggestions: dynamicSuggestions, onSwitchToCli: () => switchMode("cli"), execMode: execMode, onExecModeChange: switchExecMode, storageKey: storageKey, restoreActiveThread: restoreActiveThread, scope: scope, showScopeBadge: showScopeBadge, browserTabId: browserTabId, threadUrlSync: threadUrlSync }) })) }), canUseCodeTools
984
+ : undefined, contentHidden: mode !== "chat", emptyStateText: emptyStateText, emptyStateAddon: emptyStateAddon, suggestions: suggestions, dynamicSuggestions: dynamicSuggestions, onSwitchToCli: () => switchMode("cli"), execMode: execMode, onExecModeChange: switchExecMode, storageKey: storageKey, restoreActiveThread: restoreActiveThread, scope: scope, isolateHistoryByScope: isolateHistoryByScope, showScopeBadge: showScopeBadge, browserTabId: browserTabId, threadUrlSync: threadUrlSync }) })) }), canUseCodeTools
985
985
  ? mode === "cli" &&
986
986
  cliTabs.map((id) => (_jsx("div", { className: "min-h-0 relative flex-1", style: {
987
987
  display: id === activeCliTab ? undefined : "none",
@@ -1418,7 +1418,7 @@ export function AgentChatSurface({ mode = "panel", className, defaultMode = "cha
1418
1418
  * Wraps app content with a toggleable agent sidebar.
1419
1419
  * Use AgentToggleButton in your header to open/close it.
1420
1420
  */
1421
- export function AgentSidebar({ children, emptyStateText = "How can I help you?", suggestions, dynamicSuggestions, composerToolbarSlot, composerSlot, onComposerTextChange, imageModelMenu, availableAgents, availableModels, modelListLoading, selectedAgent, onAgentChange, onConnectLocalRuntime, runtime, adapterReloadKey, threadFooterSlot, defaultSidebarWidth, sidebarWidth, position = "right", defaultOpen = false, animateMobile = true, animateDesktop = true, chatViewTransition = false, chatViewTransitionHandoff = false, storageKey, openStorageKey, apiUrl, agentChatSurface, desktopIdentityUnauthenticated, desktopIdentityAuthenticated, showTabBar = true, suppressInlineOpenApp, composerPlaceholder, openOnChatRunning = false, onFullscreenRequest, scope, showScopeBadge, browserTabId, threadUrlSync, agentPageHref, suppressFirstRunOnboarding = false, }) {
1421
+ export function AgentSidebar({ children, emptyStateText = "How can I help you?", suggestions, dynamicSuggestions, composerToolbarSlot, composerSlot, onComposerTextChange, imageModelMenu, availableAgents, availableModels, modelListLoading, selectedAgent, onAgentChange, onConnectLocalRuntime, runtime, adapterReloadKey, threadFooterSlot, defaultSidebarWidth, sidebarWidth, position = "right", defaultOpen = false, animateMobile = true, animateDesktop = true, chatViewTransition = false, chatViewTransitionHandoff = false, storageKey, openStorageKey, apiUrl, agentChatSurface, desktopIdentityUnauthenticated, desktopIdentityAuthenticated, showTabBar = true, suppressInlineOpenApp, composerPlaceholder, openOnChatRunning = false, onFullscreenRequest, scope, isolateHistoryByScope = false, showScopeBadge, browserTabId, threadUrlSync, agentPageHref, suppressFirstRunOnboarding = false, }) {
1422
1422
  const staticHostedHarnessEnabled = isHostedHarnessConfigured(injectedAgentNativeConfig().harness);
1423
1423
  const hostedHarnessQuery = useActionQuery("get-hosted-harness-config", undefined, { enabled: staticHostedHarnessEnabled });
1424
1424
  const hostedHarnessStatus = hostedHarnessQuery.data;
@@ -2059,7 +2059,7 @@ export function AgentSidebar({ children, emptyStateText = "How can I help you?",
2059
2059
  ? "desktop"
2060
2060
  : undefined, "data-agent-sidebar-layout": panelLayout, "data-agent-sidebar-position": effectivePosition, "data-agent-native-hosted-harness-ui": hostedHarnessUi ? "desktop" : undefined, "data-agent-sidebar-state": panelOpen ? "open" : "closed", "data-agent-sidebar-per-app-chat": isPerAppChatSidebar ? "true" : undefined, "data-agent-sidebar-resizing": isResizing ? "true" : undefined, "data-agent-sidebar-chat-handoff": chatViewTransitionHandoff ? "true" : undefined, style: chatViewTransition
2061
2061
  ? getAgentChatViewTransitionStyle(panelStyle)
2062
- : 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, availableAgents: effectiveAvailableAgents, availableModels: availableModels, modelListLoading: modelListLoading, selectedAgent: effectiveSelectedAgent, onAgentChange: effectiveOnAgentChange, hostedHarness: hostedHarnessEnabled, onConnectLocalRuntime: onConnectLocalRuntime, runtime: runtime, adapterReloadKey: adapterReloadKey, threadFooterSlot: threadFooterSlot, apiUrl: apiUrl, agentChatSurface: agentChatSurface, desktopIdentityUnauthenticated: desktopIdentityUnauthenticated, desktopIdentityAuthenticated: desktopIdentityAuthenticated, showTabBar: effectiveShowTabBar, 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: effectivePosition, onDrag: handleDrag, onResizeStart: handleResizeStart, onResizeEnd: handleResizeEnd }))] })) : null;
2062
+ : 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, availableAgents: effectiveAvailableAgents, availableModels: availableModels, modelListLoading: modelListLoading, selectedAgent: effectiveSelectedAgent, onAgentChange: effectiveOnAgentChange, hostedHarness: hostedHarnessEnabled, onConnectLocalRuntime: onConnectLocalRuntime, runtime: runtime, adapterReloadKey: adapterReloadKey, threadFooterSlot: threadFooterSlot, apiUrl: apiUrl, agentChatSurface: agentChatSurface, desktopIdentityUnauthenticated: desktopIdentityUnauthenticated, desktopIdentityAuthenticated: desktopIdentityAuthenticated, showTabBar: effectiveShowTabBar, 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, isolateHistoryByScope: isolateHistoryByScope, showScopeBadge: showScopeBadge, browserTabId: browserTabId, threadUrlSync: threadUrlSync, agentPageHref: agentPageHref, allowSettingsMode: false, chatOnly: true }) }) }), showResizeHandle && isLeft && (_jsx(ResizeHandle, { position: effectivePosition, onDrag: handleDrag, onResizeStart: handleResizeStart, onResizeEnd: handleResizeEnd }))] })) : null;
2063
2063
  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;
2064
2064
  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": effectivePosition, "data-agent-native-hosted-harness-ui": hostedHarnessUi ? "desktop" : undefined, "data-agent-native-hosted-chat": isPerAppChatHosted ? "true" : undefined, "data-agent-sidebar-resizing": isResizing ? "true" : undefined, children: [isMobile &&
2065
2065
  !isPerAppChatHosted &&
@@ -11,7 +11,7 @@ import { type AgentChatRuntime } from "./chat/runtime.js";
11
11
  import { type AgentComposerLayoutVariant, type ComposerSubmitIntent, type Reference } from "./composer/index.js";
12
12
  import { type AgentDynamicSuggestionsOption } from "./dynamic-suggestions.js";
13
13
  import { type ContentPart } from "./sse-event-processor.js";
14
- import type { ChatThreadScope, ChatThreadSnapshot } from "./use-chat-threads.js";
14
+ import { type ChatThreadScope, type ChatThreadSnapshot } from "./use-chat-threads.js";
15
15
  export { AssistantMessageListErrorBoundary, AssistantUiStaleIndexErrorBoundary, assistantUiRecoverableRenderErrorKind, isAssistantUiRecoverableRenderError, isAssistantUiStaleIndexError, } from "./assistant-ui-recovery.js";
16
16
  export { displayableUserMessageText } from "./chat/message-components.js";
17
17
  export declare function shouldSuppressUnauthenticatedDesktopThreadRestore(surface: AgentChatSurfaceKind, status: number, desktopIdentityUnauthenticated?: boolean): boolean;
@@ -269,6 +269,8 @@ export interface AssistantChatProps {
269
269
  threadId?: string;
270
270
  /** Resource scope to include with chat requests for server-side context. */
271
271
  contextScope?: ChatThreadScope | null;
272
+ /** Restrict server-side thread restores to the supplied app scope. */
273
+ isolateHistoryByScope?: boolean;
272
274
  /** Namespace used to hide ambient composer context from other host surfaces. */
273
275
  contextNamespace?: string;
274
276
  /** Whether this chat owns the active visible composer context snapshot. */
@@ -39,6 +39,7 @@ import { McpConnectionSuggestion } from "./resources/McpConnectionSuggestion.js"
39
39
  import { claimRunStream, createRunStreamToken, ownsRunStream, releaseRunStream, } from "./run-stream-ownership.js";
40
40
  import { AgentAutoContinueSignal, readSSEStreamRaw, settleInterruptedToolCalls, } from "./sse-event-processor.js";
41
41
  import { useAgentEngineConfigured } from "./use-agent-engine-configured.js";
42
+ import { appendChatThreadScopeParams, } from "./use-chat-threads.js";
42
43
  import { useDevMode } from "./use-dev-mode.js";
43
44
  import { useRunStuckDetection } from "./use-run-stuck-detection.js";
44
45
  import { cn } from "./utils.js";
@@ -1386,7 +1387,7 @@ function approvalResolutionIdentity(approvalKey, toolCallId,
1386
1387
  askId) {
1387
1388
  return `${toolCallId ?? ""}\u0000${approvalKey}\u0000${askId ?? ""}`;
1388
1389
  }
1389
- const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateText, suggestions, dynamicSuggestions, threadFooterSlot, emptyStateAddon, showHeader = true, onSwitchToCli, className, apiUrl, tabId, browserTabId, threadId, contextScope, contextNamespace, isActiveComposer = true, onMessageCountChange, onSaveThread, onGenerateTitle, composerSlot, onComposerTextChange, composerAreaClassName, composerPlaceholder, missingApiKeySetupLayout = "default", composerLayoutVariant = "default", centerComposerWhenEmpty = false, emptyStateDisplay = "default", composerToolbarSlot, composerExtraActionButton, showModelSelector = true, composerDisabled = false, composerDisabledPlaceholder, isNewThread, isThreadStateLoading, onSlashCommand, execMode, onExecModeChange, approvalActions, planModeDisabled, planModeDisabledReason, selectedModel, defaultModel, selectedEngine, selectedEffort, availableModels, modelListLoading, onModelChange, onEffortChange, availableAgents, selectedAgent, hostedHarness, onAgentChange, imageModelMenu, onForkChat, onConnectProvider, onConnectLocalRuntime, plusMenuMode = "full", providerStatusChecksEnabled = true, loadHistoryRepository, historyReloadKey, externalStreaming = false, agentChatSurface = "app", desktopIdentityUnauthenticated = false, desktopIdentityAuthenticated = false, suppressInlineOpenApp = false, }, ref) {
1390
+ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateText, suggestions, dynamicSuggestions, threadFooterSlot, emptyStateAddon, showHeader = true, onSwitchToCli, className, apiUrl, tabId, browserTabId, threadId, contextScope, isolateHistoryByScope = false, contextNamespace, isActiveComposer = true, onMessageCountChange, onSaveThread, onGenerateTitle, composerSlot, onComposerTextChange, composerAreaClassName, composerPlaceholder, missingApiKeySetupLayout = "default", composerLayoutVariant = "default", centerComposerWhenEmpty = false, emptyStateDisplay = "default", composerToolbarSlot, composerExtraActionButton, showModelSelector = true, composerDisabled = false, composerDisabledPlaceholder, isNewThread, isThreadStateLoading, onSlashCommand, execMode, onExecModeChange, approvalActions, planModeDisabled, planModeDisabledReason, selectedModel, defaultModel, selectedEngine, selectedEffort, availableModels, modelListLoading, onModelChange, onEffortChange, availableAgents, selectedAgent, hostedHarness, onAgentChange, imageModelMenu, onForkChat, onConnectProvider, onConnectLocalRuntime, plusMenuMode = "full", providerStatusChecksEnabled = true, loadHistoryRepository, historyReloadKey, externalStreaming = false, agentChatSurface = "app", desktopIdentityUnauthenticated = false, desktopIdentityAuthenticated = false, suppressInlineOpenApp = false, }, ref) {
1390
1391
  const t = useT();
1391
1392
  const thread = useThread();
1392
1393
  const threadRuntime = useThreadRuntime();
@@ -1418,6 +1419,14 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1418
1419
  enabled: messages.length === 0,
1419
1420
  });
1420
1421
  const messageListResetKey = useMemo(() => assistantUiMessageListStructureKey(messages), [messages]);
1422
+ const threadScopeQuery = useMemo(() => {
1423
+ if (!isolateHistoryByScope || !contextScope)
1424
+ return "";
1425
+ const params = new URLSearchParams();
1426
+ appendChatThreadScopeParams(params, contextScope);
1427
+ const query = params.toString();
1428
+ return query ? `?${query}` : "";
1429
+ }, [contextScope?.id, contextScope?.type, isolateHistoryByScope]);
1421
1430
  // Chat-wide drag-and-drop: users expect to drop a file anywhere on the agent
1422
1431
  // sidebar (thread, header, composer) and have it attach — same as ChatGPT,
1423
1432
  // Claude.ai, Linear, Slack, etc. Tiptap's own `handleDrop` only fires inside
@@ -1989,7 +1998,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1989
1998
  ? setTimeout(() => ownAbort.abort(), getPollAbortMs(2000))
1990
1999
  : null;
1991
2000
  try {
1992
- const refreshRes = await fetch(`${apiUrl}/threads/${encodeURIComponent(threadId)}`, { signal: signal ?? ownAbort?.signal });
2001
+ const refreshRes = await fetch(`${apiUrl}/threads/${encodeURIComponent(threadId)}${threadScopeQuery}`, { signal: signal ?? ownAbort?.signal });
1993
2002
  if (!refreshRes.ok)
1994
2003
  return null;
1995
2004
  const refreshData = await refreshRes.json();
@@ -2006,7 +2015,13 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2006
2015
  if (ownAbortTimer)
2007
2016
  clearTimeout(ownAbortTimer);
2008
2017
  }
2009
- }, [apiUrl, importThreadData, loadHistoryRepository, threadId]);
2018
+ }, [
2019
+ apiUrl,
2020
+ importThreadData,
2021
+ loadHistoryRepository,
2022
+ threadId,
2023
+ threadScopeQuery,
2024
+ ]);
2010
2025
  const exportCleanThreadRepo = useCallback(() => ensureMessageMetadata(normalizeThreadRepository(threadRuntime.export())), [threadRuntime]);
2011
2026
  const exportPersistableThreadRepo = useCallback(() => withLastAssistantRunDuration(exportCleanThreadRepo(), showRunningInUI ? null : lastChatRunDurationMs), [exportCleanThreadRepo, lastChatRunDurationMs, showRunningInUI]);
2012
2027
  useEffect(() => {
@@ -2698,7 +2713,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2698
2713
  (async () => {
2699
2714
  let canReconnect = false;
2700
2715
  try {
2701
- const res = await fetch(`${apiUrl}/threads/${encodeURIComponent(threadId)}`);
2716
+ const res = await fetch(`${apiUrl}/threads/${encodeURIComponent(threadId)}${threadScopeQuery}`);
2702
2717
  if (!res.ok) {
2703
2718
  if (!cancelled) {
2704
2719
  setThreadRestoreError(shouldSuppressUnauthenticatedDesktopThreadRestore(agentChatSurface, res.status, desktopIdentityUnauthenticated)
@@ -2811,6 +2826,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2811
2826
  isThreadStateLoading,
2812
2827
  desktopIdentityUnauthenticated,
2813
2828
  restoreAttempt,
2829
+ threadScopeQuery,
2814
2830
  ]);
2815
2831
  useEffect(() => {
2816
2832
  if (!loadHistoryRepository ||
@@ -2992,7 +3008,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
2992
3008
  const timer = setTimeout(() => {
2993
3009
  (async () => {
2994
3010
  try {
2995
- const res = await fetch(`${apiUrl}/threads/${encodeURIComponent(threadId)}/queued`, {
3011
+ const res = await fetch(`${apiUrl}/threads/${encodeURIComponent(threadId)}/queued${threadScopeQuery}`, {
2996
3012
  method: "POST",
2997
3013
  headers: { "Content-Type": "application/json" },
2998
3014
  body: JSON.stringify({ queuedMessages }),
@@ -3010,7 +3026,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
3010
3026
  })();
3011
3027
  }, 300);
3012
3028
  return () => clearTimeout(timer);
3013
- }, [queuedMessages, threadId, apiUrl]);
3029
+ }, [queuedMessages, threadId, apiUrl, threadScopeQuery]);
3014
3030
  // Nudge the shared hook to re-check after a Builder connect.
3015
3031
  const handleBuilderConnected = useCallback(() => {
3016
3032
  focusComposerAfterConnectRef.current = true;
@@ -64,9 +64,11 @@ export type MultiTabAssistantChatProps = Omit<AssistantChatProps, "tabId" | "thr
64
64
  threadUrlSync?: boolean | ChatThreadUrlSyncOptions;
65
65
  /** Ambient resource context to show as a composer chip. */
66
66
  scope?: ChatThreadScope | null;
67
+ /** Keep app-owned chat history isolated to the supplied scope. */
68
+ isolateHistoryByScope?: boolean;
67
69
  /** @deprecated Scope context is now rendered in the composer. */
68
70
  showScopeBadge?: boolean;
69
71
  /** Cadence for hydrating agent-team sub-agent tab status. Default: 3000. */
70
72
  agentTeamPollMs?: number;
71
73
  };
72
- export declare function MultiTabAssistantChat({ showTabBar, renderHeader, renderOverlay, contentHidden, apiUrl, storageKey, restoreActiveThread, browserTabId, threadUrlSync, scope, agentTeamPollMs, ...props }: MultiTabAssistantChatProps): React.JSX.Element;
74
+ export declare function MultiTabAssistantChat({ showTabBar, renderHeader, renderOverlay, contentHidden, apiUrl, storageKey, restoreActiveThread, browserTabId, threadUrlSync, scope, isolateHistoryByScope, agentTeamPollMs, ...props }: MultiTabAssistantChatProps): React.JSX.Element;
@@ -402,7 +402,7 @@ function runToAgentTeamTabInfo(run) {
402
402
  status: run.status ?? "unknown",
403
403
  };
404
404
  }
405
- export function MultiTabAssistantChat({ showTabBar = true, renderHeader, renderOverlay, contentHidden = false, apiUrl = agentNativePath("/_agent-native/agent-chat"), storageKey, restoreActiveThread = true, browserTabId, threadUrlSync = false, scope = null, agentTeamPollMs = DEFAULT_AGENT_TEAM_POLL_MS, ...props }) {
405
+ export function MultiTabAssistantChat({ showTabBar = true, renderHeader, renderOverlay, contentHidden = false, apiUrl = agentNativePath("/_agent-native/agent-chat"), storageKey, restoreActiveThread = true, browserTabId, threadUrlSync = false, scope = null, isolateHistoryByScope = false, agentTeamPollMs = DEFAULT_AGENT_TEAM_POLL_MS, ...props }) {
406
406
  const translate = useT();
407
407
  const contextNamespace = scope
408
408
  ? scope.contextKey?.trim() || `scope:${scope.type}:${scope.id}`
@@ -520,6 +520,7 @@ export function MultiTabAssistantChat({ showTabBar = true, renderHeader, renderO
520
520
  routeThreadId: threadUrlSyncEnabled
521
521
  ? urlThreadId
522
522
  : (activeDeepLinkedThreadId ?? undefined),
523
+ isolateHistoryByScope,
523
524
  });
524
525
  const switchThread = useCallback((threadId, options = {}) => {
525
526
  switchThreadState(threadId);
@@ -1911,7 +1912,7 @@ export function MultiTabAssistantChat({ showTabBar = true, renderHeader, renderO
1911
1912
  const isActive = tab.id === activeThreadId ||
1912
1913
  (tab.id === focusParentId &&
1913
1914
  activeTab?.parentThreadId === tab.id);
1914
- return (_jsxs("div", { ref: isActive ? activeTabRefCb : undefined, className: cn("agent-tab relative flex items-center rounded-md text-[11px] font-medium shrink-0 max-w-[130px]", isActive
1915
+ return (_jsxs("div", { ref: isActive ? activeTabRefCb : undefined, className: cn("agent-tab relative flex items-center rounded-md text-[11px] font-medium shrink-0 min-w-[56px] max-w-[130px]", isActive
1915
1916
  ? "bg-accent text-foreground ring-1 ring-inset ring-border/60 shadow-sm"
1916
1917
  : "text-muted-foreground hover:text-foreground hover:bg-accent/50"), children: [_jsxs("button", { type: "button", onClick: () => switchThread(tab.id), className: "flex items-center gap-1 px-2.5 py-1.5 min-w-0 flex-1 text-start", children: [_jsx("span", { className: "truncate pe-1", children: tab.label }), tab.status === "running" && (_jsx("span", { className: "w-1.5 h-1.5 rounded-full bg-muted-foreground/50 shrink-0 animate-pulse" }))] }), _jsx("button", { type: "button", "aria-label": translate("agentChat.tabs.closeTab"), onClick: (e) => {
1917
1918
  e.stopPropagation();
@@ -1931,7 +1932,7 @@ export function MultiTabAssistantChat({ showTabBar = true, renderHeader, renderO
1931
1932
  ? "bg-accent text-foreground"
1932
1933
  : "text-muted-foreground hover:bg-accent hover:text-foreground"), children: translate("agentChat.tabs.main") }), childTabs.map((tab) => (_jsxs("div", { ref: tab.id === activeThreadId
1933
1934
  ? activeTabRefCb
1934
- : undefined, className: cn("agent-tab relative flex shrink-0 items-center rounded-md text-[10px] font-medium max-w-[130px]", tab.id === activeThreadId
1935
+ : undefined, className: cn("agent-tab relative flex shrink-0 items-center rounded-md text-[10px] font-medium min-w-[48px] max-w-[130px]", tab.id === activeThreadId
1935
1936
  ? "bg-accent text-foreground"
1936
1937
  : "text-muted-foreground hover:bg-accent hover:text-foreground"), children: [_jsxs("button", { type: "button", onClick: () => switchThread(tab.id), className: "flex items-center gap-1 px-2 py-1 min-w-0 flex-1 text-start", children: [_jsx("span", { className: "truncate pe-1", children: tab.subAgentName || tab.label }), tab.status === "running" && (_jsx("span", { className: "w-1 h-1 rounded-full bg-muted-foreground/50 shrink-0 animate-pulse" }))] }), _jsx("button", { type: "button", "aria-label": translate("agentChat.tabs.closeTab"), onClick: (e) => {
1937
1938
  e.stopPropagation();
@@ -1969,7 +1970,7 @@ export function MultiTabAssistantChat({ showTabBar = true, renderHeader, renderO
1969
1970
  else {
1970
1971
  chatRefs.current.delete(tabId);
1971
1972
  }
1972
- }, threadId: tabId, tabId: tabId, browserTabId: browserTabId, contextNamespace: contextNamespace, isActiveComposer: tabId === activeThreadId, apiUrl: apiUrl, isNewThread: newThreadIds.current.has(tabId) || isNewThread(tabId), isThreadStateLoading: isLoading, onMessageCountChange: (count) => setMessageCounts((prev) => prev[tabId] === count
1973
+ }, threadId: tabId, tabId: tabId, browserTabId: browserTabId, contextScope: scope, contextNamespace: contextNamespace, isolateHistoryByScope: isolateHistoryByScope, isActiveComposer: tabId === activeThreadId, apiUrl: apiUrl, isNewThread: newThreadIds.current.has(tabId) || isNewThread(tabId), isThreadStateLoading: isLoading, onMessageCountChange: (count) => setMessageCounts((prev) => prev[tabId] === count
1973
1974
  ? prev
1974
1975
  : { ...prev, [tabId]: count }), onSaveThread: handleSaveThread, onGenerateTitle: handleGenerateTitle, onSlashCommand: handleSlashCommand, selectedModel: modelSelection?.model, selectedEngine: modelSelection?.engine, selectedEffort: modelSelection?.effort ?? DEFAULT_REASONING_EFFORT, composerSlot: props.composerSlot, defaultModel: defaultModel, availableModels: availableModels, modelListLoading: modelListLoading, onModelChange: handleModelChange, onEffortChange: handleEffortChange, onForkChat: () => handleForkChat(tabId),
1975
1976
  // Sub-agent tabs are read-only: sending a new message from the
@@ -66,7 +66,14 @@ export interface UseChatThreadsOptions {
66
66
  routeThreadId?: string | null;
67
67
  /** Include connected and other-app chats in list/search results. */
68
68
  includeExternal?: boolean;
69
+ /**
70
+ * Restrict history to the supplied scope. App-owned chat rails use this to
71
+ * keep one app's conversations out of another app's history; ordinary
72
+ * resource scopes intentionally continue to show general chats.
73
+ */
74
+ isolateHistoryByScope?: boolean;
69
75
  }
76
+ export declare function appendChatThreadScopeParams(params: URLSearchParams, scope?: ChatThreadScope | null): void;
70
77
  export declare function useChatThreads(apiUrl?: string, storageKey?: string, scope?: ChatThreadScope | null, options?: UseChatThreadsOptions): {
71
78
  threads: ChatThreadSummary[];
72
79
  activeThreadId: string | null;
@@ -3,12 +3,13 @@ import { agentNativePath } from "./api-path.js";
3
3
  const ACTIVE_THREAD_KEY = "agent-chat-active-thread";
4
4
  const THREADS_UPDATED_EVENT = "agent-chat:threads-updated";
5
5
  const THREADS_PAGE_SIZE = 50;
6
- async function fetchThreadListPage(apiUrl, offset, includeExternal) {
6
+ async function fetchThreadListPage(apiUrl, offset, includeExternal, scope) {
7
7
  const params = new URLSearchParams();
8
8
  if (offset > 0)
9
9
  params.set("offset", String(offset));
10
10
  if (includeExternal)
11
11
  params.set("includeExternal", "1");
12
+ appendChatThreadScopeParams(params, scope);
12
13
  const query = params.toString();
13
14
  return fetch(`${apiUrl}/threads${query ? `?${query}` : ""}`).then(async (res) => {
14
15
  if (!res.ok)
@@ -22,9 +23,12 @@ async function fetchThreadListPage(apiUrl, offset, includeExternal) {
22
23
  * the caller must not collapse: the thread (found), `null` (the server denies it
23
24
  * exists), and `undefined` (unreachable — nothing was learned).
24
25
  */
25
- async function fetchThreadById(apiUrl, id) {
26
+ async function fetchThreadById(apiUrl, id, scope) {
26
27
  try {
27
- const res = await fetch(`${apiUrl}/threads/${encodeURIComponent(id)}`);
28
+ const params = new URLSearchParams();
29
+ appendChatThreadScopeParams(params, scope);
30
+ const query = params.toString();
31
+ const res = await fetch(`${apiUrl}/threads/${encodeURIComponent(id)}${query ? `?${query}` : ""}`);
28
32
  if (res.status === 404)
29
33
  return null;
30
34
  if (!res.ok)
@@ -35,6 +39,20 @@ async function fetchThreadById(apiUrl, id) {
35
39
  return undefined;
36
40
  }
37
41
  }
42
+ export function appendChatThreadScopeParams(params, scope) {
43
+ if (!scope)
44
+ return;
45
+ params.set("scopeType", scope.type);
46
+ params.set("scopeId", scope.id);
47
+ }
48
+ function withChatThreadScope(url, scope) {
49
+ const params = new URLSearchParams();
50
+ appendChatThreadScopeParams(params, scope);
51
+ const query = params.toString();
52
+ if (!query)
53
+ return url;
54
+ return `${url}${url.includes("?") ? "&" : "?"}${query}`;
55
+ }
38
56
  function emitThreadsUpdated() {
39
57
  if (typeof window === "undefined")
40
58
  return;
@@ -91,6 +109,12 @@ function threadCanStayVisibleInScope(threadScope, currentScope) {
91
109
  return true;
92
110
  return scopesMatch(threadScope, currentScope);
93
111
  }
112
+ function threadCanStayVisibleInHistory(threadScope, currentScope, isolateHistoryByScope = false) {
113
+ if (!isolateHistoryByScope) {
114
+ return threadCanStayVisibleInScope(threadScope, currentScope);
115
+ }
116
+ return scopesMatch(threadScope, currentScope);
117
+ }
94
118
  function nextThreadTitle(currentTitle, incomingTitle, incomingPreview, source = "extracted", options = {}) {
95
119
  if (options.preserveUserTitle && currentTitle)
96
120
  return currentTitle;
@@ -109,6 +133,12 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
109
133
  const autoCreate = options?.autoCreate !== false;
110
134
  const restoreActiveThread = options?.restoreActiveThread !== false;
111
135
  const includeExternal = options?.includeExternal === true;
136
+ const isolateHistoryByScope = options?.isolateHistoryByScope === true;
137
+ const isolateHistory = isolateHistoryByScope && Boolean(scope);
138
+ const historyScope = useMemo(() => (isolateHistory && scope ? { type: scope.type, id: scope.id } : null), [isolateHistory, scope?.id, scope?.type]);
139
+ const historyScopeKey = historyScope
140
+ ? `${historyScope.type}:${historyScope.id}`
141
+ : null;
112
142
  const routeControlsActiveThread = options?.routeThreadId !== undefined;
113
143
  const routeThreadId = normalizeThreadId(options?.routeThreadId);
114
144
  // Each (storageKey, scope) pair gets its own active-thread localStorage key
@@ -170,6 +200,7 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
170
200
  ? new Set([initialActiveThreadRef.current.id])
171
201
  : new Set());
172
202
  const optimisticThreadScopesRef = useRef(new Map());
203
+ const knownThreadScopesRef = useRef(new Map());
173
204
  const pendingPinnedAtRef = useRef(new Map());
174
205
  const pendingArchivedAtRef = useRef(new Map());
175
206
  const userRenamedThreadIdsRef = useRef(new Set());
@@ -215,6 +246,9 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
215
246
  if (optimisticThreadScopesRef.current.has(id)) {
216
247
  return optimisticThreadScopesRef.current.get(id) ?? null;
217
248
  }
249
+ if (knownThreadScopesRef.current.has(id)) {
250
+ return knownThreadScopesRef.current.get(id) ?? null;
251
+ }
218
252
  return undefined;
219
253
  }, []);
220
254
  // Add a client-generated thread to the local list optimistically.
@@ -290,7 +324,7 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
290
324
  if (currentThreadScope === undefined) {
291
325
  return;
292
326
  }
293
- if (threadCanStayVisibleInScope(currentThreadScope, scopeRef.current)) {
327
+ if (threadCanStayVisibleInHistory(currentThreadScope, scopeRef.current, isolateHistory)) {
294
328
  persistedKeyRef.current = activeThreadKey;
295
329
  return;
296
330
  }
@@ -308,7 +342,7 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
308
342
  if (nextActiveThreadId) {
309
343
  const savedScope = readKnownThreadScope(nextActiveThreadId);
310
344
  if (savedScope !== undefined &&
311
- !threadCanStayVisibleInScope(savedScope, scopeRef.current)) {
345
+ !threadCanStayVisibleInHistory(savedScope, scopeRef.current, isolateHistory)) {
312
346
  nextActiveThreadId = null;
313
347
  }
314
348
  }
@@ -356,7 +390,7 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
356
390
  const requestId = ++latestFetchRequestRef.current;
357
391
  try {
358
392
  const offset = options?.append ? nextThreadsOffsetRef.current : 0;
359
- const loaded = await fetchThreadListPage(apiUrl, offset, includeExternal);
393
+ const loaded = await fetchThreadListPage(apiUrl, offset, includeExternal, historyScope);
360
394
  if (requestId !== latestFetchRequestRef.current)
361
395
  return undefined;
362
396
  if (!loaded) {
@@ -365,6 +399,9 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
365
399
  }
366
400
  return;
367
401
  }
402
+ for (const thread of loaded) {
403
+ knownThreadScopesRef.current.set(thread.id, thread.scope ?? null);
404
+ }
368
405
  setThreadsLoadError(null);
369
406
  if (!options?.append) {
370
407
  nextThreadsOffsetRef.current = loaded.length;
@@ -373,8 +410,11 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
373
410
  nextThreadsOffsetRef.current += loaded.length;
374
411
  }
375
412
  setHasMoreThreads(loaded.length >= THREADS_PAGE_SIZE);
413
+ const visibleLoaded = isolateHistory
414
+ ? loaded.filter((thread) => threadCanStayVisibleInHistory(thread.scope, historyScope, isolateHistory))
415
+ : loaded;
376
416
  setThreads((prev) => {
377
- const loadedIds = new Set(loaded.map((t) => t.id));
417
+ const loadedIds = new Set(visibleLoaded.map((t) => t.id));
378
418
  // Preserve any optimistic threads we've created this session that
379
419
  // haven't shown up in the server list yet — the server only learns
380
420
  // about a thread when the user actually sends a message and the
@@ -387,13 +427,15 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
387
427
  // and get preserved forever instead of disappearing once archived.
388
428
  const optimisticOnly = prev.filter((t) => newlyCreatedRef.current.has(t.id) &&
389
429
  !loadedIds.has(t.id) &&
390
- !t.archivedAt);
430
+ !t.archivedAt &&
431
+ (!isolateHistory ||
432
+ threadCanStayVisibleInHistory(t.scope, historyScope, isolateHistory)));
391
433
  // Reconcile each server thread against our local copy. If the local
392
434
  // copy has a newer updatedAt or higher messageCount, keep those
393
435
  // fields — the server probably hasn't observed the user's latest
394
436
  // send yet, and naively replacing makes the recent-chats list
395
437
  // visibly jump back to older timestamps right after a send.
396
- const merged = loaded.map((server) => {
438
+ const merged = visibleLoaded.map((server) => {
397
439
  const local = prev.find((t) => t.id === server.id);
398
440
  if (!local)
399
441
  return server;
@@ -438,7 +480,7 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
438
480
  }
439
481
  return [...optimisticOnly, ...merged];
440
482
  });
441
- return loaded;
483
+ return visibleLoaded;
442
484
  }
443
485
  catch {
444
486
  if (requestId !== latestFetchRequestRef.current)
@@ -448,7 +490,28 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
448
490
  }
449
491
  return undefined;
450
492
  }
451
- }, [apiUrl, includeExternal]);
493
+ }, [apiUrl, historyScope, includeExternal, isolateHistory]);
494
+ const loadedHistoryScopeKeyRef = useRef(historyScopeKey);
495
+ useEffect(() => {
496
+ if (loadedHistoryScopeKeyRef.current === historyScopeKey)
497
+ return;
498
+ loadedHistoryScopeKeyRef.current = historyScopeKey;
499
+ if (!isolateHistoryByScope)
500
+ return;
501
+ nextThreadsOffsetRef.current = 0;
502
+ setHasMoreThreads(false);
503
+ setThreadsLoadError(null);
504
+ setThreads((prev) => prev.filter((thread) => !isolateHistory ||
505
+ threadCanStayVisibleInHistory(thread.scope, historyScope, isolateHistory)));
506
+ setIsLoading(true);
507
+ void fetchThreads().finally(() => setIsLoading(false));
508
+ }, [
509
+ fetchThreads,
510
+ historyScope,
511
+ historyScopeKey,
512
+ isolateHistory,
513
+ isolateHistoryByScope,
514
+ ]);
452
515
  const loadMoreThreads = useCallback(async () => {
453
516
  if (isLoadingMoreThreads || !hasMoreThreads)
454
517
  return;
@@ -509,8 +572,11 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
509
572
  // One page, so absence from it is not absence from the server — this is what
510
573
  // separates an older real thread from an unavailable saved id below.
511
574
  const restoredThread = lookupRestored && !restoredOnPage
512
- ? await fetchThreadById(apiUrl, restoredId)
575
+ ? await fetchThreadById(apiUrl, restoredId, historyScope)
513
576
  : restoredOnPage;
577
+ if (restoredThread) {
578
+ knownThreadScopesRef.current.set(restoredThread.id, restoredThread.scope ?? null);
579
+ }
514
580
  if (restoredThread === undefined && lookupRestored && !restoredOnPage) {
515
581
  // Lookup unreachable. Reclassifying now would stamp this thread with the
516
582
  // current scope on a guess; leave it untouched for the next mount.
@@ -519,7 +585,7 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
519
585
  }
520
586
  const restoredIsUnavailable = restoredThread === null && lookupRestored && !restoredOnPage;
521
587
  const restoredBelongsElsewhere = Boolean(restoredThread &&
522
- !threadCanStayVisibleInScope(restoredThread.scope ?? null, scopeRef.current));
588
+ !threadCanStayVisibleInHistory(restoredThread.scope ?? null, scopeRef.current, isolateHistory));
523
589
  // A missing saved id is stale local UI state on a normal home surface,
524
590
  // not a reason to show an error above a fresh composer. Preserve it for
525
591
  // explicit routes and list-only readers, where the caller still owns
@@ -587,6 +653,8 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
587
653
  fetchThreads,
588
654
  addOptimisticThread,
589
655
  autoCreate,
656
+ historyScope,
657
+ isolateHistory,
590
658
  routeControlsActiveThread,
591
659
  routeThreadId,
592
660
  ]);
@@ -641,7 +709,7 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
641
709
  // list right away.
642
710
  const detachThread = useCallback(async (threadId) => {
643
711
  try {
644
- const res = await fetch(`${apiUrl}/threads/${encodeURIComponent(threadId)}`, {
712
+ const res = await fetch(withChatThreadScope(`${apiUrl}/threads/${encodeURIComponent(threadId)}`, historyScope), {
645
713
  method: "PUT",
646
714
  headers: { "Content-Type": "application/json" },
647
715
  body: JSON.stringify({ scope: null }),
@@ -652,14 +720,42 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
652
720
  await fetchThreads();
653
721
  return;
654
722
  }
655
- setThreads((prev) => prev.map((t) => (t.id === threadId ? { ...t, scope: null } : t)));
723
+ knownThreadScopesRef.current.set(threadId, null);
656
724
  optimisticThreadScopesRef.current.set(threadId, null);
725
+ const wasActive = activeThreadIdRef.current === threadId;
726
+ if (isolateHistory) {
727
+ setThreads((prev) => prev.filter((thread) => thread.id !== threadId &&
728
+ threadCanStayVisibleInHistory(thread.scope, historyScope, isolateHistory)));
729
+ if (wasActive) {
730
+ const remaining = threadsRef.current.filter((thread) => thread.id !== threadId &&
731
+ threadCanStayVisibleInHistory(thread.scope, historyScope, isolateHistory));
732
+ if (remaining.length > 0) {
733
+ setActiveThreadId(remaining[0].id);
734
+ }
735
+ else if (autoCreate) {
736
+ void createThread();
737
+ }
738
+ else {
739
+ setActiveThreadId(null);
740
+ }
741
+ }
742
+ }
743
+ else {
744
+ setThreads((prev) => prev.map((t) => (t.id === threadId ? { ...t, scope: null } : t)));
745
+ }
657
746
  emitThreadsUpdated();
658
747
  }
659
748
  catch {
660
749
  await fetchThreads().catch(() => { });
661
750
  }
662
- }, [apiUrl, fetchThreads]);
751
+ }, [
752
+ apiUrl,
753
+ autoCreate,
754
+ createThread,
755
+ fetchThreads,
756
+ historyScope,
757
+ isolateHistory,
758
+ ]);
663
759
  const pinThread = useCallback(async (threadId, pinned) => {
664
760
  const previousPinnedAt = threadsRef.current.find((t) => t.id === threadId)?.pinnedAt ?? null;
665
761
  const previousUpdatedAt = threadsRef.current.find((t) => t.id === threadId)?.updatedAt ?? null;
@@ -680,7 +776,7 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
680
776
  };
681
777
  setThreads((prev) => prev.map((t) => (t.id === threadId ? { ...t, pinnedAt } : t)));
682
778
  try {
683
- const res = await fetch(`${apiUrl}/threads/${encodeURIComponent(threadId)}/pin`, {
779
+ const res = await fetch(withChatThreadScope(`${apiUrl}/threads/${encodeURIComponent(threadId)}/pin`, historyScope), {
684
780
  method: "POST",
685
781
  headers: { "Content-Type": "application/json" },
686
782
  body: JSON.stringify({ pinned }),
@@ -701,7 +797,7 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
701
797
  await fetchThreads();
702
798
  return false;
703
799
  }
704
- }, [apiUrl, fetchThreads]);
800
+ }, [apiUrl, fetchThreads, historyScope]);
705
801
  const archiveThread = useCallback(async (threadId) => {
706
802
  const previousArchivedAt = threadsRef.current.find((t) => t.id === threadId)?.archivedAt ?? null;
707
803
  const previousUpdatedAt = threadsRef.current.find((t) => t.id === threadId)?.updatedAt ?? null;
@@ -726,7 +822,7 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
726
822
  };
727
823
  setThreads((prev) => prev.map((t) => (t.id === threadId ? { ...t, archivedAt } : t)));
728
824
  try {
729
- const res = await fetch(`${apiUrl}/threads/${encodeURIComponent(threadId)}/archive`, {
825
+ const res = await fetch(withChatThreadScope(`${apiUrl}/threads/${encodeURIComponent(threadId)}/archive`, historyScope), {
730
826
  method: "POST",
731
827
  headers: { "Content-Type": "application/json" },
732
828
  body: JSON.stringify({ archived: true }),
@@ -750,7 +846,7 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
750
846
  await fetchThreads();
751
847
  return false;
752
848
  }
753
- }, [apiUrl, fetchThreads]);
849
+ }, [apiUrl, fetchThreads, historyScope]);
754
850
  const renameThread = useCallback(async (threadId, title) => {
755
851
  const nextTitle = title.replace(/\s+/g, " ").trim().slice(0, 160);
756
852
  if (!nextTitle)
@@ -768,7 +864,7 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
768
864
  markUserRenamedThread(threadId);
769
865
  setThreads((prev) => prev.map((t) => (t.id === threadId ? { ...t, title: nextTitle } : t)));
770
866
  try {
771
- const res = await fetch(`${apiUrl}/threads/${encodeURIComponent(threadId)}/rename`, {
867
+ const res = await fetch(withChatThreadScope(`${apiUrl}/threads/${encodeURIComponent(threadId)}/rename`, historyScope), {
772
868
  method: "POST",
773
869
  headers: { "Content-Type": "application/json" },
774
870
  body: JSON.stringify({ title: nextTitle }),
@@ -786,7 +882,13 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
786
882
  await fetchThreads();
787
883
  return false;
788
884
  }
789
- }, [apiUrl, clearUserRenamedThread, fetchThreads, markUserRenamedThread]);
885
+ }, [
886
+ apiUrl,
887
+ clearUserRenamedThread,
888
+ fetchThreads,
889
+ historyScope,
890
+ markUserRenamedThread,
891
+ ]);
790
892
  const isNewThread = useCallback((id) => newlyCreatedRef.current.has(id), []);
791
893
  const switchThread = useCallback((id) => {
792
894
  persistActiveThreadId(id);
@@ -794,7 +896,7 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
794
896
  }, [persistActiveThreadId]);
795
897
  const removeThread = useCallback(async (id) => {
796
898
  try {
797
- await fetch(`${apiUrl}/threads/${encodeURIComponent(id)}`, {
899
+ await fetch(withChatThreadScope(`${apiUrl}/threads/${encodeURIComponent(id)}`, historyScope), {
798
900
  method: "DELETE",
799
901
  });
800
902
  emitThreadsUpdated();
@@ -816,7 +918,7 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
816
918
  createThread();
817
919
  }
818
920
  }
819
- }, [apiUrl, clearUserRenamedThread, createThread]);
921
+ }, [apiUrl, clearUserRenamedThread, createThread, historyScope]);
820
922
  // Reads scope through refs so this callback survives every setThreads. Scope
821
923
  // rides only on creation: a periodic save must never move an existing thread
822
924
  // between resources, however stale this client's guess is.
@@ -828,7 +930,7 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
828
930
  const preserveUserTitle = userRenamedThreadIdsRef.current.has(id);
829
931
  const title = nextThreadTitle(localThread?.title, data.title, data.preview, titleSource, { preserveUserTitle });
830
932
  const payload = { ...threadDataPayload, title };
831
- let response = await fetch(`${apiUrl}/threads/${encodeURIComponent(id)}`, {
933
+ let response = await fetch(withChatThreadScope(`${apiUrl}/threads/${encodeURIComponent(id)}`, historyScope), {
832
934
  method: "PUT",
833
935
  headers: { "Content-Type": "application/json" },
834
936
  body: JSON.stringify(payload),
@@ -837,7 +939,7 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
837
939
  // client-created thread, so no agent run has created its SQL row yet.
838
940
  // Materialize that row idempotently and retry the same full save.
839
941
  if (response.status === 404) {
840
- const created = await fetch(`${apiUrl}/threads`, {
942
+ const created = await fetch(withChatThreadScope(`${apiUrl}/threads`, historyScope), {
841
943
  method: "POST",
842
944
  headers: { "Content-Type": "application/json" },
843
945
  body: JSON.stringify({
@@ -848,7 +950,7 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
848
950
  });
849
951
  if (!created.ok)
850
952
  return;
851
- response = await fetch(`${apiUrl}/threads/${encodeURIComponent(id)}`, {
953
+ response = await fetch(withChatThreadScope(`${apiUrl}/threads/${encodeURIComponent(id)}`, historyScope), {
852
954
  method: "PUT",
853
955
  headers: { "Content-Type": "application/json" },
854
956
  body: JSON.stringify(payload),
@@ -894,7 +996,7 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
894
996
  });
895
997
  }
896
998
  catch { }
897
- }, [apiUrl, readKnownThreadScope]);
999
+ }, [apiUrl, historyScope, readKnownThreadScope]);
898
1000
  const generateTitle = useCallback(async (threadId, message) => {
899
1001
  try {
900
1002
  const res = await fetch(`${apiUrl}/generate-title`, {
@@ -923,7 +1025,7 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
923
1025
  const fallbackForkFromSnapshot = async (source) => {
924
1026
  const title = source.title ? `${source.title} (fork)` : "";
925
1027
  const createdAt = Date.now();
926
- const createRes = await fetch(`${apiUrl}/threads`, {
1028
+ const createRes = await fetch(withChatThreadScope(`${apiUrl}/threads`, historyScope), {
927
1029
  method: "POST",
928
1030
  headers: { "Content-Type": "application/json" },
929
1031
  body: JSON.stringify({
@@ -934,7 +1036,7 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
934
1036
  });
935
1037
  if (!createRes.ok)
936
1038
  return null;
937
- const saveRes = await fetch(`${apiUrl}/threads/${encodeURIComponent(id)}`, {
1039
+ const saveRes = await fetch(withChatThreadScope(`${apiUrl}/threads/${encodeURIComponent(id)}`, historyScope), {
938
1040
  method: "PUT",
939
1041
  headers: { "Content-Type": "application/json" },
940
1042
  body: JSON.stringify({
@@ -962,7 +1064,7 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
962
1064
  const source = sourceSnapshot && sourceSnapshot.messageCount > 0
963
1065
  ? { ...sourceSnapshot, scope: localScope }
964
1066
  : undefined;
965
- const res = await fetch(`${apiUrl}/threads/${encodeURIComponent(sourceId)}/fork`, {
1067
+ const res = await fetch(withChatThreadScope(`${apiUrl}/threads/${encodeURIComponent(sourceId)}/fork`, historyScope), {
966
1068
  method: "POST",
967
1069
  headers: { "Content-Type": "application/json" },
968
1070
  body: JSON.stringify({ id, ...(source ? { source } : {}) }),
@@ -1003,25 +1105,27 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
1003
1105
  console.error(`[chat] fork threw for ${sourceId}:`, err);
1004
1106
  return null;
1005
1107
  }
1006
- }, [apiUrl]);
1108
+ }, [apiUrl, historyScope]);
1007
1109
  const searchThreads = useCallback(async (query) => {
1008
1110
  try {
1009
1111
  const params = new URLSearchParams({ q: query });
1010
1112
  if (includeExternal)
1011
1113
  params.set("includeExternal", "1");
1114
+ appendChatThreadScopeParams(params, historyScope);
1012
1115
  const res = await fetch(`${apiUrl}/threads?${params.toString()}`);
1013
1116
  if (!res.ok)
1014
1117
  return [];
1015
1118
  const data = await res.json();
1016
- return data.threads ?? [];
1119
+ return (data.threads ?? []).filter((thread) => !isolateHistory ||
1120
+ threadCanStayVisibleInHistory(thread.scope, historyScope, isolateHistory));
1017
1121
  }
1018
1122
  catch {
1019
1123
  return [];
1020
1124
  }
1021
- }, [apiUrl, includeExternal]);
1125
+ }, [apiUrl, historyScope, includeExternal, isolateHistory]);
1022
1126
  const getThreadShareState = useCallback(async (threadId) => {
1023
1127
  try {
1024
- const res = await fetch(`${apiUrl}/threads/${encodeURIComponent(threadId)}/share`);
1128
+ const res = await fetch(withChatThreadScope(`${apiUrl}/threads/${encodeURIComponent(threadId)}/share`, historyScope));
1025
1129
  if (!res.ok)
1026
1130
  return null;
1027
1131
  const data = await res.json();
@@ -1030,10 +1134,10 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
1030
1134
  catch {
1031
1135
  return null;
1032
1136
  }
1033
- }, [apiUrl]);
1137
+ }, [apiUrl, historyScope]);
1034
1138
  const createThreadShareLink = useCallback(async (threadId) => {
1035
1139
  try {
1036
- const res = await fetch(`${apiUrl}/threads/${encodeURIComponent(threadId)}/share`, { method: "POST" });
1140
+ const res = await fetch(withChatThreadScope(`${apiUrl}/threads/${encodeURIComponent(threadId)}/share`, historyScope), { method: "POST" });
1037
1141
  if (!res.ok)
1038
1142
  return null;
1039
1143
  const data = await res.json();
@@ -1044,10 +1148,10 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
1044
1148
  catch {
1045
1149
  return null;
1046
1150
  }
1047
- }, [apiUrl]);
1151
+ }, [apiUrl, historyScope]);
1048
1152
  const revokeThreadShareLink = useCallback(async (threadId) => {
1049
1153
  try {
1050
- const res = await fetch(`${apiUrl}/threads/${encodeURIComponent(threadId)}/share`, { method: "DELETE" });
1154
+ const res = await fetch(withChatThreadScope(`${apiUrl}/threads/${encodeURIComponent(threadId)}/share`, historyScope), { method: "DELETE" });
1051
1155
  if (!res.ok)
1052
1156
  return null;
1053
1157
  const data = await res.json();
@@ -1056,7 +1160,7 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
1056
1160
  catch {
1057
1161
  return null;
1058
1162
  }
1059
- }, [apiUrl]);
1163
+ }, [apiUrl, historyScope]);
1060
1164
  const refreshThreads = useCallback(() => {
1061
1165
  fetchThreads();
1062
1166
  }, [fetchThreads]);
@@ -26,8 +26,8 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
26
26
  * Body: { update: string (base64), requestSource?: string }
27
27
  */
28
28
  export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
29
- ok?: undefined;
30
29
  error: string;
30
+ ok?: undefined;
31
31
  } | {
32
32
  error?: undefined;
33
33
  ok: boolean;
@@ -51,8 +51,8 @@ export declare function handleDeleteResource(event: any): Promise<{
51
51
  error: string;
52
52
  ok?: undefined;
53
53
  } | {
54
- error?: undefined;
55
54
  ok: boolean;
55
+ error?: undefined;
56
56
  }>;
57
57
  /** POST /_agent-native/resources/upload — upload a file as a resource */
58
58
  export declare function handleUploadResource(event: any): Promise<import("./store.js").Resource | {
@@ -73,10 +73,10 @@ export declare function handleUploadResource(event: any): Promise<import("./stor
73
73
  runId: string | null;
74
74
  expiresAt: number | null;
75
75
  metadata: string | null;
76
- error?: undefined;
77
76
  url: string;
78
77
  provider: string;
79
78
  storageSetupRequired?: undefined;
79
+ error?: undefined;
80
80
  } | {
81
81
  error: string;
82
82
  storageSetupRequired: boolean;
@@ -23,7 +23,7 @@ import { attachToolSearch } from "../agent/tool-search.js";
23
23
  import { getAppConfig } from "../app-config/index.js";
24
24
  import { readAppStateForCurrentTab } from "../application-state/script-helpers.js";
25
25
  import { runChatThreadDataMigrations } from "../chat-threads/migrations.js";
26
- import { adoptThreadScopeIfUnscoped, createThread, forkThread, getThread, registerChatThreadsShareable, resolveRunThreadScope, resolveThreadAccess, listThreads, searchThreads, renameThread, createThreadShareLink, getThreadByShareToken, getThreadShareState, revokeThreadShareLink, setThreadArchived, setThreadPinned, setThreadScope, updateThreadData, withThreadDataLock, deleteThread, setThreadQueuedMessages, setThreadSourceIfMissing, } from "../chat-threads/store.js";
26
+ import { adoptThreadScopeIfUnscoped, createThread, forkThread, getThread, registerChatThreadsShareable, resolveRunThreadScope, resolveThreadAccess, listThreads, searchThreads, renameThread, createThreadShareLink, getThreadByShareToken, getThreadShareState, revokeThreadShareLink, setThreadArchived, setThreadPinned, setThreadScope, updateThreadData, withThreadDataLock, deleteThread, setThreadQueuedMessages, setThreadSourceIfMissing, isAppOwnedChatScope, threadScopeMismatch, } from "../chat-threads/store.js";
27
27
  import { isCheckpointRestorePath } from "../checkpoints/route-match.js";
28
28
  import { createDbAdminAgentTools } from "../db-admin/agent-tools.js";
29
29
  import { isProductionServerlessFunctionRuntime, isTransientDatabaseError, } from "../db/client.js";
@@ -2290,17 +2290,23 @@ export function createAgentChatPlugin(options) {
2290
2290
  thread = await getThread(threadId);
2291
2291
  }
2292
2292
  }
2293
- if (options?.appId) {
2294
- await setThreadSourceIfMissing(threadId, {
2295
- appId: options.appId,
2293
+ if (!thread) {
2294
+ throw createError({
2295
+ statusCode: 404,
2296
+ statusMessage: "Thread not found",
2296
2297
  });
2297
2298
  }
2298
- if (!thread) {
2299
+ if (threadScopeMismatch(thread.scope, runScope)) {
2299
2300
  throw createError({
2300
2301
  statusCode: 404,
2301
2302
  statusMessage: "Thread not found",
2302
2303
  });
2303
2304
  }
2305
+ if (options?.appId) {
2306
+ await setThreadSourceIfMissing(threadId, {
2307
+ appId: options.appId,
2308
+ });
2309
+ }
2304
2310
  const access = await resolveThreadAccess(ownerEmail, threadId, "editor", { orgId: getRequestOrgId() });
2305
2311
  if (!access) {
2306
2312
  throw createError({
@@ -2820,6 +2826,12 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
2820
2826
  if (details.threadId && details.ownerEmail) {
2821
2827
  const existingThread = await getThread(details.threadId);
2822
2828
  if (existingThread) {
2829
+ if (threadScopeMismatch(existingThread.scope, getRequestRunContext()?.chatScope)) {
2830
+ throw createError({
2831
+ statusCode: 404,
2832
+ statusMessage: "Thread not found",
2833
+ });
2834
+ }
2823
2835
  const access = await resolveThreadAccess(details.ownerEmail, details.threadId, "editor", { orgId: await getOrgIdFromEvent(details.event) });
2824
2836
  if (!access) {
2825
2837
  throw createError({
@@ -3096,6 +3108,12 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
3096
3108
  if (details.threadId && details.ownerEmail) {
3097
3109
  const existingThread = await getThread(details.threadId);
3098
3110
  if (existingThread) {
3111
+ if (threadScopeMismatch(existingThread.scope, getRequestRunContext()?.chatScope)) {
3112
+ throw createError({
3113
+ statusCode: 404,
3114
+ statusMessage: "Thread not found",
3115
+ });
3116
+ }
3099
3117
  const access = await resolveThreadAccess(details.ownerEmail, details.threadId, "editor", { orgId: await getOrgIdFromEvent(details.event) });
3100
3118
  if (!access) {
3101
3119
  throw createError({
@@ -4473,11 +4491,16 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
4473
4491
  const method = getMethod(event);
4474
4492
  const { threadId, tail: threadTail } = parseThreadRoute(event);
4475
4493
  const isThreadSubroute = (subroute) => threadTail[0] === subroute;
4494
+ const requestedScope = parseScopeFromQuery(getQuery(event));
4495
+ const scopesMatch = (left, right) => Boolean(left && right && left.type === right.type && left.id === right.id);
4496
+ const threadMatchesRequestedScope = (thread, incomingScope) => !requestedScope ||
4497
+ scopesMatch(thread.scope, requestedScope) ||
4498
+ (!thread.scope && scopesMatch(incomingScope, requestedScope));
4476
4499
  // ── Specific thread: GET/PUT/DELETE /threads/:id ──
4477
4500
  if (threadId) {
4478
4501
  if (method === "GET") {
4479
4502
  const thread = await resolveThreadAccess(owner, threadId, "viewer", { orgId });
4480
- if (!thread) {
4503
+ if (!thread || !threadMatchesRequestedScope(thread)) {
4481
4504
  setResponseStatus(event, 404);
4482
4505
  return { error: "Thread not found" };
4483
4506
  }
@@ -4491,12 +4514,33 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
4491
4514
  // run could clobber the assistant message the server just
4492
4515
  // appended (and vice versa).
4493
4516
  return await withThreadDataLock(threadId, async () => {
4517
+ const body = await readBody(event);
4518
+ const bodyIncludesScope = Boolean(body &&
4519
+ typeof body === "object" &&
4520
+ Object.prototype.hasOwnProperty.call(body, "scope"));
4521
+ const incomingScope = bodyIncludesScope
4522
+ ? parseScopeFromBody(body.scope)
4523
+ : undefined;
4494
4524
  const thread = await resolveThreadAccess(owner, threadId, "editor", { orgId });
4495
- if (!thread) {
4525
+ if (!thread ||
4526
+ !threadMatchesRequestedScope(thread, incomingScope)) {
4527
+ setResponseStatus(event, 404);
4528
+ return { error: "Thread not found" };
4529
+ }
4530
+ const bodyScopeMatchesRequestedScope = !requestedScope ||
4531
+ !incomingScope ||
4532
+ scopesMatch(incomingScope, requestedScope);
4533
+ const unauthorizedScopeChange = bodyIncludesScope &&
4534
+ ((incomingScope !== null &&
4535
+ (threadScopeMismatch(thread.scope, incomingScope) ||
4536
+ !bodyScopeMatchesRequestedScope)) ||
4537
+ (incomingScope === null &&
4538
+ isAppOwnedChatScope(thread.scope) &&
4539
+ !requestedScope));
4540
+ if (unauthorizedScopeChange) {
4496
4541
  setResponseStatus(event, 404);
4497
4542
  return { error: "Thread not found" };
4498
4543
  }
4499
- const body = await readBody(event);
4500
4544
  let newThreadData = body.threadData || thread.threadData;
4501
4545
  let newMessageCount = body.messageCount ?? thread.messageCount;
4502
4546
  let nextTitle = typeof body.title === "string" ? body.title : thread.title;
@@ -4543,9 +4587,8 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
4543
4587
  }
4544
4588
  await updateThreadData(threadId, newThreadData, nextTitle, nextPreview, newMessageCount, { ignoreConflicts: true });
4545
4589
  // Scope updates piggyback on the PUT — the client uses this
4546
- // path for both "detach" (scope: null) and "retag" flows.
4547
- // Send the field as `scope: undefined` (or omit it) when
4548
- // you don't want to touch the existing scope.
4590
+ // path for detach and for claiming a legacy unscoped thread.
4591
+ // A scoped thread cannot be retagged across resources here.
4549
4592
  if (Object.prototype.hasOwnProperty.call(body, "scope")) {
4550
4593
  const incomingScope = parseScopeFromBody(body.scope);
4551
4594
  await setThreadScope(threadId, incomingScope);
@@ -4559,7 +4602,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
4559
4602
  // on full-thread saves.
4560
4603
  if (method === "POST" && isThreadSubroute("queued")) {
4561
4604
  const thread = await resolveThreadAccess(owner, threadId, "editor", { orgId });
4562
- if (!thread) {
4605
+ if (!thread || !threadMatchesRequestedScope(thread)) {
4563
4606
  setResponseStatus(event, 404);
4564
4607
  return { error: "Thread not found" };
4565
4608
  }
@@ -4576,7 +4619,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
4576
4619
  }
4577
4620
  if (method === "POST" && isThreadSubroute("rename")) {
4578
4621
  const thread = await resolveThreadAccess(owner, threadId, "editor", { orgId });
4579
- if (!thread) {
4622
+ if (!thread || !threadMatchesRequestedScope(thread)) {
4580
4623
  setResponseStatus(event, 404);
4581
4624
  return { error: "Thread not found" };
4582
4625
  }
@@ -4597,7 +4640,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
4597
4640
  }
4598
4641
  if (method === "POST" && isThreadSubroute("pin")) {
4599
4642
  const thread = await resolveThreadAccess(owner, threadId, "editor", { orgId });
4600
- if (!thread) {
4643
+ if (!thread || !threadMatchesRequestedScope(thread)) {
4601
4644
  setResponseStatus(event, 404);
4602
4645
  return { error: "Thread not found" };
4603
4646
  }
@@ -4615,7 +4658,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
4615
4658
  }
4616
4659
  if (method === "POST" && isThreadSubroute("archive")) {
4617
4660
  const thread = await resolveThreadAccess(owner, threadId, "editor", { orgId });
4618
- if (!thread) {
4661
+ if (!thread || !threadMatchesRequestedScope(thread)) {
4619
4662
  setResponseStatus(event, 404);
4620
4663
  return { error: "Thread not found" };
4621
4664
  }
@@ -4634,7 +4677,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
4634
4677
  // POST /threads/:id/fork — duplicate a thread with all its messages
4635
4678
  if (method === "POST" && isThreadSubroute("fork")) {
4636
4679
  const thread = await resolveThreadAccess(owner, threadId, "viewer", { orgId });
4637
- if (!thread) {
4680
+ if (!thread || !threadMatchesRequestedScope(thread)) {
4638
4681
  setResponseStatus(event, 404);
4639
4682
  return { error: "Thread not found" };
4640
4683
  }
@@ -4652,7 +4695,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
4652
4695
  }
4653
4696
  if (isThreadSubroute("share")) {
4654
4697
  const thread = await resolveThreadAccess(owner, threadId, "admin", { orgId });
4655
- if (!thread) {
4698
+ if (!thread || !threadMatchesRequestedScope(thread)) {
4656
4699
  setResponseStatus(event, 404);
4657
4700
  return { error: "Thread not found" };
4658
4701
  }
@@ -4686,7 +4729,9 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
4686
4729
  }
4687
4730
  if (method === "DELETE") {
4688
4731
  const thread = await getThread(threadId);
4689
- if (!thread || thread.ownerEmail !== owner) {
4732
+ if (!thread ||
4733
+ thread.ownerEmail !== owner ||
4734
+ !threadMatchesRequestedScope(thread)) {
4690
4735
  setResponseStatus(event, 404);
4691
4736
  return { error: "Thread not found" };
4692
4737
  }
@@ -4727,6 +4772,46 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
4727
4772
  }
4728
4773
  if (method === "POST") {
4729
4774
  const body = await readBody(event);
4775
+ const requestedScope = parseScopeFromQuery(getQuery(event));
4776
+ const bodyIncludesScope = Boolean(body &&
4777
+ typeof body === "object" &&
4778
+ Object.prototype.hasOwnProperty.call(body, "scope"));
4779
+ const bodyScope = bodyIncludesScope
4780
+ ? parseScopeFromBody(body.scope)
4781
+ : undefined;
4782
+ const bodyScopeMatchesRequestedScope = !requestedScope ||
4783
+ !bodyIncludesScope ||
4784
+ (bodyScope !== null && scopesMatch(bodyScope, requestedScope));
4785
+ if (!bodyScopeMatchesRequestedScope) {
4786
+ setResponseStatus(event, 404);
4787
+ return { error: "Thread not found" };
4788
+ }
4789
+ const resolveExistingOwnedThread = async (existing) => {
4790
+ if ((requestedScope &&
4791
+ threadScopeMismatch(existing.scope, requestedScope)) ||
4792
+ (bodyIncludesScope &&
4793
+ threadScopeMismatch(existing.scope, bodyScope))) {
4794
+ setResponseStatus(event, 404);
4795
+ return null;
4796
+ }
4797
+ const scopeToAdopt = bodyIncludesScope
4798
+ ? bodyScope
4799
+ : requestedScope;
4800
+ if (!existing.scope && scopeToAdopt) {
4801
+ const adoptedScope = await adoptThreadScopeIfUnscoped(existing.id, scopeToAdopt);
4802
+ if (!scopesMatch(adoptedScope, scopeToAdopt)) {
4803
+ setResponseStatus(event, 404);
4804
+ return null;
4805
+ }
4806
+ const adopted = await getThread(existing.id);
4807
+ if (!adopted) {
4808
+ setResponseStatus(event, 404);
4809
+ return null;
4810
+ }
4811
+ return adopted;
4812
+ }
4813
+ return existing;
4814
+ };
4730
4815
  // Idempotent: when the caller supplies an id and a thread with
4731
4816
  // that id already exists for this owner, return it instead of
4732
4817
  // 500'ing on the UNIQUE constraint. The client can race with
@@ -4737,8 +4822,10 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
4737
4822
  if (body?.id) {
4738
4823
  const existing = await getThread(body.id);
4739
4824
  if (existing) {
4740
- if (existing.ownerEmail === owner)
4741
- return existing;
4825
+ if (existing.ownerEmail === owner) {
4826
+ const resolved = await resolveExistingOwnedThread(existing);
4827
+ return resolved ?? { error: "Thread not found" };
4828
+ }
4742
4829
  setResponseStatus(event, 409);
4743
4830
  return { error: "Thread id already in use" };
4744
4831
  }
@@ -4747,7 +4834,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
4747
4834
  const thread = await createThread(owner, {
4748
4835
  id: body?.id,
4749
4836
  title: body?.title ?? "",
4750
- scope: parseScopeFromBody(body?.scope),
4837
+ scope: bodyIncludesScope ? bodyScope : requestedScope,
4751
4838
  source: options?.appId ? { appId: options.appId } : null,
4752
4839
  });
4753
4840
  return thread;
@@ -4758,8 +4845,10 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
4758
4845
  // return the row that actually landed.
4759
4846
  if (body?.id) {
4760
4847
  const existing = await getThread(body.id);
4761
- if (existing && existing.ownerEmail === owner)
4762
- return existing;
4848
+ if (existing && existing.ownerEmail === owner) {
4849
+ const resolved = await resolveExistingOwnedThread(existing);
4850
+ return resolved ?? { error: "Thread not found" };
4851
+ }
4763
4852
  }
4764
4853
  throw err;
4765
4854
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.0.0-beta-20260820222620",
3
+ "version": "0.0.0-beta-20260820224008",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {