@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
|
@@ -63,6 +63,12 @@ export interface UseChatThreadsOptions {
|
|
|
63
63
|
autoCreate?: boolean;
|
|
64
64
|
/** Restore the active thread from localStorage. Defaults to true. */
|
|
65
65
|
restoreActiveThread?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Route-owned active thread. `undefined` preserves the legacy localStorage
|
|
68
|
+
* source of truth; a string opens that thread; `null` means the URL is in
|
|
69
|
+
* create/new-chat mode.
|
|
70
|
+
*/
|
|
71
|
+
routeThreadId?: string | null;
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
const ACTIVE_THREAD_KEY = "agent-chat-active-thread";
|
|
@@ -114,6 +120,12 @@ function createLocalThreadId(): string {
|
|
|
114
120
|
return `thread-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
115
121
|
}
|
|
116
122
|
|
|
123
|
+
function normalizeThreadId(value: string | null | undefined): string | null {
|
|
124
|
+
if (typeof value !== "string") return null;
|
|
125
|
+
const trimmed = value.trim();
|
|
126
|
+
return trimmed || null;
|
|
127
|
+
}
|
|
128
|
+
|
|
117
129
|
function scopesMatch(
|
|
118
130
|
a?: ChatThreadScope | null,
|
|
119
131
|
b?: ChatThreadScope | null,
|
|
@@ -155,6 +167,8 @@ export function useChatThreads(
|
|
|
155
167
|
) {
|
|
156
168
|
const autoCreate = options?.autoCreate !== false;
|
|
157
169
|
const restoreActiveThread = options?.restoreActiveThread !== false;
|
|
170
|
+
const routeControlsActiveThread = options?.routeThreadId !== undefined;
|
|
171
|
+
const routeThreadId = normalizeThreadId(options?.routeThreadId);
|
|
158
172
|
// Each (storageKey, scope) pair gets its own active-thread localStorage key
|
|
159
173
|
// for chats that belong to a resource. General chats keep using the unscoped
|
|
160
174
|
// key even while the user is looking at a resource, so clicking into a deck,
|
|
@@ -180,10 +194,16 @@ export function useChatThreads(
|
|
|
180
194
|
let id: string | null = null;
|
|
181
195
|
let isNew = false;
|
|
182
196
|
if (typeof window !== "undefined") {
|
|
183
|
-
|
|
184
|
-
id =
|
|
185
|
-
}
|
|
186
|
-
|
|
197
|
+
if (routeControlsActiveThread) {
|
|
198
|
+
id = routeThreadId;
|
|
199
|
+
} else {
|
|
200
|
+
try {
|
|
201
|
+
id = restoreActiveThread
|
|
202
|
+
? localStorage.getItem(activeThreadKey)
|
|
203
|
+
: null;
|
|
204
|
+
} catch {
|
|
205
|
+
id = null;
|
|
206
|
+
}
|
|
187
207
|
}
|
|
188
208
|
if (!id && autoCreate) {
|
|
189
209
|
id = createLocalThreadId();
|
|
@@ -323,6 +343,10 @@ export function useChatThreads(
|
|
|
323
343
|
const persistedKeyRef = useRef(activeThreadKey);
|
|
324
344
|
useEffect(() => {
|
|
325
345
|
if (persistedKeyRef.current !== activeThreadKey) {
|
|
346
|
+
if (routeControlsActiveThread) {
|
|
347
|
+
persistedKeyRef.current = activeThreadKey;
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
326
350
|
const currentId = activeThreadIdRef.current;
|
|
327
351
|
if (currentId) {
|
|
328
352
|
const currentThreadScope = readKnownThreadScope(currentId);
|
|
@@ -357,6 +381,11 @@ export function useChatThreads(
|
|
|
357
381
|
return;
|
|
358
382
|
}
|
|
359
383
|
try {
|
|
384
|
+
if (routeControlsActiveThread && !routeThreadId) {
|
|
385
|
+
localStorage.removeItem(activeThreadKey);
|
|
386
|
+
localStorage.removeItem(activeThreadSeenKey);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
360
389
|
if (activeThreadId) {
|
|
361
390
|
const threadScope = readKnownThreadScope(activeThreadId);
|
|
362
391
|
if (threadScope === undefined) return;
|
|
@@ -378,6 +407,8 @@ export function useChatThreads(
|
|
|
378
407
|
addOptimisticThread,
|
|
379
408
|
autoCreate,
|
|
380
409
|
readKnownThreadScope,
|
|
410
|
+
routeControlsActiveThread,
|
|
411
|
+
routeThreadId,
|
|
381
412
|
storageKey,
|
|
382
413
|
threads,
|
|
383
414
|
]);
|
|
@@ -484,6 +515,10 @@ export function useChatThreads(
|
|
|
484
515
|
const loadedHasSavedId = Boolean(
|
|
485
516
|
savedId && loadedThreads.some((t) => t.id === savedId),
|
|
486
517
|
);
|
|
518
|
+
const savedIdCameFromRoute =
|
|
519
|
+
Boolean(savedId) &&
|
|
520
|
+
routeControlsActiveThread &&
|
|
521
|
+
routeThreadId === savedId;
|
|
487
522
|
|
|
488
523
|
if (
|
|
489
524
|
savedId &&
|
|
@@ -491,6 +526,11 @@ export function useChatThreads(
|
|
|
491
526
|
!loadedHasSavedId
|
|
492
527
|
) {
|
|
493
528
|
addOptimisticThread(savedId, scopeRef.current ?? null);
|
|
529
|
+
} else if (savedId && savedIdCameFromRoute && !loadedHasSavedId) {
|
|
530
|
+
// A deep link may point to a thread that is not in the current list
|
|
531
|
+
// response. Keep it route-owned so AssistantChat can restore it via
|
|
532
|
+
// /threads/:id instead of reclassifying it as a new empty tab.
|
|
533
|
+
setActiveThreadId(savedId);
|
|
494
534
|
} else if (
|
|
495
535
|
savedId &&
|
|
496
536
|
!newlyCreatedRef.current.has(savedId) &&
|
|
@@ -526,7 +566,13 @@ export function useChatThreads(
|
|
|
526
566
|
}
|
|
527
567
|
setIsLoading(false);
|
|
528
568
|
})();
|
|
529
|
-
}, [
|
|
569
|
+
}, [
|
|
570
|
+
fetchThreads,
|
|
571
|
+
addOptimisticThread,
|
|
572
|
+
autoCreate,
|
|
573
|
+
routeControlsActiveThread,
|
|
574
|
+
routeThreadId,
|
|
575
|
+
]);
|
|
530
576
|
|
|
531
577
|
const createThread = useCallback(
|
|
532
578
|
(preferredId?: string): Promise<string | null> => {
|
|
@@ -543,6 +589,41 @@ export function useChatThreads(
|
|
|
543
589
|
[addOptimisticThread],
|
|
544
590
|
);
|
|
545
591
|
|
|
592
|
+
useEffect(() => {
|
|
593
|
+
if (!routeControlsActiveThread) return;
|
|
594
|
+
if (routeThreadId) {
|
|
595
|
+
if (activeThreadIdRef.current !== routeThreadId) {
|
|
596
|
+
setActiveThreadId(routeThreadId);
|
|
597
|
+
}
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const currentId = activeThreadIdRef.current;
|
|
602
|
+
const currentThread = currentId
|
|
603
|
+
? threadsRef.current.find((thread) => thread.id === currentId)
|
|
604
|
+
: undefined;
|
|
605
|
+
const currentIsUnsavedNewThread =
|
|
606
|
+
currentId !== null &&
|
|
607
|
+
newlyCreatedRef.current.has(currentId) &&
|
|
608
|
+
(currentThread?.messageCount ?? 0) === 0;
|
|
609
|
+
if (currentIsUnsavedNewThread) return;
|
|
610
|
+
|
|
611
|
+
if (!autoCreate) {
|
|
612
|
+
if (currentId !== null) setActiveThreadId(null);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const id = createLocalThreadId();
|
|
617
|
+
newlyCreatedRef.current.add(id);
|
|
618
|
+
addOptimisticThread(id, scopeRef.current ?? null);
|
|
619
|
+
setActiveThreadId(id);
|
|
620
|
+
}, [
|
|
621
|
+
addOptimisticThread,
|
|
622
|
+
autoCreate,
|
|
623
|
+
routeControlsActiveThread,
|
|
624
|
+
routeThreadId,
|
|
625
|
+
]);
|
|
626
|
+
|
|
546
627
|
// Drop a thread's scope so it becomes a general (cross-resource) chat.
|
|
547
628
|
// This is the "Detach from <deck>" escape hatch in the UI. The PUT
|
|
548
629
|
// also bumps the thread's updatedAt so it surfaces in the All Chats
|
|
@@ -4,7 +4,7 @@ import { z } from "zod";
|
|
|
4
4
|
|
|
5
5
|
export default defineAction({
|
|
6
6
|
description:
|
|
7
|
-
"Navigate the Assets UI. Views (internal keys, with the surface they open): create, picker (the image Library browser), libraries (the Brand Kits list), library (a single Brand Kit), asset, generation-session, generation-run, extensions, audit, settings. Use libraryId, assetId, sessionId, runId, or extensionId where appropriate.",
|
|
7
|
+
"Navigate the Assets UI. Views (internal keys, with the surface they open): create, picker (the image Library browser), libraries (the Brand Kits list), library (a single Brand Kit), asset, generation-session, generation-run, extensions, audit, settings. Use threadId to open a specific create/chat thread; use libraryId, assetId, sessionId, runId, or extensionId where appropriate.",
|
|
8
8
|
schema: z.object({
|
|
9
9
|
view: z
|
|
10
10
|
.enum([
|
|
@@ -25,6 +25,7 @@ export default defineAction({
|
|
|
25
25
|
assetId: z.string().optional(),
|
|
26
26
|
sessionId: z.string().optional(),
|
|
27
27
|
runId: z.string().optional(),
|
|
28
|
+
threadId: z.string().optional(),
|
|
28
29
|
presetId: z.string().optional(),
|
|
29
30
|
mediaType: z.enum(["image", "video"]).optional(),
|
|
30
31
|
query: z.string().optional(),
|
|
@@ -36,7 +36,8 @@ export function Layout({ children }: LayoutProps) {
|
|
|
36
36
|
const location = useLocation();
|
|
37
37
|
const navigate = useNavigate();
|
|
38
38
|
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
|
39
|
-
const isCreateRoute =
|
|
39
|
+
const isCreateRoute =
|
|
40
|
+
location.pathname === "/" || location.pathname.startsWith("/chat/");
|
|
40
41
|
const chatHomeHandoffActive = useAgentChatHomeHandoff({
|
|
41
42
|
storageKey: ASSETS_CHAT_STORAGE_KEY,
|
|
42
43
|
activePath: location.pathname,
|
|
@@ -44,7 +45,7 @@ export function Layout({ children }: LayoutProps) {
|
|
|
44
45
|
});
|
|
45
46
|
useAgentChatHomeHandoffLinks({
|
|
46
47
|
storageKey: ASSETS_CHAT_STORAGE_KEY,
|
|
47
|
-
|
|
48
|
+
isChatPath: (pathname) => pathname === "/" || pathname.startsWith("/chat/"),
|
|
48
49
|
});
|
|
49
50
|
|
|
50
51
|
useEffect(() => {
|
|
@@ -100,8 +100,24 @@ function persistedActiveThreadId() {
|
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
function threadIdFromPath(pathname: string) {
|
|
104
|
+
const match = pathname.match(/^\/chat\/([^/]+)/);
|
|
105
|
+
if (!match) return null;
|
|
106
|
+
try {
|
|
107
|
+
const value = decodeURIComponent(match[1]).trim();
|
|
108
|
+
return value || null;
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function chatThreadPath(threadId: string) {
|
|
115
|
+
return `/chat/${encodeURIComponent(threadId)}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
103
118
|
function AssetsChatsSection() {
|
|
104
119
|
const navigate = useNavigate();
|
|
120
|
+
const location = useLocation();
|
|
105
121
|
const {
|
|
106
122
|
threads,
|
|
107
123
|
activeThreadId,
|
|
@@ -159,7 +175,10 @@ function AssetsChatsSection() {
|
|
|
159
175
|
|
|
160
176
|
function openThread(threadId: string, options?: { isNew?: boolean }) {
|
|
161
177
|
switchThread(threadId);
|
|
162
|
-
navigateWithAgentChatViewTransition(
|
|
178
|
+
navigateWithAgentChatViewTransition(
|
|
179
|
+
navigate,
|
|
180
|
+
options?.isNew ? "/" : chatThreadPath(threadId),
|
|
181
|
+
);
|
|
163
182
|
window.requestAnimationFrame(() => {
|
|
164
183
|
window.dispatchEvent(
|
|
165
184
|
new CustomEvent("agent-chat:open-thread", {
|
|
@@ -261,7 +280,10 @@ function AssetsChatsSection() {
|
|
|
261
280
|
</div>
|
|
262
281
|
<div className="grid gap-0.5">
|
|
263
282
|
{visibleThreads.map((thread) => {
|
|
264
|
-
const isActive =
|
|
283
|
+
const isActive =
|
|
284
|
+
thread.id ===
|
|
285
|
+
(threadIdFromPath(location.pathname) ??
|
|
286
|
+
(location.pathname === "/" ? null : activeThreadId));
|
|
265
287
|
const isRenaming = thread.id === renamingThreadId;
|
|
266
288
|
return (
|
|
267
289
|
<div
|
|
@@ -367,7 +389,8 @@ function AssetsChatsSection() {
|
|
|
367
389
|
export function Sidebar() {
|
|
368
390
|
const location = useLocation();
|
|
369
391
|
const navigate = useNavigate();
|
|
370
|
-
const isCreateRoute =
|
|
392
|
+
const isCreateRoute =
|
|
393
|
+
location.pathname === "/" || location.pathname.startsWith("/chat/");
|
|
371
394
|
const { data: auditAdmin } = useActionQuery("is-audit-admin", {}, {
|
|
372
395
|
refetchInterval: 30_000,
|
|
373
396
|
} as any) as { data: { allowed?: boolean } | undefined };
|
|
@@ -453,7 +476,7 @@ export function Sidebar() {
|
|
|
453
476
|
const Icon = item.icon;
|
|
454
477
|
const isActive =
|
|
455
478
|
item.href === "/"
|
|
456
|
-
?
|
|
479
|
+
? isCreateRoute
|
|
457
480
|
: item.href === "/brand-kits"
|
|
458
481
|
? location.pathname === "/brand-kits" ||
|
|
459
482
|
location.pathname.startsWith("/brand-kits/") ||
|
|
@@ -22,11 +22,18 @@ function optionalLibraryTab(params: URLSearchParams) {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
function navigationFromPath(pathname: string, search = "") {
|
|
25
|
+
const params = new URLSearchParams(search);
|
|
26
|
+
const chat = pathname.match(/^\/chat\/([^/]+)/);
|
|
27
|
+
if (chat) {
|
|
28
|
+
return {
|
|
29
|
+
view: "create",
|
|
30
|
+
threadId: decodePathParam(chat[1]),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
25
33
|
// The "library" view is the brand-kit detail page (route /brand-kits/:id).
|
|
26
34
|
// Keep the internal view key stable for the agent/MCP contract.
|
|
27
35
|
const library = pathname.match(/^\/brand-kits\/([^/]+)/);
|
|
28
36
|
if (library) {
|
|
29
|
-
const params = new URLSearchParams(search);
|
|
30
37
|
return {
|
|
31
38
|
view: "library",
|
|
32
39
|
libraryId: library[1],
|
|
@@ -37,10 +44,13 @@ function navigationFromPath(pathname: string, search = "") {
|
|
|
37
44
|
if (asset) return { view: "asset", assetId: asset[1] };
|
|
38
45
|
const image = pathname.match(/^\/image\/([^/]+)/);
|
|
39
46
|
if (image) return { view: "asset", assetId: image[1] };
|
|
40
|
-
if (pathname === "/")
|
|
47
|
+
if (pathname === "/") {
|
|
48
|
+
return {
|
|
49
|
+
view: "create",
|
|
50
|
+
};
|
|
51
|
+
}
|
|
41
52
|
// The "picker" view is the image Library browser (route /library).
|
|
42
53
|
if (pathname === "/library") {
|
|
43
|
-
const params = new URLSearchParams(search);
|
|
44
54
|
return {
|
|
45
55
|
view: "picker",
|
|
46
56
|
mediaType:
|
|
@@ -93,7 +103,12 @@ function pathFromCommand(command: any): string | null {
|
|
|
93
103
|
}
|
|
94
104
|
if (command.view === "audit") return "/audit";
|
|
95
105
|
if (command.view === "settings") return "/settings";
|
|
96
|
-
if (command.view === "create")
|
|
106
|
+
if (command.view === "create") {
|
|
107
|
+
if (typeof command.threadId === "string" && command.threadId.trim()) {
|
|
108
|
+
return `/chat/${encodeURIComponent(command.threadId.trim())}`;
|
|
109
|
+
}
|
|
110
|
+
return "/";
|
|
111
|
+
}
|
|
97
112
|
if (command.view === "picker") {
|
|
98
113
|
const params = new URLSearchParams();
|
|
99
114
|
if (command.mediaType === "image" || command.mediaType === "video") {
|
|
@@ -131,7 +146,10 @@ export function useNavigationState() {
|
|
|
131
146
|
navigationFromPath(pathname, search),
|
|
132
147
|
getCommandPath: (command) => pathFromCommand(command),
|
|
133
148
|
onNavigate: (_command, path) => {
|
|
134
|
-
if (
|
|
149
|
+
if (
|
|
150
|
+
isCreatePath(location.pathname) &&
|
|
151
|
+
!isCreatePath(pathnameFromPath(path))
|
|
152
|
+
) {
|
|
135
153
|
markAgentChatHomeHandoff(ASSETS_CHAT_STORAGE_KEY);
|
|
136
154
|
}
|
|
137
155
|
},
|
|
@@ -141,3 +159,15 @@ export function useNavigationState() {
|
|
|
141
159
|
function pathnameFromPath(path: string): string {
|
|
142
160
|
return path.split(/[?#]/, 1)[0] || "/";
|
|
143
161
|
}
|
|
162
|
+
|
|
163
|
+
function decodePathParam(value: string): string {
|
|
164
|
+
try {
|
|
165
|
+
return decodeURIComponent(value);
|
|
166
|
+
} catch {
|
|
167
|
+
return value;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function isCreatePath(pathname: string): boolean {
|
|
172
|
+
return pathname === "/" || pathname.startsWith("/chat/");
|
|
173
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import { useNavigate, useParams } from "react-router";
|
|
2
3
|
import {
|
|
3
4
|
AgentChatSurface,
|
|
4
5
|
getBrowserTabId,
|
|
@@ -61,7 +62,13 @@ export function meta() {
|
|
|
61
62
|
];
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
function chatThreadPath(threadId: string | null) {
|
|
66
|
+
return threadId ? `/chat/${encodeURIComponent(threadId)}` : "/";
|
|
67
|
+
}
|
|
68
|
+
|
|
64
69
|
export default function CreatePage() {
|
|
70
|
+
const { threadId } = useParams();
|
|
71
|
+
const navigate = useNavigate();
|
|
65
72
|
const [imageModel, setImageModel] = useState<string>(DEFAULT_IMAGE_MODEL);
|
|
66
73
|
|
|
67
74
|
useEffect(() => {
|
|
@@ -113,6 +120,11 @@ export default function CreatePage() {
|
|
|
113
120
|
className="assets-create-chat-panel"
|
|
114
121
|
defaultMode="chat"
|
|
115
122
|
storageKey={ASSETS_CHAT_STORAGE_KEY}
|
|
123
|
+
threadUrlSync={{
|
|
124
|
+
routeThreadId: threadId ?? null,
|
|
125
|
+
getPath: chatThreadPath,
|
|
126
|
+
navigate,
|
|
127
|
+
}}
|
|
116
128
|
browserTabId={getBrowserTabId()}
|
|
117
129
|
imageModelMenu={{
|
|
118
130
|
value: imageModel,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default, meta } from "./_index";
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* Options:
|
|
11
11
|
* --view View name to navigate to
|
|
12
12
|
* --path URL path to navigate to
|
|
13
|
+
* --threadId Chat thread ID to open on the chat route
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
16
|
import { defineAction } from "@agent-native/core/action";
|
|
@@ -22,6 +23,7 @@ export default defineAction({
|
|
|
22
23
|
schema: z.object({
|
|
23
24
|
view: z.string().optional().describe("View name to navigate to"),
|
|
24
25
|
path: z.string().optional().describe("URL path to navigate to"),
|
|
26
|
+
threadId: z.string().optional().describe("Chat thread ID to open"),
|
|
25
27
|
}),
|
|
26
28
|
http: false,
|
|
27
29
|
run: async (args) => {
|
|
@@ -31,6 +33,7 @@ export default defineAction({
|
|
|
31
33
|
const nav: Record<string, string> = {};
|
|
32
34
|
if (args.view) nav.view = args.view;
|
|
33
35
|
if (args.path) nav.path = args.path;
|
|
36
|
+
if (args.threadId) nav.threadId = args.threadId;
|
|
34
37
|
nav._writeId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
35
38
|
await writeAppState("navigate", nav);
|
|
36
39
|
return `Navigating to ${args.view || args.path}`;
|
|
@@ -35,6 +35,7 @@ const SIDEBAR_COLLAPSE_KEY = "chat.sidebar.collapsed";
|
|
|
35
35
|
function routeOwnsToolbar(pathname: string): boolean {
|
|
36
36
|
return (
|
|
37
37
|
pathname === "/" ||
|
|
38
|
+
pathname.startsWith("/chat/") ||
|
|
38
39
|
pathname === "/database" ||
|
|
39
40
|
pathname.startsWith("/extensions")
|
|
40
41
|
);
|
|
@@ -45,13 +46,17 @@ export function Layout({ children }: LayoutProps) {
|
|
|
45
46
|
const navigate = useNavigate();
|
|
46
47
|
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
|
47
48
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(true);
|
|
48
|
-
const isChatRoute =
|
|
49
|
+
const isChatRoute =
|
|
50
|
+
location.pathname === "/" || location.pathname.startsWith("/chat/");
|
|
49
51
|
const chatHomeHandoffActive = useAgentChatHomeHandoff({
|
|
50
52
|
storageKey: "chat",
|
|
51
53
|
activePath: location.pathname,
|
|
52
54
|
enabled: !isChatRoute,
|
|
53
55
|
});
|
|
54
|
-
useAgentChatHomeHandoffLinks({
|
|
56
|
+
useAgentChatHomeHandoffLinks({
|
|
57
|
+
storageKey: "chat",
|
|
58
|
+
isChatPath: (pathname) => pathname === "/" || pathname.startsWith("/chat/"),
|
|
59
|
+
});
|
|
55
60
|
|
|
56
61
|
useEffect(() => {
|
|
57
62
|
setMobileSidebarOpen(false);
|
|
@@ -104,8 +104,24 @@ function persistedActiveThreadId() {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
function threadIdFromPath(pathname: string) {
|
|
108
|
+
const match = pathname.match(/^\/chat\/([^/]+)/);
|
|
109
|
+
if (!match) return null;
|
|
110
|
+
try {
|
|
111
|
+
const value = decodeURIComponent(match[1]).trim();
|
|
112
|
+
return value || null;
|
|
113
|
+
} catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function chatThreadPath(threadId: string) {
|
|
119
|
+
return `/chat/${encodeURIComponent(threadId)}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
107
122
|
function ChatThreadsSection() {
|
|
108
123
|
const navigate = useNavigate();
|
|
124
|
+
const location = useLocation();
|
|
109
125
|
const {
|
|
110
126
|
threads,
|
|
111
127
|
activeThreadId,
|
|
@@ -162,7 +178,10 @@ function ChatThreadsSection() {
|
|
|
162
178
|
|
|
163
179
|
function openThread(threadId: string, options?: { isNew?: boolean }) {
|
|
164
180
|
switchThread(threadId);
|
|
165
|
-
navigateWithAgentChatViewTransition(
|
|
181
|
+
navigateWithAgentChatViewTransition(
|
|
182
|
+
navigate,
|
|
183
|
+
options?.isNew ? "/" : chatThreadPath(threadId),
|
|
184
|
+
);
|
|
166
185
|
window.requestAnimationFrame(() => {
|
|
167
186
|
window.dispatchEvent(
|
|
168
187
|
new CustomEvent("agent-chat:open-thread", {
|
|
@@ -244,7 +263,10 @@ function ChatThreadsSection() {
|
|
|
244
263
|
</div>
|
|
245
264
|
<div className="grid gap-0.5">
|
|
246
265
|
{visibleThreads.map((thread) => {
|
|
247
|
-
const isActive =
|
|
266
|
+
const isActive =
|
|
267
|
+
thread.id ===
|
|
268
|
+
(threadIdFromPath(location.pathname) ??
|
|
269
|
+
(location.pathname === "/" ? null : activeThreadId));
|
|
248
270
|
const isRenaming = thread.id === renamingThreadId;
|
|
249
271
|
return (
|
|
250
272
|
<div
|
|
@@ -348,7 +370,8 @@ export function Sidebar({
|
|
|
348
370
|
}: SidebarProps) {
|
|
349
371
|
const location = useLocation();
|
|
350
372
|
const navigate = useNavigate();
|
|
351
|
-
const isChatRoute =
|
|
373
|
+
const isChatRoute =
|
|
374
|
+
location.pathname === "/" || location.pathname.startsWith("/chat/");
|
|
352
375
|
const ToggleIcon = collapsed
|
|
353
376
|
? IconLayoutSidebarLeftExpand
|
|
354
377
|
: IconLayoutSidebarLeftCollapse;
|
|
@@ -440,7 +463,7 @@ export function Sidebar({
|
|
|
440
463
|
const Icon = item.icon;
|
|
441
464
|
const isActive =
|
|
442
465
|
item.href === "/"
|
|
443
|
-
?
|
|
466
|
+
? isChatRoute
|
|
444
467
|
: location.pathname.startsWith(item.href);
|
|
445
468
|
const link = (
|
|
446
469
|
<Link
|
|
@@ -10,6 +10,7 @@ import { TAB_ID } from "@/lib/tab-id";
|
|
|
10
10
|
export interface NavigationState {
|
|
11
11
|
view: string;
|
|
12
12
|
path?: string;
|
|
13
|
+
threadId?: string;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export function useNavigationState() {
|
|
@@ -17,14 +18,21 @@ export function useNavigationState() {
|
|
|
17
18
|
useAgentRouteState<NavigationState>({
|
|
18
19
|
browserTabId: TAB_ID,
|
|
19
20
|
requestSource: TAB_ID,
|
|
20
|
-
getNavigationState: ({ pathname
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
getNavigationState: ({ pathname }) => {
|
|
22
|
+
const threadId = threadIdFromPath(pathname);
|
|
23
|
+
return {
|
|
24
|
+
view: viewForPath(pathname),
|
|
25
|
+
path: appPath(pathname),
|
|
26
|
+
...(threadId ? { threadId } : {}),
|
|
27
|
+
};
|
|
28
|
+
},
|
|
24
29
|
getCommandPath: (command) =>
|
|
25
|
-
routerPath(command.path ||
|
|
30
|
+
routerPath(command.path || pathForCommand(command)),
|
|
26
31
|
onNavigate: (_command, path) => {
|
|
27
|
-
if (
|
|
32
|
+
if (
|
|
33
|
+
isChatPath(location.pathname) &&
|
|
34
|
+
!isChatPath(pathnameFromPath(path))
|
|
35
|
+
) {
|
|
28
36
|
markAgentChatHomeHandoff("chat");
|
|
29
37
|
}
|
|
30
38
|
},
|
|
@@ -35,7 +43,19 @@ function pathnameFromPath(path: string): string {
|
|
|
35
43
|
return path.split(/[?#]/, 1)[0] || "/";
|
|
36
44
|
}
|
|
37
45
|
|
|
46
|
+
function threadIdFromPath(pathname: string): string | null {
|
|
47
|
+
const match = pathname.match(/^\/chat\/([^/]+)/);
|
|
48
|
+
if (!match) return null;
|
|
49
|
+
try {
|
|
50
|
+
const value = decodeURIComponent(match[1]).trim();
|
|
51
|
+
return value || null;
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
38
57
|
function viewForPath(pathname: string): string {
|
|
58
|
+
if (isChatPath(pathname)) return "chat";
|
|
39
59
|
if (pathname.startsWith("/database")) return "database";
|
|
40
60
|
if (pathname.startsWith("/extensions")) return "extensions";
|
|
41
61
|
if (pathname.startsWith("/observability")) return "observability";
|
|
@@ -62,6 +82,14 @@ function pathForView(view?: string): string {
|
|
|
62
82
|
}
|
|
63
83
|
}
|
|
64
84
|
|
|
85
|
+
function pathForCommand(command: any): string {
|
|
86
|
+
const path = pathForView(command?.view);
|
|
87
|
+
if (path !== "/") return path;
|
|
88
|
+
const threadId =
|
|
89
|
+
typeof command?.threadId === "string" ? command.threadId.trim() : "";
|
|
90
|
+
return threadId ? `/chat/${encodeURIComponent(threadId)}` : "/";
|
|
91
|
+
}
|
|
92
|
+
|
|
65
93
|
function routerPath(path: string): string {
|
|
66
94
|
const basePath = appBasePath();
|
|
67
95
|
if (!basePath) return path;
|
|
@@ -71,3 +99,7 @@ function routerPath(path: string): string {
|
|
|
71
99
|
}
|
|
72
100
|
return path;
|
|
73
101
|
}
|
|
102
|
+
|
|
103
|
+
function isChatPath(pathname: string): boolean {
|
|
104
|
+
return pathname === "/" || pathname.startsWith("/chat/");
|
|
105
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEffect } from "react";
|
|
2
|
+
import { useNavigate, useParams } from "react-router";
|
|
2
3
|
import {
|
|
3
4
|
AgentChatSurface,
|
|
4
5
|
markAgentChatHomeHandoff,
|
|
@@ -25,7 +26,14 @@ export function meta() {
|
|
|
25
26
|
];
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
function chatThreadPath(threadId: string | null) {
|
|
30
|
+
return threadId ? `/chat/${encodeURIComponent(threadId)}` : "/";
|
|
31
|
+
}
|
|
32
|
+
|
|
28
33
|
export default function ChatRoute() {
|
|
34
|
+
const { threadId } = useParams();
|
|
35
|
+
const navigate = useNavigate();
|
|
36
|
+
|
|
29
37
|
useEffect(() => {
|
|
30
38
|
function handleChatRunning(event: Event) {
|
|
31
39
|
const detail = (event as CustomEvent).detail;
|
|
@@ -45,6 +53,11 @@ export default function ChatRoute() {
|
|
|
45
53
|
className="h-full"
|
|
46
54
|
defaultMode="chat"
|
|
47
55
|
storageKey="chat"
|
|
56
|
+
threadUrlSync={{
|
|
57
|
+
routeThreadId: threadId ?? null,
|
|
58
|
+
getPath: chatThreadPath,
|
|
59
|
+
navigate,
|
|
60
|
+
}}
|
|
48
61
|
browserTabId={TAB_ID}
|
|
49
62
|
showHeader={false}
|
|
50
63
|
showTabBar={false}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default, meta } from "./_index";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export { default } from "@agent-native/dispatch/routes/pages/chat";
|
|
2
|
+
import type { LoaderFunctionArgs } from "react-router";
|
|
3
|
+
import {
|
|
4
|
+
buildThreadLinkPreviewMeta,
|
|
5
|
+
type ThreadLinkPreview,
|
|
6
|
+
} from "@agent-native/dispatch/lib/thread-link-preview";
|
|
7
|
+
|
|
8
|
+
export async function loader({ params, request }: LoaderFunctionArgs) {
|
|
9
|
+
const threadId =
|
|
10
|
+
params.threadId ?? new URL(request.url).searchParams.get("thread");
|
|
11
|
+
const { loadThreadLinkPreview } =
|
|
12
|
+
await import("@agent-native/dispatch/server/lib/thread-link-preview");
|
|
13
|
+
return {
|
|
14
|
+
threadPreview: await loadThreadLinkPreview(threadId),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function meta({
|
|
19
|
+
data,
|
|
20
|
+
}: {
|
|
21
|
+
data?: { threadPreview: ThreadLinkPreview | null };
|
|
22
|
+
}) {
|
|
23
|
+
return data?.threadPreview
|
|
24
|
+
? buildThreadLinkPreviewMeta(data.threadPreview)
|
|
25
|
+
: [{ title: "Chat — Dispatch" }];
|
|
26
|
+
}
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* <AgentChatSurface mode="page" className="h-screen" />
|
|
22
22
|
*/
|
|
23
23
|
import React from "react";
|
|
24
|
-
import type { MultiTabAssistantChatHeaderProps } from "./MultiTabAssistantChat.js";
|
|
24
|
+
import type { MultiTabAssistantChatHeaderProps, MultiTabAssistantChatProps } from "./MultiTabAssistantChat.js";
|
|
25
25
|
import type { AssistantChatProps } from "./AssistantChat.js";
|
|
26
26
|
export declare function getAgentPanelChatTabGroups(tabs: MultiTabAssistantChatHeaderProps["tabs"], activeTabId: string): {
|
|
27
27
|
activeTab: import("./MultiTabAssistantChat.js").ChatTab | undefined;
|
|
@@ -80,6 +80,8 @@ export interface AgentPanelProps extends Omit<AssistantChatProps, "onSwitchToCli
|
|
|
80
80
|
scope?: import("./use-chat-threads.js").ChatThreadScope | null;
|
|
81
81
|
/** Stable browser tab id used for tab-scoped app-state context. */
|
|
82
82
|
browserTabId?: string;
|
|
83
|
+
/** Keep chat thread selection in URL state. */
|
|
84
|
+
threadUrlSync?: MultiTabAssistantChatProps["threadUrlSync"];
|
|
83
85
|
/** Optional notice rendered below the main header while Chat mode is active. */
|
|
84
86
|
chatNotice?: React.ReactNode;
|
|
85
87
|
/** Show the chat thread tab row when the panel header is hidden. Default: true. */
|
|
@@ -150,12 +152,14 @@ export interface AgentSidebarProps {
|
|
|
150
152
|
scope?: import("./use-chat-threads.js").ChatThreadScope | null;
|
|
151
153
|
/** Stable browser tab id used for tab-scoped app-state context. */
|
|
152
154
|
browserTabId?: string;
|
|
155
|
+
/** Keep chat thread selection in URL state. */
|
|
156
|
+
threadUrlSync?: MultiTabAssistantChatProps["threadUrlSync"];
|
|
153
157
|
}
|
|
154
158
|
/**
|
|
155
159
|
* Wraps app content with a toggleable agent sidebar.
|
|
156
160
|
* Use AgentToggleButton in your header to open/close it.
|
|
157
161
|
*/
|
|
158
|
-
export declare function AgentSidebar({ children, emptyStateText, suggestions, dynamicSuggestions, defaultSidebarWidth, sidebarWidth, position, defaultOpen, animateMobile, chatViewTransition, storageKey, openOnChatRunning, onFullscreenRequest, scope, browserTabId, }: AgentSidebarProps): import("react/jsx-runtime").JSX.Element;
|
|
162
|
+
export declare function AgentSidebar({ children, emptyStateText, suggestions, dynamicSuggestions, defaultSidebarWidth, sidebarWidth, position, defaultOpen, animateMobile, chatViewTransition, storageKey, openOnChatRunning, onFullscreenRequest, scope, browserTabId, threadUrlSync, }: AgentSidebarProps): import("react/jsx-runtime").JSX.Element;
|
|
159
163
|
/**
|
|
160
164
|
* Focus the agent chat composer input.
|
|
161
165
|
* Opens the sidebar if closed, then focuses the text input.
|