@agent-native/core 0.72.1 → 0.72.3
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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +14 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/AgentPanel.tsx +12 -1
- package/corpus/core/src/client/MultiTabAssistantChat.tsx +263 -13
- package/corpus/core/src/client/use-agent-chat-home-handoff.ts +16 -4
- package/corpus/core/src/client/use-chat-threads.ts +86 -5
- package/corpus/templates/assets/actions/navigate.ts +2 -1
- package/corpus/templates/assets/app/components/layout/Layout.tsx +3 -2
- package/corpus/templates/assets/app/components/layout/Sidebar.tsx +27 -4
- package/corpus/templates/assets/app/hooks/use-navigation-state.ts +35 -5
- package/corpus/templates/assets/app/routes/_index.tsx +12 -0
- package/corpus/templates/assets/app/routes/chat.$threadId.tsx +1 -0
- package/corpus/templates/chat/actions/navigate.ts +3 -0
- package/corpus/templates/chat/app/components/layout/Layout.tsx +7 -2
- package/corpus/templates/chat/app/components/layout/Sidebar.tsx +27 -4
- package/corpus/templates/chat/app/hooks/use-navigation-state.ts +38 -6
- package/corpus/templates/chat/app/routes/_index.tsx +13 -0
- package/corpus/templates/chat/app/routes/chat.$threadId.tsx +1 -0
- package/corpus/templates/dispatch/app/routes/chat.$threadId.tsx +26 -0
- package/dist/client/AgentPanel.d.ts +6 -2
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +4 -4
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts +22 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +215 -13
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/use-agent-chat-home-handoff.d.ts +7 -1
- package/dist/client/use-agent-chat-home-handoff.d.ts.map +1 -1
- package/dist/client/use-agent-chat-home-handoff.js +9 -5
- package/dist/client/use-agent-chat-home-handoff.js.map +1 -1
- package/dist/client/use-chat-threads.d.ts +6 -0
- package/dist/client/use-chat-threads.d.ts.map +1 -1
- package/dist/client/use-chat-threads.js +79 -5
- package/dist/client/use-chat-threads.js.map +1 -1
- package/package.json +1 -1
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @agent-native/core
|
|
2
2
|
|
|
3
|
+
## 0.72.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 6605885: Add opt-in URL sync for durable chat threads and route chat-first templates (chat, assets, and Dispatch) through `/chat/:threadId` deep links.
|
|
8
|
+
|
|
9
|
+
## 0.72.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- cc21a1c: Build the audit-redaction test fixture from concatenated parts so Netlify's
|
|
14
|
+
secret scanner no longer flags a literal `sk-` token pattern and blocks the
|
|
15
|
+
deploy.
|
|
16
|
+
|
|
3
17
|
## 0.72.1
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/corpus/core/package.json
CHANGED
|
@@ -75,7 +75,10 @@ const MultiTabAssistantChatLazy = lazy(() =>
|
|
|
75
75
|
default: m.MultiTabAssistantChat,
|
|
76
76
|
})),
|
|
77
77
|
);
|
|
78
|
-
import type {
|
|
78
|
+
import type {
|
|
79
|
+
MultiTabAssistantChatHeaderProps,
|
|
80
|
+
MultiTabAssistantChatProps,
|
|
81
|
+
} from "./MultiTabAssistantChat.js";
|
|
79
82
|
import type { AssistantChatProps } from "./AssistantChat.js";
|
|
80
83
|
import { assistantUiRecoverableRenderErrorKind } from "./assistant-ui-recovery.js";
|
|
81
84
|
import { useDevMode } from "./use-dev-mode.js";
|
|
@@ -518,6 +521,8 @@ export interface AgentPanelProps extends Omit<
|
|
|
518
521
|
scope?: import("./use-chat-threads.js").ChatThreadScope | null;
|
|
519
522
|
/** Stable browser tab id used for tab-scoped app-state context. */
|
|
520
523
|
browserTabId?: string;
|
|
524
|
+
/** Keep chat thread selection in URL state. */
|
|
525
|
+
threadUrlSync?: MultiTabAssistantChatProps["threadUrlSync"];
|
|
521
526
|
/** Optional notice rendered below the main header while Chat mode is active. */
|
|
522
527
|
chatNotice?: React.ReactNode;
|
|
523
528
|
/** Show the chat thread tab row when the panel header is hidden. Default: true. */
|
|
@@ -636,6 +641,7 @@ function AgentPanelInner({
|
|
|
636
641
|
restoreActiveThread = true,
|
|
637
642
|
scope,
|
|
638
643
|
browserTabId,
|
|
644
|
+
threadUrlSync,
|
|
639
645
|
chatNotice,
|
|
640
646
|
showTabBar = true,
|
|
641
647
|
codeAccess,
|
|
@@ -1589,6 +1595,7 @@ function AgentPanelInner({
|
|
|
1589
1595
|
restoreActiveThread={restoreActiveThread}
|
|
1590
1596
|
scope={scope}
|
|
1591
1597
|
browserTabId={browserTabId}
|
|
1598
|
+
threadUrlSync={threadUrlSync}
|
|
1592
1599
|
/>
|
|
1593
1600
|
</Suspense>
|
|
1594
1601
|
)}
|
|
@@ -2240,6 +2247,8 @@ export interface AgentSidebarProps {
|
|
|
2240
2247
|
scope?: import("./use-chat-threads.js").ChatThreadScope | null;
|
|
2241
2248
|
/** Stable browser tab id used for tab-scoped app-state context. */
|
|
2242
2249
|
browserTabId?: string;
|
|
2250
|
+
/** Keep chat thread selection in URL state. */
|
|
2251
|
+
threadUrlSync?: MultiTabAssistantChatProps["threadUrlSync"];
|
|
2243
2252
|
}
|
|
2244
2253
|
|
|
2245
2254
|
/**
|
|
@@ -2262,6 +2271,7 @@ export function AgentSidebar({
|
|
|
2262
2271
|
onFullscreenRequest,
|
|
2263
2272
|
scope,
|
|
2264
2273
|
browserTabId,
|
|
2274
|
+
threadUrlSync,
|
|
2265
2275
|
}: AgentSidebarProps) {
|
|
2266
2276
|
const initialWidth = defaultSidebarWidth ?? sidebarWidth ?? 380;
|
|
2267
2277
|
const [open, setOpen] = useState(() =>
|
|
@@ -2730,6 +2740,7 @@ export function AgentSidebar({
|
|
|
2730
2740
|
storageKey={storageKey}
|
|
2731
2741
|
scope={scope}
|
|
2732
2742
|
browserTabId={browserTabId}
|
|
2743
|
+
threadUrlSync={threadUrlSync}
|
|
2733
2744
|
/>
|
|
2734
2745
|
</div>
|
|
2735
2746
|
{showResizeHandle && isLeft && (
|
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
type ChatThreadScope,
|
|
39
39
|
type ChatThreadSummary,
|
|
40
40
|
} from "./use-chat-threads.js";
|
|
41
|
-
import { agentNativePath } from "./api-path.js";
|
|
41
|
+
import { agentNativePath, appPath } from "./api-path.js";
|
|
42
42
|
import { callAction } from "./use-action.js";
|
|
43
43
|
import { RunStuckBanner } from "./RunStuckBanner.js";
|
|
44
44
|
import { DEFAULT_MODEL } from "../agent/default-model.js";
|
|
@@ -781,6 +781,107 @@ function chatTabStatusFromAgentTeamStatus(
|
|
|
781
781
|
}
|
|
782
782
|
|
|
783
783
|
const STALE_THREAD_THRESHOLD_MS = 12 * 60 * 60 * 1000;
|
|
784
|
+
const DEFAULT_THREAD_URL_PARAM = "thread";
|
|
785
|
+
const THREAD_URL_CHANGED_EVENT = "agent-chat:url-thread-changed";
|
|
786
|
+
const hasOwn = Object.prototype.hasOwnProperty;
|
|
787
|
+
|
|
788
|
+
// The history patch is installed once and shared via a ref count so that
|
|
789
|
+
// multiple synced chats (or a remount) don't restore a stale `pushState`
|
|
790
|
+
// reference and silently drop a wrapper installed by another instance.
|
|
791
|
+
let historyPatchRefCount = 0;
|
|
792
|
+
let restoreHistoryPatch: (() => void) | null = null;
|
|
793
|
+
|
|
794
|
+
function installHistoryThreadUrlPatch(): () => void {
|
|
795
|
+
if (typeof window === "undefined") return () => {};
|
|
796
|
+
historyPatchRefCount += 1;
|
|
797
|
+
if (historyPatchRefCount === 1) {
|
|
798
|
+
const originalPushState = window.history.pushState;
|
|
799
|
+
const originalReplaceState = window.history.replaceState;
|
|
800
|
+
const dispatchUrlChange = () => {
|
|
801
|
+
window.dispatchEvent(new Event(THREAD_URL_CHANGED_EVENT));
|
|
802
|
+
};
|
|
803
|
+
window.history.pushState = function pushState(...args) {
|
|
804
|
+
const result = originalPushState.apply(this, args);
|
|
805
|
+
dispatchUrlChange();
|
|
806
|
+
return result;
|
|
807
|
+
};
|
|
808
|
+
window.history.replaceState = function replaceState(...args) {
|
|
809
|
+
const result = originalReplaceState.apply(this, args);
|
|
810
|
+
dispatchUrlChange();
|
|
811
|
+
return result;
|
|
812
|
+
};
|
|
813
|
+
restoreHistoryPatch = () => {
|
|
814
|
+
window.history.pushState = originalPushState;
|
|
815
|
+
window.history.replaceState = originalReplaceState;
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
return () => {
|
|
819
|
+
historyPatchRefCount = Math.max(0, historyPatchRefCount - 1);
|
|
820
|
+
if (historyPatchRefCount === 0) {
|
|
821
|
+
restoreHistoryPatch?.();
|
|
822
|
+
restoreHistoryPatch = null;
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
export interface ChatThreadUrlSyncOptions {
|
|
828
|
+
/** Query-string parameter used by the generic URL adapter. Default: `thread`. */
|
|
829
|
+
paramName?: string;
|
|
830
|
+
/**
|
|
831
|
+
* Route-owned thread id. Pass `null` for the create route and a string for
|
|
832
|
+
* thread routes like `/chat/:threadId`.
|
|
833
|
+
*/
|
|
834
|
+
routeThreadId?: string | null;
|
|
835
|
+
/** Build the URL path for a thread id, or for create mode when id is null. */
|
|
836
|
+
getPath?: (threadId: string | null) => string;
|
|
837
|
+
/** Optional router navigation callback used with `getPath`. */
|
|
838
|
+
navigate?: (path: string, options?: { replace?: boolean }) => void;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function normalizeUrlThreadId(value: string | null | undefined): string | null {
|
|
842
|
+
if (typeof value !== "string") return null;
|
|
843
|
+
const trimmed = value.trim();
|
|
844
|
+
return trimmed || null;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function resolveThreadUrlSync(
|
|
848
|
+
value: MultiTabAssistantChatProps["threadUrlSync"],
|
|
849
|
+
): {
|
|
850
|
+
enabled: boolean;
|
|
851
|
+
paramName: string;
|
|
852
|
+
routeThreadId?: string | null;
|
|
853
|
+
getPath?: (threadId: string | null) => string;
|
|
854
|
+
navigate?: (path: string, options?: { replace?: boolean }) => void;
|
|
855
|
+
} {
|
|
856
|
+
if (!value) return { enabled: false, paramName: DEFAULT_THREAD_URL_PARAM };
|
|
857
|
+
if (value === true) {
|
|
858
|
+
return { enabled: true, paramName: DEFAULT_THREAD_URL_PARAM };
|
|
859
|
+
}
|
|
860
|
+
return {
|
|
861
|
+
enabled: true,
|
|
862
|
+
paramName: value.paramName?.trim() || DEFAULT_THREAD_URL_PARAM,
|
|
863
|
+
...(hasOwn.call(value, "routeThreadId")
|
|
864
|
+
? { routeThreadId: normalizeUrlThreadId(value.routeThreadId) }
|
|
865
|
+
: {}),
|
|
866
|
+
...(value.getPath ? { getPath: value.getPath } : {}),
|
|
867
|
+
...(value.navigate ? { navigate: value.navigate } : {}),
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function readUrlThreadId(paramName: string): string | null {
|
|
872
|
+
if (typeof window === "undefined") return null;
|
|
873
|
+
try {
|
|
874
|
+
const params = new URLSearchParams(window.location.search);
|
|
875
|
+
return normalizeUrlThreadId(
|
|
876
|
+
params.get(paramName) ??
|
|
877
|
+
(paramName === DEFAULT_THREAD_URL_PARAM
|
|
878
|
+
? params.get("threadId")
|
|
879
|
+
: null),
|
|
880
|
+
);
|
|
881
|
+
} catch {
|
|
882
|
+
return null;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
784
885
|
|
|
785
886
|
function runToAgentTeamTabInfo(
|
|
786
887
|
run: AgentTeamRunSummary,
|
|
@@ -845,6 +946,12 @@ export type MultiTabAssistantChatProps = Omit<
|
|
|
845
946
|
restoreActiveThread?: boolean;
|
|
846
947
|
/** Stable browser tab id used for tab-scoped app-state context. */
|
|
847
948
|
browserTabId?: string;
|
|
949
|
+
/**
|
|
950
|
+
* Keep the active thread in URL state. `true` uses the generic `?thread=id`
|
|
951
|
+
* adapter; passing `routeThreadId` + `getPath` lets an app bind chats to
|
|
952
|
+
* route params such as `/chat/:threadId`.
|
|
953
|
+
*/
|
|
954
|
+
threadUrlSync?: boolean | ChatThreadUrlSyncOptions;
|
|
848
955
|
/**
|
|
849
956
|
* Bind new chats to a resource (deck, design, dashboard, etc.). When set,
|
|
850
957
|
* new chats automatically inherit this scope and scoped chats tuck away when
|
|
@@ -864,15 +971,114 @@ export function MultiTabAssistantChat({
|
|
|
864
971
|
storageKey,
|
|
865
972
|
restoreActiveThread = true,
|
|
866
973
|
browserTabId,
|
|
974
|
+
threadUrlSync = false,
|
|
867
975
|
scope = null,
|
|
868
976
|
...props
|
|
869
977
|
}: MultiTabAssistantChatProps) {
|
|
978
|
+
const {
|
|
979
|
+
enabled: threadUrlSyncEnabled,
|
|
980
|
+
paramName: threadUrlParamName,
|
|
981
|
+
routeThreadId,
|
|
982
|
+
getPath: getThreadPath,
|
|
983
|
+
navigate: navigateThreadUrl,
|
|
984
|
+
} = resolveThreadUrlSync(threadUrlSync);
|
|
985
|
+
const threadRouteControlsActiveThread =
|
|
986
|
+
threadUrlSyncEnabled &&
|
|
987
|
+
threadUrlSync !== true &&
|
|
988
|
+
typeof threadUrlSync === "object" &&
|
|
989
|
+
hasOwn.call(threadUrlSync, "routeThreadId");
|
|
990
|
+
const [urlThreadId, setUrlThreadId] = useState<string | null>(() =>
|
|
991
|
+
threadUrlSyncEnabled
|
|
992
|
+
? threadRouteControlsActiveThread
|
|
993
|
+
? (routeThreadId ?? null)
|
|
994
|
+
: readUrlThreadId(threadUrlParamName)
|
|
995
|
+
: null,
|
|
996
|
+
);
|
|
997
|
+
const urlThreadIdRef = useRef(urlThreadId);
|
|
998
|
+
urlThreadIdRef.current = urlThreadId;
|
|
999
|
+
|
|
1000
|
+
useEffect(() => {
|
|
1001
|
+
if (!threadUrlSyncEnabled || threadRouteControlsActiveThread) return;
|
|
1002
|
+
const update = () => setUrlThreadId(readUrlThreadId(threadUrlParamName));
|
|
1003
|
+
const uninstallHistoryPatch = installHistoryThreadUrlPatch();
|
|
1004
|
+
update();
|
|
1005
|
+
window.addEventListener("popstate", update);
|
|
1006
|
+
window.addEventListener(THREAD_URL_CHANGED_EVENT, update);
|
|
1007
|
+
return () => {
|
|
1008
|
+
uninstallHistoryPatch();
|
|
1009
|
+
window.removeEventListener("popstate", update);
|
|
1010
|
+
window.removeEventListener(THREAD_URL_CHANGED_EVENT, update);
|
|
1011
|
+
};
|
|
1012
|
+
}, [
|
|
1013
|
+
threadRouteControlsActiveThread,
|
|
1014
|
+
threadUrlParamName,
|
|
1015
|
+
threadUrlSyncEnabled,
|
|
1016
|
+
]);
|
|
1017
|
+
|
|
1018
|
+
useEffect(() => {
|
|
1019
|
+
if (!threadUrlSyncEnabled || !threadRouteControlsActiveThread) return;
|
|
1020
|
+
setUrlThreadId(routeThreadId ?? null);
|
|
1021
|
+
}, [routeThreadId, threadRouteControlsActiveThread, threadUrlSyncEnabled]);
|
|
1022
|
+
|
|
1023
|
+
const writeThreadUrl = useCallback(
|
|
1024
|
+
(threadId: string | null, options: { replace?: boolean } = {}): void => {
|
|
1025
|
+
if (!threadUrlSyncEnabled || typeof window === "undefined") return;
|
|
1026
|
+
try {
|
|
1027
|
+
const normalizedThreadId = normalizeUrlThreadId(threadId);
|
|
1028
|
+
let next: string;
|
|
1029
|
+
if (getThreadPath) {
|
|
1030
|
+
next = getThreadPath(normalizedThreadId);
|
|
1031
|
+
} else {
|
|
1032
|
+
const url = new URL(window.location.href);
|
|
1033
|
+
if (normalizedThreadId) {
|
|
1034
|
+
url.searchParams.set(threadUrlParamName, normalizedThreadId);
|
|
1035
|
+
} else {
|
|
1036
|
+
url.searchParams.delete(threadUrlParamName);
|
|
1037
|
+
}
|
|
1038
|
+
if (threadUrlParamName !== "threadId") {
|
|
1039
|
+
url.searchParams.delete("threadId");
|
|
1040
|
+
}
|
|
1041
|
+
next = `${url.pathname}${url.search}${url.hash}`;
|
|
1042
|
+
}
|
|
1043
|
+
const current = `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
|
1044
|
+
if (next === current) {
|
|
1045
|
+
setUrlThreadId(normalizedThreadId);
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
if (getThreadPath && navigateThreadUrl) {
|
|
1049
|
+
navigateThreadUrl(next, { replace: options.replace === true });
|
|
1050
|
+
setUrlThreadId(normalizedThreadId);
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
const method = options.replace ? "replaceState" : "pushState";
|
|
1054
|
+
// `getThreadPath` returns a router-local path (no app basename). When we
|
|
1055
|
+
// fall back to the raw History API instead of a router navigate, resolve
|
|
1056
|
+
// the basename so deep-link reloads work in mounted apps.
|
|
1057
|
+
const historyTarget = getThreadPath ? appPath(next) : next;
|
|
1058
|
+
window.history[method](window.history.state, "", historyTarget);
|
|
1059
|
+
setUrlThreadId(normalizedThreadId);
|
|
1060
|
+
window.dispatchEvent(new Event(THREAD_URL_CHANGED_EVENT));
|
|
1061
|
+
const popstate =
|
|
1062
|
+
typeof PopStateEvent === "function"
|
|
1063
|
+
? new PopStateEvent("popstate", { state: window.history.state })
|
|
1064
|
+
: new Event("popstate");
|
|
1065
|
+
window.dispatchEvent(popstate);
|
|
1066
|
+
} catch {}
|
|
1067
|
+
},
|
|
1068
|
+
[
|
|
1069
|
+
getThreadPath,
|
|
1070
|
+
navigateThreadUrl,
|
|
1071
|
+
threadUrlParamName,
|
|
1072
|
+
threadUrlSyncEnabled,
|
|
1073
|
+
],
|
|
1074
|
+
);
|
|
1075
|
+
|
|
870
1076
|
const {
|
|
871
1077
|
threads,
|
|
872
1078
|
activeThreadId,
|
|
873
1079
|
isLoading,
|
|
874
1080
|
createThread,
|
|
875
|
-
switchThread,
|
|
1081
|
+
switchThread: switchThreadState,
|
|
876
1082
|
detachThread,
|
|
877
1083
|
forkThread,
|
|
878
1084
|
saveThreadData,
|
|
@@ -880,7 +1086,18 @@ export function MultiTabAssistantChat({
|
|
|
880
1086
|
searchThreads,
|
|
881
1087
|
refreshThreads,
|
|
882
1088
|
isNewThread,
|
|
883
|
-
} = useChatThreads(apiUrl, storageKey, scope, {
|
|
1089
|
+
} = useChatThreads(apiUrl, storageKey, scope, {
|
|
1090
|
+
restoreActiveThread,
|
|
1091
|
+
routeThreadId: threadUrlSyncEnabled ? urlThreadId : undefined,
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
const switchThread = useCallback(
|
|
1095
|
+
(threadId: string, options: { replace?: boolean } = {}) => {
|
|
1096
|
+
switchThreadState(threadId);
|
|
1097
|
+
writeThreadUrl(threadId, options);
|
|
1098
|
+
},
|
|
1099
|
+
[switchThreadState, writeThreadUrl],
|
|
1100
|
+
);
|
|
884
1101
|
|
|
885
1102
|
// Namespace all localStorage keys by storageKey when provided (for per-app isolation in frame)
|
|
886
1103
|
const keyPrefix = storageKey ? `:${storageKey}` : "";
|
|
@@ -1370,9 +1587,18 @@ export function MultiTabAssistantChat({
|
|
|
1370
1587
|
|
|
1371
1588
|
// If active thread is stale, start fresh
|
|
1372
1589
|
if (!parentMap[activeThreadId] && isStale(activeThreadId)) {
|
|
1373
|
-
createThread()
|
|
1590
|
+
createThread().then((id) => {
|
|
1591
|
+
if (id) writeThreadUrl(null);
|
|
1592
|
+
});
|
|
1374
1593
|
}
|
|
1375
|
-
}, [
|
|
1594
|
+
}, [
|
|
1595
|
+
activeThreadId,
|
|
1596
|
+
threads,
|
|
1597
|
+
parentMap,
|
|
1598
|
+
switchThread,
|
|
1599
|
+
createThread,
|
|
1600
|
+
writeThreadUrl,
|
|
1601
|
+
]);
|
|
1376
1602
|
|
|
1377
1603
|
// Ensure active thread is always in open tabs.
|
|
1378
1604
|
// Use functional update to check inside the setter — avoids race with the
|
|
@@ -1412,10 +1638,11 @@ export function MultiTabAssistantChat({
|
|
|
1412
1638
|
if (id) {
|
|
1413
1639
|
newThreadIds.current.add(id);
|
|
1414
1640
|
setOpenTabIds([id]);
|
|
1641
|
+
writeThreadUrl(null, { replace: true });
|
|
1415
1642
|
}
|
|
1416
1643
|
});
|
|
1417
1644
|
}
|
|
1418
|
-
}, [isLoading, openTabIds, activeThreadId, createThread]);
|
|
1645
|
+
}, [isLoading, openTabIds, activeThreadId, createThread, writeThreadUrl]);
|
|
1419
1646
|
|
|
1420
1647
|
useEffect(() => {
|
|
1421
1648
|
let stopped = false;
|
|
@@ -1700,9 +1927,12 @@ export function MultiTabAssistantChat({
|
|
|
1700
1927
|
setOpenTabIds((prev) =>
|
|
1701
1928
|
prev.includes(newId) ? prev : [...prev, newId],
|
|
1702
1929
|
);
|
|
1930
|
+
if (!background) {
|
|
1931
|
+
writeThreadUrl(newId);
|
|
1932
|
+
}
|
|
1703
1933
|
sendToTab(newId);
|
|
1704
1934
|
if (background && previousTabId) {
|
|
1705
|
-
|
|
1935
|
+
switchThreadState(previousTabId);
|
|
1706
1936
|
}
|
|
1707
1937
|
}
|
|
1708
1938
|
});
|
|
@@ -1729,6 +1959,8 @@ export function MultiTabAssistantChat({
|
|
|
1729
1959
|
removeContextInTab,
|
|
1730
1960
|
setContextInTab,
|
|
1731
1961
|
switchThread,
|
|
1962
|
+
switchThreadState,
|
|
1963
|
+
writeThreadUrl,
|
|
1732
1964
|
]);
|
|
1733
1965
|
|
|
1734
1966
|
// Replay submits posted before this lazy panel's listener attached. Dedup in
|
|
@@ -1800,9 +2032,10 @@ export function MultiTabAssistantChat({
|
|
|
1800
2032
|
const id = await createThread();
|
|
1801
2033
|
if (id) {
|
|
1802
2034
|
newThreadIds.current.add(id);
|
|
2035
|
+
writeThreadUrl(null);
|
|
1803
2036
|
}
|
|
1804
2037
|
return id;
|
|
1805
|
-
}, [createThread]);
|
|
2038
|
+
}, [createThread, writeThreadUrl]);
|
|
1806
2039
|
|
|
1807
2040
|
const cleanupClosedTab = useCallback((tabId: string) => {
|
|
1808
2041
|
if (parentMapRef.current[tabId]) {
|
|
@@ -1842,6 +2075,7 @@ export function MultiTabAssistantChat({
|
|
|
1842
2075
|
if (newId) {
|
|
1843
2076
|
newThreadIds.current.add(newId);
|
|
1844
2077
|
setOpenTabIds([newId]);
|
|
2078
|
+
writeThreadUrl(null);
|
|
1845
2079
|
}
|
|
1846
2080
|
});
|
|
1847
2081
|
return prev; // Keep old tab until new one is ready
|
|
@@ -1855,7 +2089,7 @@ export function MultiTabAssistantChat({
|
|
|
1855
2089
|
});
|
|
1856
2090
|
cleanupClosedTab(tabId);
|
|
1857
2091
|
},
|
|
1858
|
-
[switchThread, createThread, cleanupClosedTab],
|
|
2092
|
+
[switchThread, createThread, cleanupClosedTab, writeThreadUrl],
|
|
1859
2093
|
);
|
|
1860
2094
|
|
|
1861
2095
|
const closeOtherTabs = useCallback(
|
|
@@ -1906,7 +2140,8 @@ export function MultiTabAssistantChat({
|
|
|
1906
2140
|
if (id) {
|
|
1907
2141
|
newThreadIds.current.add(id);
|
|
1908
2142
|
setOpenTabIds([id]);
|
|
1909
|
-
|
|
2143
|
+
switchThreadState(id);
|
|
2144
|
+
writeThreadUrl(null);
|
|
1910
2145
|
dismissedSubAgentTabsRef.current.clear();
|
|
1911
2146
|
// Clean up all old refs
|
|
1912
2147
|
chatRefs.current.clear();
|
|
@@ -1917,7 +2152,7 @@ export function MultiTabAssistantChat({
|
|
|
1917
2152
|
setSubAgentNames({});
|
|
1918
2153
|
setSubAgentStatuses({});
|
|
1919
2154
|
}
|
|
1920
|
-
}, [createThread,
|
|
2155
|
+
}, [createThread, switchThreadState, writeThreadUrl]);
|
|
1921
2156
|
|
|
1922
2157
|
// Keyboard shortcuts dispatched from AgentPanel based on the active mode
|
|
1923
2158
|
useEffect(() => {
|
|
@@ -1955,6 +2190,14 @@ export function MultiTabAssistantChat({
|
|
|
1955
2190
|
|
|
1956
2191
|
if (detail?.newThread === true) {
|
|
1957
2192
|
newThreadIds.current.add(threadId);
|
|
2193
|
+
void createThread(threadId).then((createdId) => {
|
|
2194
|
+
if (!createdId) return;
|
|
2195
|
+
setOpenTabIds((prev) =>
|
|
2196
|
+
prev.includes(createdId) ? prev : [...prev, createdId],
|
|
2197
|
+
);
|
|
2198
|
+
writeThreadUrl(null);
|
|
2199
|
+
});
|
|
2200
|
+
return;
|
|
1958
2201
|
}
|
|
1959
2202
|
setOpenTabIds((prev) =>
|
|
1960
2203
|
prev.includes(threadId) ? prev : [...prev, threadId],
|
|
@@ -1965,7 +2208,7 @@ export function MultiTabAssistantChat({
|
|
|
1965
2208
|
window.addEventListener("agent-chat:open-thread", handleOpenThread);
|
|
1966
2209
|
return () =>
|
|
1967
2210
|
window.removeEventListener("agent-chat:open-thread", handleOpenThread);
|
|
1968
|
-
}, [switchThread]);
|
|
2211
|
+
}, [createThread, switchThread, writeThreadUrl]);
|
|
1969
2212
|
|
|
1970
2213
|
const clearActiveTab = useCallback(() => {
|
|
1971
2214
|
const tabIdToClear = activeThreadIdRef.current;
|
|
@@ -2125,8 +2368,15 @@ export function MultiTabAssistantChat({
|
|
|
2125
2368
|
},
|
|
2126
2369
|
) => {
|
|
2127
2370
|
saveThreadData(threadId, data);
|
|
2371
|
+
if (
|
|
2372
|
+
data.messageCount > 0 &&
|
|
2373
|
+
threadId === activeThreadIdRef.current &&
|
|
2374
|
+
urlThreadIdRef.current !== threadId
|
|
2375
|
+
) {
|
|
2376
|
+
writeThreadUrl(threadId);
|
|
2377
|
+
}
|
|
2128
2378
|
},
|
|
2129
|
-
[saveThreadData],
|
|
2379
|
+
[saveThreadData, writeThreadUrl],
|
|
2130
2380
|
);
|
|
2131
2381
|
|
|
2132
2382
|
// ─── Slash command handler ──────────────────────────────────────────
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
2
|
import { useLocation, useNavigate } from "react-router";
|
|
3
3
|
import { appBasePath } from "./api-path.js";
|
|
4
4
|
import {
|
|
@@ -21,6 +21,12 @@ export interface UseAgentChatHomeHandoffLinksOptions {
|
|
|
21
21
|
storageKey?: string | null;
|
|
22
22
|
/** Router-local path for the full-page chat route. Defaults to "/". */
|
|
23
23
|
chatPath?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Matches every router-local pathname owned by the full-page chat surface.
|
|
26
|
+
* Defaults to an exact match against `chatPath`. Pass this to cover deep
|
|
27
|
+
* links such as `/chat/:threadId`.
|
|
28
|
+
*/
|
|
29
|
+
isChatPath?: (pathname: string) => boolean;
|
|
24
30
|
/** Disable link interception without changing hook call order. */
|
|
25
31
|
enabled?: boolean;
|
|
26
32
|
}
|
|
@@ -120,13 +126,19 @@ export function useAgentChatHomeHandoff({
|
|
|
120
126
|
export function useAgentChatHomeHandoffLinks({
|
|
121
127
|
storageKey,
|
|
122
128
|
chatPath = "/",
|
|
129
|
+
isChatPath,
|
|
123
130
|
enabled = true,
|
|
124
131
|
}: UseAgentChatHomeHandoffLinksOptions): void {
|
|
125
132
|
const location = useLocation();
|
|
126
133
|
const navigate = useNavigate();
|
|
134
|
+
const matchesChatPathRef = useRef<(pathname: string) => boolean>(() => false);
|
|
135
|
+
matchesChatPathRef.current =
|
|
136
|
+
isChatPath ?? ((pathname: string) => pathname === chatPath);
|
|
127
137
|
|
|
128
138
|
useEffect(() => {
|
|
129
|
-
|
|
139
|
+
const matchesChatPath = (pathname: string) =>
|
|
140
|
+
matchesChatPathRef.current(pathname);
|
|
141
|
+
if (!enabled || !matchesChatPath(stripBasePath(location.pathname))) return;
|
|
130
142
|
if (typeof document === "undefined") return;
|
|
131
143
|
|
|
132
144
|
function handleClick(event: MouseEvent) {
|
|
@@ -141,7 +153,7 @@ export function useAgentChatHomeHandoffLinks({
|
|
|
141
153
|
const pathname = path ? pathnameFromLocalPath(path) : "";
|
|
142
154
|
if (
|
|
143
155
|
!path ||
|
|
144
|
-
pathname
|
|
156
|
+
matchesChatPath(pathname) ||
|
|
145
157
|
isFrameworkOrApiPath(pathname) ||
|
|
146
158
|
isStaticAssetPath(pathname)
|
|
147
159
|
) {
|
|
@@ -155,5 +167,5 @@ export function useAgentChatHomeHandoffLinks({
|
|
|
155
167
|
|
|
156
168
|
document.addEventListener("click", handleClick, true);
|
|
157
169
|
return () => document.removeEventListener("click", handleClick, true);
|
|
158
|
-
}, [
|
|
170
|
+
}, [enabled, location.pathname, navigate, storageKey]);
|
|
159
171
|
}
|