@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.
- package/dist/chat-threads/store.d.ts +7 -0
- package/dist/chat-threads/store.js +15 -0
- package/dist/client/AgentPanel.d.ts +5 -1
- package/dist/client/AgentPanel.js +8 -8
- package/dist/client/AssistantChat.d.ts +3 -1
- package/dist/client/AssistantChat.js +22 -6
- package/dist/client/MultiTabAssistantChat.d.ts +3 -1
- package/dist/client/MultiTabAssistantChat.js +5 -4
- package/dist/client/use-chat-threads.d.ts +7 -0
- package/dist/client/use-chat-threads.js +144 -40
- package/dist/collab/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/server/agent-chat-plugin.js +112 -23
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
}, [
|
|
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
|
|
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 (
|
|
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
|
-
!
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
-
!
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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
|
-
}, [
|
|
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]);
|
package/dist/collab/routes.d.ts
CHANGED
|
@@ -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 (
|
|
2294
|
-
|
|
2295
|
-
|
|
2293
|
+
if (!thread) {
|
|
2294
|
+
throw createError({
|
|
2295
|
+
statusCode: 404,
|
|
2296
|
+
statusMessage: "Thread not found",
|
|
2296
2297
|
});
|
|
2297
2298
|
}
|
|
2298
|
-
if (
|
|
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
|
|
4547
|
-
//
|
|
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 ||
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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-
|
|
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": {
|