@agent-native/dispatch 0.5.1 → 0.6.0

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.
@@ -1,46 +1,147 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useMemo, useState } from "react";
2
3
  import { useActionMutation, useActionQuery } from "@agent-native/core/client";
4
+ import { useQueryClient } from "@tanstack/react-query";
3
5
  import { toast } from "sonner";
4
- import { IconCheck, IconCircleDashed, IconKey, IconRefresh, IconShieldCheck, IconWifi, IconWifiOff, } from "@tabler/icons-react";
6
+ import { IconCheck, IconChevronRight, IconCircleDashed, IconKey, IconLink, IconPlugConnected, } from "@tabler/icons-react";
5
7
  import { DispatchShell } from "../../components/dispatch-shell.js";
6
8
  import { Badge } from "../../components/ui/badge.js";
7
9
  import { Button } from "../../components/ui/button.js";
8
- import { Progress } from "../../components/ui/progress.js";
10
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "../../components/ui/collapsible.js";
11
+ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "../../components/ui/dialog.js";
12
+ import { Input } from "../../components/ui/input.js";
13
+ import { Label } from "../../components/ui/label.js";
9
14
  export function meta() {
10
- return [{ title: "Integrations — Dispatch" }];
15
+ return [{ title: "Connections — Dispatch" }];
11
16
  }
