@agent-native/dispatch 0.8.17 → 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/create-workspace-resource-grant.js +1 -1
- package/dist/actions/create-workspace-resource-grant.js.map +1 -1
- package/dist/actions/create-workspace-resource.js +5 -7
- package/dist/actions/create-workspace-resource.js.map +1 -1
- package/dist/actions/grant-workspace-resources-to-app.js +1 -1
- package/dist/actions/grant-workspace-resources-to-app.js.map +1 -1
- package/dist/actions/list-workspace-resource-grants.js +1 -1
- package/dist/actions/list-workspace-resource-grants.js.map +1 -1
- package/dist/actions/list-workspace-resource-options.js +1 -1
- package/dist/actions/list-workspace-resource-options.js.map +1 -1
- package/dist/actions/list-workspace-resources.js +2 -2
- package/dist/actions/list-workspace-resources.js.map +1 -1
- 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/create-app-popover.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/db/schema.js +2 -2
- package/dist/db/schema.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/dist/routes/pages/workspace.d.ts.map +1 -1
- package/dist/routes/pages/workspace.js +18 -5
- package/dist/routes/pages/workspace.js.map +1 -1
- package/dist/server/lib/workspace-resources-store.d.ts +2 -1
- package/dist/server/lib/workspace-resources-store.d.ts.map +1 -1
- package/dist/server/lib/workspace-resources-store.js +7 -1
- package/dist/server/lib/workspace-resources-store.js.map +1 -1
- package/dist/server/plugins/integrations.js +2 -2
- package/dist/server/plugins/integrations.js.map +1 -1
- package/package.json +1 -1
- package/src/actions/create-workspace-resource-grant.ts +1 -1
- package/src/actions/create-workspace-resource.ts +7 -7
- package/src/actions/grant-workspace-resources-to-app.ts +1 -1
- package/src/actions/list-workspace-resource-grants.ts +1 -1
- package/src/actions/list-workspace-resource-options.ts +1 -1
- package/src/actions/list-workspace-resources.ts +2 -2
- package/src/actions/navigate.ts +1 -1
- package/src/actions/view-screen.ts +7 -0
- package/src/components/create-app-popover.tsx +1 -1
- package/src/components/layout/Layout.tsx +187 -18
- package/src/db/schema.ts +2 -2
- 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/routes/pages/workspace.tsx +41 -7
- package/src/server/lib/workspace-resources-store.spec.ts +2 -0
- package/src/server/lib/workspace-resources-store.ts +13 -5
- package/src/server/plugins/integrations.ts +2 -2
- 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
|
);
|
package/src/db/schema.ts
CHANGED
|
@@ -174,13 +174,13 @@ export const vaultAuditLog = table("vault_audit_log", {
|
|
|
174
174
|
createdAt: integer("created_at").notNull(),
|
|
175
175
|
});
|
|
176
176
|
|
|
177
|
-
// ─── Workspace Resources: shared skills, instructions, agents, knowledge
|
|
177
|
+
// ─── Workspace Resources: shared skills, instructions, agents, knowledge, MCP ─
|
|
178
178
|
|
|
179
179
|
export const workspaceResources = table("workspace_resources", {
|
|
180
180
|
id: text("id").primaryKey(),
|
|
181
181
|
ownerEmail: text("owner_email").notNull(),
|
|
182
182
|
orgId: text("org_id"),
|
|
183
|
-
kind: text("kind").notNull(), // "skill" | "instruction" | "agent" | "knowledge"
|
|
183
|
+
kind: text("kind").notNull(), // "skill" | "instruction" | "agent" | "knowledge" | "mcp-server"
|
|
184
184
|
name: text("name").notNull(),
|
|
185
185
|
description: text("description"),
|
|
186
186
|
path: text("path").notNull(), // resource path, e.g. "skills/designer.md"
|
|
@@ -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">
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
IconEdit,
|
|
12
12
|
IconFileText,
|
|
13
13
|
IconPlus,
|
|
14
|
+
IconPlugConnected,
|
|
14
15
|
IconTrash,
|
|
15
16
|
IconUser,
|
|
16
17
|
IconX,
|
|
@@ -87,6 +88,13 @@ const KIND_CONFIG = {
|
|
|
87
88
|
description:
|
|
88
89
|
"Reference resources - brand, positioning, persona, and domain context",
|
|
89
90
|
},
|
|
91
|
+
"mcp-server": {
|
|
92
|
+
label: "MCP Server",
|
|
93
|
+
icon: IconPlugConnected,
|
|
94
|
+
pathPrefix: "mcp-servers/",
|
|
95
|
+
description:
|
|
96
|
+
"HTTP MCP servers - external tools centrally granted to app agents",
|
|
97
|
+
},
|
|
90
98
|
} as const;
|
|
91
99
|
|
|
92
100
|
const STARTER_GLOBAL_CONTEXT = [
|
|
@@ -135,6 +143,7 @@ function defaultResourcePath(kind: string, name: string): string {
|
|
|
135
143
|
if (kind === "instruction") return `instructions/${slug}.md`;
|
|
136
144
|
if (kind === "agent") return `agents/${slug}.md`;
|
|
137
145
|
if (kind === "knowledge") return `context/${slug}.md`;
|
|
146
|
+
if (kind === "mcp-server") return `mcp-servers/${slug}.json`;
|
|
138
147
|
return `${slug}.md`;
|
|
139
148
|
}
|
|
140
149
|
|
|
@@ -347,8 +356,8 @@ function AddResourceDialog() {
|
|
|
347
356
|
<DialogHeader>
|
|
348
357
|
<DialogTitle>Add workspace resource</DialogTitle>
|
|
349
358
|
<DialogDescription>
|
|
350
|
-
Create a skill, instruction, agent profile,
|
|
351
|
-
that can be shared across workspace apps.
|
|
359
|
+
Create a skill, instruction, agent profile, reference resource, or
|
|
360
|
+
MCP server that can be shared across workspace apps.
|
|
352
361
|
</DialogDescription>
|
|
353
362
|
</DialogHeader>
|
|
354
363
|
<div className="space-y-4 py-2">
|
|
@@ -364,6 +373,7 @@ function AddResourceDialog() {
|
|
|
364
373
|
<SelectItem value="instruction">Instruction</SelectItem>
|
|
365
374
|
<SelectItem value="agent">Agent</SelectItem>
|
|
366
375
|
<SelectItem value="knowledge">Knowledge pack</SelectItem>
|
|
376
|
+
<SelectItem value="mcp-server">MCP server</SelectItem>
|
|
367
377
|
</SelectContent>
|
|
368
378
|
</Select>
|
|
369
379
|
</div>
|
|
@@ -390,7 +400,9 @@ function AddResourceDialog() {
|
|
|
390
400
|
? "Research Specialist"
|
|
391
401
|
: kind === "knowledge"
|
|
392
402
|
? "Core GTM Messaging"
|
|
393
|
-
:
|
|
403
|
+
: kind === "mcp-server"
|
|
404
|
+
? "Zapier MCP"
|
|
405
|
+
: "Code Style Guide"
|
|
394
406
|
}
|
|
395
407
|
value={name}
|
|
396
408
|
onChange={(e) => setName(e.target.value)}
|
|
@@ -407,7 +419,9 @@ function AddResourceDialog() {
|
|
|
407
419
|
<p className="text-xs text-muted-foreground">
|
|
408
420
|
Skills use skills/name/SKILL.md. Guardrails in AGENTS.md or
|
|
409
421
|
instructions/ auto-load in app chat. Reference resources in
|
|
410
|
-
context/ are indexed so agents can read them when relevant.
|
|
422
|
+
context/ are indexed so agents can read them when relevant. MCP
|
|
423
|
+
server resources use mcp-servers/name.json and are loaded as HTTP
|
|
424
|
+
MCP tools.
|
|
411
425
|
</p>
|
|
412
426
|
</div>
|
|
413
427
|
<div className="space-y-2">
|
|
@@ -428,7 +442,9 @@ function AddResourceDialog() {
|
|
|
428
442
|
? "---\nname: Research Specialist\ndescription: Handles research tasks\n---\n\n# Instructions\n\n..."
|
|
429
443
|
: kind === "knowledge"
|
|
430
444
|
? "# Core GTM Messaging\n\n## Positioning\n\n## ICP\n\n## Proof points\n\n## Source\n\n"
|
|
431
|
-
:
|
|
445
|
+
: kind === "mcp-server"
|
|
446
|
+
? '{\n "type": "http",\n "url": "https://example.com/mcp",\n "headers": {\n "Authorization": "Bearer ${keys.MCP_SERVER_TOKEN}"\n },\n "description": "Shared MCP tools for workspace apps"\n}\n'
|
|
447
|
+
: "# Instructions\n\nAlways-on guardrails for agents across apps..."
|
|
432
448
|
}
|
|
433
449
|
value={content}
|
|
434
450
|
onChange={(e) => setContent(e.target.value)}
|
|
@@ -447,7 +463,12 @@ function AddResourceDialog() {
|
|
|
447
463
|
<Button
|
|
448
464
|
onClick={() =>
|
|
449
465
|
create.mutate({
|
|
450
|
-
kind: kind as
|
|
466
|
+
kind: kind as
|
|
467
|
+
| "skill"
|
|
468
|
+
| "instruction"
|
|
469
|
+
| "agent"
|
|
470
|
+
| "knowledge"
|
|
471
|
+
| "mcp-server",
|
|
451
472
|
name,
|
|
452
473
|
description: description || undefined,
|
|
453
474
|
path: path || defaultResourcePath(kind, name),
|
|
@@ -1010,6 +1031,9 @@ export default function WorkspaceRoute() {
|
|
|
1010
1031
|
const knowledge = (resources || []).filter(
|
|
1011
1032
|
(r: any) => r.kind === "knowledge",
|
|
1012
1033
|
);
|
|
1034
|
+
const mcpServers = (resources || []).filter(
|
|
1035
|
+
(r: any) => r.kind === "mcp-server",
|
|
1036
|
+
);
|
|
1013
1037
|
|
|
1014
1038
|
function ResourceList({
|
|
1015
1039
|
items,
|
|
@@ -1056,7 +1080,7 @@ export default function WorkspaceRoute() {
|
|
|
1056
1080
|
return (
|
|
1057
1081
|
<DispatchShell
|
|
1058
1082
|
title="Workspace Resources"
|
|
1059
|
-
description="Manage inherited workspace skills, guardrail instructions, agent profiles, and
|
|
1083
|
+
description="Manage inherited workspace skills, guardrail instructions, agent profiles, reference resources, and MCP servers. All-app resources are available to every app without syncing."
|
|
1060
1084
|
>
|
|
1061
1085
|
<div className="flex items-center justify-between">
|
|
1062
1086
|
<div className="text-sm text-muted-foreground">
|
|
@@ -1087,6 +1111,9 @@ export default function WorkspaceRoute() {
|
|
|
1087
1111
|
<TabsTrigger value="knowledge">
|
|
1088
1112
|
Knowledge {knowledge.length > 0 && `(${knowledge.length})`}
|
|
1089
1113
|
</TabsTrigger>
|
|
1114
|
+
<TabsTrigger value="mcp">
|
|
1115
|
+
MCP {mcpServers.length > 0 && `(${mcpServers.length})`}
|
|
1116
|
+
</TabsTrigger>
|
|
1090
1117
|
</TabsList>
|
|
1091
1118
|
|
|
1092
1119
|
<TabsContent value="skills" className="mt-4">
|
|
@@ -1116,6 +1143,13 @@ export default function WorkspaceRoute() {
|
|
|
1116
1143
|
emptyText="No knowledge packs yet. Add GTM, product, or domain context that apps can reuse."
|
|
1117
1144
|
/>
|
|
1118
1145
|
</TabsContent>
|
|
1146
|
+
|
|
1147
|
+
<TabsContent value="mcp" className="mt-4">
|
|
1148
|
+
<ResourceList
|
|
1149
|
+
items={mcpServers}
|
|
1150
|
+
emptyText="No workspace MCP servers yet. Add an HTTP MCP server to share external tools across apps."
|
|
1151
|
+
/>
|
|
1152
|
+
</TabsContent>
|
|
1119
1153
|
</Tabs>
|
|
1120
1154
|
</DispatchShell>
|
|
1121
1155
|
);
|
|
@@ -703,6 +703,7 @@ describe("workspace resource materialization", () => {
|
|
|
703
703
|
expect(mocks.resourceEffectiveContext).toHaveBeenCalledWith(
|
|
704
704
|
"person@example.test",
|
|
705
705
|
"context/brand.md",
|
|
706
|
+
{ workspaceAppId: "analytics", orgId: "org_123" },
|
|
706
707
|
);
|
|
707
708
|
expect(mocks.resourcePut).toHaveBeenCalledWith(
|
|
708
709
|
"__workspace__",
|
|
@@ -899,6 +900,7 @@ describe("workspace resource materialization", () => {
|
|
|
899
900
|
expect(mocks.resourceEffectiveContext).toHaveBeenCalledWith(
|
|
900
901
|
"owner@example.test",
|
|
901
902
|
"context/analytics-launch.md",
|
|
903
|
+
{ workspaceAppId: "analytics", orgId: "org_123" },
|
|
902
904
|
);
|
|
903
905
|
});
|
|
904
906
|
|