import { useActionMutation, useActionQuery, } from "@agent-native/core/client/hooks"; import { IconAlertCircle, IconBook, IconChevronDown, IconChevronRight, IconCircleCheck, IconCode, IconEdit, IconFileText, IconPlus, IconPlugConnected, IconTrash, IconUser, IconX, } from "@tabler/icons-react"; import { useEffect, useState, type ReactNode } from "react"; import { toast } from "sonner"; import { ActionQueryError } from "../../components/action-query-error"; import { DispatchShell } from "../../components/dispatch-shell"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "../../components/ui/alert-dialog"; import { Badge } from "../../components/ui/badge"; import { Button } from "../../components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "../../components/ui/dialog"; import { Input } from "../../components/ui/input"; import { Label } from "../../components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "../../components/ui/select"; import { Skeleton } from "../../components/ui/skeleton"; import { Tabs, TabsContent, TabsList, TabsTrigger, } from "../../components/ui/tabs"; import { Textarea } from "../../components/ui/textarea"; import { ImpactPreview, workspaceResourceMutationMessage, } from "../../components/workspace-resource-impact-preview"; export function meta() { return [{ title: "Resources — Dispatch" }]; } const KIND_CONFIG = { skill: { label: "Skill", icon: IconCode, pathPrefix: "skills/", description: "Agent skills - on-demand guidance available across workspace apps", }, instruction: { label: "Instruction", icon: IconBook, pathPrefix: "instructions/", description: "Global instructions - guardrails loaded by every app agent", }, agent: { label: "Agent", icon: IconUser, pathPrefix: "agents/", description: "Reusable agent profiles - specialist agents shared across apps", }, knowledge: { label: "Knowledge", icon: IconFileText, pathPrefix: "context/", description: "Reference resources - brand, positioning, persona, and domain context", }, "mcp-server": { label: "MCP Server", icon: IconPlugConnected, pathPrefix: "mcp-servers/", description: "HTTP MCP servers - external tools centrally granted to app agents", }, } as const; const STARTER_GLOBAL_CONTEXT = [ { path: "context/company.md", label: "Company", kind: "knowledge", description: "Company facts, ICP, products, and canonical links", }, { path: "context/brand.md", label: "Brand", kind: "knowledge", description: "Voice, visual identity, naming, and style rules", }, { path: "context/messaging.md", label: "Messaging", kind: "knowledge", description: "Positioning, value props, proof points, and objections", }, { path: "instructions/guardrails.md", label: "Guardrails", kind: "instruction", description: "Always-on rules loaded by every app agent", }, { path: "skills/company-voice/SKILL.md", label: "Company Voice", kind: "skill", description: "On-demand guidance for customer-facing writing", }, ] as const; function resourceSlug(value: string): string { return value .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); } function defaultResourcePath(kind: string, name: string): string { const slug = resourceSlug(name) || "example"; if (kind === "skill") return `skills/${slug}/SKILL.md`; if (kind === "instruction") return `instructions/${slug}.md`; if (kind === "agent") return `agents/${slug}.md`; if (kind === "knowledge") return `context/${slug}.md`; if (kind === "mcp-server") return `mcp-servers/${slug}.json`; return `${slug}.md`; } function isAutoLoadedInstruction(resource: any): boolean { return ( resource.kind === "instruction" && (resource.path === "AGENTS.md" || String(resource.path).startsWith("instructions/")) ); } function formatTimestamp(value?: number | null): string { if (!value) return "Not present"; return new Date(value).toLocaleString(); } function availabilityLabel(value?: string): string { switch (value) { case "all-apps": return "Inherited by all apps"; case "selected-granted": return "Granted to selected app"; case "selected-not-granted": return "Not granted to this app"; case "selected-no-app": return "Select an app to check grant"; case "path-not-managed": return "Path is not a Dispatch resource"; default: return "Checking availability"; } } function layerState(layer: any): { label: string; className: string; } { if (layer.effective) { return { label: "Active", className: "border-green-500/30 bg-green-500/10 text-green-700", }; } if (layer.overridden) { return { label: "Overridden", className: "border-amber-500/30 bg-amber-500/10 text-amber-700", }; } return { label: "Missing", className: "text-muted-foreground", }; } function EditResourceDialog({ resource, trigger, }: { resource: any; trigger?: ReactNode; }) { const [open, setOpen] = useState(false); const [name, setName] = useState(resource.name || ""); const [description, setDescription] = useState(resource.description || ""); const [content, setContent] = useState(resource.content || ""); const [scope, setScope] = useState(resource.scope || "all"); useEffect(() => { if (!open) return; setName(resource.name || ""); setDescription(resource.description || ""); setContent(resource.content || ""); setScope(resource.scope || "all"); }, [open, resource]); const update = useActionMutation("update-workspace-resource", { onSuccess: (result: any) => { toast.success( workspaceResourceMutationMessage(result, "Resource updated"), ); setOpen(false); }, onError: (err) => toast.error(String(err)), }); return ( {trigger || ( )} Edit agent resource Updates apply immediately anywhere this agent resource is inherited. App shared or personal resources can override it locally.
setName(e.target.value)} />
setDescription(e.target.value)} />