@agent-native/dispatch 0.24.5 → 0.25.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.
Files changed (151) hide show
  1. package/README.md +12 -1
  2. package/dist/actions/connect-external-agent.d.ts +18 -0
  3. package/dist/actions/connect-external-agent.d.ts.map +1 -0
  4. package/dist/actions/connect-external-agent.js +68 -0
  5. package/dist/actions/connect-external-agent.js.map +1 -0
  6. package/dist/actions/create-workspace-resource.d.ts +14 -14
  7. package/dist/actions/create-workspace-resource.js +9 -2
  8. package/dist/actions/create-workspace-resource.js.map +1 -1
  9. package/dist/actions/create_workspace_app_embed_session.d.ts +13 -0
  10. package/dist/actions/create_workspace_app_embed_session.d.ts.map +1 -0
  11. package/dist/actions/create_workspace_app_embed_session.js +30 -0
  12. package/dist/actions/create_workspace_app_embed_session.js.map +1 -0
  13. package/dist/actions/import-agent-pack.d.ts +96 -0
  14. package/dist/actions/import-agent-pack.d.ts.map +1 -0
  15. package/dist/actions/import-agent-pack.js +116 -0
  16. package/dist/actions/import-agent-pack.js.map +1 -0
  17. package/dist/actions/import-agent.d.ts +60 -0
  18. package/dist/actions/import-agent.d.ts.map +1 -0
  19. package/dist/actions/import-agent.js +82 -0
  20. package/dist/actions/import-agent.js.map +1 -0
  21. package/dist/actions/index.d.ts.map +1 -1
  22. package/dist/actions/index.js +10 -0
  23. package/dist/actions/index.js.map +1 -1
  24. package/dist/actions/list-agent-pack.d.ts +36 -0
  25. package/dist/actions/list-agent-pack.d.ts.map +1 -0
  26. package/dist/actions/list-agent-pack.js +32 -0
  27. package/dist/actions/list-agent-pack.js.map +1 -0
  28. package/dist/actions/list-workspace-resource-options.d.ts +1 -1
  29. package/dist/actions/list-workspace-resource-options.js +8 -1
  30. package/dist/actions/list-workspace-resource-options.js.map +1 -1
  31. package/dist/actions/list-workspace-resources.d.ts +1 -1
  32. package/dist/actions/list-workspace-resources.js +8 -1
  33. package/dist/actions/list-workspace-resources.js.map +1 -1
  34. package/dist/actions/navigate.js +1 -1
  35. package/dist/actions/navigate.js.map +1 -1
  36. package/dist/actions/provider-api-register.d.ts +10 -10
  37. package/dist/actions/update-workspace-resource.d.ts +13 -13
  38. package/dist/actions/view-screen.js +18 -1
  39. package/dist/actions/view-screen.js.map +1 -1
  40. package/dist/components/index.d.ts +1 -0
  41. package/dist/components/index.d.ts.map +1 -1
  42. package/dist/components/index.js +1 -0
  43. package/dist/components/index.js.map +1 -1
  44. package/dist/components/layout/Layout.d.ts +1 -8
  45. package/dist/components/layout/Layout.d.ts.map +1 -1
  46. package/dist/components/layout/Layout.js +33 -106
  47. package/dist/components/layout/Layout.js.map +1 -1
  48. package/dist/components/simple-agents-panel.d.ts +9 -0
  49. package/dist/components/simple-agents-panel.d.ts.map +1 -0
  50. package/dist/components/simple-agents-panel.js +405 -0
  51. package/dist/components/simple-agents-panel.js.map +1 -0
  52. package/dist/components/workspace-app-host.d.ts +29 -0
  53. package/dist/components/workspace-app-host.d.ts.map +1 -0
  54. package/dist/components/workspace-app-host.js +145 -0
  55. package/dist/components/workspace-app-host.js.map +1 -0
  56. package/dist/db/migrations.d.ts +2 -4
  57. package/dist/db/migrations.d.ts.map +1 -1
  58. package/dist/db/migrations.js +25 -0
  59. package/dist/db/migrations.js.map +1 -1
  60. package/dist/db/schema.js +1 -1
  61. package/dist/db/schema.js.map +1 -1
  62. package/dist/hooks/use-navigation-state.d.ts +1 -0
  63. package/dist/hooks/use-navigation-state.d.ts.map +1 -1
  64. package/dist/hooks/use-navigation-state.js +10 -0
  65. package/dist/hooks/use-navigation-state.js.map +1 -1
  66. package/dist/lib/agent-pack.d.ts +31 -0
  67. package/dist/lib/agent-pack.d.ts.map +1 -0
  68. package/dist/lib/agent-pack.js +149 -0
  69. package/dist/lib/agent-pack.js.map +1 -0
  70. package/dist/lib/simple-agent-profile.d.ts +36 -0
  71. package/dist/lib/simple-agent-profile.d.ts.map +1 -0
  72. package/dist/lib/simple-agent-profile.js +183 -0
  73. package/dist/lib/simple-agent-profile.js.map +1 -0
  74. package/dist/routes/pages/agents.d.ts.map +1 -1
  75. package/dist/routes/pages/agents.js +11 -1
  76. package/dist/routes/pages/agents.js.map +1 -1
  77. package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
  78. package/dist/routes/pages/apps.$appId.js +4 -95
  79. package/dist/routes/pages/apps.$appId.js.map +1 -1
  80. package/dist/routes/pages/chat.d.ts.map +1 -1
  81. package/dist/routes/pages/chat.js +62 -10
  82. package/dist/routes/pages/chat.js.map +1 -1
  83. package/dist/server/lib/agent-pack-store.d.ts +24 -0
  84. package/dist/server/lib/agent-pack-store.d.ts.map +1 -0
  85. package/dist/server/lib/agent-pack-store.js +22 -0
  86. package/dist/server/lib/agent-pack-store.js.map +1 -0
  87. package/dist/server/lib/dispatch-store.d.ts.map +1 -1
  88. package/dist/server/lib/dispatch-store.js +4 -0
  89. package/dist/server/lib/dispatch-store.js.map +1 -1
  90. package/dist/server/lib/mcp-gateway.d.ts +11 -0
  91. package/dist/server/lib/mcp-gateway.d.ts.map +1 -1
  92. package/dist/server/lib/mcp-gateway.js +155 -9
  93. package/dist/server/lib/mcp-gateway.js.map +1 -1
  94. package/dist/server/lib/workspace-resources-store.d.ts +15 -1
  95. package/dist/server/lib/workspace-resources-store.d.ts.map +1 -1
  96. package/dist/server/lib/workspace-resources-store.js +11 -2
  97. package/dist/server/lib/workspace-resources-store.js.map +1 -1
  98. package/dist/server/plugins/agent-chat.js +13 -0
  99. package/dist/server/plugins/agent-chat.js.map +1 -1
  100. package/dist/shared/feature-flags.d.ts +7 -0
  101. package/dist/shared/feature-flags.d.ts.map +1 -0
  102. package/dist/shared/feature-flags.js +12 -0
  103. package/dist/shared/feature-flags.js.map +1 -0
  104. package/dist/shared/workspace-sso.d.ts +36 -0
  105. package/dist/shared/workspace-sso.d.ts.map +1 -0
  106. package/dist/shared/workspace-sso.js +99 -0
  107. package/dist/shared/workspace-sso.js.map +1 -0
  108. package/package.json +5 -3
  109. package/src/actions/connect-external-agent.ts +92 -0
  110. package/src/actions/create-workspace-resource.ts +9 -2
  111. package/src/actions/create_workspace_app_embed_session.ts +32 -0
  112. package/src/actions/import-agent-pack.ts +146 -0
  113. package/src/actions/import-agent.ts +102 -0
  114. package/src/actions/index.spec.ts +13 -0
  115. package/src/actions/index.ts +10 -0
  116. package/src/actions/list-agent-pack.ts +43 -0
  117. package/src/actions/list-workspace-resource-options.ts +8 -1
  118. package/src/actions/list-workspace-resources.ts +8 -1
  119. package/src/actions/navigate.ts +1 -1
  120. package/src/actions/view-screen.spec.ts +29 -1
  121. package/src/actions/view-screen.ts +19 -1
  122. package/src/components/app-icon.spec.tsx +10 -0
  123. package/src/components/index.ts +1 -0
  124. package/src/components/layout/Layout.spec.tsx +2 -0
  125. package/src/components/layout/Layout.tsx +59 -171
  126. package/src/components/simple-agents-panel.spec.tsx +73 -0
  127. package/src/components/simple-agents-panel.tsx +1226 -0
  128. package/src/components/workspace-app-host.spec.tsx +204 -0
  129. package/src/components/workspace-app-host.tsx +359 -0
  130. package/src/db/migrations.spec.ts +5 -1
  131. package/src/db/migrations.ts +28 -1
  132. package/src/db/schema.ts +1 -1
  133. package/src/hooks/use-navigation-state.spec.ts +25 -0
  134. package/src/hooks/use-navigation-state.ts +11 -0
  135. package/src/lib/agent-pack.spec.ts +47 -0
  136. package/src/lib/agent-pack.ts +226 -0
  137. package/src/lib/simple-agent-profile.spec.ts +77 -0
  138. package/src/lib/simple-agent-profile.ts +247 -0
  139. package/src/routes/index.spec.ts +1 -0
  140. package/src/routes/pages/agents.tsx +21 -1
  141. package/src/routes/pages/apps.$appId.tsx +3 -240
  142. package/src/routes/pages/chat.spec.tsx +62 -0
  143. package/src/routes/pages/chat.tsx +117 -9
  144. package/src/server/lib/agent-pack-store.ts +35 -0
  145. package/src/server/lib/dispatch-store.ts +8 -0
  146. package/src/server/lib/mcp-gateway.spec.ts +141 -0
  147. package/src/server/lib/mcp-gateway.ts +227 -15
  148. package/src/server/lib/workspace-resources-store.ts +9 -2
  149. package/src/server/plugins/agent-chat.ts +13 -0
  150. package/src/shared/feature-flags.ts +13 -0
  151. package/src/shared/workspace-sso.ts +118 -0