12
- function StatusBadge({ configured, vaultGranted, }) {
13
- if (configured && vaultGranted) {
14
- return (_jsxs(Badge, { variant: "secondary", className: "bg-green-500/10 text-green-700 dark:text-green-400", children: [_jsx(IconShieldCheck, { size: 12, className: "mr-1" }), "Vault"] }));
17
+ function inferProviderFromKey(key, label) {
18
+ const haystack = `${key} ${label}`.toLowerCase();
19
+ for (const provider of [
20
+ "google",
21
+ "slack",
22
+ "sendgrid",
23
+ "github",
24
+ "stripe",
25
+ "hubspot",
26
+ "jira",
27
+ "bigquery",
28
+ "anthropic",
29
+ "openai",
30
+ ]) {
31
+ if (haystack.includes(provider))
32
+ return provider;
15
33
  }
16
- if (configured) {
17
- return (_jsxs(Badge, { variant: "secondary", className: "bg-green-500/10 text-green-700 dark:text-green-400", children: [_jsx(IconCheck, { size: 12, className: "mr-1" }), "Configured"] }));
34
+ return "other";
35
+ }
36
+ function ConnectDialog({ service, open, onOpenChange, }) {
37
+ const [value, setValue] = useState("");
38
+ const qc = useQueryClient();
39
+ const createSecret = useActionMutation("create-vault-secret", {});
40
+ const createGrant = useActionMutation("create-vault-grant", {});
41
+ const syncToApp = useActionMutation("sync-vault-to-app", {});
42
+ function reset() {
43
+ setValue("");
18
44
  }
19
- if (vaultGranted) {
20
- return (_jsxs(Badge, { variant: "secondary", className: "bg-blue-500/10 text-blue-700 dark:text-blue-400", children: [_jsx(IconKey, { size: 12, className: "mr-1" }), "Granted (not synced)"] }));
45
+ async function handleSave() {
46
+ const trimmed = value.trim();
47
+ if (!trimmed) {
48
+ toast.error("Enter a value to save");
49
+ return;
50
+ }
51
+ try {
52
+ // 1. Create the secret (or get the existing one — server treats key as
53
+ // the unique identifier). The server returns { secret: { id, ... } }.
54
+ const created = await createSecret.mutateAsync({
55
+ credentialKey: service.key,
56
+ name: service.label,
57
+ value: trimmed,
58
+ provider: inferProviderFromKey(service.key, service.label),
59
+ });
60
+ const secretId = created?.secret?.id ??
61
+ created?.id;
62
+ if (!secretId) {
63
+ throw new Error("Secret created but id missing");
64
+ }
65
+ // 2. Grant + sync to every app that declared this credential.
66
+ const targets = service.apps.filter((a) => !a.vaultGranted);
67
+ for (const app of targets) {
68
+ try {
69
+ await createGrant.mutateAsync({
70
+ secretId,
71
+ appId: app.appId,
72
+ });
73
+ }
74
+ catch (err) {
75
+ console.warn(`grant to ${app.appId} failed`, err);
76
+ }
77
+ }
78
+ for (const app of service.apps) {
79
+ try {
80
+ await syncToApp.mutateAsync({ appId: app.appId });
81
+ }
82
+ catch (err) {
83
+ console.warn(`sync to ${app.appId} failed`, err);
84
+ }
85
+ }
86
+ qc.invalidateQueries({
87
+ queryKey: ["action", "list-integrations-catalog"],
88
+ });
89
+ toast.success(`Connected ${service.label}`);
90
+ onOpenChange(false);
91
+ reset();
92
+ }
93
+ catch (err) {
94
+ toast.error(err?.message ?? "Failed to save credential");
95
+ }
21
96
  }
22
- return (_jsxs(Badge, { variant: "secondary", className: "bg-amber-500/10 text-amber-700 dark:text-amber-400", children: [_jsx(IconCircleDashed, { size: 12, className: "mr-1" }), "Missing"] }));
97
+ const pending = createSecret.isPending || createGrant.isPending || syncToApp.isPending;
98
+ return (_jsx(Dialog, { open: open, onOpenChange: (next) => {
99
+ if (!next)
100
+ reset();
101
+ onOpenChange(next);
102
+ }, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { children: ["Connect ", service.label] }), _jsxs(DialogDescription, { children: ["Used by", " ", service.apps.length === 1
103
+ ? service.apps[0].appName
104
+ : `${service.apps.length} apps`, ". Saved to the workspace vault and synced to every app that needs it."] })] }), _jsxs("div", { className: "space-y-3", children: [_jsxs("div", { children: [_jsx(Label, { className: "text-xs text-muted-foreground", children: "Key" }), _jsx("div", { className: "font-mono text-sm", children: service.key })] }), _jsxs("div", { children: [_jsx(Label, { htmlFor: "connector-value", children: "Value" }), _jsx(Input, { id: "connector-value", type: "password", autoComplete: "off", value: value, onChange: (e) => setValue(e.target.value), placeholder: `Paste your ${service.label} key…`, autoFocus: true })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: pending, children: "Cancel" }), _jsx(Button, { onClick: handleSave, disabled: pending || !value.trim(), children: pending ? "Saving…" : "Connect" })] })] }) }));
105
+ }
106
+ function ConnectorCard({ service }) {
107
+ const [open, setOpen] = useState(false);
108
+ const isConnected = service.apps.some((a) => a.configured);
109
+ const appCount = service.apps.length;
110
+ return (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", onClick: () => setOpen(true), className: "group flex flex-col items-start gap-2 rounded-2xl border bg-card p-5 text-left transition hover:border-foreground/20 hover:bg-card/80 cursor-pointer", children: [_jsxs("div", { className: "flex w-full items-start justify-between gap-2", children: [_jsx("div", { className: "flex h-9 w-9 items-center justify-center rounded-xl bg-muted", children: _jsx(IconKey, { size: 16, className: "text-muted-foreground" }) }), isConnected ? (_jsxs(Badge, { variant: "secondary", className: "bg-green-500/10 text-green-700 dark:text-green-400 gap-1", children: [_jsx(IconCheck, { size: 12 }), "Connected"] })) : (_jsxs(Badge, { variant: "secondary", className: "bg-amber-500/10 text-amber-700 dark:text-amber-400 gap-1", children: [_jsx(IconCircleDashed, { size: 12 }), "Connect"] }))] }), _jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "text-sm font-semibold text-foreground truncate", children: service.label }), _jsx("div", { className: "font-mono text-xs text-muted-foreground/80 truncate", children: service.key })] }), _jsxs("div", { className: "text-xs text-muted-foreground", children: ["Used by ", appCount, " ", appCount === 1 ? "app" : "apps"] })] }), _jsx(ConnectDialog, { service: service, open: open, onOpenChange: setOpen })] }));
23
111
  }
24
- function AppCard({ app }) {
25
- const syncToApp = useActionMutation("sync-vault-to-app", {
26
- onSuccess: (data) => toast.success(`Synced ${data.synced} key(s) to ${data.appId}`),
27
- onError: (err) => toast.error(String(err)),
28
- });
29
- const integrations = app.integrations || [];
30
- const configuredCount = integrations.filter((i) => i.configured).length;
31
- const total = integrations.length;
32
- const coverage = total > 0 ? Math.round((configuredCount / total) * 100) : 0;
33
- return (_jsxs("div", { className: "rounded-2xl border bg-card", children: [_jsxs("div", { className: "flex items-center justify-between border-b px-5 py-4", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-9 w-9 items-center justify-center rounded-xl text-white text-xs font-bold", style: { backgroundColor: app.color }, children: app.appName.charAt(0).toUpperCase() }), _jsxs("div", { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("h3", { className: "text-sm font-semibold text-foreground", children: app.appName }), app.reachable ? (_jsx(IconWifi, { size: 14, className: "text-green-500" })) : (_jsx(IconWifiOff, { size: 14, className: "text-muted-foreground/50" }))] }), _jsx("div", { className: "text-xs text-muted-foreground", children: app.appId })] })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => syncToApp.mutate({ appId: app.appId }), disabled: syncToApp.isPending || !app.reachable, children: [_jsx(IconRefresh, { size: 14, className: syncToApp.isPending ? "animate-spin" : "" }), _jsx("span", { className: "ml-1.5", children: "Sync" })] })] }), _jsx("div", { className: "px-5 py-4", children: total > 0 ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex items-center justify-between text-xs text-muted-foreground", children: [_jsxs("span", { children: [configuredCount, "/", total, " configured"] }), _jsxs("span", { children: [coverage, "%"] })] }), _jsx(Progress, { value: coverage, className: "mt-2 h-1.5" }), _jsx("div", { className: "mt-4 space-y-2", children: integrations.map((integration) => (_jsxs("div", { className: "flex items-center justify-between rounded-lg border px-3 py-2", children: [_jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-sm text-foreground", children: integration.label }), integration.required && (_jsx("span", { className: "text-xs text-red-500", children: "required" }))] }), _jsx("div", { className: "font-mono text-xs text-muted-foreground", children: integration.key })] }), _jsx(StatusBadge, { configured: integration.configured, vaultGranted: integration.vaultGranted })] }, integration.key))) })] })) : app.reachable ? (_jsx("div", { className: "py-4 text-center text-sm text-muted-foreground", children: "No declared integrations." })) : (_jsx("div", { className: "py-4 text-center text-sm text-muted-foreground", children: "App is not reachable. Start the app to see its integrations." })) })] }));
112
+ function PerAppDetailRow({ app }) {
113
+ const total = (app.integrations ?? []).length;
114
+ const ok = (app.integrations ?? []).filter((i) => i.configured).length;
115
+ return (_jsxs("div", { className: "flex items-center justify-between border-t px-4 py-2.5 first:border-t-0", children: [_jsxs("div", { className: "flex items-center gap-2 min-w-0", children: [_jsx("div", { className: "h-5 w-5 rounded text-[10px] font-bold text-white flex items-center justify-center shrink-0", style: { backgroundColor: app.color }, children: app.appName.charAt(0).toUpperCase() }), _jsx("span", { className: "text-sm truncate", children: app.appName }), !app.reachable && (_jsx("span", { className: "text-xs text-muted-foreground", children: "offline" }))] }), _jsx("span", { className: "text-xs text-muted-foreground", children: total === 0 ? "no integrations" : `${ok}/${total}` })] }));
34
116
  }
35
- export default function IntegrationsRoute() {
117
+ export default function ConnectionsRoute() {
36
118
  const { data: catalog, isLoading } = useActionQuery("list-integrations-catalog", {});
37
119
  const apps = catalog || [];
38
- const reachableApps = apps.filter((a) => a.reachable);
39
- const unreachableApps = apps.filter((a) => !a.reachable);
40
- const totalIntegrations = apps.reduce((sum, a) => sum + (a.integrations?.length || 0), 0);
41
- const configuredIntegrations = apps.reduce((sum, a) => sum + (a.integrations?.filter((i) => i.configured)?.length || 0), 0);
42
- return (_jsxs(DispatchShell, { title: "Integrations", description: "See what credentials each app needs and their configuration status across the workspace.", children: [!isLoading && apps.length > 0 && (_jsxs("div", { className: "grid gap-4 md:grid-cols-3", children: [_jsxs("div", { className: "rounded-2xl border bg-card p-5", children: [_jsx("div", { className: "text-sm font-medium text-muted-foreground", children: "Apps discovered" }), _jsx("div", { className: "mt-2 text-3xl font-semibold text-foreground", children: apps.length }), _jsxs("div", { className: "mt-1 text-xs text-muted-foreground", children: [reachableApps.length, " reachable"] })] }), _jsxs("div", { className: "rounded-2xl border bg-card p-5", children: [_jsx("div", { className: "text-sm font-medium text-muted-foreground", children: "Total integrations" }), _jsx("div", { className: "mt-2 text-3xl font-semibold text-foreground", children: totalIntegrations }), _jsx("div", { className: "mt-1 text-xs text-muted-foreground", children: "across all apps" })] }), _jsxs("div", { className: "rounded-2xl border bg-card p-5", children: [_jsx("div", { className: "text-sm font-medium text-muted-foreground", children: "Configured" }), _jsxs("div", { className: "mt-2 text-3xl font-semibold text-foreground", children: [configuredIntegrations, "/", totalIntegrations] }), _jsx(Progress, { value: totalIntegrations > 0
43
- ? Math.round((configuredIntegrations / totalIntegrations) * 100)
44
- : 0, className: "mt-2 h-1.5" })] })] })), isLoading && (_jsx("div", { className: "rounded-2xl border border-dashed px-6 py-12 text-center text-sm text-muted-foreground", children: "Discovering apps and fetching integration status..." })), reachableApps.length > 0 && (_jsx("div", { className: "grid gap-4 xl:grid-cols-2", children: reachableApps.map((app) => (_jsx(AppCard, { app: app }, app.appId))) })), unreachableApps.length > 0 && (_jsxs("div", { children: [_jsx("h2", { className: "mb-3 text-sm font-medium text-muted-foreground", children: "Offline apps" }), _jsx("div", { className: "grid gap-4 xl:grid-cols-2", children: unreachableApps.map((app) => (_jsx(AppCard, { app: app }, app.appId))) })] })), !isLoading && apps.length === 0 && (_jsx("div", { className: "rounded-2xl border border-dashed px-6 py-12 text-center text-sm text-muted-foreground", children: "No workspace apps found." }))] }));
120
+ const services = useMemo(() => {
121
+ const map = new Map();
122
+ for (const app of apps) {
123
+ for (const intg of app.integrations ?? []) {
124
+ if (!map.has(intg.key)) {
125
+ map.set(intg.key, {
126
+ key: intg.key,
127
+ label: intg.label,
128
+ apps: [],
129
+ });
130
+ }
131
+ map.get(intg.key).apps.push({
132
+ appId: app.appId,
133
+ appName: app.appName,
134
+ color: app.color,
135
+ configured: intg.configured,
136
+ vaultGranted: intg.vaultGranted,
137
+ vaultSecretId: intg.vaultSecretId,
138
+ });
139
+ }
140
+ }
141
+ return Array.from(map.values()).sort((a, b) => a.label.localeCompare(b.label));
142
+ }, [apps]);
143
+ const available = services.filter((s) => !s.apps.some((a) => a.configured));
144
+ const connected = services.filter((s) => s.apps.some((a) => a.configured));
145
+ return (_jsxs(DispatchShell, { title: "Connections", description: "Connect services once. Apps that need them pick up the key automatically.", children: [isLoading && services.length === 0 && (_jsx("div", { className: "rounded-2xl border border-dashed px-6 py-12 text-center text-sm text-muted-foreground", children: "Discovering apps and credentials\u2026" })), !isLoading && services.length === 0 && (_jsx("div", { className: "rounded-2xl border border-dashed px-6 py-12 text-center text-sm text-muted-foreground", children: "No apps with declared integrations are reachable yet." })), available.length > 0 && (_jsxs("section", { children: [_jsxs("div", { className: "mb-3 flex items-baseline justify-between", children: [_jsx("h2", { className: "text-sm font-medium text-foreground", children: "Available to connect" }), _jsx("span", { className: "text-xs text-muted-foreground", children: available.length })] }), _jsx("div", { className: "grid gap-3 sm:grid-cols-2 xl:grid-cols-3", children: available.map((service) => (_jsx(ConnectorCard, { service: service }, service.key))) })] })), connected.length > 0 && (_jsxs("section", { children: [_jsxs("div", { className: "mb-3 mt-2 flex items-baseline justify-between", children: [_jsx("h2", { className: "text-sm font-medium text-foreground", children: "Connected" }), _jsx("span", { className: "text-xs text-muted-foreground", children: connected.length })] }), _jsx("div", { className: "grid gap-3 sm:grid-cols-2 xl:grid-cols-3", children: connected.map((service) => (_jsx(ConnectorCard, { service: service }, service.key))) })] })), apps.length > 0 && (_jsxs(Collapsible, { className: "mt-6 rounded-2xl border bg-card", children: [_jsxs(CollapsibleTrigger, { className: "flex w-full items-center justify-between px-4 py-3 text-sm", children: [_jsxs("span", { className: "flex items-center gap-2 text-muted-foreground", children: [_jsx(IconPlugConnected, { size: 14 }), "Per-app status"] }), _jsx(IconChevronRight, { size: 14, className: "text-muted-foreground transition group-data-[state=open]:rotate-90" })] }), _jsxs(CollapsibleContent, { children: [_jsx("div", { className: "border-t", children: apps.map((app) => (_jsx(PerAppDetailRow, { app: app }, app.appId))) }), _jsxs("div", { className: "flex items-center justify-end gap-1.5 border-t px-4 py-2.5 text-xs text-muted-foreground", children: [_jsx(IconLink, { size: 12 }), _jsx("a", { href: "/vault", className: "hover:underline", children: "Open vault for advanced sharing" })] })] })] }))] }));
45
146
  }
46
147
  //# sourceMappingURL=integrations.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"integrations.js","sourceRoot":"","sources":["../../../src/routes/pages/integrations.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useActionMutation, useActionQuery } from \"@agent-native/core/client\";\nimport { toast } from \"sonner\";\nimport {\n IconCheck,\n IconCircleDashed,\n IconKey,\n IconRefresh,\n IconShieldCheck,\n IconWifi,\n IconWifiOff,\n} from \"@tabler/icons-react\";\nimport { DispatchShell } from \"@/components/dispatch-shell\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Progress } from \"@/components/ui/progress\";\n\nexport function meta() {\n return [{ title: \"Integrations — Dispatch\" }];\n}\n\nfunction StatusBadge({\n configured,\n vaultGranted,\n}: {\n configured: boolean;\n vaultGranted: boolean;\n}) {\n if (configured && vaultGranted) {\n return (\n <Badge\n variant=\"secondary\"\n className=\"bg-green-500/10 text-green-700 dark:text-green-400\"\n >\n <IconShieldCheck size={12} className=\"mr-1\" />\n Vault\n </Badge>\n );\n }\n if (configured) {\n return (\n <Badge\n variant=\"secondary\"\n className=\"bg-green-500/10 text-green-700 dark:text-green-400\"\n >\n <IconCheck size={12} className=\"mr-1\" />\n Configured\n </Badge>\n );\n }\n if (vaultGranted) {\n return (\n <Badge\n variant=\"secondary\"\n className=\"bg-blue-500/10 text-blue-700 dark:text-blue-400\"\n >\n <IconKey size={12} className=\"mr-1\" />\n Granted (not synced)\n </Badge>\n );\n }\n return (\n <Badge\n variant=\"secondary\"\n className=\"bg-amber-500/10 text-amber-700 dark:text-amber-400\"\n >\n <IconCircleDashed size={12} className=\"mr-1\" />\n Missing\n </Badge>\n );\n}\n\nfunction AppCard({ app }: { app: any }) {\n const syncToApp = useActionMutation(\"sync-vault-to-app\", {\n onSuccess: (data: any) =>\n toast.success(`Synced ${data.synced} key(s) to ${data.appId}`),\n onError: (err) => toast.error(String(err)),\n });\n\n const integrations = app.integrations || [];\n const configuredCount = integrations.filter((i: any) => i.configured).length;\n const total = integrations.length;\n const coverage = total > 0 ? Math.round((configuredCount / total) * 100) : 0;\n\n return (\n <div className=\"rounded-2xl border bg-card\">\n <div className=\"flex items-center justify-between border-b px-5 py-4\">\n <div className=\"flex items-center gap-3\">\n <div\n className=\"flex h-9 w-9 items-center justify-center rounded-xl text-white text-xs font-bold\"\n style={{ backgroundColor: app.color }}\n >\n {app.appName.charAt(0).toUpperCase()}\n </div>\n <div>\n <div className=\"flex items-center gap-2\">\n <h3 className=\"text-sm font-semibold text-foreground\">\n {app.appName}\n </h3>\n {app.reachable ? (\n <IconWifi size={14} className=\"text-green-500\" />\n ) : (\n <IconWifiOff size={14} className=\"text-muted-foreground/50\" />\n )}\n </div>\n <div className=\"text-xs text-muted-foreground\">{app.appId}</div>\n </div>\n </div>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => syncToApp.mutate({ appId: app.appId })}\n disabled={syncToApp.isPending || !app.reachable}\n >\n <IconRefresh\n size={14}\n className={syncToApp.isPending ? \"animate-spin\" : \"\"}\n />\n <span className=\"ml-1.5\">Sync</span>\n </Button>\n </div>\n\n <div className=\"px-5 py-4\">\n {total > 0 ? (\n <>\n <div className=\"flex items-center justify-between text-xs text-muted-foreground\">\n <span>\n {configuredCount}/{total} configured\n </span>\n <span>{coverage}%</span>\n </div>\n <Progress value={coverage} className=\"mt-2 h-1.5\" />\n\n <div className=\"mt-4 space-y-2\">\n {integrations.map((integration: any) => (\n <div\n key={integration.key}\n className=\"flex items-center justify-between rounded-lg border px-3 py-2\"\n >\n <div className=\"min-w-0\">\n <div className=\"flex items-center gap-2\">\n <span className=\"text-sm text-foreground\">\n {integration.label}\n </span>\n {integration.required && (\n <span className=\"text-xs text-red-500\">required</span>\n )}\n </div>\n <div className=\"font-mono text-xs text-muted-foreground\">\n {integration.key}\n </div>\n </div>\n <StatusBadge\n configured={integration.configured}\n vaultGranted={integration.vaultGranted}\n />\n </div>\n ))}\n </div>\n </>\n ) : app.reachable ? (\n <div className=\"py-4 text-center text-sm text-muted-foreground\">\n No declared integrations.\n </div>\n ) : (\n <div className=\"py-4 text-center text-sm text-muted-foreground\">\n App is not reachable. Start the app to see its integrations.\n </div>\n )}\n </div>\n </div>\n );\n}\n\nexport default function IntegrationsRoute() {\n const { data: catalog, isLoading } = useActionQuery(\n \"list-integrations-catalog\",\n {},\n );\n\n const apps = catalog || [];\n const reachableApps = apps.filter((a: any) => a.reachable);\n const unreachableApps = apps.filter((a: any) => !a.reachable);\n\n const totalIntegrations = apps.reduce(\n (sum: number, a: any) => sum + (a.integrations?.length || 0),\n 0,\n );\n const configuredIntegrations = apps.reduce(\n (sum: number, a: any) =>\n sum + (a.integrations?.filter((i: any) => i.configured)?.length || 0),\n 0,\n );\n\n return (\n <DispatchShell\n title=\"Integrations\"\n description=\"See what credentials each app needs and their configuration status across the workspace.\"\n >\n {!isLoading && apps.length > 0 && (\n <div className=\"grid gap-4 md:grid-cols-3\">\n <div className=\"rounded-2xl border bg-card p-5\">\n <div className=\"text-sm font-medium text-muted-foreground\">\n Apps discovered\n </div>\n <div className=\"mt-2 text-3xl font-semibold text-foreground\">\n {apps.length}\n </div>\n <div className=\"mt-1 text-xs text-muted-foreground\">\n {reachableApps.length} reachable\n </div>\n </div>\n <div className=\"rounded-2xl border bg-card p-5\">\n <div className=\"text-sm font-medium text-muted-foreground\">\n Total integrations\n </div>\n <div className=\"mt-2 text-3xl font-semibold text-foreground\">\n {totalIntegrations}\n </div>\n <div className=\"mt-1 text-xs text-muted-foreground\">\n across all apps\n </div>\n </div>\n <div className=\"rounded-2xl border bg-card p-5\">\n <div className=\"text-sm font-medium text-muted-foreground\">\n Configured\n </div>\n <div className=\"mt-2 text-3xl font-semibold text-foreground\">\n {configuredIntegrations}/{totalIntegrations}\n </div>\n <Progress\n value={\n totalIntegrations > 0\n ? Math.round(\n (configuredIntegrations / totalIntegrations) * 100,\n )\n : 0\n }\n className=\"mt-2 h-1.5\"\n />\n </div>\n </div>\n )}\n\n {isLoading && (\n <div className=\"rounded-2xl border border-dashed px-6 py-12 text-center text-sm text-muted-foreground\">\n Discovering apps and fetching integration status...\n </div>\n )}\n\n {reachableApps.length > 0 && (\n <div className=\"grid gap-4 xl:grid-cols-2\">\n {reachableApps.map((app: any) => (\n <AppCard key={app.appId} app={app} />\n ))}\n </div>\n )}\n\n {unreachableApps.length > 0 && (\n <div>\n <h2 className=\"mb-3 text-sm font-medium text-muted-foreground\">\n Offline apps\n </h2>\n <div className=\"grid gap-4 xl:grid-cols-2\">\n {unreachableApps.map((app: any) => (\n <AppCard key={app.appId} app={app} />\n ))}\n </div>\n </div>\n )}\n\n {!isLoading && apps.length === 0 && (\n <div className=\"rounded-2xl border border-dashed px-6 py-12 text-center text-sm text-muted-foreground\">\n No workspace apps found.\n </div>\n )}\n </DispatchShell>\n );\n}\n"]}
1
+ {"version":3,"file":"integrations.js","sourceRoot":"","sources":["../../../src/routes/pages/integrations.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo, useState } from \"react\";\nimport { useActionMutation, useActionQuery } from \"@agent-native/core/client\";\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport { toast } from \"sonner\";\nimport {\n IconCheck,\n IconChevronRight,\n IconCircleDashed,\n IconKey,\n IconLink,\n IconPlugConnected,\n} from \"@tabler/icons-react\";\nimport { DispatchShell } from \"@/components/dispatch-shell\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Collapsible,\n CollapsibleContent,\n CollapsibleTrigger,\n} from \"@/components/ui/collapsible\";\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from \"@/components/ui/dialog\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\n\nexport function meta() {\n return [{ title: \"Connections — Dispatch\" }];\n}\n\ninterface AppRef {\n appId: string;\n appName: string;\n color: string;\n configured: boolean;\n vaultGranted: boolean;\n vaultSecretId?: string;\n}\n\ninterface Service {\n /** Credential key shared across apps (e.g. `OPENAI_API_KEY`). */\n key: string;\n /** Human label from the first app that declares it (`\"OpenAI\"`, `\"Stripe\"`). */\n label: string;\n /** Apps in the workspace that declare this credential. */\n apps: AppRef[];\n}\n\ninterface CatalogApp {\n appId: string;\n appName: string;\n color: string;\n url: string;\n reachable: boolean;\n integrations?: Array<{\n key: string;\n label: string;\n required: boolean;\n configured: boolean;\n vaultGranted: boolean;\n vaultSecretId?: string;\n }>;\n}\n\nfunction inferProviderFromKey(key: string, label: string): string {\n const haystack = `${key} ${label}`.toLowerCase();\n for (const provider of [\n \"google\",\n \"slack\",\n \"sendgrid\",\n \"github\",\n \"stripe\",\n \"hubspot\",\n \"jira\",\n \"bigquery\",\n \"anthropic\",\n \"openai\",\n ]) {\n if (haystack.includes(provider)) return provider;\n }\n return \"other\";\n}\n\nfunction ConnectDialog({\n service,\n open,\n onOpenChange,\n}: {\n service: Service;\n open: boolean;\n onOpenChange: (next: boolean) => void;\n}) {\n const [value, setValue] = useState(\"\");\n const qc = useQueryClient();\n\n const createSecret = useActionMutation(\"create-vault-secret\", {});\n const createGrant = useActionMutation(\"create-vault-grant\", {});\n const syncToApp = useActionMutation(\"sync-vault-to-app\", {});\n\n function reset() {\n setValue(\"\");\n }\n\n async function handleSave() {\n const trimmed = value.trim();\n if (!trimmed) {\n toast.error(\"Enter a value to save\");\n return;\n }\n try {\n // 1. Create the secret (or get the existing one — server treats key as\n // the unique identifier). The server returns { secret: { id, ... } }.\n const created = await createSecret.mutateAsync({\n credentialKey: service.key,\n name: service.label,\n value: trimmed,\n provider: inferProviderFromKey(service.key, service.label),\n });\n const secretId =\n (created as { secret?: { id?: string } })?.secret?.id ??\n (created as { id?: string })?.id;\n if (!secretId) {\n throw new Error(\"Secret created but id missing\");\n }\n\n // 2. Grant + sync to every app that declared this credential.\n const targets = service.apps.filter((a) => !a.vaultGranted);\n for (const app of targets) {\n try {\n await createGrant.mutateAsync({\n secretId,\n appId: app.appId,\n });\n } catch (err) {\n console.warn(`grant to ${app.appId} failed`, err);\n }\n }\n for (const app of service.apps) {\n try {\n await syncToApp.mutateAsync({ appId: app.appId });\n } catch (err) {\n console.warn(`sync to ${app.appId} failed`, err);\n }\n }\n\n qc.invalidateQueries({\n queryKey: [\"action\", \"list-integrations-catalog\"],\n });\n toast.success(`Connected ${service.label}`);\n onOpenChange(false);\n reset();\n } catch (err: any) {\n toast.error(err?.message ?? \"Failed to save credential\");\n }\n }\n\n const pending =\n createSecret.isPending || createGrant.isPending || syncToApp.isPending;\n\n return (\n <Dialog\n open={open}\n onOpenChange={(next) => {\n if (!next) reset();\n onOpenChange(next);\n }}\n >\n <DialogContent>\n <DialogHeader>\n <DialogTitle>Connect {service.label}</DialogTitle>\n <DialogDescription>\n Used by{\" \"}\n {service.apps.length === 1\n ? service.apps[0].appName\n : `${service.apps.length} apps`}\n . Saved to the workspace vault and synced to every app that needs\n it.\n </DialogDescription>\n </DialogHeader>\n <div className=\"space-y-3\">\n <div>\n <Label className=\"text-xs text-muted-foreground\">Key</Label>\n <div className=\"font-mono text-sm\">{service.key}</div>\n </div>\n <div>\n <Label htmlFor=\"connector-value\">Value</Label>\n <Input\n id=\"connector-value\"\n type=\"password\"\n autoComplete=\"off\"\n value={value}\n onChange={(e) => setValue(e.target.value)}\n placeholder={`Paste your ${service.label} key…`}\n autoFocus\n />\n </div>\n </div>\n <DialogFooter>\n <Button\n variant=\"outline\"\n onClick={() => onOpenChange(false)}\n disabled={pending}\n >\n Cancel\n </Button>\n <Button onClick={handleSave} disabled={pending || !value.trim()}>\n {pending ? \"Saving…\" : \"Connect\"}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n );\n}\n\nfunction ConnectorCard({ service }: { service: Service }) {\n const [open, setOpen] = useState(false);\n const isConnected = service.apps.some((a) => a.configured);\n const appCount = service.apps.length;\n\n return (\n <>\n <button\n type=\"button\"\n onClick={() => setOpen(true)}\n className=\"group flex flex-col items-start gap-2 rounded-2xl border bg-card p-5 text-left transition hover:border-foreground/20 hover:bg-card/80 cursor-pointer\"\n >\n <div className=\"flex w-full items-start justify-between gap-2\">\n <div className=\"flex h-9 w-9 items-center justify-center rounded-xl bg-muted\">\n <IconKey size={16} className=\"text-muted-foreground\" />\n </div>\n {isConnected ? (\n <Badge\n variant=\"secondary\"\n className=\"bg-green-500/10 text-green-700 dark:text-green-400 gap-1\"\n >\n <IconCheck size={12} />\n Connected\n </Badge>\n ) : (\n <Badge\n variant=\"secondary\"\n className=\"bg-amber-500/10 text-amber-700 dark:text-amber-400 gap-1\"\n >\n <IconCircleDashed size={12} />\n Connect\n </Badge>\n )}\n </div>\n <div className=\"min-w-0\">\n <div className=\"text-sm font-semibold text-foreground truncate\">\n {service.label}\n </div>\n <div className=\"font-mono text-xs text-muted-foreground/80 truncate\">\n {service.key}\n </div>\n </div>\n <div className=\"text-xs text-muted-foreground\">\n Used by {appCount} {appCount === 1 ? \"app\" : \"apps\"}\n </div>\n </button>\n <ConnectDialog service={service} open={open} onOpenChange={setOpen} />\n </>\n );\n}\n\nfunction PerAppDetailRow({ app }: { app: CatalogApp }) {\n const total = (app.integrations ?? []).length;\n const ok = (app.integrations ?? []).filter((i) => i.configured).length;\n return (\n <div className=\"flex items-center justify-between border-t px-4 py-2.5 first:border-t-0\">\n <div className=\"flex items-center gap-2 min-w-0\">\n <div\n className=\"h-5 w-5 rounded text-[10px] font-bold text-white flex items-center justify-center shrink-0\"\n style={{ backgroundColor: app.color }}\n >\n {app.appName.charAt(0).toUpperCase()}\n </div>\n <span className=\"text-sm truncate\">{app.appName}</span>\n {!app.reachable && (\n <span className=\"text-xs text-muted-foreground\">offline</span>\n )}\n </div>\n <span className=\"text-xs text-muted-foreground\">\n {total === 0 ? \"no integrations\" : `${ok}/${total}`}\n </span>\n </div>\n );\n}\n\nexport default function ConnectionsRoute() {\n const { data: catalog, isLoading } = useActionQuery(\n \"list-integrations-catalog\",\n {},\n );\n const apps = (catalog as CatalogApp[]) || [];\n\n const services = useMemo<Service[]>(() => {\n const map = new Map<string, Service>();\n for (const app of apps) {\n for (const intg of app.integrations ?? []) {\n if (!map.has(intg.key)) {\n map.set(intg.key, {\n key: intg.key,\n label: intg.label,\n apps: [],\n });\n }\n map.get(intg.key)!.apps.push({\n appId: app.appId,\n appName: app.appName,\n color: app.color,\n configured: intg.configured,\n vaultGranted: intg.vaultGranted,\n vaultSecretId: intg.vaultSecretId,\n });\n }\n }\n return Array.from(map.values()).sort((a, b) =>\n a.label.localeCompare(b.label),\n );\n }, [apps]);\n\n const available = services.filter((s) => !s.apps.some((a) => a.configured));\n const connected = services.filter((s) => s.apps.some((a) => a.configured));\n\n return (\n <DispatchShell\n title=\"Connections\"\n description=\"Connect services once. Apps that need them pick up the key automatically.\"\n >\n {isLoading && services.length === 0 && (\n <div className=\"rounded-2xl border border-dashed px-6 py-12 text-center text-sm text-muted-foreground\">\n Discovering apps and credentials…\n </div>\n )}\n\n {!isLoading && services.length === 0 && (\n <div className=\"rounded-2xl border border-dashed px-6 py-12 text-center text-sm text-muted-foreground\">\n No apps with declared integrations are reachable yet.\n </div>\n )}\n\n {available.length > 0 && (\n <section>\n <div className=\"mb-3 flex items-baseline justify-between\">\n <h2 className=\"text-sm font-medium text-foreground\">\n Available to connect\n </h2>\n <span className=\"text-xs text-muted-foreground\">\n {available.length}\n </span>\n </div>\n <div className=\"grid gap-3 sm:grid-cols-2 xl:grid-cols-3\">\n {available.map((service) => (\n <ConnectorCard key={service.key} service={service} />\n ))}\n </div>\n </section>\n )}\n\n {connected.length > 0 && (\n <section>\n <div className=\"mb-3 mt-2 flex items-baseline justify-between\">\n <h2 className=\"text-sm font-medium text-foreground\">Connected</h2>\n <span className=\"text-xs text-muted-foreground\">\n {connected.length}\n </span>\n </div>\n <div className=\"grid gap-3 sm:grid-cols-2 xl:grid-cols-3\">\n {connected.map((service) => (\n <ConnectorCard key={service.key} service={service} />\n ))}\n </div>\n </section>\n )}\n\n {apps.length > 0 && (\n <Collapsible className=\"mt-6 rounded-2xl border bg-card\">\n <CollapsibleTrigger className=\"flex w-full items-center justify-between px-4 py-3 text-sm\">\n <span className=\"flex items-center gap-2 text-muted-foreground\">\n <IconPlugConnected size={14} />\n Per-app status\n </span>\n <IconChevronRight\n size={14}\n className=\"text-muted-foreground transition group-data-[state=open]:rotate-90\"\n />\n </CollapsibleTrigger>\n <CollapsibleContent>\n <div className=\"border-t\">\n {apps.map((app) => (\n <PerAppDetailRow key={app.appId} app={app} />\n ))}\n </div>\n <div className=\"flex items-center justify-end gap-1.5 border-t px-4 py-2.5 text-xs text-muted-foreground\">\n <IconLink size={12} />\n <a href=\"/vault\" className=\"hover:underline\">\n Open vault for advanced sharing\n </a>\n </div>\n </CollapsibleContent>\n </Collapsible>\n )}\n </DispatchShell>\n );\n}\n"]}
@@ -19,7 +19,7 @@ When a user asks for something:
19
19
  - Exception: if the downstream agent reports a missing model/provider credential, do not name exact env vars, Vault keys, tokens, or secrets. Say the target app needs an LLM connection and recommend connecting Builder/managed LLM for that app; keep bring-your-own provider keys as a secondary option only if the user asks.
20
20
  - If the user asks to create, build, make, scaffold, or generate an "agent" from Dispatch chat or by tagging @agent-native in Slack, email, or Telegram, first classify the ask. If it is a simple Dispatch-native behavior like a reminder, digest, monitor, routing rule, saved instruction, or recurring workflow, create or update the recurring job/resource/destination in Dispatch. If it is a robust unique product or teammate that needs its own UI, data model, actions, integrations, or domain workflow, treat it as a new workspace app and call start-workspace-app-creation.
21
21
  - If a new-app prompt asks for access to Mail, Calendar, Analytics, or similar first-party app data/agents, keep using the existing hosted/connected app and A2A path. Do not ask Builder to scaffold those apps as children of the new app unless the user explicitly asks for a customized fork/copy.
22
- - If the user explicitly asks for a new app or workspace app, call start-workspace-app-creation with their prompt. Do not satisfy a new-app request by adding a route, page, component, or file inside apps/starter or another existing app unless the user explicitly asks to modify that existing app. If the request is too vague to classify, ask one concise follow-up. If the action returns mode "builder", reply with the Builder branch URL; Builder is responsible for creating the separate workspace app under apps/<app-id>, mounting it at /<app-id>, ensuring apps/<app-id>/package.json exists so Dispatch discovers it, using relative /<app-id> links instead of hardcoded localhost/dev ports, and preserving APP_BASE_PATH/VITE_APP_BASE_PATH via appBasePath() in the React Router client entry. There is no separate workspace app registry to edit. If it returns mode "local-agent", tell the user it is ready for the local code agent and include the returned app path/prompt summary. If it returns mode "coming-soon" or "builder-unavailable", explain the missing Builder setup and ask them to connect/configure Builder.
22
+ - If the user explicitly asks for a new app or workspace app, call start-workspace-app-creation with their prompt. Do not satisfy a new-app request by adding a route, page, component, or file inside apps/starter or another existing app unless the user explicitly asks to modify that existing app. If the request is too vague to classify, ask one concise follow-up. If the action returns mode "builder", reply with the Builder branch URL; Builder is responsible for creating the separate workspace app under apps/<app-id>, mounting it at /<app-id>, ensuring apps/<app-id>/package.json exists so Dispatch discovers it, using relative /<app-id> links instead of hardcoded localhost/dev ports, and preserving APP_BASE_PATH/VITE_APP_BASE_PATH via appBasePath() in the React Router client entry. The new app lives at the workspace root /<app-id>, NOT under /dispatch/<app-id>, /apps/<app-id>, or any other Dispatch tab — when telling the user where to find it, link to /<app-id> only. There is no separate workspace app registry to edit. If it returns mode "local-agent", tell the user it is ready for the local code agent and include the returned app path/prompt summary. If it returns mode "coming-soon" or "builder-unavailable", explain the missing Builder setup and ask them to connect/configure Builder.
23
23
  - For digests, reminders, or saved behavior, prefer recurring jobs, resources, or destinations over chat replies.
24
24
  - Keep responses concise and operational — messaging platforms have character limits.
25
25
  - Use markdown sparingly (bold and lists are fine, avoid complex formatting).
@@ -1 +1 @@
1
- {"version":3,"file":"integrations.js","sourceRoot":"","sources":["../../../src/server/plugins/integrations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AACrE,OAAO,EACL,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEzD,MAAM,kCAAkC,GAAG;;;;;;;;;;;;;;;;;;;;;4FAqBiD,CAAC;AAE7F;;;;GAIG;AACH,MAAM,0BAA0B,GAAG,KAAK,EAAE,QAAa,EAAE,EAAE;IACzD,MAAM,EAAE,YAAY,GAAG,EAAE,EAAE,GAAG,iBAAiB,EAAE,CAAC;IAClD,MAAM,cAAc,GAAG,YAAY,CAAC,YAAY,CAAC;IACjD,MAAM,YAAY,GAChB,OAAO,cAAc,KAAK,QAAQ;QAChC,CAAC,CAAC,cAAc;QAChB,CAAC,CAAC,OAAO,cAAc,KAAK,UAAU;YACpC,CAAC,CAAC,cAAc,CAAC,kCAAkC,CAAC;YACpD,CAAC,CAAC,kCAAkC,CAAC;IAE3C,MAAM,MAAM,GAAG,wBAAwB,CAAC;QACtC,KAAK,EAAE,UAAU;QACjB,OAAO,EAAE,eAAe;QACxB,YAAY,EAAE,oBAAoB;QAClC,aAAa,EAAE,qBAAqB;QACpC,YAAY;QACZ,wDAAwD;QACxD,yEAAyE;QACzE,+DAA+D;QAC/D,6EAA6E;KAC9E,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC;AAC1B,CAAC,CAAC;AAEF,eAAe,0BAA0B,CAAC","sourcesContent":["import { createIntegrationsPlugin } from \"@agent-native/core/server\";\nimport {\n beforeDispatchProcess,\n resolveDispatchOwner,\n} from \"../lib/dispatch-integrations.js\";\nimport { getDispatchConfig } from \"../index.js\";\nimport { dispatchActions } from \"../../actions/index.js\";\n\nconst DISPATCH_INTEGRATION_SYSTEM_PROMPT = `You are the central dispatch for this workspace, responding via a messaging platform integration (Slack, Telegram, email, etc.).\n\nDefault posture:\n- Treat Slack, Telegram, and email as shared entrypoints into the workspace.\n- Heavily delegate domain work to specialized agents through A2A (call-agent) when another app owns the job. Apps you can delegate to include slides (decks/presentations), analytics (data/dashboards), content (docs/articles), videos (Remotion compositions), forms (form builder), clips (screen recordings), design (visual designs), and images (brand image libraries and generated raster imagery).\n- Use list-connected-agents to see what agents are available before assuming a request must be handled locally.\n- When asked whether workspace apps expose agent cards or A2A endpoints, call list-workspace-apps with includeAgentCards=true. Without that probe, missing agent-card fields mean unchecked, not unavailable.\n- Treat first-party apps such as Mail, Calendar, Analytics, and Dispatch as existing hosted/connected neighbors available through links and A2A/default connected agents. Do not create wrapper apps, child apps, nested routes, or cloned template copies just to give a new app access to them; build only the genuinely new workflow and delegate cross-app work to those existing apps.\n- Keep durable memory and operating instructions in resources rather than ephemeral chat.\n- Reply in the originating thread unless the user explicitly asks you to send to a saved destination.\n\nWhen a user asks for something:\n- If it belongs to analytics, content, slides, videos, images, etc., delegate via call-agent — do not re-implement the domain logic in dispatch.\n- After call-agent returns an answer, RELAY IT DIRECTLY to the user with at most a one-line preface — do not rephrase, summarize, or add commentary. The downstream agent already crafted the answer; your job is delivery, not editing. This minimizes round-trips and keeps the user-visible reply fast.\n- Exception: if the downstream agent reports a missing model/provider credential, do not name exact env vars, Vault keys, tokens, or secrets. Say the target app needs an LLM connection and recommend connecting Builder/managed LLM for that app; keep bring-your-own provider keys as a secondary option only if the user asks.\n- If the user asks to create, build, make, scaffold, or generate an \"agent\" from Dispatch chat or by tagging @agent-native in Slack, email, or Telegram, first classify the ask. If it is a simple Dispatch-native behavior like a reminder, digest, monitor, routing rule, saved instruction, or recurring workflow, create or update the recurring job/resource/destination in Dispatch. If it is a robust unique product or teammate that needs its own UI, data model, actions, integrations, or domain workflow, treat it as a new workspace app and call start-workspace-app-creation.\n- If a new-app prompt asks for access to Mail, Calendar, Analytics, or similar first-party app data/agents, keep using the existing hosted/connected app and A2A path. Do not ask Builder to scaffold those apps as children of the new app unless the user explicitly asks for a customized fork/copy.\n- If the user explicitly asks for a new app or workspace app, call start-workspace-app-creation with their prompt. Do not satisfy a new-app request by adding a route, page, component, or file inside apps/starter or another existing app unless the user explicitly asks to modify that existing app. If the request is too vague to classify, ask one concise follow-up. If the action returns mode \"builder\", reply with the Builder branch URL; Builder is responsible for creating the separate workspace app under apps/<app-id>, mounting it at /<app-id>, ensuring apps/<app-id>/package.json exists so Dispatch discovers it, using relative /<app-id> links instead of hardcoded localhost/dev ports, and preserving APP_BASE_PATH/VITE_APP_BASE_PATH via appBasePath() in the React Router client entry. There is no separate workspace app registry to edit. If it returns mode \"local-agent\", tell the user it is ready for the local code agent and include the returned app path/prompt summary. If it returns mode \"coming-soon\" or \"builder-unavailable\", explain the missing Builder setup and ask them to connect/configure Builder.\n- For digests, reminders, or saved behavior, prefer recurring jobs, resources, or destinations over chat replies.\n- Keep responses concise and operational — messaging platforms have character limits.\n- Use markdown sparingly (bold and lists are fine, avoid complex formatting).\n- If a task requires many steps, summarize what you did rather than streaming every detail.`;\n\n/**\n * Defer plugin construction until the Nitro plugin actually fires so the\n * config-aware system prompt resolves AFTER `setupDispatch(config)` has\n * stamped the active config (plugin module load order is not guaranteed).\n */\nconst dispatchIntegrationsPlugin = async (nitroApp: any) => {\n const { integrations = {} } = getDispatchConfig();\n const promptOverride = integrations.systemPrompt;\n const systemPrompt =\n typeof promptOverride === \"string\"\n ? promptOverride\n : typeof promptOverride === \"function\"\n ? promptOverride(DISPATCH_INTEGRATION_SYSTEM_PROMPT)\n : DISPATCH_INTEGRATION_SYSTEM_PROMPT;\n\n const plugin = createIntegrationsPlugin({\n appId: \"dispatch\",\n actions: dispatchActions,\n resolveOwner: resolveDispatchOwner,\n beforeProcess: beforeDispatchProcess,\n systemPrompt,\n // Inherit the framework default (claude-sonnet-4-6 from\n // packages/core/src/integrations/plugin.ts). Haiku was tried for latency\n // but hallucinated URLs/IDs after delegated call-agent results\n // (e.g. inventing `https://slides.workspace.com/deck/builder-io-deck-2024`).\n });\n\n return plugin(nitroApp);\n};\n\nexport default dispatchIntegrationsPlugin;\n"]}
1
+ {"version":3,"file":"integrations.js","sourceRoot":"","sources":["../../../src/server/plugins/integrations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AACrE,OAAO,EACL,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEzD,MAAM,kCAAkC,GAAG;;;;;;;;;;;;;;;;;;;;;4FAqBiD,CAAC;AAE7F;;;;GAIG;AACH,MAAM,0BAA0B,GAAG,KAAK,EAAE,QAAa,EAAE,EAAE;IACzD,MAAM,EAAE,YAAY,GAAG,EAAE,EAAE,GAAG,iBAAiB,EAAE,CAAC;IAClD,MAAM,cAAc,GAAG,YAAY,CAAC,YAAY,CAAC;IACjD,MAAM,YAAY,GAChB,OAAO,cAAc,KAAK,QAAQ;QAChC,CAAC,CAAC,cAAc;QAChB,CAAC,CAAC,OAAO,cAAc,KAAK,UAAU;YACpC,CAAC,CAAC,cAAc,CAAC,kCAAkC,CAAC;YACpD,CAAC,CAAC,kCAAkC,CAAC;IAE3C,MAAM,MAAM,GAAG,wBAAwB,CAAC;QACtC,KAAK,EAAE,UAAU;QACjB,OAAO,EAAE,eAAe;QACxB,YAAY,EAAE,oBAAoB;QAClC,aAAa,EAAE,qBAAqB;QACpC,YAAY;QACZ,wDAAwD;QACxD,yEAAyE;QACzE,+DAA+D;QAC/D,6EAA6E;KAC9E,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC;AAC1B,CAAC,CAAC;AAEF,eAAe,0BAA0B,CAAC","sourcesContent":["import { createIntegrationsPlugin } from \"@agent-native/core/server\";\nimport {\n beforeDispatchProcess,\n resolveDispatchOwner,\n} from \"../lib/dispatch-integrations.js\";\nimport { getDispatchConfig } from \"../index.js\";\nimport { dispatchActions } from \"../../actions/index.js\";\n\nconst DISPATCH_INTEGRATION_SYSTEM_PROMPT = `You are the central dispatch for this workspace, responding via a messaging platform integration (Slack, Telegram, email, etc.).\n\nDefault posture:\n- Treat Slack, Telegram, and email as shared entrypoints into the workspace.\n- Heavily delegate domain work to specialized agents through A2A (call-agent) when another app owns the job. Apps you can delegate to include slides (decks/presentations), analytics (data/dashboards), content (docs/articles), videos (Remotion compositions), forms (form builder), clips (screen recordings), design (visual designs), and images (brand image libraries and generated raster imagery).\n- Use list-connected-agents to see what agents are available before assuming a request must be handled locally.\n- When asked whether workspace apps expose agent cards or A2A endpoints, call list-workspace-apps with includeAgentCards=true. Without that probe, missing agent-card fields mean unchecked, not unavailable.\n- Treat first-party apps such as Mail, Calendar, Analytics, and Dispatch as existing hosted/connected neighbors available through links and A2A/default connected agents. Do not create wrapper apps, child apps, nested routes, or cloned template copies just to give a new app access to them; build only the genuinely new workflow and delegate cross-app work to those existing apps.\n- Keep durable memory and operating instructions in resources rather than ephemeral chat.\n- Reply in the originating thread unless the user explicitly asks you to send to a saved destination.\n\nWhen a user asks for something:\n- If it belongs to analytics, content, slides, videos, images, etc., delegate via call-agent — do not re-implement the domain logic in dispatch.\n- After call-agent returns an answer, RELAY IT DIRECTLY to the user with at most a one-line preface — do not rephrase, summarize, or add commentary. The downstream agent already crafted the answer; your job is delivery, not editing. This minimizes round-trips and keeps the user-visible reply fast.\n- Exception: if the downstream agent reports a missing model/provider credential, do not name exact env vars, Vault keys, tokens, or secrets. Say the target app needs an LLM connection and recommend connecting Builder/managed LLM for that app; keep bring-your-own provider keys as a secondary option only if the user asks.\n- If the user asks to create, build, make, scaffold, or generate an \"agent\" from Dispatch chat or by tagging @agent-native in Slack, email, or Telegram, first classify the ask. If it is a simple Dispatch-native behavior like a reminder, digest, monitor, routing rule, saved instruction, or recurring workflow, create or update the recurring job/resource/destination in Dispatch. If it is a robust unique product or teammate that needs its own UI, data model, actions, integrations, or domain workflow, treat it as a new workspace app and call start-workspace-app-creation.\n- If a new-app prompt asks for access to Mail, Calendar, Analytics, or similar first-party app data/agents, keep using the existing hosted/connected app and A2A path. Do not ask Builder to scaffold those apps as children of the new app unless the user explicitly asks for a customized fork/copy.\n- If the user explicitly asks for a new app or workspace app, call start-workspace-app-creation with their prompt. Do not satisfy a new-app request by adding a route, page, component, or file inside apps/starter or another existing app unless the user explicitly asks to modify that existing app. If the request is too vague to classify, ask one concise follow-up. If the action returns mode \"builder\", reply with the Builder branch URL; Builder is responsible for creating the separate workspace app under apps/<app-id>, mounting it at /<app-id>, ensuring apps/<app-id>/package.json exists so Dispatch discovers it, using relative /<app-id> links instead of hardcoded localhost/dev ports, and preserving APP_BASE_PATH/VITE_APP_BASE_PATH via appBasePath() in the React Router client entry. The new app lives at the workspace root /<app-id>, NOT under /dispatch/<app-id>, /apps/<app-id>, or any other Dispatch tab — when telling the user where to find it, link to /<app-id> only. There is no separate workspace app registry to edit. If it returns mode \"local-agent\", tell the user it is ready for the local code agent and include the returned app path/prompt summary. If it returns mode \"coming-soon\" or \"builder-unavailable\", explain the missing Builder setup and ask them to connect/configure Builder.\n- For digests, reminders, or saved behavior, prefer recurring jobs, resources, or destinations over chat replies.\n- Keep responses concise and operational — messaging platforms have character limits.\n- Use markdown sparingly (bold and lists are fine, avoid complex formatting).\n- If a task requires many steps, summarize what you did rather than streaming every detail.`;\n\n/**\n * Defer plugin construction until the Nitro plugin actually fires so the\n * config-aware system prompt resolves AFTER `setupDispatch(config)` has\n * stamped the active config (plugin module load order is not guaranteed).\n */\nconst dispatchIntegrationsPlugin = async (nitroApp: any) => {\n const { integrations = {} } = getDispatchConfig();\n const promptOverride = integrations.systemPrompt;\n const systemPrompt =\n typeof promptOverride === \"string\"\n ? promptOverride\n : typeof promptOverride === \"function\"\n ? promptOverride(DISPATCH_INTEGRATION_SYSTEM_PROMPT)\n : DISPATCH_INTEGRATION_SYSTEM_PROMPT;\n\n const plugin = createIntegrationsPlugin({\n appId: \"dispatch\",\n actions: dispatchActions,\n resolveOwner: resolveDispatchOwner,\n beforeProcess: beforeDispatchProcess,\n systemPrompt,\n // Inherit the framework default (claude-sonnet-4-6 from\n // packages/core/src/integrations/plugin.ts). Haiku was tried for latency\n // but hallucinated URLs/IDs after delegated call-agent results\n // (e.g. inventing `https://slides.workspace.com/deck/builder-io-deck-2024`).\n });\n\n return plugin(nitroApp);\n};\n\nexport default dispatchIntegrationsPlugin;\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/dispatch",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "description": "Dispatch — workspace control plane for agent-native apps. Vault, integrations, destinations, scheduled jobs, and cross-app delegation, shipped as a single drop-in package.",
6
6
  "license": "MIT",
@@ -96,7 +96,7 @@
96
96
  "typescript": "^6.0.3",
97
97
  "vite": "8.0.3",
98
98
  "vitest": "^4.1.5",
99
- "@agent-native/core": "0.13.0"
99
+ "@agent-native/core": "0.14.0"
100
100
  },
101
101
  "scripts": {
102
102
  "build": "tsc && tsc-alias --resolve-full-paths",
@@ -124,7 +124,7 @@ function buildAppCreationPrompt(input: {
124
124
  `- Update the app manifest/package/deploy metadata needed by the existing workspace deployment model.`,
125
125
  `- Ensure the React Router client entry preserves APP_BASE_PATH/VITE_APP_BASE_PATH via appBasePath() so /${input.appId} hydrates correctly.`,
126
126
  `- Verify the app's agent card/A2A metadata is ready so Dispatch can discover and delegate to the app after deployment.`,
127
- `When it is ready, start or update the workspace dev server and navigate the user to /${input.appId}.`,
127
+ `When it is ready, start or update the workspace dev server and navigate the user to the absolute path /${input.appId} on the workspace origin. Do not prefix with /dispatch/, /apps/, /workspace/, or any other Dispatch tab — the new app is mounted at the workspace root, not under Dispatch. If you have a navigate tool available, pass /${input.appId} verbatim; if you only have a window.location-style escape hatch, set it to /${input.appId}.`,
128
128
  ].join("\n");
129
129
  }
130
130
 
@@ -170,9 +170,9 @@ const OPERATIONS_NAV_ITEMS = [
170
170
  const EMPTY_NAV_ITEMS: readonly DispatchNavItem[] = [];
171
171
 
172
172
  const SIDEBAR_SUGGESTIONS = [
173
- "Create a new app",
174
- "Grant a key to an app",
175
- "Check integration health",
173
+ "Build a workspace app for X",
174
+ "Route Slack mentions to my analytics app",
175
+ "Grant my OpenAI key to this app",
176
176
  ];
177
177
 
178
178
  const CHROMELESS_PATHS = ["/approval"];
@@ -50,4 +50,9 @@ export const dispatchRoutes: RouteConfig = [
50
50
  route("team", "./pages/team.js"),
51
51
  route("extensions", "./pages/extensions._index.js"),
52
52
  route("extensions/:id", "./pages/extensions.$id.js"),
53
+ // Catch-all for /:appId — bounces /dispatch/<appId> to /<appId> when the
54
+ // segment names a workspace app sibling (e.g. Builder.io routing a "go to
55
+ // /todo" call through Dispatch's mount). Declared last so React Router 7's
56
+ // specificity ranking still matches explicit static routes above first.
57
+ route(":appId", "./pages/$appId.js"),
53
58
  ];
@@ -0,0 +1,153 @@
1
+ import { useEffect, useMemo } from "react";
2
+ import {
3
+ Link,
4
+ redirect,
5
+ useParams,
6
+ type LoaderFunctionArgs,
7
+ } from "react-router";
8
+ import { useActionQuery, appPath } from "@agent-native/core/client";
9
+ import { loadWorkspaceAppsManifest } from "@agent-native/core/server/agent-discovery";
10
+ import {
11
+ IconArrowLeft,
12
+ IconArrowUpRight,
13
+ IconClockHour4,
14
+ } from "@tabler/icons-react";
15
+ import { DispatchShell } from "@/components/dispatch-shell";
16
+ import { Spinner } from "@/components/ui/spinner";
17
+ import { Badge } from "@/components/ui/badge";
18
+ import { Button } from "@/components/ui/button";
19
+ import {
20
+ workspaceAppHref,
21
+ type WorkspaceAppSummary,
22
+ } from "@/lib/workspace-apps";
23
+
24
+ export function meta() {
25
+ return [{ title: "Workspace app - Dispatch" }];
26
+ }
27
+
28
+ /**
29
+ * Catch-all for `/dispatch/<segment>` paths that don't match an explicit
30
+ * Dispatch route. When `<segment>` is the id of a workspace app sibling
31
+ * (e.g. `/dispatch/todo` after Builder.io routes a "navigate to /todo"
32
+ * call through Dispatch's mount point), bounce to the absolute `/<appId>`
33
+ * so the user lands on the actual app instead of a 404 inside Dispatch.
34
+ *
35
+ * Server-side redirect: we resolve the workspace app manifest via the
36
+ * shared `loadWorkspaceAppsManifest()` helper, which checks the
37
+ * `AGENT_NATIVE_WORKSPACE_APPS_JSON` env var, then the
38
+ * `.agent-native/workspace-apps.json` file written by `workspace-deploy.ts`,
39
+ * then a live filesystem scan of `apps/` for local dev. We then throw
40
+ * `redirect("/<appId>")`. React Router 7 does not prepend the basename to
41
+ * absolute paths returned from a loader, so the redirect escapes Dispatch's
42
+ * `/dispatch` mount cleanly.
43
+ *
44
+ * Why a catch-all instead of fixing the agent prompt: Builder.io currently
45
+ * resolves "navigate to /todo" relative to Dispatch's mount, sending the
46
+ * user to /dispatch/todo. The same wrong path then gets captured as the
47
+ * OAuth callbackURL, so Google sign-in completes back at /dispatch/todo
48
+ * and looks broken. This route fixes both the post-creation navigation
49
+ * and the OAuth round-trip from a single place.
50
+ */
51
+ export function loader({ params }: LoaderFunctionArgs) {
52
+ const appId = params.appId;
53
+ if (!appId) return null;
54
+ const apps = loadWorkspaceAppsManifest();
55
+ if (!apps) return null;
56
+ const app = apps.find((entry) => entry?.id === appId);
57
+ const target =
58
+ app?.path && app.path.startsWith("/") ? app.path : app ? `/${appId}` : null;
59
+ if (target) throw redirect(target);
60
+ return null;
61
+ }
62
+
63
+ export default function WorkspaceAppCatchAllRoute() {
64
+ const { appId } = useParams();
65
+ const { data: apps = [], isLoading } = useActionQuery(
66
+ "list-workspace-apps",
67
+ { includeAgentCards: false },
68
+ { refetchInterval: 2_000 },
69
+ );
70
+ const app = useMemo(
71
+ () =>
72
+ (apps as WorkspaceAppSummary[]).find((item) => item.id === appId) ?? null,
73
+ [appId, apps],
74
+ );
75
+ const href = app ? workspaceAppHref(app) : null;
76
+
77
+ useEffect(() => {
78
+ if (!app || app.status === "pending" || !href) return;
79
+ window.location.assign(href);
80
+ }, [app, href]);
81
+
82
+ if ((isLoading && !app) || (app && app.status !== "pending" && href)) {
83
+ return (
84
+ <div className="flex h-screen w-full items-center justify-center">
85
+ <Spinner className="size-8" />
86
+ </div>
87
+ );
88
+ }
89
+
90
+ return (
91
+ <DispatchShell
92
+ title={app?.name || "Page not found"}
93
+ description="This route is not in the workspace app list yet."
94
+ >
95
+ <div className="max-w-2xl rounded-lg border bg-card p-5">
96
+ <Button asChild size="sm" variant="ghost" className="-ml-2 mb-4">
97
+ <Link to={appPath("/overview")}>
98
+ <IconArrowLeft size={15} className="mr-1.5" />
99
+ Overview
100
+ </Link>
101
+ </Button>
102
+
103
+ {app?.status === "pending" ? (
104
+ <div className="space-y-4">
105
+ <div className="flex flex-wrap items-center gap-2">
106
+ <h2 className="text-base font-semibold text-foreground">
107
+ {app.name}
108
+ </h2>
109
+ <Badge
110
+ variant="outline"
111
+ className="gap-1 border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300"
112
+ >
113
+ <IconClockHour4 size={12} />
114
+ Building
115
+ </Badge>
116
+ </div>
117
+ <p className="text-sm text-muted-foreground">
118
+ This app is being created. It will be available at{" "}
119
+ <span className="font-mono text-foreground">{app.path}</span>{" "}
120
+ after its branch is merged and the workspace deploy finishes.
121
+ </p>
122
+ {app.branchName ? (
123
+ <p className="text-xs text-muted-foreground">
124
+ Branch: {app.branchName}
125
+ </p>
126
+ ) : null}
127
+ {app.builderUrl ? (
128
+ <Button asChild>
129
+ <a href={app.builderUrl} target="_blank" rel="noreferrer">
130
+ Open Builder branch
131
+ <IconArrowUpRight size={15} className="ml-1.5" />
132
+ </a>
133
+ </Button>
134
+ ) : null}
135
+ </div>
136
+ ) : (
137
+ <div className="space-y-3">
138
+ <h2 className="text-base font-semibold text-foreground">
139
+ Page not found
140
+ </h2>
141
+ <p className="text-sm text-muted-foreground">
142
+ <span className="font-mono text-foreground">/{appId}</span> isn't
143
+ a Dispatch tab or a workspace app in this workspace.
144
+ </p>
145
+ <Button asChild>
146
+ <Link to={appPath("/apps")}>Browse apps</Link>
147
+ </Button>
148
+ </div>
149
+ )}
150
+ </div>
151
+ </DispatchShell>
152
+ );
153
+ }