@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.
- package/README.md +12 -1
- package/dist/actions/connect-external-agent.d.ts +18 -0
- package/dist/actions/connect-external-agent.d.ts.map +1 -0
- package/dist/actions/connect-external-agent.js +68 -0
- package/dist/actions/connect-external-agent.js.map +1 -0
- package/dist/actions/create-workspace-resource.d.ts +14 -14
- package/dist/actions/create-workspace-resource.js +9 -2
- package/dist/actions/create-workspace-resource.js.map +1 -1
- package/dist/actions/create_workspace_app_embed_session.d.ts +13 -0
- package/dist/actions/create_workspace_app_embed_session.d.ts.map +1 -0
- package/dist/actions/create_workspace_app_embed_session.js +30 -0
- package/dist/actions/create_workspace_app_embed_session.js.map +1 -0
- package/dist/actions/import-agent-pack.d.ts +96 -0
- package/dist/actions/import-agent-pack.d.ts.map +1 -0
- package/dist/actions/import-agent-pack.js +116 -0
- package/dist/actions/import-agent-pack.js.map +1 -0
- package/dist/actions/import-agent.d.ts +60 -0
- package/dist/actions/import-agent.d.ts.map +1 -0
- package/dist/actions/import-agent.js +82 -0
- package/dist/actions/import-agent.js.map +1 -0
- package/dist/actions/index.d.ts.map +1 -1
- package/dist/actions/index.js +10 -0
- package/dist/actions/index.js.map +1 -1
- package/dist/actions/list-agent-pack.d.ts +36 -0
- package/dist/actions/list-agent-pack.d.ts.map +1 -0
- package/dist/actions/list-agent-pack.js +32 -0
- package/dist/actions/list-agent-pack.js.map +1 -0
- package/dist/actions/list-workspace-resource-options.d.ts +1 -1
- package/dist/actions/list-workspace-resource-options.js +8 -1
- package/dist/actions/list-workspace-resource-options.js.map +1 -1
- package/dist/actions/list-workspace-resources.d.ts +1 -1
- package/dist/actions/list-workspace-resources.js +8 -1
- package/dist/actions/list-workspace-resources.js.map +1 -1
- package/dist/actions/navigate.js +1 -1
- package/dist/actions/navigate.js.map +1 -1
- package/dist/actions/provider-api-register.d.ts +10 -10
- package/dist/actions/update-workspace-resource.d.ts +13 -13
- package/dist/actions/view-screen.js +18 -1
- package/dist/actions/view-screen.js.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -0
- package/dist/components/index.js.map +1 -1
- package/dist/components/layout/Layout.d.ts +1 -8
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +33 -106
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/simple-agents-panel.d.ts +9 -0
- package/dist/components/simple-agents-panel.d.ts.map +1 -0
- package/dist/components/simple-agents-panel.js +405 -0
- package/dist/components/simple-agents-panel.js.map +1 -0
- package/dist/components/workspace-app-host.d.ts +29 -0
- package/dist/components/workspace-app-host.d.ts.map +1 -0
- package/dist/components/workspace-app-host.js +145 -0
- package/dist/components/workspace-app-host.js.map +1 -0
- package/dist/db/migrations.d.ts +2 -4
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/migrations.js +25 -0
- package/dist/db/migrations.js.map +1 -1
- package/dist/db/schema.js +1 -1
- package/dist/db/schema.js.map +1 -1
- package/dist/hooks/use-navigation-state.d.ts +1 -0
- package/dist/hooks/use-navigation-state.d.ts.map +1 -1
- package/dist/hooks/use-navigation-state.js +10 -0
- package/dist/hooks/use-navigation-state.js.map +1 -1
- package/dist/lib/agent-pack.d.ts +31 -0
- package/dist/lib/agent-pack.d.ts.map +1 -0
- package/dist/lib/agent-pack.js +149 -0
- package/dist/lib/agent-pack.js.map +1 -0
- package/dist/lib/simple-agent-profile.d.ts +36 -0
- package/dist/lib/simple-agent-profile.d.ts.map +1 -0
- package/dist/lib/simple-agent-profile.js +183 -0
- package/dist/lib/simple-agent-profile.js.map +1 -0
- package/dist/routes/pages/agents.d.ts.map +1 -1
- package/dist/routes/pages/agents.js +11 -1
- package/dist/routes/pages/agents.js.map +1 -1
- package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
- package/dist/routes/pages/apps.$appId.js +4 -95
- package/dist/routes/pages/apps.$appId.js.map +1 -1
- package/dist/routes/pages/chat.d.ts.map +1 -1
- package/dist/routes/pages/chat.js +62 -10
- package/dist/routes/pages/chat.js.map +1 -1
- package/dist/server/lib/agent-pack-store.d.ts +24 -0
- package/dist/server/lib/agent-pack-store.d.ts.map +1 -0
- package/dist/server/lib/agent-pack-store.js +22 -0
- package/dist/server/lib/agent-pack-store.js.map +1 -0
- package/dist/server/lib/dispatch-store.d.ts.map +1 -1
- package/dist/server/lib/dispatch-store.js +4 -0
- package/dist/server/lib/dispatch-store.js.map +1 -1
- package/dist/server/lib/mcp-gateway.d.ts +11 -0
- package/dist/server/lib/mcp-gateway.d.ts.map +1 -1
- package/dist/server/lib/mcp-gateway.js +155 -9
- package/dist/server/lib/mcp-gateway.js.map +1 -1
- package/dist/server/lib/workspace-resources-store.d.ts +15 -1
- package/dist/server/lib/workspace-resources-store.d.ts.map +1 -1
- package/dist/server/lib/workspace-resources-store.js +11 -2
- package/dist/server/lib/workspace-resources-store.js.map +1 -1
- package/dist/server/plugins/agent-chat.js +13 -0
- package/dist/server/plugins/agent-chat.js.map +1 -1
- package/dist/shared/feature-flags.d.ts +7 -0
- package/dist/shared/feature-flags.d.ts.map +1 -0
- package/dist/shared/feature-flags.js +12 -0
- package/dist/shared/feature-flags.js.map +1 -0
- package/dist/shared/workspace-sso.d.ts +36 -0
- package/dist/shared/workspace-sso.d.ts.map +1 -0
- package/dist/shared/workspace-sso.js +99 -0
- package/dist/shared/workspace-sso.js.map +1 -0
- package/package.json +5 -3
- package/src/actions/connect-external-agent.ts +92 -0
- package/src/actions/create-workspace-resource.ts +9 -2
- package/src/actions/create_workspace_app_embed_session.ts +32 -0
- package/src/actions/import-agent-pack.ts +146 -0
- package/src/actions/import-agent.ts +102 -0
- package/src/actions/index.spec.ts +13 -0
- package/src/actions/index.ts +10 -0
- package/src/actions/list-agent-pack.ts +43 -0
- package/src/actions/list-workspace-resource-options.ts +8 -1
- package/src/actions/list-workspace-resources.ts +8 -1
- package/src/actions/navigate.ts +1 -1
- package/src/actions/view-screen.spec.ts +29 -1
- package/src/actions/view-screen.ts +19 -1
- package/src/components/app-icon.spec.tsx +10 -0
- package/src/components/index.ts +1 -0
- package/src/components/layout/Layout.spec.tsx +2 -0
- package/src/components/layout/Layout.tsx +59 -171
- package/src/components/simple-agents-panel.spec.tsx +73 -0
- package/src/components/simple-agents-panel.tsx +1226 -0
- package/src/components/workspace-app-host.spec.tsx +204 -0
- package/src/components/workspace-app-host.tsx +359 -0
- package/src/db/migrations.spec.ts +5 -1
- package/src/db/migrations.ts +28 -1
- package/src/db/schema.ts +1 -1
- package/src/hooks/use-navigation-state.spec.ts +25 -0
- package/src/hooks/use-navigation-state.ts +11 -0
- package/src/lib/agent-pack.spec.ts +47 -0
- package/src/lib/agent-pack.ts +226 -0
- package/src/lib/simple-agent-profile.spec.ts +77 -0
- package/src/lib/simple-agent-profile.ts +247 -0
- package/src/routes/index.spec.ts +1 -0
- package/src/routes/pages/agents.tsx +21 -1
- package/src/routes/pages/apps.$appId.tsx +3 -240
- package/src/routes/pages/chat.spec.tsx +62 -0
- package/src/routes/pages/chat.tsx +117 -9
- package/src/server/lib/agent-pack-store.ts +35 -0
- package/src/server/lib/dispatch-store.ts +8 -0
- package/src/server/lib/mcp-gateway.spec.ts +141 -0
- package/src/server/lib/mcp-gateway.ts +227 -15
- package/src/server/lib/workspace-resources-store.ts +9 -2
- package/src/server/plugins/agent-chat.ts +13 -0
- package/src/shared/feature-flags.ts +13 -0
- 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
|
+
}
|