SubagentCreateDialog.tsx4.1 KBView on GitHub 'use client';
import { useEffect, useState } from 'react';
import { useMutation } from '@tanstack/react-query';
import { Loader2 } from 'lucide-react';
import { toast } from 'sonner';
import { useTRPC } from '@/providers/query-provider';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';
export interface CreatedSubagent {
documentId: string;
slug: string;
name: string;
}
interface SubagentCreateDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
/** Which folder the subagent is created in — set from the cursor's scope card. */
scope: 'user' | 'org';
/** User AOP id (user scope, and to resolve the org link for org scope). */
aopId?: string;
/** Org AOP id when known directly (composite passes the loaded orgAopId). */
orgAopId?: string;
/**
* The playbook's owner. Threaded from the editor that opened the dialog rather
* than read from the member picker, which the `/brain` editor does not mount.
*/
targetUserId?: string;
/**
* Called after the subagent doc is created — the editor inserts a `<ref>`,
* persists the playbook, and navigates to the new doc. May be async.
*/
onCreated: (result: CreatedSubagent) => void | Promise<void>;
}
/**
* Names a new subagent and creates its instruction doc. A subagent is a passive
* document — it has no trigger of its own; its timing is set by the playbook
* `<trigger>` block that references it. So this only asks for a name. On submit
* it calls `aop.createSubagent`, opens the new doc's page in a new tab (leaving
* the playbook editor intact), and hands the result back so the editor can drop
* a `<ref>` at the cursor.
*/
export function SubagentCreateDialog({
open,
onOpenChange,
scope,
aopId,
orgAopId,
targetUserId,
onCreated,
}: SubagentCreateDialogProps) {
const trpc = useTRPC();
const [name, setName] = useState('');
const { mutateAsync, isPending } = useMutation(trpc.aop.createSubagent.mutationOptions());
useEffect(() => {
if (open) setName('');
}, [open]);
const submit = async () => {
const trimmed = name.trim();
if (!trimmed) {
toast.error('Give the subagent a name');
return;
}
try {
const result = await mutateAsync({ name: trimmed, scope, aopId, orgAopId, targetUserId });
// Editor inserts the ref, persists the playbook, then navigates here.
await onCreated(result);
} catch (err) {
toast.error(err instanceof Error ? err.message : 'Failed to create subagent');
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>New {scope === 'org' ? 'org' : 'user'} subagent</DialogTitle>
<DialogDescription>
A subagent is a reusable instruction document, created in the{' '}
{scope === 'org' ? 'org' : 'user'} playbook. It runs whenever a trigger
references it — set the timing on the trigger, not here.
</DialogDescription>
</DialogHeader>
<div className="space-y-1.5">
<Label htmlFor="subagent-name" className="text-xs">
Name
</Label>
<Input
id="subagent-name"
autoFocus
value={name}
onChange={(e) => setName(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !isPending) void submit();
}}
placeholder="Deal risk coach"
className="h-8 text-sm"
/>
</div>
<DialogFooter>
<Button variant="ghost" size="sm" onClick={() => onOpenChange(false)} disabled={isPending}>
Cancel
</Button>
<Button size="sm" onClick={submit} disabled={isPending}>
{isPending ? <Loader2 className="mr-1 h-3 w-3 animate-spin" /> : null}
Create & open
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}