@agent-native/dispatch 0.8.18 → 0.8.19
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/actions/navigate.js +1 -1
- package/dist/actions/navigate.js.map +1 -1
- package/dist/actions/view-screen.d.ts.map +1 -1
- package/dist/actions/view-screen.js +6 -0
- package/dist/actions/view-screen.js.map +1 -1
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +91 -14
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/hooks/use-navigation-state.js +5 -0
- package/dist/hooks/use-navigation-state.js.map +1 -1
- package/dist/lib/overview-chat.d.ts +3 -1
- package/dist/lib/overview-chat.d.ts.map +1 -1
- package/dist/lib/overview-chat.js +2 -1
- package/dist/lib/overview-chat.js.map +1 -1
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/index.js +1 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/pages/_index.js +1 -1
- package/dist/routes/pages/_index.js.map +1 -1
- package/dist/routes/pages/chat.d.ts +5 -0
- package/dist/routes/pages/chat.d.ts.map +1 -0
- package/dist/routes/pages/chat.js +55 -0
- package/dist/routes/pages/chat.js.map +1 -0
- package/dist/routes/pages/overview.d.ts.map +1 -1
- package/dist/routes/pages/overview.js +20 -7
- package/dist/routes/pages/overview.js.map +1 -1
- package/package.json +1 -1
- package/src/actions/navigate.ts +1 -1
- package/src/actions/view-screen.ts +7 -0
- package/src/components/layout/Layout.tsx +187 -18
- package/src/hooks/use-navigation-state.spec.ts +7 -0
- package/src/hooks/use-navigation-state.ts +4 -0
- package/src/lib/overview-chat.spec.ts +15 -0
- package/src/lib/overview-chat.ts +2 -0
- package/src/routes/index.ts +1 -0
- package/src/routes/pages/_index.tsx +1 -1
- package/src/routes/pages/chat.tsx +99 -0
- package/src/routes/pages/overview.tsx +21 -5
- package/src/styles/dispatch-css.spec.ts +9 -0
- package/src/styles/dispatch.css +103 -0
|
@@ -1,11 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
useEffect,
|
|
3
|
+
useMemo,
|
|
4
|
+
useState,
|
|
5
|
+
type ComponentType,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
} from "react";
|
|
8
|
+
import { NavLink, useLocation, useNavigate } from "react-router";
|
|
3
9
|
import {
|
|
4
10
|
AgentSidebar,
|
|
5
11
|
FeedbackButton,
|
|
6
12
|
appBasePath,
|
|
7
13
|
appPath,
|
|
8
14
|
useActionQuery,
|
|
15
|
+
useChatThreads,
|
|
16
|
+
type ChatThreadSummary,
|
|
9
17
|
} from "@agent-native/core/client";
|
|
10
18
|
import { ExtensionsSidebarSection } from "@agent-native/core/client/extensions";
|
|
11
19
|
import { InvitationBanner, OrgSwitcher } from "@agent-native/core/client/org";
|
|
@@ -18,7 +26,9 @@ import {
|
|
|
18
26
|
IconKey,
|
|
19
27
|
IconChevronDown,
|
|
20
28
|
IconLayersSubtract,
|
|
29
|
+
IconMessageQuestion,
|
|
21
30
|
IconMessages,
|
|
31
|
+
IconPlus,
|
|
22
32
|
IconPlugConnected,
|
|
23
33
|
IconBroadcast,
|
|
24
34
|
IconFingerprint,
|
|
@@ -34,6 +44,11 @@ import {
|
|
|
34
44
|
SheetDescription,
|
|
35
45
|
SheetTitle,
|
|
36
46
|
} from "@/components/ui/sheet";
|
|
47
|
+
import {
|
|
48
|
+
Tooltip,
|
|
49
|
+
TooltipContent,
|
|
50
|
+
TooltipTrigger,
|
|
51
|
+
} from "@/components/ui/tooltip";
|
|
37
52
|
import { Header } from "./Header";
|
|
38
53
|
import { HeaderActionsProvider } from "./HeaderActions";
|
|
39
54
|
|
|
@@ -65,6 +80,13 @@ export interface DispatchExtensionConfig {
|
|
|
65
80
|
}
|
|
66
81
|
|
|
67
82
|
const PRIMARY_NAV_ITEMS = [
|
|
83
|
+
{
|
|
84
|
+
id: "chat",
|
|
85
|
+
to: "/chat",
|
|
86
|
+
label: "Chat",
|
|
87
|
+
icon: IconMessageQuestion,
|
|
88
|
+
section: "primary",
|
|
89
|
+
},
|
|
68
90
|
{
|
|
69
91
|
id: "overview",
|
|
70
92
|
to: "/overview",
|
|
@@ -249,6 +271,142 @@ function dispatchNavLinkTarget(path: string): string {
|
|
|
249
271
|
return routerHasBasename ? path : appPath(path);
|
|
250
272
|
}
|
|
251
273
|
|
|
274
|
+
function formatThreadAge(updatedAt: number) {
|
|
275
|
+
const diffMs = Math.max(0, Date.now() - updatedAt);
|
|
276
|
+
const minutes = Math.floor(diffMs / 60_000);
|
|
277
|
+
if (minutes < 1) return "now";
|
|
278
|
+
if (minutes < 60) return `${minutes}m`;
|
|
279
|
+
const hours = Math.floor(minutes / 60);
|
|
280
|
+
if (hours < 24) return `${hours}h`;
|
|
281
|
+
const days = Math.floor(hours / 24);
|
|
282
|
+
if (days < 7) return `${days}d`;
|
|
283
|
+
return new Date(updatedAt).toLocaleDateString([], {
|
|
284
|
+
month: "short",
|
|
285
|
+
day: "numeric",
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function threadTitle(thread: ChatThreadSummary) {
|
|
290
|
+
return thread.title || thread.preview || "New chat";
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function DispatchChatsSection({ onNavigate }: { onNavigate?: () => void }) {
|
|
294
|
+
const navigate = useNavigate();
|
|
295
|
+
const {
|
|
296
|
+
threads,
|
|
297
|
+
activeThreadId,
|
|
298
|
+
createThread,
|
|
299
|
+
switchThread,
|
|
300
|
+
refreshThreads,
|
|
301
|
+
} = useChatThreads(undefined, undefined, undefined, { autoCreate: false });
|
|
302
|
+
|
|
303
|
+
const visibleThreads = useMemo(
|
|
304
|
+
() =>
|
|
305
|
+
threads
|
|
306
|
+
.filter(
|
|
307
|
+
(thread) => thread.messageCount > 0 || thread.id === activeThreadId,
|
|
308
|
+
)
|
|
309
|
+
.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
310
|
+
.slice(0, 8),
|
|
311
|
+
[activeThreadId, threads],
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
useEffect(() => {
|
|
315
|
+
const refresh = () => refreshThreads();
|
|
316
|
+
const handleRunning = (event: Event) => {
|
|
317
|
+
const detail = (event as CustomEvent).detail as
|
|
318
|
+
| { isRunning?: unknown }
|
|
319
|
+
| undefined;
|
|
320
|
+
if (detail?.isRunning === false) refreshThreads();
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
window.addEventListener("agent-chat:threads-updated", refresh);
|
|
324
|
+
window.addEventListener("agentNative.chatRunning", handleRunning);
|
|
325
|
+
window.addEventListener("focus", refresh);
|
|
326
|
+
return () => {
|
|
327
|
+
window.removeEventListener("agent-chat:threads-updated", refresh);
|
|
328
|
+
window.removeEventListener("agentNative.chatRunning", handleRunning);
|
|
329
|
+
window.removeEventListener("focus", refresh);
|
|
330
|
+
};
|
|
331
|
+
}, [refreshThreads]);
|
|
332
|
+
|
|
333
|
+
function openThread(threadId: string, options?: { isNew?: boolean }) {
|
|
334
|
+
switchThread(threadId);
|
|
335
|
+
navigate(dispatchNavLinkTarget("/chat"));
|
|
336
|
+
onNavigate?.();
|
|
337
|
+
window.requestAnimationFrame(() => {
|
|
338
|
+
window.dispatchEvent(
|
|
339
|
+
new CustomEvent("agent-chat:open-thread", {
|
|
340
|
+
detail: { threadId, newThread: options?.isNew === true },
|
|
341
|
+
}),
|
|
342
|
+
);
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function handleNewChat() {
|
|
347
|
+
const threadId = await createThread();
|
|
348
|
+
if (threadId) openThread(threadId, { isNew: true });
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return (
|
|
352
|
+
<div className="mt-2 border-l border-sidebar-border/70 pl-3">
|
|
353
|
+
<div className="mb-1 flex h-7 items-center gap-2 pr-1">
|
|
354
|
+
<div className="min-w-0 flex-1 text-xs font-medium text-sidebar-foreground/70">
|
|
355
|
+
Chats
|
|
356
|
+
</div>
|
|
357
|
+
<Tooltip>
|
|
358
|
+
<TooltipTrigger asChild>
|
|
359
|
+
<button
|
|
360
|
+
type="button"
|
|
361
|
+
onClick={handleNewChat}
|
|
362
|
+
className="flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-md text-sidebar-foreground/65 transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
|
363
|
+
aria-label="New Dispatch chat"
|
|
364
|
+
>
|
|
365
|
+
<IconPlus className="size-3.5" />
|
|
366
|
+
</button>
|
|
367
|
+
</TooltipTrigger>
|
|
368
|
+
<TooltipContent>New chat</TooltipContent>
|
|
369
|
+
</Tooltip>
|
|
370
|
+
</div>
|
|
371
|
+
<div className="grid gap-0.5">
|
|
372
|
+
{visibleThreads.length > 0 ? (
|
|
373
|
+
visibleThreads.map((thread) => {
|
|
374
|
+
const isActive = thread.id === activeThreadId;
|
|
375
|
+
return (
|
|
376
|
+
<button
|
|
377
|
+
key={thread.id}
|
|
378
|
+
type="button"
|
|
379
|
+
onClick={() => openThread(thread.id)}
|
|
380
|
+
className={cn(
|
|
381
|
+
"flex h-8 min-w-0 cursor-pointer items-center gap-2 rounded-md px-2 text-left text-sm transition-colors",
|
|
382
|
+
isActive
|
|
383
|
+
? "bg-sidebar-accent text-sidebar-accent-foreground"
|
|
384
|
+
: "text-sidebar-foreground/80 hover:bg-sidebar-accent/65 hover:text-sidebar-accent-foreground",
|
|
385
|
+
)}
|
|
386
|
+
>
|
|
387
|
+
<span className="min-w-0 flex-1 truncate">
|
|
388
|
+
{threadTitle(thread)}
|
|
389
|
+
</span>
|
|
390
|
+
<span className="shrink-0 text-[11px] text-sidebar-foreground/50">
|
|
391
|
+
{isActive ? "" : formatThreadAge(thread.updatedAt)}
|
|
392
|
+
</span>
|
|
393
|
+
</button>
|
|
394
|
+
);
|
|
395
|
+
})
|
|
396
|
+
) : (
|
|
397
|
+
<button
|
|
398
|
+
type="button"
|
|
399
|
+
onClick={handleNewChat}
|
|
400
|
+
className="flex h-8 cursor-pointer items-center rounded-md px-2 text-left text-sm text-sidebar-foreground/70 transition-colors hover:bg-sidebar-accent/65 hover:text-sidebar-accent-foreground"
|
|
401
|
+
>
|
|
402
|
+
<span className="truncate">New chat</span>
|
|
403
|
+
</button>
|
|
404
|
+
)}
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
252
410
|
export function NavContent({
|
|
253
411
|
onNavigate,
|
|
254
412
|
extensions,
|
|
@@ -302,6 +460,9 @@ export function NavContent({
|
|
|
302
460
|
)}
|
|
303
461
|
<span className="truncate">{item.label}</span>
|
|
304
462
|
</NavLink>
|
|
463
|
+
{item.id === "chat" ? (
|
|
464
|
+
<DispatchChatsSection onNavigate={onNavigate} />
|
|
465
|
+
) : null}
|
|
305
466
|
</li>
|
|
306
467
|
);
|
|
307
468
|
};
|
|
@@ -384,17 +545,24 @@ export function Layout({
|
|
|
384
545
|
}) {
|
|
385
546
|
const location = useLocation();
|
|
386
547
|
const [mobileOpen, setMobileOpen] = useState(false);
|
|
548
|
+
const localPathname = localDispatchPath(location.pathname);
|
|
387
549
|
|
|
388
|
-
if (CHROMELESS_PATHS.some((path) =>
|
|
550
|
+
if (CHROMELESS_PATHS.some((path) => localPathname === path)) {
|
|
389
551
|
return <>{children}</>;
|
|
390
552
|
}
|
|
391
553
|
|
|
392
|
-
const
|
|
554
|
+
const isChatRoute = localPathname === "/chat";
|
|
555
|
+
const showHeader = !isChatRoute && !pageOwnsToolbar(localPathname);
|
|
393
556
|
const appContent = (
|
|
394
|
-
<div className="flex h-full flex-1 flex-col overflow-hidden">
|
|
557
|
+
<div className="flex h-full min-w-0 flex-1 flex-col overflow-hidden">
|
|
395
558
|
{showHeader ? <Header onOpenMobile={() => setMobileOpen(true)} /> : null}
|
|
396
559
|
<InvitationBanner />
|
|
397
|
-
<main
|
|
560
|
+
<main
|
|
561
|
+
className={cn(
|
|
562
|
+
"flex-1",
|
|
563
|
+
isChatRoute ? "min-h-0 overflow-hidden" : "overflow-y-auto",
|
|
564
|
+
)}
|
|
565
|
+
>
|
|
398
566
|
{showHeader ? (
|
|
399
567
|
<div className="mx-auto max-w-7xl space-y-10 px-4 py-6 sm:px-6">
|
|
400
568
|
{children}
|
|
@@ -405,6 +573,18 @@ export function Layout({
|
|
|
405
573
|
</main>
|
|
406
574
|
</div>
|
|
407
575
|
);
|
|
576
|
+
const content = isChatRoute ? (
|
|
577
|
+
appContent
|
|
578
|
+
) : (
|
|
579
|
+
<AgentSidebar
|
|
580
|
+
position="right"
|
|
581
|
+
defaultOpen={false}
|
|
582
|
+
emptyStateText="Create apps, manage vault keys, and route work across the workspace."
|
|
583
|
+
suggestions={SIDEBAR_SUGGESTIONS}
|
|
584
|
+
>
|
|
585
|
+
{appContent}
|
|
586
|
+
</AgentSidebar>
|
|
587
|
+
);
|
|
408
588
|
|
|
409
589
|
return (
|
|
410
590
|
<HeaderActionsProvider>
|
|
@@ -431,18 +611,7 @@ export function Layout({
|
|
|
431
611
|
</SheetContent>
|
|
432
612
|
</Sheet>
|
|
433
613
|
|
|
434
|
-
{
|
|
435
|
-
* Always mount AgentSidebar so home composer's sendToAgentChat
|
|
436
|
-
* fallback can pop it via agent-panel:open.
|
|
437
|
-
*/}
|
|
438
|
-
<AgentSidebar
|
|
439
|
-
position="right"
|
|
440
|
-
defaultOpen={false}
|
|
441
|
-
emptyStateText="Create apps, manage vault keys, and route work across the workspace."
|
|
442
|
-
suggestions={SIDEBAR_SUGGESTIONS}
|
|
443
|
-
>
|
|
444
|
-
{appContent}
|
|
445
|
-
</AgentSidebar>
|
|
614
|
+
{content}
|
|
446
615
|
</div>
|
|
447
616
|
</HeaderActionsProvider>
|
|
448
617
|
);
|
|
@@ -2,6 +2,13 @@ import { describe, expect, it } from "vitest";
|
|
|
2
2
|
import { buildDispatchNavigationState } from "./use-navigation-state.js";
|
|
3
3
|
|
|
4
4
|
describe("buildDispatchNavigationState", () => {
|
|
5
|
+
it("recognizes the full-page chat route", () => {
|
|
6
|
+
expect(buildDispatchNavigationState("/chat")).toEqual({
|
|
7
|
+
view: "chat",
|
|
8
|
+
path: "/chat",
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
|
|
5
12
|
it("exposes the current extension id from extension routes", () => {
|
|
6
13
|
expect(
|
|
7
14
|
buildDispatchNavigationState("/extensions/ext-1/github-stars-over-time"),
|
|
@@ -173,6 +173,7 @@ function resolveView(
|
|
|
173
173
|
if (pathname === "/extensions" || pathname.startsWith("/extensions/")) {
|
|
174
174
|
return "extensions";
|
|
175
175
|
}
|
|
176
|
+
if (pathname.startsWith("/chat")) return "chat";
|
|
176
177
|
if (pathname.startsWith("/apps")) return "apps";
|
|
177
178
|
if (pathname.startsWith("/metrics")) return "metrics";
|
|
178
179
|
if (pathname.startsWith("/new-app")) return "new-app";
|
|
@@ -197,6 +198,9 @@ function resolvePath(
|
|
|
197
198
|
command?: Pick<NavigationState, "extensionId">,
|
|
198
199
|
): string | undefined {
|
|
199
200
|
switch (view) {
|
|
201
|
+
case "chat":
|
|
202
|
+
case "ask":
|
|
203
|
+
return "/chat";
|
|
200
204
|
case "overview":
|
|
201
205
|
return "/overview";
|
|
202
206
|
case "apps":
|
|
@@ -28,6 +28,21 @@ describe("submitOverviewPrompt", () => {
|
|
|
28
28
|
});
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
+
it("can submit to a mounted page chat without opening the sidebar", () => {
|
|
32
|
+
const tabId = submitOverviewPrompt(" build a metrics app ", "auto", {
|
|
33
|
+
openSidebar: false,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
expect(tabId).toBe("chat-tab");
|
|
37
|
+
expect(sendToAgentChatMock).toHaveBeenCalledWith({
|
|
38
|
+
message: "build a metrics app",
|
|
39
|
+
submit: true,
|
|
40
|
+
newTab: true,
|
|
41
|
+
model: "auto",
|
|
42
|
+
openSidebar: false,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
31
46
|
it("routes overview prompts to Builder chat inside Builder", () => {
|
|
32
47
|
frameState.inBuilderFrame = true;
|
|
33
48
|
|
package/src/lib/overview-chat.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { isInBuilderFrame, sendToAgentChat } from "@agent-native/core/client";
|
|
|
3
3
|
export function submitOverviewPrompt(
|
|
4
4
|
message: string,
|
|
5
5
|
selectedModel?: string | null,
|
|
6
|
+
options?: { openSidebar?: boolean },
|
|
6
7
|
): string | null {
|
|
7
8
|
const trimmed = message.trim();
|
|
8
9
|
if (!trimmed) return null;
|
|
@@ -20,5 +21,6 @@ export function submitOverviewPrompt(
|
|
|
20
21
|
submit: true,
|
|
21
22
|
newTab: true,
|
|
22
23
|
model: selectedModel || undefined,
|
|
24
|
+
...(options?.openSidebar === false ? { openSidebar: false } : {}),
|
|
23
25
|
});
|
|
24
26
|
}
|
package/src/routes/index.ts
CHANGED
|
@@ -31,6 +31,7 @@ import { type RouteConfig, route, index } from "@react-router/dev/routes";
|
|
|
31
31
|
*/
|
|
32
32
|
export const dispatchRoutes: RouteConfig = [
|
|
33
33
|
index("./pages/_index.js"),
|
|
34
|
+
route("chat", "./pages/chat.js"),
|
|
34
35
|
route("overview", "./pages/overview.js"),
|
|
35
36
|
route("metrics", "./pages/metrics.js"),
|
|
36
37
|
route("apps", "./pages/apps.js"),
|
|
@@ -23,7 +23,7 @@ export function meta() {
|
|
|
23
23
|
*
|
|
24
24
|
* We preserve `?` and `#` so deep-links like `?thread=<id>` from a Slack
|
|
25
25
|
* "Open thread" button survive the bounce — `useThreadDeepLink` in
|
|
26
|
-
* `root.tsx` reads them after the redirect lands
|
|
26
|
+
* `root.tsx` reads them after the redirect lands and opens `/chat`.
|
|
27
27
|
*/
|
|
28
28
|
function buildTarget(request: Request): string {
|
|
29
29
|
const url = new URL(request.url);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { useLocation, useNavigate } from "react-router";
|
|
3
|
+
import { AgentChatSurface } from "@agent-native/core/client";
|
|
4
|
+
import { submitOverviewPrompt } from "@/lib/overview-chat";
|
|
5
|
+
|
|
6
|
+
interface DispatchChatLocationState {
|
|
7
|
+
dispatchPrompt?: {
|
|
8
|
+
id?: string | number;
|
|
9
|
+
message?: string;
|
|
10
|
+
selectedModel?: string | null;
|
|
11
|
+
};
|
|
12
|
+
dispatchThread?: {
|
|
13
|
+
id?: string | number;
|
|
14
|
+
threadId?: string;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function meta() {
|
|
19
|
+
return [{ title: "Chat — Dispatch" }];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default function ChatRoute() {
|
|
23
|
+
const location = useLocation();
|
|
24
|
+
const navigate = useNavigate();
|
|
25
|
+
const handledStateIds = useRef(new Set<string>());
|
|
26
|
+
const state = location.state as DispatchChatLocationState | null;
|
|
27
|
+
const prompt = state?.dispatchPrompt;
|
|
28
|
+
const thread = state?.dispatchThread;
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
const message = prompt?.message?.trim();
|
|
32
|
+
const threadId = thread?.threadId?.trim();
|
|
33
|
+
if (!message && !threadId) return;
|
|
34
|
+
|
|
35
|
+
const stateId = String(
|
|
36
|
+
prompt?.id ?? thread?.id ?? `${message ?? ""}:${threadId ?? ""}`,
|
|
37
|
+
);
|
|
38
|
+
if (handledStateIds.current.has(stateId)) return;
|
|
39
|
+
handledStateIds.current.add(stateId);
|
|
40
|
+
|
|
41
|
+
const timer = window.setTimeout(() => {
|
|
42
|
+
if (threadId) {
|
|
43
|
+
window.dispatchEvent(
|
|
44
|
+
new CustomEvent("agent-chat:open-thread", {
|
|
45
|
+
detail: { threadId },
|
|
46
|
+
}),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
if (message) {
|
|
50
|
+
submitOverviewPrompt(message, prompt?.selectedModel, {
|
|
51
|
+
openSidebar: false,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
navigate(`${location.pathname}${location.search}${location.hash}`, {
|
|
55
|
+
replace: true,
|
|
56
|
+
state: null,
|
|
57
|
+
});
|
|
58
|
+
}, 0);
|
|
59
|
+
|
|
60
|
+
return () => window.clearTimeout(timer);
|
|
61
|
+
}, [
|
|
62
|
+
location.hash,
|
|
63
|
+
location.pathname,
|
|
64
|
+
location.search,
|
|
65
|
+
navigate,
|
|
66
|
+
prompt?.id,
|
|
67
|
+
prompt?.message,
|
|
68
|
+
prompt?.selectedModel,
|
|
69
|
+
thread?.id,
|
|
70
|
+
thread?.threadId,
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className="flex h-full min-h-0 flex-col bg-background">
|
|
75
|
+
<AgentChatSurface
|
|
76
|
+
mode="page"
|
|
77
|
+
className="dispatch-chat-panel"
|
|
78
|
+
defaultMode="chat"
|
|
79
|
+
showHeader={false}
|
|
80
|
+
showTabBar={false}
|
|
81
|
+
dynamicSuggestions={false}
|
|
82
|
+
suggestions={[]}
|
|
83
|
+
emptyStateText="Ask Dispatch to create apps, route work, or manage the workspace."
|
|
84
|
+
emptyStateDisplay="hidden"
|
|
85
|
+
centerComposerWhenEmpty
|
|
86
|
+
composerLayoutVariant="hero"
|
|
87
|
+
composerPlaceholder="Ask Dispatch..."
|
|
88
|
+
composerSlot={
|
|
89
|
+
<div className="dispatch-chat-intro">
|
|
90
|
+
<h1>What should Dispatch do next?</h1>
|
|
91
|
+
<p>
|
|
92
|
+
Create apps, manage shared keys, and route work across agents.
|
|
93
|
+
</p>
|
|
94
|
+
</div>
|
|
95
|
+
}
|
|
96
|
+
/>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { useEffect, useMemo, useState } from "react";
|
|
2
|
-
import { Link } from "react-router";
|
|
2
|
+
import { Link, useNavigate } from "react-router";
|
|
3
3
|
import {
|
|
4
4
|
PromptComposer,
|
|
5
5
|
useActionQuery,
|
|
6
6
|
useChatModels,
|
|
7
7
|
agentNativePath,
|
|
8
|
+
isInBuilderFrame,
|
|
8
9
|
} from "@agent-native/core/client";
|
|
9
10
|
import {
|
|
10
11
|
IconActivity,
|
|
@@ -75,9 +76,26 @@ const HOME_CHAT_SUGGESTIONS = [
|
|
|
75
76
|
|
|
76
77
|
function HomeChatPanel() {
|
|
77
78
|
const { selectedModel } = useChatModels();
|
|
79
|
+
const navigate = useNavigate();
|
|
78
80
|
|
|
79
81
|
const send = (message: string) => {
|
|
80
|
-
|
|
82
|
+
const trimmed = message.trim();
|
|
83
|
+
if (!trimmed) return;
|
|
84
|
+
|
|
85
|
+
if (isInBuilderFrame()) {
|
|
86
|
+
submitOverviewPrompt(trimmed, selectedModel);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
navigate("/chat", {
|
|
91
|
+
state: {
|
|
92
|
+
dispatchPrompt: {
|
|
93
|
+
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
94
|
+
message: trimmed,
|
|
95
|
+
selectedModel,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
});
|
|
81
99
|
};
|
|
82
100
|
|
|
83
101
|
return (
|
|
@@ -90,9 +108,7 @@ function HomeChatPanel() {
|
|
|
90
108
|
<PromptComposer
|
|
91
109
|
placeholder="Message agent…"
|
|
92
110
|
onSubmit={(text) => {
|
|
93
|
-
|
|
94
|
-
if (!trimmed) return;
|
|
95
|
-
send(trimmed);
|
|
111
|
+
send(text);
|
|
96
112
|
}}
|
|
97
113
|
/>
|
|
98
114
|
<div className="flex flex-wrap justify-center gap-2">
|
|
@@ -52,4 +52,13 @@ describe("dispatch route shells", () => {
|
|
|
52
52
|
expect(indexRoute).toContain("HydrateFallback");
|
|
53
53
|
expect(indexRoute).toContain("@agent-native/dispatch/routes/pages/_index");
|
|
54
54
|
});
|
|
55
|
+
|
|
56
|
+
it("re-exports the chat route from the Dispatch template", () => {
|
|
57
|
+
const chatRoute = fs.readFileSync(
|
|
58
|
+
path.join(repoRoot, "templates/dispatch/app/routes/chat.tsx"),
|
|
59
|
+
"utf-8",
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
expect(chatRoute).toContain("@agent-native/dispatch/routes/pages/chat");
|
|
63
|
+
});
|
|
55
64
|
});
|
package/src/styles/dispatch.css
CHANGED
|
@@ -7,3 +7,106 @@
|
|
|
7
7
|
@source "../components/**/*.{js,mjs,ts,tsx}";
|
|
8
8
|
@source "../hooks/**/*.{js,mjs,ts,tsx}";
|
|
9
9
|
@source "../routes/**/*.{js,mjs,ts,tsx}";
|
|
10
|
+
|
|
11
|
+
.dispatch-chat-panel [data-agent-empty-state="centered"] {
|
|
12
|
+
justify-content: center;
|
|
13
|
+
padding-bottom: clamp(4rem, 12vh, 8rem);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.dispatch-chat-intro {
|
|
17
|
+
display: none;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.dispatch-chat-panel [data-agent-empty-state="centered"] .dispatch-chat-intro {
|
|
21
|
+
display: grid;
|
|
22
|
+
width: min(760px, calc(100vw - 3rem));
|
|
23
|
+
max-width: 760px;
|
|
24
|
+
gap: 0.5rem;
|
|
25
|
+
margin: 0 auto 1rem;
|
|
26
|
+
text-align: center;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.dispatch-chat-intro h1 {
|
|
30
|
+
margin: 0;
|
|
31
|
+
color: hsl(var(--foreground));
|
|
32
|
+
font-size: 2.5rem;
|
|
33
|
+
font-weight: 600;
|
|
34
|
+
letter-spacing: 0;
|
|
35
|
+
line-height: 1.12;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.dispatch-chat-intro p {
|
|
39
|
+
margin: 0;
|
|
40
|
+
color: hsl(var(--muted-foreground));
|
|
41
|
+
font-size: 0.9375rem;
|
|
42
|
+
line-height: 1.5;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.dispatch-chat-panel [data-agent-empty-state="centered"] .agent-chat-scroll {
|
|
46
|
+
flex: 0 0 auto;
|
|
47
|
+
min-height: 0;
|
|
48
|
+
overflow: visible;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.dispatch-chat-panel [data-agent-empty-state="centered"] .agent-composer-area {
|
|
52
|
+
width: min(760px, calc(100vw - 3rem));
|
|
53
|
+
max-width: 760px;
|
|
54
|
+
padding: 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.dispatch-chat-panel [data-agent-empty-state="centered"] .agent-composer-root {
|
|
58
|
+
min-height: 10.5rem;
|
|
59
|
+
border-color: hsl(var(--border));
|
|
60
|
+
border-radius: 1rem;
|
|
61
|
+
background: hsl(var(--card));
|
|
62
|
+
box-shadow:
|
|
63
|
+
0 20px 60px hsl(220 10% 2% / 0.12),
|
|
64
|
+
0 0 0 1px hsl(var(--border) / 0.45);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.dark
|
|
68
|
+
.dispatch-chat-panel
|
|
69
|
+
[data-agent-empty-state="centered"]
|
|
70
|
+
.agent-composer-root {
|
|
71
|
+
box-shadow:
|
|
72
|
+
0 24px 80px hsl(220 10% 2% / 0.4),
|
|
73
|
+
0 0 0 1px hsl(var(--border) / 0.7);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.dispatch-chat-panel
|
|
77
|
+
[data-agent-empty-state="centered"]
|
|
78
|
+
.agent-composer-prosemirror {
|
|
79
|
+
min-height: 6.75rem;
|
|
80
|
+
font-size: 1rem;
|
|
81
|
+
line-height: 1.625rem;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@media (max-width: 767px) {
|
|
85
|
+
.dispatch-chat-panel [data-agent-empty-state="centered"] {
|
|
86
|
+
padding: 1rem;
|
|
87
|
+
padding-bottom: 5rem;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.dispatch-chat-panel
|
|
91
|
+
[data-agent-empty-state="centered"]
|
|
92
|
+
.dispatch-chat-intro {
|
|
93
|
+
width: 100%;
|
|
94
|
+
margin-bottom: 0.875rem;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.dispatch-chat-intro h1 {
|
|
98
|
+
font-size: 1.75rem;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.dispatch-chat-panel
|
|
102
|
+
[data-agent-empty-state="centered"]
|
|
103
|
+
.agent-composer-area {
|
|
104
|
+
width: 100%;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.dispatch-chat-panel
|
|
108
|
+
[data-agent-empty-state="centered"]
|
|
109
|
+
.agent-composer-root {
|
|
110
|
+
min-height: 9rem;
|
|
111
|
+
}
|
|
112
|
+
}
|