@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.
- package/dist/components/create-app-popover.js +1 -1
- package/dist/components/create-app-popover.js.map +1 -1
- package/dist/components/layout/Layout.js +3 -3
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/index.js +5 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/pages/$appId.d.ts +30 -0
- package/dist/routes/pages/$appId.d.ts.map +1 -0
- package/dist/routes/pages/$appId.js +66 -0
- package/dist/routes/pages/$appId.js.map +1 -0
- package/dist/routes/pages/integrations.d.ts +1 -1
- package/dist/routes/pages/integrations.d.ts.map +1 -1
- package/dist/routes/pages/integrations.js +131 -30
- package/dist/routes/pages/integrations.js.map +1 -1
- package/dist/server/plugins/integrations.js +1 -1
- package/dist/server/plugins/integrations.js.map +1 -1
- package/package.json +2 -2
- package/src/components/create-app-popover.tsx +1 -1
- package/src/components/layout/Layout.tsx +3 -3
- package/src/routes/index.ts +5 -0
- package/src/routes/pages/$appId.tsx +153 -0
- package/src/routes/pages/integrations.tsx +348 -215
- package/src/server/plugins/integrations.ts +1 -1
|
@@ -1,277 +1,410 @@
|
|
|
1
|
+
import { useMemo, useState } from "react";
|
|
1
2
|
import { useActionMutation, useActionQuery } from "@agent-native/core/client";
|
|
3
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
2
4
|
import { toast } from "sonner";
|
|
3
5
|
import {
|
|
4
6
|
IconCheck,
|
|
7
|
+
IconChevronRight,
|
|
5
8
|
IconCircleDashed,
|
|
6
9
|
IconKey,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
IconWifi,
|
|
10
|
-
IconWifiOff,
|
|
10
|
+
IconLink,
|
|
11
|
+
IconPlugConnected,
|
|
11
12
|
} from "@tabler/icons-react";
|
|
12
13
|
import { DispatchShell } from "@/components/dispatch-shell";
|
|
13
14
|
import { Badge } from "@/components/ui/badge";
|
|
14
15
|
import { Button } from "@/components/ui/button";
|
|
15
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
Collapsible,
|
|
18
|
+
CollapsibleContent,
|
|
19
|
+
CollapsibleTrigger,
|
|
20
|
+
} from "@/components/ui/collapsible";
|
|
21
|
+
import {
|
|
22
|
+
Dialog,
|
|
23
|
+
DialogContent,
|
|
24
|
+
DialogDescription,
|
|
25
|
+
DialogFooter,
|
|
26
|
+
DialogHeader,
|
|
27
|
+
DialogTitle,
|
|
28
|
+
} from "@/components/ui/dialog";
|
|
29
|
+
import { Input } from "@/components/ui/input";
|
|
30
|
+
import { Label } from "@/components/ui/label";
|
|
16
31
|
|
|
17
32
|
export function meta() {
|
|
18
|
-
return [{ title: "
|
|
33
|
+
return [{ title: "Connections — Dispatch" }];
|
|
19
34
|
}
|
|
20
35
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
36
|
+
interface AppRef {
|
|
37
|
+
appId: string;
|
|
38
|
+
appName: string;
|
|
39
|
+
color: string;
|
|
25
40
|
configured: boolean;
|
|
26
41
|
vaultGranted: boolean;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
42
|
+
vaultSecretId?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface Service {
|
|
46
|
+
/** Credential key shared across apps (e.g. `OPENAI_API_KEY`). */
|
|
47
|
+
key: string;
|
|
48
|
+
/** Human label from the first app that declares it (`"OpenAI"`, `"Stripe"`). */
|
|
49
|
+
label: string;
|
|
50
|
+
/** Apps in the workspace that declare this credential. */
|
|
51
|
+
apps: AppRef[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface CatalogApp {
|
|
55
|
+
appId: string;
|
|
56
|
+
appName: string;
|
|
57
|
+
color: string;
|
|
58
|
+
url: string;
|
|
59
|
+
reachable: boolean;
|
|
60
|
+
integrations?: Array<{
|
|
61
|
+
key: string;
|
|
62
|
+
label: string;
|
|
63
|
+
required: boolean;
|
|
64
|
+
configured: boolean;
|
|
65
|
+
vaultGranted: boolean;
|
|
66
|
+
vaultSecretId?: string;
|
|
67
|
+
}>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function inferProviderFromKey(key: string, label: string): string {
|
|
71
|
+
const haystack = `${key} ${label}`.toLowerCase();
|
|
72
|
+
for (const provider of [
|
|
73
|
+
"google",
|
|
74
|
+
"slack",
|
|
75
|
+
"sendgrid",
|
|
76
|
+
"github",
|
|
77
|
+
"stripe",
|
|
78
|
+
"hubspot",
|
|
79
|
+
"jira",
|
|
80
|
+
"bigquery",
|
|
81
|
+
"anthropic",
|
|
82
|
+
"openai",
|
|
83
|
+
]) {
|
|
84
|
+
if (haystack.includes(provider)) return provider;
|
|
60
85
|
}
|
|
61
|
-
return
|
|
62
|
-
<Badge
|
|
63
|
-
variant="secondary"
|
|
64
|
-
className="bg-amber-500/10 text-amber-700 dark:text-amber-400"
|
|
65
|
-
>
|
|
66
|
-
<IconCircleDashed size={12} className="mr-1" />
|
|
67
|
-
Missing
|
|
68
|
-
</Badge>
|
|
69
|
-
);
|
|
86
|
+
return "other";
|
|
70
87
|
}
|
|
71
88
|
|
|
72
|
-
function
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
89
|
+
function ConnectDialog({
|
|
90
|
+
service,
|
|
91
|
+
open,
|
|
92
|
+
onOpenChange,
|
|
93
|
+
}: {
|
|
94
|
+
service: Service;
|
|
95
|
+
open: boolean;
|
|
96
|
+
onOpenChange: (next: boolean) => void;
|
|
97
|
+
}) {
|
|
98
|
+
const [value, setValue] = useState("");
|
|
99
|
+
const qc = useQueryClient();
|
|
100
|
+
|
|
101
|
+
const createSecret = useActionMutation("create-vault-secret", {});
|
|
102
|
+
const createGrant = useActionMutation("create-vault-grant", {});
|
|
103
|
+
const syncToApp = useActionMutation("sync-vault-to-app", {});
|
|
104
|
+
|
|
105
|
+
function reset() {
|
|
106
|
+
setValue("");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function handleSave() {
|
|
110
|
+
const trimmed = value.trim();
|
|
111
|
+
if (!trimmed) {
|
|
112
|
+
toast.error("Enter a value to save");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
// 1. Create the secret (or get the existing one — server treats key as
|
|
117
|
+
// the unique identifier). The server returns { secret: { id, ... } }.
|
|
118
|
+
const created = await createSecret.mutateAsync({
|
|
119
|
+
credentialKey: service.key,
|
|
120
|
+
name: service.label,
|
|
121
|
+
value: trimmed,
|
|
122
|
+
provider: inferProviderFromKey(service.key, service.label),
|
|
123
|
+
});
|
|
124
|
+
const secretId =
|
|
125
|
+
(created as { secret?: { id?: string } })?.secret?.id ??
|
|
126
|
+
(created as { id?: string })?.id;
|
|
127
|
+
if (!secretId) {
|
|
128
|
+
throw new Error("Secret created but id missing");
|
|
129
|
+
}
|
|
78
130
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
131
|
+
// 2. Grant + sync to every app that declared this credential.
|
|
132
|
+
const targets = service.apps.filter((a) => !a.vaultGranted);
|
|
133
|
+
for (const app of targets) {
|
|
134
|
+
try {
|
|
135
|
+
await createGrant.mutateAsync({
|
|
136
|
+
secretId,
|
|
137
|
+
appId: app.appId,
|
|
138
|
+
});
|
|
139
|
+
} catch (err) {
|
|
140
|
+
console.warn(`grant to ${app.appId} failed`, err);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
for (const app of service.apps) {
|
|
144
|
+
try {
|
|
145
|
+
await syncToApp.mutateAsync({ appId: app.appId });
|
|
146
|
+
} catch (err) {
|
|
147
|
+
console.warn(`sync to ${app.appId} failed`, err);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
qc.invalidateQueries({
|
|
152
|
+
queryKey: ["action", "list-integrations-catalog"],
|
|
153
|
+
});
|
|
154
|
+
toast.success(`Connected ${service.label}`);
|
|
155
|
+
onOpenChange(false);
|
|
156
|
+
reset();
|
|
157
|
+
} catch (err: any) {
|
|
158
|
+
toast.error(err?.message ?? "Failed to save credential");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const pending =
|
|
163
|
+
createSecret.isPending || createGrant.isPending || syncToApp.isPending;
|
|
83
164
|
|
|
84
165
|
return (
|
|
85
|
-
<
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
166
|
+
<Dialog
|
|
167
|
+
open={open}
|
|
168
|
+
onOpenChange={(next) => {
|
|
169
|
+
if (!next) reset();
|
|
170
|
+
onOpenChange(next);
|
|
171
|
+
}}
|
|
172
|
+
>
|
|
173
|
+
<DialogContent>
|
|
174
|
+
<DialogHeader>
|
|
175
|
+
<DialogTitle>Connect {service.label}</DialogTitle>
|
|
176
|
+
<DialogDescription>
|
|
177
|
+
Used by{" "}
|
|
178
|
+
{service.apps.length === 1
|
|
179
|
+
? service.apps[0].appName
|
|
180
|
+
: `${service.apps.length} apps`}
|
|
181
|
+
. Saved to the workspace vault and synced to every app that needs
|
|
182
|
+
it.
|
|
183
|
+
</DialogDescription>
|
|
184
|
+
</DialogHeader>
|
|
185
|
+
<div className="space-y-3">
|
|
186
|
+
<div>
|
|
187
|
+
<Label className="text-xs text-muted-foreground">Key</Label>
|
|
188
|
+
<div className="font-mono text-sm">{service.key}</div>
|
|
93
189
|
</div>
|
|
94
190
|
<div>
|
|
95
|
-
<
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
<div className="text-xs text-muted-foreground">{app.appId}</div>
|
|
191
|
+
<Label htmlFor="connector-value">Value</Label>
|
|
192
|
+
<Input
|
|
193
|
+
id="connector-value"
|
|
194
|
+
type="password"
|
|
195
|
+
autoComplete="off"
|
|
196
|
+
value={value}
|
|
197
|
+
onChange={(e) => setValue(e.target.value)}
|
|
198
|
+
placeholder={`Paste your ${service.label} key…`}
|
|
199
|
+
autoFocus
|
|
200
|
+
/>
|
|
106
201
|
</div>
|
|
107
202
|
</div>
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
</
|
|
120
|
-
</
|
|
203
|
+
<DialogFooter>
|
|
204
|
+
<Button
|
|
205
|
+
variant="outline"
|
|
206
|
+
onClick={() => onOpenChange(false)}
|
|
207
|
+
disabled={pending}
|
|
208
|
+
>
|
|
209
|
+
Cancel
|
|
210
|
+
</Button>
|
|
211
|
+
<Button onClick={handleSave} disabled={pending || !value.trim()}>
|
|
212
|
+
{pending ? "Saving…" : "Connect"}
|
|
213
|
+
</Button>
|
|
214
|
+
</DialogFooter>
|
|
215
|
+
</DialogContent>
|
|
216
|
+
</Dialog>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
121
219
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
<span>
|
|
127
|
-
{configuredCount}/{total} configured
|
|
128
|
-
</span>
|
|
129
|
-
<span>{coverage}%</span>
|
|
130
|
-
</div>
|
|
131
|
-
<Progress value={coverage} className="mt-2 h-1.5" />
|
|
220
|
+
function ConnectorCard({ service }: { service: Service }) {
|
|
221
|
+
const [open, setOpen] = useState(false);
|
|
222
|
+
const isConnected = service.apps.some((a) => a.configured);
|
|
223
|
+
const appCount = service.apps.length;
|
|
132
224
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
</span>
|
|
144
|
-
{integration.required && (
|
|
145
|
-
<span className="text-xs text-red-500">required</span>
|
|
146
|
-
)}
|
|
147
|
-
</div>
|
|
148
|
-
<div className="font-mono text-xs text-muted-foreground">
|
|
149
|
-
{integration.key}
|
|
150
|
-
</div>
|
|
151
|
-
</div>
|
|
152
|
-
<StatusBadge
|
|
153
|
-
configured={integration.configured}
|
|
154
|
-
vaultGranted={integration.vaultGranted}
|
|
155
|
-
/>
|
|
156
|
-
</div>
|
|
157
|
-
))}
|
|
158
|
-
</div>
|
|
159
|
-
</>
|
|
160
|
-
) : app.reachable ? (
|
|
161
|
-
<div className="py-4 text-center text-sm text-muted-foreground">
|
|
162
|
-
No declared integrations.
|
|
225
|
+
return (
|
|
226
|
+
<>
|
|
227
|
+
<button
|
|
228
|
+
type="button"
|
|
229
|
+
onClick={() => setOpen(true)}
|
|
230
|
+
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"
|
|
231
|
+
>
|
|
232
|
+
<div className="flex w-full items-start justify-between gap-2">
|
|
233
|
+
<div className="flex h-9 w-9 items-center justify-center rounded-xl bg-muted">
|
|
234
|
+
<IconKey size={16} className="text-muted-foreground" />
|
|
163
235
|
</div>
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
236
|
+
{isConnected ? (
|
|
237
|
+
<Badge
|
|
238
|
+
variant="secondary"
|
|
239
|
+
className="bg-green-500/10 text-green-700 dark:text-green-400 gap-1"
|
|
240
|
+
>
|
|
241
|
+
<IconCheck size={12} />
|
|
242
|
+
Connected
|
|
243
|
+
</Badge>
|
|
244
|
+
) : (
|
|
245
|
+
<Badge
|
|
246
|
+
variant="secondary"
|
|
247
|
+
className="bg-amber-500/10 text-amber-700 dark:text-amber-400 gap-1"
|
|
248
|
+
>
|
|
249
|
+
<IconCircleDashed size={12} />
|
|
250
|
+
Connect
|
|
251
|
+
</Badge>
|
|
252
|
+
)}
|
|
253
|
+
</div>
|
|
254
|
+
<div className="min-w-0">
|
|
255
|
+
<div className="text-sm font-semibold text-foreground truncate">
|
|
256
|
+
{service.label}
|
|
257
|
+
</div>
|
|
258
|
+
<div className="font-mono text-xs text-muted-foreground/80 truncate">
|
|
259
|
+
{service.key}
|
|
167
260
|
</div>
|
|
261
|
+
</div>
|
|
262
|
+
<div className="text-xs text-muted-foreground">
|
|
263
|
+
Used by {appCount} {appCount === 1 ? "app" : "apps"}
|
|
264
|
+
</div>
|
|
265
|
+
</button>
|
|
266
|
+
<ConnectDialog service={service} open={open} onOpenChange={setOpen} />
|
|
267
|
+
</>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function PerAppDetailRow({ app }: { app: CatalogApp }) {
|
|
272
|
+
const total = (app.integrations ?? []).length;
|
|
273
|
+
const ok = (app.integrations ?? []).filter((i) => i.configured).length;
|
|
274
|
+
return (
|
|
275
|
+
<div className="flex items-center justify-between border-t px-4 py-2.5 first:border-t-0">
|
|
276
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
277
|
+
<div
|
|
278
|
+
className="h-5 w-5 rounded text-[10px] font-bold text-white flex items-center justify-center shrink-0"
|
|
279
|
+
style={{ backgroundColor: app.color }}
|
|
280
|
+
>
|
|
281
|
+
{app.appName.charAt(0).toUpperCase()}
|
|
282
|
+
</div>
|
|
283
|
+
<span className="text-sm truncate">{app.appName}</span>
|
|
284
|
+
{!app.reachable && (
|
|
285
|
+
<span className="text-xs text-muted-foreground">offline</span>
|
|
168
286
|
)}
|
|
169
287
|
</div>
|
|
288
|
+
<span className="text-xs text-muted-foreground">
|
|
289
|
+
{total === 0 ? "no integrations" : `${ok}/${total}`}
|
|
290
|
+
</span>
|
|
170
291
|
</div>
|
|
171
292
|
);
|
|
172
293
|
}
|
|
173
294
|
|
|
174
|
-
export default function
|
|
295
|
+
export default function ConnectionsRoute() {
|
|
175
296
|
const { data: catalog, isLoading } = useActionQuery(
|
|
176
297
|
"list-integrations-catalog",
|
|
177
298
|
{},
|
|
178
299
|
);
|
|
300
|
+
const apps = (catalog as CatalogApp[]) || [];
|
|
179
301
|
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
302
|
+
const services = useMemo<Service[]>(() => {
|
|
303
|
+
const map = new Map<string, Service>();
|
|
304
|
+
for (const app of apps) {
|
|
305
|
+
for (const intg of app.integrations ?? []) {
|
|
306
|
+
if (!map.has(intg.key)) {
|
|
307
|
+
map.set(intg.key, {
|
|
308
|
+
key: intg.key,
|
|
309
|
+
label: intg.label,
|
|
310
|
+
apps: [],
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
map.get(intg.key)!.apps.push({
|
|
314
|
+
appId: app.appId,
|
|
315
|
+
appName: app.appName,
|
|
316
|
+
color: app.color,
|
|
317
|
+
configured: intg.configured,
|
|
318
|
+
vaultGranted: intg.vaultGranted,
|
|
319
|
+
vaultSecretId: intg.vaultSecretId,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return Array.from(map.values()).sort((a, b) =>
|
|
324
|
+
a.label.localeCompare(b.label),
|
|
325
|
+
);
|
|
326
|
+
}, [apps]);
|
|
183
327
|
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
0,
|
|
187
|
-
);
|
|
188
|
-
const configuredIntegrations = apps.reduce(
|
|
189
|
-
(sum: number, a: any) =>
|
|
190
|
-
sum + (a.integrations?.filter((i: any) => i.configured)?.length || 0),
|
|
191
|
-
0,
|
|
192
|
-
);
|
|
328
|
+
const available = services.filter((s) => !s.apps.some((a) => a.configured));
|
|
329
|
+
const connected = services.filter((s) => s.apps.some((a) => a.configured));
|
|
193
330
|
|
|
194
331
|
return (
|
|
195
332
|
<DispatchShell
|
|
196
|
-
title="
|
|
197
|
-
description="
|
|
333
|
+
title="Connections"
|
|
334
|
+
description="Connect services once. Apps that need them pick up the key automatically."
|
|
198
335
|
>
|
|
199
|
-
{
|
|
200
|
-
<div className="
|
|
201
|
-
|
|
202
|
-
<div className="text-sm font-medium text-muted-foreground">
|
|
203
|
-
Apps discovered
|
|
204
|
-
</div>
|
|
205
|
-
<div className="mt-2 text-3xl font-semibold text-foreground">
|
|
206
|
-
{apps.length}
|
|
207
|
-
</div>
|
|
208
|
-
<div className="mt-1 text-xs text-muted-foreground">
|
|
209
|
-
{reachableApps.length} reachable
|
|
210
|
-
</div>
|
|
211
|
-
</div>
|
|
212
|
-
<div className="rounded-2xl border bg-card p-5">
|
|
213
|
-
<div className="text-sm font-medium text-muted-foreground">
|
|
214
|
-
Total integrations
|
|
215
|
-
</div>
|
|
216
|
-
<div className="mt-2 text-3xl font-semibold text-foreground">
|
|
217
|
-
{totalIntegrations}
|
|
218
|
-
</div>
|
|
219
|
-
<div className="mt-1 text-xs text-muted-foreground">
|
|
220
|
-
across all apps
|
|
221
|
-
</div>
|
|
222
|
-
</div>
|
|
223
|
-
<div className="rounded-2xl border bg-card p-5">
|
|
224
|
-
<div className="text-sm font-medium text-muted-foreground">
|
|
225
|
-
Configured
|
|
226
|
-
</div>
|
|
227
|
-
<div className="mt-2 text-3xl font-semibold text-foreground">
|
|
228
|
-
{configuredIntegrations}/{totalIntegrations}
|
|
229
|
-
</div>
|
|
230
|
-
<Progress
|
|
231
|
-
value={
|
|
232
|
-
totalIntegrations > 0
|
|
233
|
-
? Math.round(
|
|
234
|
-
(configuredIntegrations / totalIntegrations) * 100,
|
|
235
|
-
)
|
|
236
|
-
: 0
|
|
237
|
-
}
|
|
238
|
-
className="mt-2 h-1.5"
|
|
239
|
-
/>
|
|
240
|
-
</div>
|
|
336
|
+
{isLoading && services.length === 0 && (
|
|
337
|
+
<div className="rounded-2xl border border-dashed px-6 py-12 text-center text-sm text-muted-foreground">
|
|
338
|
+
Discovering apps and credentials…
|
|
241
339
|
</div>
|
|
242
340
|
)}
|
|
243
341
|
|
|
244
|
-
{isLoading && (
|
|
342
|
+
{!isLoading && services.length === 0 && (
|
|
245
343
|
<div className="rounded-2xl border border-dashed px-6 py-12 text-center text-sm text-muted-foreground">
|
|
246
|
-
|
|
344
|
+
No apps with declared integrations are reachable yet.
|
|
247
345
|
</div>
|
|
248
346
|
)}
|
|
249
347
|
|
|
250
|
-
{
|
|
251
|
-
<
|
|
252
|
-
|
|
253
|
-
<
|
|
254
|
-
|
|
255
|
-
|
|
348
|
+
{available.length > 0 && (
|
|
349
|
+
<section>
|
|
350
|
+
<div className="mb-3 flex items-baseline justify-between">
|
|
351
|
+
<h2 className="text-sm font-medium text-foreground">
|
|
352
|
+
Available to connect
|
|
353
|
+
</h2>
|
|
354
|
+
<span className="text-xs text-muted-foreground">
|
|
355
|
+
{available.length}
|
|
356
|
+
</span>
|
|
357
|
+
</div>
|
|
358
|
+
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
|
359
|
+
{available.map((service) => (
|
|
360
|
+
<ConnectorCard key={service.key} service={service} />
|
|
361
|
+
))}
|
|
362
|
+
</div>
|
|
363
|
+
</section>
|
|
256
364
|
)}
|
|
257
365
|
|
|
258
|
-
{
|
|
259
|
-
<
|
|
260
|
-
<
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
366
|
+
{connected.length > 0 && (
|
|
367
|
+
<section>
|
|
368
|
+
<div className="mb-3 mt-2 flex items-baseline justify-between">
|
|
369
|
+
<h2 className="text-sm font-medium text-foreground">Connected</h2>
|
|
370
|
+
<span className="text-xs text-muted-foreground">
|
|
371
|
+
{connected.length}
|
|
372
|
+
</span>
|
|
373
|
+
</div>
|
|
374
|
+
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
|
375
|
+
{connected.map((service) => (
|
|
376
|
+
<ConnectorCard key={service.key} service={service} />
|
|
266
377
|
))}
|
|
267
378
|
</div>
|
|
268
|
-
</
|
|
379
|
+
</section>
|
|
269
380
|
)}
|
|
270
381
|
|
|
271
|
-
{
|
|
272
|
-
<
|
|
273
|
-
|
|
274
|
-
|
|
382
|
+
{apps.length > 0 && (
|
|
383
|
+
<Collapsible className="mt-6 rounded-2xl border bg-card">
|
|
384
|
+
<CollapsibleTrigger className="flex w-full items-center justify-between px-4 py-3 text-sm">
|
|
385
|
+
<span className="flex items-center gap-2 text-muted-foreground">
|
|
386
|
+
<IconPlugConnected size={14} />
|
|
387
|
+
Per-app status
|
|
388
|
+
</span>
|
|
389
|
+
<IconChevronRight
|
|
390
|
+
size={14}
|
|
391
|
+
className="text-muted-foreground transition group-data-[state=open]:rotate-90"
|
|
392
|
+
/>
|
|
393
|
+
</CollapsibleTrigger>
|
|
394
|
+
<CollapsibleContent>
|
|
395
|
+
<div className="border-t">
|
|
396
|
+
{apps.map((app) => (
|
|
397
|
+
<PerAppDetailRow key={app.appId} app={app} />
|
|
398
|
+
))}
|
|
399
|
+
</div>
|
|
400
|
+
<div className="flex items-center justify-end gap-1.5 border-t px-4 py-2.5 text-xs text-muted-foreground">
|
|
401
|
+
<IconLink size={12} />
|
|
402
|
+
<a href="/vault" className="hover:underline">
|
|
403
|
+
Open vault for advanced sharing
|
|
404
|
+
</a>
|
|
405
|
+
</div>
|
|
406
|
+
</CollapsibleContent>
|
|
407
|
+
</Collapsible>
|
|
275
408
|
)}
|
|
276
409
|
</DispatchShell>
|
|
277
410
|
);
|
|
@@ -23,7 +23,7 @@ When a user asks for something:
|
|
|
23
23
|
- 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.
|
|
24
24
|
- 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.
|
|
25
25
|
- 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.
|
|
26
|
-
- 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.
|
|
26
|
+
- 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.
|
|
27
27
|
- For digests, reminders, or saved behavior, prefer recurring jobs, resources, or destinations over chat replies.
|
|
28
28
|
- Keep responses concise and operational — messaging platforms have character limits.
|
|
29
29
|
- Use markdown sparingly (bold and lists are fine, avoid complex formatting).
|