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 &amp; open
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}