@@ -0,0 +1,1226 @@
1
+ import {
2
+ navigateWithAgentChatViewTransition,
3
+ sendToAgentChat,
4
+ } from "@agent-native/core/client/agent-chat";
5
+ import {
6
+ useActionMutation,
7
+ useActionQuery,
8
+ } from "@agent-native/core/client/hooks";
9
+ import { parseCustomAgentProfile } from "@agent-native/core/resources/metadata";
10
+ import {
11
+ IconAdjustmentsHorizontal,
12
+ IconChevronDown,
13
+ IconEdit,
14
+ IconFileImport,
15
+ IconFolder,
16
+ IconLayoutGrid,
17
+ IconMessageCircle,
18
+ IconPlugConnected,
19
+ IconPlus,
20
+ IconTrash,
21
+ IconUpload,
22
+ IconUser,
23
+ } from "@tabler/icons-react";
24
+ import {
25
+ useEffect,
26
+ useRef,
27
+ useState,
28
+ type ChangeEvent,
29
+ type ReactNode,
30
+ } from "react";
31
+ import { useNavigate } from "react-router";
32
+ import { toast } from "sonner";
33
+
34
+ import {
35
+ buildSimpleAgentContent,
36
+ slugifyAgentName,
37
+ } from "../lib/simple-agent-profile.js";
38
+ import { ActionQueryError } from "./action-query-error";
39
+ import {
40
+ AlertDialog,
41
+ AlertDialogAction,
42
+ AlertDialogCancel,
43
+ AlertDialogContent,
44
+ AlertDialogDescription,
45
+ AlertDialogFooter,
46
+ AlertDialogHeader,
47
+ AlertDialogTitle,
48
+ AlertDialogTrigger,
49
+ } from "./ui/alert-dialog";
50
+ import { Badge } from "./ui/badge";
51
+ import { Button } from "./ui/button";
52
+ import {
53
+ Collapsible,
54
+ CollapsibleContent,
55
+ CollapsibleTrigger,
56
+ } from "./ui/collapsible";
57
+ import {
58
+ Dialog,
59
+ DialogContent,
60
+ DialogDescription,
61
+ DialogFooter,
62
+ DialogHeader,
63
+ DialogTitle,
64
+ DialogTrigger,
65
+ } from "./ui/dialog";
66
+ import { Input } from "./ui/input";
67
+ import { Label } from "./ui/label";
68
+ import {
69
+ Select,
70
+ SelectContent,
71
+ SelectItem,
72
+ SelectTrigger,
73
+ SelectValue,
74
+ } from "./ui/select";
75
+ import { Skeleton } from "./ui/skeleton";
76
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs";
77
+ import { Textarea } from "./ui/textarea";
78
+
79
+ interface WorkspaceAgentResource {
80
+ id: string;
81
+ name: string;
82
+ description: string | null;
83
+ path: string;
84
+ content: string;
85
+ scope: "all" | "selected";
86
+ updatedAt: number;
87
+ }
88
+
89
+ interface AgentPackFileResource extends WorkspaceAgentResource {
90
+ kind: "agent-file" | "skill";
91
+ }
92
+
93
+ interface AgentPackResponse {
94
+ profile: WorkspaceAgentResource;
95
+ root: string;
96
+ files: AgentPackFileResource[];
97
+ }
98
+
99
+ interface AgentPackFileInput {
100
+ path: string;
101
+ content: string;
102
+ }
103
+
104
+ export function isPendingWorkspaceResourceApproval(result: unknown) {
105
+ if (!result || typeof result !== "object") return false;
106
+ const mutation = result as { status?: unknown; changeType?: unknown };
107
+ return (
108
+ mutation.status === "pending" &&
109
+ typeof mutation.changeType === "string" &&
110
+ mutation.changeType.startsWith("workspace-resource.")
111
+ );
112
+ }
113
+
114
+ export function handleAgentPackMutationSuccess(
115
+ result: unknown,
116
+ options: {
117
+ appliedMessage: string;
118
+ approvalMessage: string;
119
+ onApplied: () => void;
120
+ notify?: (message: string) => void;
121
+ },
122
+ ) {
123
+ const notify = options.notify || ((message) => toast.success(message));
124
+ if (isPendingWorkspaceResourceApproval(result)) {
125
+ notify(options.approvalMessage);
126
+ return;
127
+ }
128
+ notify(options.appliedMessage);
129
+ options.onApplied();
130
+ }
131
+
132
+ interface AgentEditorProps {
133
+ resource?: WorkspaceAgentResource;
134
+ trigger?: ReactNode;
135
+ onSaved?: () => void;
136
+ }
137
+
138
+ function profileFields(resource?: WorkspaceAgentResource) {
139
+ if (!resource) {
140
+ return {
141
+ name: "",
142
+ description: "",
143
+ instructions: "# Role\n\nDescribe how this agent should work.\n",
144
+ model: "inherit",
145
+ tools: "inherit",
146
+ scope: "all" as const,
147
+ };
148
+ }
149
+
150
+ const profile = parseCustomAgentProfile(resource.content, resource.path);
151
+ return {
152
+ name: profile?.name || resource.name,
153
+ description: profile?.description || resource.description || "",
154
+ instructions: profile?.instructions || resource.content,
155
+ model: profile?.model || "inherit",
156
+ tools: profile?.tools || "inherit",
157
+ scope: resource.scope,
158
+ };
159
+ }
160
+
161
+ function AgentEditorDialog({ resource, trigger, onSaved }: AgentEditorProps) {
162
+ const isEditing = Boolean(resource);
163
+ const [open, setOpen] = useState(false);
164
+ const [name, setName] = useState("");
165
+ const [description, setDescription] = useState("");
166
+ const [instructions, setInstructions] = useState("");
167
+ const [model, setModel] = useState("inherit");
168
+ const [tools, setTools] = useState("inherit");
169
+ const [scope, setScope] = useState<"all" | "selected">("all");
170
+ const [advancedOpen, setAdvancedOpen] = useState(false);
171
+
172
+ useEffect(() => {
173
+ if (!open) return;
174
+ const fields = profileFields(resource);
175
+ setName(fields.name);
176
+ setDescription(fields.description);
177
+ setInstructions(fields.instructions);
178
+ setModel(fields.model);
179
+ setTools(fields.tools);
180
+ setScope(fields.scope);
181
+ setAdvancedOpen(fields.model !== "inherit" || fields.tools !== "inherit");
182
+ }, [open, resource]);
183
+
184
+ const create = useActionMutation("create-workspace-resource", {
185
+ onSuccess: () => {
186
+ toast.success("Agent created");
187
+ setOpen(false);
188
+ onSaved?.();
189
+ },
190
+ onError: (error) => toast.error(error.message),
191
+ });
192
+ const update = useActionMutation("update-workspace-resource", {
193
+ onSuccess: () => {
194
+ toast.success("Agent updated");
195
+ setOpen(false);
196
+ onSaved?.();
197
+ },
198
+ onError: (error) => toast.error(error.message),
199
+ });
200
+
201
+ const pending = create.isPending || update.isPending;
202
+ const canSave = Boolean(name.trim() && instructions.trim() && !pending);
203
+
204
+ function save() {
205
+ const content = buildSimpleAgentContent({
206
+ name,
207
+ description,
208
+ model,
209
+ tools,
210
+ instructions,
211
+ });
212
+ if (resource) {
213
+ update.mutate({
214
+ id: resource.id,
215
+ name: name.trim(),
216
+ description: description.trim(),
217
+ content,
218
+ scope,
219
+ });
220
+ return;
221
+ }
222
+ create.mutate({
223
+ kind: "agent",
224
+ name: name.trim(),
225
+ description: description.trim() || undefined,
226
+ path: `agents/${slugifyAgentName(name)}.md`,
227
+ content,
228
+ scope,
229
+ });
230
+ }
231
+
232
+ return (
233
+ <Dialog open={open} onOpenChange={setOpen}>
234
+ <DialogTrigger asChild>
235
+ {trigger || (
236
+ <Button>
237
+ <IconPlus size={16} />
238
+ Create agent
239
+ </Button>
240
+ )}
241
+ </DialogTrigger>
242
+ <DialogContent className="max-w-2xl">
243
+ <DialogHeader>
244
+ <DialogTitle>
245
+ {isEditing ? "Manage agent" : "Create agent"}
246
+ </DialogTitle>
247
+ <DialogDescription className="sr-only">
248
+ {isEditing
249
+ ? "Update this reusable agent profile."
250
+ : "Create a reusable agent profile."}
251
+ </DialogDescription>
252
+ </DialogHeader>
253
+ <div className="flex flex-col gap-4 py-2">
254
+ <div className="grid gap-4 sm:grid-cols-2">
255
+ <div className="flex flex-col gap-2">
256
+ <Label htmlFor="simple-agent-name">Name</Label>
257
+ <Input
258
+ id="simple-agent-name"
259
+ value={name}
260
+ onChange={(event) => setName(event.target.value)}
261
+ placeholder="User Research"
262
+ />
263
+ </div>
264
+ <div className="flex flex-col gap-2">
265
+ <Label htmlFor="simple-agent-scope">Availability</Label>
266
+ <Select
267
+ value={scope}
268
+ onValueChange={(value: "all" | "selected") => setScope(value)}
269
+ >
270
+ <SelectTrigger id="simple-agent-scope">
271
+ <SelectValue />
272
+ </SelectTrigger>
273
+ <SelectContent>
274
+ <SelectItem value="all">All apps</SelectItem>
275
+ <SelectItem value="selected">Selected apps</SelectItem>
276
+ </SelectContent>
277
+ </Select>
278
+ </div>
279
+ </div>
280
+ <div className="flex flex-col gap-2">
281
+ <Label htmlFor="simple-agent-description">Description</Label>
282
+ <Input
283
+ id="simple-agent-description"
284
+ value={description}
285
+ onChange={(event) => setDescription(event.target.value)}
286
+ placeholder="Synthesizes user research into clear insights"
287
+ />
288
+ </div>
289
+ <Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
290
+ <CollapsibleTrigger asChild>
291
+ <Button
292
+ type="button"
293
+ variant="ghost"
294
+ size="sm"
295
+ className="group -ml-2 gap-1.5 text-muted-foreground"
296
+ >
297
+ <IconAdjustmentsHorizontal size={15} />
298
+ Advanced
299
+ <IconChevronDown
300
+ className="transition-transform group-data-[state=open]:rotate-180"
301
+ size={15}
302
+ />
303
+ </Button>
304
+ </CollapsibleTrigger>
305
+ <CollapsibleContent className="pt-3">
306
+ <div className="grid gap-4 sm:grid-cols-2">
307
+ <div className="flex flex-col gap-2">
308
+ <Label htmlFor="simple-agent-model">Model</Label>
309
+ <Input
310
+ id="simple-agent-model"
311
+ value={model}
312
+ onChange={(event) => setModel(event.target.value)}
313
+ placeholder="inherit"
314
+ />
315
+ </div>
316
+ <div className="flex flex-col gap-2">
317
+ <Label htmlFor="simple-agent-tools">Tools</Label>
318
+ <Input
319
+ id="simple-agent-tools"
320
+ value={tools}
321
+ onChange={(event) => setTools(event.target.value)}
322
+ placeholder="inherit"
323
+ />
324
+ </div>
325
+ </div>
326
+ </CollapsibleContent>
327
+ </Collapsible>
328
+ <div className="flex flex-col gap-2">
329
+ <Label htmlFor="simple-agent-instructions">Instructions</Label>
330
+ <Textarea
331
+ id="simple-agent-instructions"
332
+ value={instructions}
333
+ onChange={(event) => setInstructions(event.target.value)}
334
+ rows={12}
335
+ className="font-mono text-sm"
336
+ placeholder="# Role\n\nDescribe how this agent should work."
337
+ />
338
+ </div>
339
+ </div>
340
+ <DialogFooter>
341
+ <Button onClick={save} disabled={!canSave}>
342
+ {pending
343
+ ? "Saving..."
344
+ : isEditing
345
+ ? "Save changes"
346
+ : "Create agent"}
347
+ </Button>
348
+ </DialogFooter>
349
+ </DialogContent>
350
+ </Dialog>
351
+ );
352
+ }
353
+
354
+ function AgentPackDialog({
355
+ resource,
356
+ onChanged,
357
+ trigger,
358
+ }: {
359
+ resource: WorkspaceAgentResource;
360
+ onChanged?: () => void;
361
+ trigger?: ReactNode;
362
+ }) {
363
+ const [open, setOpen] = useState(false);
364
+ const [selectedId, setSelectedId] = useState(resource.id);
365
+ const [content, setContent] = useState(resource.content);
366
+ const [addOpen, setAddOpen] = useState(false);
367
+ const [newPath, setNewPath] = useState("");
368
+ const [newKind, setNewKind] = useState<"agent-file" | "skill">("agent-file");
369
+ const [newContent, setNewContent] = useState("");
370
+ const query = useActionQuery<AgentPackResponse>(
371
+ "list-agent-pack",
372
+ { agentId: resource.id },
373
+ { enabled: open },
374
+ );
375
+ const update = useActionMutation("update-workspace-resource", {
376
+ onSuccess: (result) => {
377
+ handleAgentPackMutationSuccess(result, {
378
+ appliedMessage: "Pack file updated",
379
+ approvalMessage: "Pack file update queued for approval",
380
+ onApplied: () => {
381
+ void query.refetch();
382
+ onChanged?.();
383
+ },
384
+ });
385
+ },
386
+ onError: (error) => toast.error(error.message),
387
+ });
388
+ const create = useActionMutation("create-workspace-resource", {
389
+ onSuccess: (result) => {
390
+ handleAgentPackMutationSuccess(result, {
391
+ appliedMessage: "Pack file added",
392
+ approvalMessage: "Pack file addition queued for approval",
393
+ onApplied: () => {
394
+ setAddOpen(false);
395
+ setNewPath("");
396
+ setNewContent("");
397
+ void query.refetch();
398
+ onChanged?.();
399
+ },
400
+ });
401
+ },
402
+ onError: (error) => toast.error(error.message),
403
+ });
404
+ const remove = useActionMutation("delete-workspace-resource", {
405
+ onSuccess: () => {
406
+ toast.success("Pack file removed");
407
+ setSelectedId(resource.id);
408
+ void query.refetch();
409
+ onChanged?.();
410
+ },
411
+ onError: (error) => toast.error(error.message),
412
+ });
413
+
414
+ const files = query.data
415
+ ? [{ ...query.data.profile, kind: "agent" as const }, ...query.data.files]
416
+ : [];
417
+ const selected = files.find((file) => file.id === selectedId) ?? files[0];
418
+
419
+ useEffect(() => {
420
+ if (!open) return;
421
+ setSelectedId(resource.id);
422
+ }, [open, resource.id]);
423
+
424
+ useEffect(() => {
425
+ if (!selected) return;
426
+ setSelectedId(selected.id);
427
+ setContent(selected.content);
428
+ }, [selected?.id, selected?.content]);
429
+
430
+ function addFile() {
431
+ const relativePath = newPath.trim().replaceAll("\\", "/");
432
+ if (
433
+ !relativePath ||
434
+ relativePath.startsWith("/") ||
435
+ relativePath.includes("..") ||
436
+ relativePath.split("/").some((part) => !part || part === ".")
437
+ ) {
438
+ toast.error("Use a relative pack path without ..");
439
+ return;
440
+ }
441
+ const packPath =
442
+ newKind === "skill" && !relativePath.startsWith("skills/")
443
+ ? `skills/${relativePath}`
444
+ : relativePath;
445
+ const name = packPath.split("/").pop() || packPath;
446
+ create.mutate({
447
+ kind: newKind,
448
+ name,
449
+ path: `${query.data?.root || `agents/${slugifyAgentName(resource.name)}`}/${packPath}`,
450
+ content: newContent,
451
+ scope: resource.scope,
452
+ });
453
+ }
454
+
455
+ return (
456
+ <Dialog
457
+ open={open}
458
+ onOpenChange={(nextOpen) => {
459
+ setOpen(nextOpen);
460
+ if (!nextOpen) setAddOpen(false);
461
+ }}
462
+ >
463
+ <DialogTrigger asChild>
464
+ {trigger || (
465
+ <Button variant="ghost" size="sm">
466
+ <IconFolder size={15} />
467
+ Pack
468
+ </Button>
469
+ )}
470
+ </DialogTrigger>
471
+ <DialogContent className="max-w-4xl">
472
+ <DialogHeader>
473
+ <DialogTitle>Agent pack</DialogTitle>
474
+ <DialogDescription className="sr-only">
475
+ Edit the profile, reference files, context, and skills that belong
476
+ to this agent.
477
+ </DialogDescription>
478
+ </DialogHeader>
479
+ <div className="grid min-h-0 gap-4 md:grid-cols-[220px_minmax(0,1fr)]">
480
+ <div className="flex min-w-0 flex-col gap-2">
481
+ <div className="flex items-center justify-between gap-2">
482
+ <span className="text-xs font-medium text-muted-foreground">
483
+ Files
484
+ </span>
485
+ <Dialog open={addOpen} onOpenChange={setAddOpen}>
486
+ <DialogTrigger asChild>
487
+ <Button variant="outline" size="sm">
488
+ <IconPlus size={14} />
489
+ Add
490
+ </Button>
491
+ </DialogTrigger>
492
+ <DialogContent>
493
+ <DialogHeader>
494
+ <DialogTitle>Add pack file</DialogTitle>
495
+ <DialogDescription className="sr-only">
496
+ Add a text reference or agent-owned skill to this pack.
497
+ </DialogDescription>
498
+ </DialogHeader>
499
+ <div className="flex flex-col gap-4 py-2">
500
+ <div className="flex flex-col gap-2">
501
+ <Label htmlFor="agent-pack-file-path">Path</Label>
502
+ <Input
503
+ id="agent-pack-file-path"
504
+ value={newPath}
505
+ onChange={(event) => setNewPath(event.target.value)}
506
+ placeholder="context/brief.md"
507
+ />
508
+ </div>
509
+ <div className="flex flex-col gap-2">
510
+ <Label htmlFor="agent-pack-file-kind">Type</Label>
511
+ <Select
512
+ value={newKind}
513
+ onValueChange={(value: "agent-file" | "skill") =>
514
+ setNewKind(value)
515
+ }
516
+ >
517
+ <SelectTrigger id="agent-pack-file-kind">
518
+ <SelectValue />
519
+ </SelectTrigger>
520
+ <SelectContent>
521
+ <SelectItem value="agent-file">
522
+ Reference file
523
+ </SelectItem>
524
+ <SelectItem value="skill">Skill</SelectItem>
525
+ </SelectContent>
526
+ </Select>
527
+ </div>
528
+ <div className="flex flex-col gap-2">
529
+ <Label htmlFor="agent-pack-file-content">Content</Label>
530
+ <Textarea
531
+ id="agent-pack-file-content"
532
+ value={newContent}
533
+ onChange={(event) => setNewContent(event.target.value)}
534
+ rows={8}
535
+ className="font-mono text-sm"
536
+ />
537
+ </div>
538
+ </div>
539
+ <DialogFooter>
540
+ <Button
541
+ onClick={addFile}
542
+ disabled={!newPath.trim() || create.isPending}
543
+ >
544
+ {create.isPending ? "Adding..." : "Add file"}
545
+ </Button>
546
+ </DialogFooter>
547
+ </DialogContent>
548
+ </Dialog>
549
+ </div>
550
+ {query.isLoading ? (
551
+ <Skeleton className="h-24 w-full" />
552
+ ) : (
553
+ <div className="flex flex-col gap-1">
554
+ {files.map((file) => (
555
+ <Button
556
+ key={file.id}
557
+ type="button"
558
+ variant={selected?.id === file.id ? "secondary" : "ghost"}
559
+ className="h-auto w-full justify-start px-2 py-2 text-left"
560
+ onClick={() => setSelectedId(file.id)}
561
+ >
562
+ <span className="min-w-0 truncate text-xs">
563
+ {file.path.replace(`${query.data?.root || ""}/`, "")}
564
+ </span>
565
+ </Button>
566
+ ))}
567
+ </div>
568
+ )}
569
+ </div>
570
+ <div className="flex min-w-0 flex-col gap-3">
571
+ <div className="flex flex-wrap items-center justify-between gap-2">
572
+ <div className="min-w-0">
573
+ <div className="truncate text-sm font-medium">
574
+ {selected?.path || "Select a file"}
575
+ </div>
576
+ {selected && selected.kind !== "agent" ? (
577
+ <Badge variant="outline">
578
+ {selected.kind === "skill" ? "Skill" : "Reference"}
579
+ </Badge>
580
+ ) : null}
581
+ </div>
582
+ <div className="flex items-center gap-1">
583
+ {selected && selected.kind !== "agent" ? (
584
+ <AlertDialog>
585
+ <AlertDialogTrigger asChild>
586
+ <Button variant="ghost" size="sm">
587
+ <IconTrash size={15} />
588
+ Remove
589
+ </Button>
590
+ </AlertDialogTrigger>
591
+ <AlertDialogContent>
592
+ <AlertDialogHeader>
593
+ <AlertDialogTitle>
594
+ Remove this pack file?
595
+ </AlertDialogTitle>
596
+ <AlertDialogDescription>
597
+ The file will no longer be available to this agent.
598
+ </AlertDialogDescription>
599
+ </AlertDialogHeader>
600
+ <AlertDialogFooter>
601
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
602
+ <AlertDialogAction
603
+ onClick={() => remove.mutate({ id: selected.id })}
604
+ disabled={remove.isPending}
605
+ >
606
+ Remove
607
+ </AlertDialogAction>
608
+ </AlertDialogFooter>
609
+ </AlertDialogContent>
610
+ </AlertDialog>
611
+ ) : null}
612
+ <Button
613
+ size="sm"
614
+ onClick={() =>
615
+ selected && update.mutate({ id: selected.id, content })
616
+ }
617
+ disabled={
618
+ !selected ||
619
+ update.isPending ||
620
+ content === selected.content
621
+ }
622
+ >
623
+ {update.isPending ? "Saving..." : "Save file"}
624
+ </Button>
625
+ </div>
626
+ </div>
627
+ <Textarea
628
+ value={content}
629
+ onChange={(event) => setContent(event.target.value)}
630
+ disabled={!selected}
631
+ rows={18}
632
+ className="min-h-[360px] font-mono text-sm"
633
+ />
634
+ </div>
635
+ </div>
636
+ </DialogContent>
637
+ </Dialog>
638
+ );
639
+ }
640
+
641
+ function ImportAgentDialog({ onImported }: { onImported?: () => void }) {
642
+ const [open, setOpen] = useState(false);
643
+ const [mode, setMode] = useState<"file" | "folder" | "endpoint">("file");
644
+ const [source, setSource] = useState("");
645
+ const [fileName, setFileName] = useState("");
646
+ const [scope, setScope] = useState<"all" | "selected">("all");
647
+ const [url, setUrl] = useState("");
648
+ const [endpointName, setEndpointName] = useState("");
649
+ const [endpointDescription, setEndpointDescription] = useState("");
650
+ const [packFiles, setPackFiles] = useState<AgentPackFileInput[]>([]);
651
+ const [packName, setPackName] = useState("");
652
+ const [packWarnings, setPackWarnings] = useState<string[]>([]);
653
+ const fileRef = useRef<HTMLInputElement>(null);
654
+ const folderRef = useRef<HTMLInputElement>(null);
655
+
656
+ const importAgent = useActionMutation("import-agent", {
657
+ onSuccess: (result: { status: string; warnings?: string[] }) => {
658
+ toast.success(
659
+ result.status === "unchanged"
660
+ ? "Agent already imported"
661
+ : "Agent imported",
662
+ );
663
+ if (result.warnings?.length) {
664
+ toast.info(result.warnings.join(" "));
665
+ }
666
+ setOpen(false);
667
+ onImported?.();
668
+ },
669
+ onError: (error) => toast.error(error.message),
670
+ });
671
+ const connect = useActionMutation("connect-external-agent", {
672
+ onSuccess: () => {
673
+ toast.success("Agent connected");
674
+ setOpen(false);
675
+ onImported?.();
676
+ },
677
+ onError: (error) => toast.error(error.message),
678
+ });
679
+ const importPack = useActionMutation("import-agent-pack", {
680
+ onSuccess: (result: { status: string; warnings?: string[] }) => {
681
+ toast.success(
682
+ result.status === "pending-approval"
683
+ ? "Pack sent for approval"
684
+ : result.status === "unchanged"
685
+ ? "Agent pack already imported"
686
+ : "Agent pack imported",
687
+ );
688
+ if (result.warnings?.length) toast.info(result.warnings.join(" "));
689
+ setOpen(false);
690
+ onImported?.();
691
+ },
692
+ onError: (error) => toast.error(error.message),
693
+ });
694
+
695
+ useEffect(() => {
696
+ if (!folderRef.current) return;
697
+ folderRef.current.setAttribute("webkitdirectory", "");
698
+ folderRef.current.setAttribute("directory", "");
699
+ }, [mode, open]);
700
+
701
+ function reset() {
702
+ setMode("file");
703
+ setSource("");
704
+ setFileName("");
705
+ setScope("all");
706
+ setUrl("");
707
+ setEndpointName("");
708
+ setEndpointDescription("");
709
+ setPackFiles([]);
710
+ setPackName("");
711
+ setPackWarnings([]);
712
+ }
713
+
714
+ async function handleFile(event: ChangeEvent<HTMLInputElement>) {
715
+ const file = event.target.files?.[0];
716
+ if (!file) return;
717
+ setFileName(file.name);
718
+ setSource(await file.text());
719
+ }
720
+
721
+ async function handleFolder(event: ChangeEvent<HTMLInputElement>) {
722
+ const selectedFiles = Array.from(event.target.files ?? []);
723
+ if (selectedFiles.length === 0) return;
724
+ const warnings: string[] = [];
725
+ const textExtensions =
726
+ /\.(md|markdown|txt|json|yaml|yml|csv|html|xml|toml|ts|tsx|js|mjs|py|sh)$/i;
727
+ const pack = await Promise.all(
728
+ selectedFiles.map(async (file) => {
729
+ const path =
730
+ (file as File & { webkitRelativePath?: string }).webkitRelativePath ||
731
+ file.name;
732
+ if (!textExtensions.test(path)) {
733
+ warnings.push(`Skipped non-text file: ${path}`);
734
+ return null;
735
+ }
736
+ return { path, content: await file.text() };
737
+ }),
738
+ );
739
+ const files = pack.filter((file): file is AgentPackFileInput =>
740
+ Boolean(file),
741
+ );
742
+ setPackFiles(files);
743
+ setPackName(files[0]?.path.split("/")[0] || "Selected folder");
744
+ setPackWarnings(warnings);
745
+ }
746
+
747
+ return (
748
+ <Dialog
749
+ open={open}
750
+ onOpenChange={(nextOpen) => {
751
+ setOpen(nextOpen);
752
+ if (!nextOpen) reset();
753
+ }}
754
+ >
755
+ <DialogTrigger asChild>
756
+ <Button variant="outline">
757
+ <IconFileImport size={16} />
758
+ Import or connect
759
+ </Button>
760
+ </DialogTrigger>
761
+ <DialogContent className="max-w-2xl">
762
+ <DialogHeader>
763
+ <DialogTitle>Import an agent</DialogTitle>
764
+ <DialogDescription className="sr-only">
765
+ Import a Claude or JSON agent definition, or connect an external
766
+ agent endpoint.
767
+ </DialogDescription>
768
+ </DialogHeader>
769
+ <Tabs
770
+ value={mode}
771
+ onValueChange={(value) =>
772
+ setMode(value as "file" | "folder" | "endpoint")
773
+ }
774
+ >
775
+ <TabsList className="w-full justify-start">
776
+ <TabsTrigger value="file">Agent file</TabsTrigger>
777
+ <TabsTrigger value="folder">Agent folder</TabsTrigger>
778
+ <TabsTrigger value="endpoint">Connect endpoint</TabsTrigger>
779
+ </TabsList>
780
+ <TabsContent value="file" className="flex flex-col gap-4 py-4">
781
+ <div className="flex flex-wrap items-center gap-3">
782
+ <Button
783
+ type="button"
784
+ variant="outline"
785
+ onClick={() => fileRef.current?.click()}
786
+ >
787
+ <IconUpload size={16} />
788
+ Choose file
789
+ </Button>
790
+ <input
791
+ ref={fileRef}
792
+ type="file"
793
+ accept=".md,.markdown,.json,.txt"
794
+ className="hidden"
795
+ onChange={(event) => void handleFile(event)}
796
+ />
797
+ <span className="text-xs text-muted-foreground">
798
+ {fileName || "Claude .md or JSON agent definition"}
799
+ </span>
800
+ </div>
801
+ <div className="flex flex-col gap-2">
802
+ <Label htmlFor="import-agent-source">Definition</Label>
803
+ <Textarea
804
+ id="import-agent-source"
805
+ value={source}
806
+ onChange={(event) => setSource(event.target.value)}
807
+ rows={12}
808
+ className="font-mono text-sm"
809
+ placeholder="Paste a Claude-style agent file or a JSON definition"
810
+ />
811
+ </div>
812
+ <div className="flex flex-col gap-2">
813
+ <Label htmlFor="import-agent-scope">Availability</Label>
814
+ <Select
815
+ value={scope}
816
+ onValueChange={(value: "all" | "selected") => setScope(value)}
817
+ >
818
+ <SelectTrigger id="import-agent-scope">
819
+ <SelectValue />
820
+ </SelectTrigger>
821
+ <SelectContent>
822
+ <SelectItem value="all">All apps</SelectItem>
823
+ <SelectItem value="selected">Selected apps</SelectItem>
824
+ </SelectContent>
825
+ </Select>
826
+ </div>
827
+ <DialogFooter>
828
+ <Button
829
+ onClick={() =>
830
+ importAgent.mutate({
831
+ source,
832
+ fileName: fileName || undefined,
833
+ scope,
834
+ })
835
+ }
836
+ disabled={!source.trim() || importAgent.isPending}
837
+ >
838
+ {importAgent.isPending ? "Importing..." : "Import agent"}
839
+ </Button>
840
+ </DialogFooter>
841
+ </TabsContent>
842
+ <TabsContent value="folder" className="flex flex-col gap-4 py-4">
843
+ <div className="flex flex-wrap items-center gap-3">
844
+ <Button
845
+ type="button"
846
+ variant="outline"
847
+ onClick={() => folderRef.current?.click()}
848
+ >
849
+ <IconFolder size={16} />
850
+ Choose folder
851
+ </Button>
852
+ <input
853
+ ref={folderRef}
854
+ type="file"
855
+ multiple
856
+ className="hidden"
857
+ onChange={(event) => void handleFolder(event)}
858
+ />
859
+ <span className="text-xs text-muted-foreground">
860
+ {packName
861
+ ? `${packName} · ${packFiles.length} files`
862
+ : "Claude Project or Cowork-style folder"}
863
+ </span>
864
+ </div>
865
+ {packWarnings.length > 0 ? (
866
+ <div className="rounded-lg border border-amber-500/30 bg-amber-500/5 px-3 py-2 text-xs text-amber-800 dark:text-amber-200">
867
+ {packWarnings.join(" ")}
868
+ </div>
869
+ ) : null}
870
+ <div className="flex flex-col gap-2">
871
+ <Label htmlFor="import-agent-pack-scope">Availability</Label>
872
+ <Select
873
+ value={scope}
874
+ onValueChange={(value: "all" | "selected") => setScope(value)}
875
+ >
876
+ <SelectTrigger id="import-agent-pack-scope">
877
+ <SelectValue />
878
+ </SelectTrigger>
879
+ <SelectContent>
880
+ <SelectItem value="all">All apps</SelectItem>
881
+ <SelectItem value="selected">Selected apps</SelectItem>
882
+ </SelectContent>
883
+ </Select>
884
+ </div>
885
+ <DialogFooter>
886
+ <Button
887
+ onClick={() => importPack.mutate({ files: packFiles, scope })}
888
+ disabled={packFiles.length === 0 || importPack.isPending}
889
+ >
890
+ {importPack.isPending ? "Importing..." : "Import agent pack"}
891
+ </Button>
892
+ </DialogFooter>
893
+ </TabsContent>
894
+ <TabsContent value="endpoint" className="flex flex-col gap-4 py-4">
895
+ <div className="flex flex-col gap-2">
896
+ <Label htmlFor="external-agent-url">Endpoint URL</Label>
897
+ <Input
898
+ id="external-agent-url"
899
+ value={url}
900
+ onChange={(event) => setUrl(event.target.value)}
901
+ placeholder="https://agent.example.com"
902
+ />
903
+ </div>
904
+ <div className="grid gap-4 sm:grid-cols-2">
905
+ <div className="flex flex-col gap-2">
906
+ <Label htmlFor="external-agent-name">Name</Label>
907
+ <Input
908
+ id="external-agent-name"
909
+ value={endpointName}
910
+ onChange={(event) => setEndpointName(event.target.value)}
911
+ placeholder="Research partner"
912
+ />
913
+ </div>
914
+ <div className="flex flex-col gap-2">
915
+ <Label>Availability</Label>
916
+ <div className="flex h-10 items-center">
917
+ <Badge variant="secondary">Workspace</Badge>
918
+ </div>
919
+ </div>
920
+ </div>
921
+ <div className="flex flex-col gap-2">
922
+ <Label htmlFor="external-agent-description">Description</Label>
923
+ <Input
924
+ id="external-agent-description"
925
+ value={endpointDescription}
926
+ onChange={(event) => setEndpointDescription(event.target.value)}
927
+ placeholder="Optional"
928
+ />
929
+ </div>
930
+ <p className="text-xs text-muted-foreground">
931
+ Dispatch stores the public endpoint only. Authenticate it after
932
+ connecting.
933
+ </p>
934
+ <DialogFooter>
935
+ <Button
936
+ onClick={() =>
937
+ connect.mutate({
938
+ url,
939
+ name: endpointName || undefined,
940
+ description: endpointDescription || undefined,
941
+ scope: "shared",
942
+ })
943
+ }
944
+ disabled={!url.trim() || connect.isPending}
945
+ >
946
+ <IconPlugConnected size={16} />
947
+ {connect.isPending ? "Connecting..." : "Connect agent"}
948
+ </Button>
949
+ </DialogFooter>
950
+ </TabsContent>
951
+ </Tabs>
952
+ </DialogContent>
953
+ </Dialog>
954
+ );
955
+ }
956
+
957
+ function DeleteAgentButton({
958
+ resource,
959
+ onDeleted,
960
+ }: {
961
+ resource: WorkspaceAgentResource;
962
+ onDeleted?: () => void;
963
+ }) {
964
+ const remove = useActionMutation("delete-workspace-resource", {
965
+ onSuccess: () => {
966
+ toast.success("Agent removed");
967
+ onDeleted?.();
968
+ },
969
+ onError: (error) => toast.error(error.message),
970
+ });
971
+
972
+ return (
973
+ <AlertDialog>
974
+ <AlertDialogTrigger asChild>
975
+ <Button
976
+ variant="ghost"
977
+ size="icon"
978
+ aria-label={`Remove ${resource.name}`}
979
+ >
980
+ <IconTrash size={16} />
981
+ </Button>
982
+ </AlertDialogTrigger>
983
+ <AlertDialogContent>
984
+ <AlertDialogHeader>
985
+ <AlertDialogTitle>Remove {resource.name}?</AlertDialogTitle>
986
+ <AlertDialogDescription>
987
+ This removes the reusable profile from the workspace apps.
988
+ </AlertDialogDescription>
989
+ </AlertDialogHeader>
990
+ <AlertDialogFooter>
991
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
992
+ <AlertDialogAction
993
+ onClick={() => remove.mutate({ id: resource.id })}
994
+ disabled={remove.isPending}
995
+ >
996
+ Remove
997
+ </AlertDialogAction>
998
+ </AlertDialogFooter>
999
+ </AlertDialogContent>
1000
+ </AlertDialog>
1001
+ );
1002
+ }
1003
+
1004
+ function AgentRow({
1005
+ resource,
1006
+ onDeleted,
1007
+ onSaved,
1008
+ }: {
1009
+ resource: WorkspaceAgentResource;
1010
+ onDeleted?: () => void;
1011
+ onSaved?: () => void;
1012
+ }) {
1013
+ const navigate = useNavigate();
1014
+ const packQuery = useActionQuery<AgentPackResponse>(
1015
+ "list-agent-pack",
1016
+ { agentId: resource.id },
1017
+ { enabled: false },
1018
+ );
1019
+ const promote = useActionMutation<
1020
+ AgentAppCreationResult,
1021
+ AgentAppCreationInput
1022
+ >("start-workspace-app-creation", {
1023
+ onSuccess: (result) => {
1024
+ if (result.mode === "local-agent") {
1025
+ sendToAgentChat({
1026
+ message: result.prompt ?? result.message,
1027
+ submit: true,
1028
+ type: "code",
1029
+ newTab: true,
1030
+ reuseEmptyTab: true,
1031
+ });
1032
+ toast.success("Sent to the local agent");
1033
+ return;
1034
+ }
1035
+ if (result.mode === "builder") {
1036
+ toast.success("App build started", {
1037
+ description: result.message,
1038
+ action: result.url
1039
+ ? {
1040
+ label: "Open in Builder",
1041
+ onClick: () =>
1042
+ window.open(result.url, "_blank", "noopener,noreferrer"),
1043
+ }
1044
+ : undefined,
1045
+ });
1046
+ return;
1047
+ }
1048
+ toast.error(result.message);
1049
+ },
1050
+ onError: (error) => toast.error(error.message),
1051
+ });
1052
+
1053
+ function chat() {
1054
+ navigateWithAgentChatViewTransition(
1055
+ navigate,
1056
+ `/chat?agent=${encodeURIComponent(resource.path)}`,
1057
+ );
1058
+ }
1059
+
1060
+ async function buildApp() {
1061
+ const pack = await packQuery.refetch();
1062
+ if (pack.error || !pack.data) {
1063
+ toast.error(pack.error?.message || "Could not load the agent pack");
1064
+ return;
1065
+ }
1066
+ promote.mutate({
1067
+ appId: slugifyAgentName(resource.name),
1068
+ description: resource.description || undefined,
1069
+ prompt: `Turn the "${resource.name}" workspace agent into a focused workspace app. Preserve its role and instructions, make the app its usable face, and keep the original reusable agent profile available.`,
1070
+ resourceIds: [resource.id, ...pack.data.files.map((file) => file.id)],
1071
+ });
1072
+ }
1073
+
1074
+ return (
1075
+ <div className="flex items-start gap-3 rounded-xl border bg-card px-4 py-3">
1076
+ <div className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary">
1077
+ <IconUser size={18} />
1078
+ </div>
1079
+ <div className="min-w-0 flex-1">
1080
+ <div className="flex flex-wrap items-center gap-2">
1081
+ <span className="text-sm font-medium text-foreground">
1082
+ {resource.name}
1083
+ </span>
1084
+ <Badge variant="outline">
1085
+ {resource.scope === "all" ? "All apps" : "Selected apps"}
1086
+ </Badge>
1087
+ </div>
1088
+ {resource.description ? (
1089
+ <div className="mt-1 text-sm text-muted-foreground">
1090
+ {resource.description}
1091
+ </div>
1092
+ ) : null}
1093
+ <div className="mt-1 font-mono text-[11px] text-muted-foreground/70">
1094
+ {resource.path}
1095
+ </div>
1096
+ </div>
1097
+ <div className="flex shrink-0 flex-wrap items-center justify-end gap-1">
1098
+ <Button
1099
+ variant="outline"
1100
+ size="sm"
1101
+ onClick={chat}
1102
+ aria-label={`Chat with ${resource.name}`}
1103
+ >
1104
+ <IconMessageCircle size={15} />
1105
+ Chat
1106
+ </Button>
1107
+ <Button
1108
+ variant="ghost"
1109
+ size="sm"
1110
+ onClick={() => void buildApp()}
1111
+ disabled={promote.isPending}
1112
+ aria-label={`Build an app for ${resource.name}`}
1113
+ >
1114
+ <IconLayoutGrid size={15} />
1115
+ {promote.isPending ? "Starting..." : "Build app"}
1116
+ </Button>
1117
+ <AgentPackDialog
1118
+ resource={resource}
1119
+ onChanged={onSaved}
1120
+ trigger={
1121
+ <Button
1122
+ variant="ghost"
1123
+ size="icon"
1124
+ aria-label={`Manage files for ${resource.name}`}
1125
+ >
1126
+ <IconFolder size={16} />
1127
+ </Button>
1128
+ }
1129
+ />
1130
+ <AgentEditorDialog
1131
+ resource={resource}
1132
+ onSaved={onSaved}
1133
+ trigger={
1134
+ <Button
1135
+ variant="ghost"
1136
+ size="icon"
1137
+ aria-label={`Edit ${resource.name}`}
1138
+ >
1139
+ <IconEdit size={16} />
1140
+ </Button>
1141
+ }
1142
+ />
1143
+ <DeleteAgentButton resource={resource} onDeleted={onDeleted} />
1144
+ </div>
1145
+ </div>
1146
+ );
1147
+ }
1148
+
1149
+ interface AgentAppCreationInput {
1150
+ appId: string;
1151
+ description?: string;
1152
+ prompt: string;
1153
+ resourceIds: string[];
1154
+ }
1155
+
1156
+ interface AgentAppCreationResult {
1157
+ mode: "builder" | "local-agent" | "builder-unavailable" | "coming-soon";
1158
+ message: string;
1159
+ prompt?: string;
1160
+ url?: string;
1161
+ }
1162
+
1163
+ export function SimpleAgentsPanel() {
1164
+ const query = useActionQuery<WorkspaceAgentResource[]>(
1165
+ "list-workspace-resources",
1166
+ { kind: "agent" },
1167
+ );
1168
+ const agents = query.data ?? [];
1169
+
1170
+ if (query.isError) {
1171
+ return (
1172
+ <ActionQueryError
1173
+ error={query.error}
1174
+ onRetry={() => void query.refetch()}
1175
+ />
1176
+ );
1177
+ }
1178
+
1179
+ return (
1180
+ <section className="flex flex-col gap-4">
1181
+ <div className="flex flex-wrap items-center justify-end gap-2">
1182
+ <ImportAgentDialog onImported={() => void query.refetch()} />
1183
+ <AgentEditorDialog onSaved={() => void query.refetch()} />
1184
+ </div>
1185
+ {query.isLoading && agents.length === 0 ? (
1186
+ <div className="flex flex-col gap-3">
1187
+ {[0, 1, 2].map((item) => (
1188
+ <div key={item} className="rounded-xl border bg-card px-4 py-3">
1189
+ <Skeleton className="h-4 w-1/3" />
1190
+ <Skeleton className="mt-2 h-3 w-2/3" />
1191
+ </div>
1192
+ ))}
1193
+ </div>
1194
+ ) : agents.length > 0 ? (
1195
+ <div className="flex flex-col gap-2">
1196
+ {agents.map((agent) => (
1197
+ <AgentRow
1198
+ key={agent.id}
1199
+ resource={agent}
1200
+ onDeleted={() => void query.refetch()}
1201
+ onSaved={() => void query.refetch()}
1202
+ />
1203
+ ))}
1204
+ </div>
1205
+ ) : (
1206
+ <div className="rounded-xl border border-dashed px-6 py-14 text-center">
1207
+ <div className="text-sm font-medium text-foreground">
1208
+ No agents yet
1209
+ </div>
1210
+ <div className="mt-4 flex flex-wrap justify-center gap-2">
1211
+ <AgentEditorDialog
1212
+ onSaved={() => void query.refetch()}
1213
+ trigger={
1214
+ <Button>
1215
+ <IconPlus size={16} />
1216
+ Create an agent
1217
+ </Button>
1218
+ }
1219
+ />
1220
+ <ImportAgentDialog onImported={() => void query.refetch()} />
1221
+ </div>
1222
+ </div>
1223
+ )}
1224
+ </section>
1225
+ );
1226
+ }