diff --git a/docs/guides/board-operator/org-structure.md b/docs/guides/board-operator/org-structure.md index b074d312..43e36b61 100644 --- a/docs/guides/board-operator/org-structure.md +++ b/docs/guides/board-operator/org-structure.md @@ -9,6 +9,7 @@ Paperclip enforces a strict organizational hierarchy. Every agent reports to exa - The **CEO** has no manager (reports to the board/human operator) - Every other agent has a `reportsTo` field pointing to their manager +- You can change an agent’s manager after creation from **Agent → Configuration → Reports to** (or via `PATCH /api/agents/{id}` with `reportsTo`) - Managers can create subtasks and delegate to their reports - Agents escalate blockers up the chain of command diff --git a/ui/src/components/AgentConfigForm.tsx b/ui/src/components/AgentConfigForm.tsx index 69c31919..bbfe5414 100644 --- a/ui/src/components/AgentConfigForm.tsx +++ b/ui/src/components/AgentConfigForm.tsx @@ -44,6 +44,7 @@ import { ClaudeLocalAdvancedFields } from "../adapters/claude-local/config-field import { MarkdownEditor } from "./MarkdownEditor"; import { ChoosePathButton } from "./PathInstructionsModal"; import { OpenCodeLogoIcon } from "./OpenCodeLogoIcon"; +import { ReportsToPicker } from "./ReportsToPicker"; /* ---- Create mode values ---- */ @@ -301,6 +302,12 @@ export function AgentConfigForm(props: AgentConfigFormProps) { }); const models = fetchedModels ?? externalModels ?? []; + const { data: companyAgents = [] } = useQuery({ + queryKey: selectedCompanyId ? queryKeys.agents.list(selectedCompanyId) : ["agents", "none", "list"], + queryFn: () => agentsApi.list(selectedCompanyId!), + enabled: Boolean(!isCreate && selectedCompanyId), + }); + /** Props passed to adapter-specific config field components */ const adapterFieldProps = { mode, @@ -447,6 +454,15 @@ export function AgentConfigForm(props: AgentConfigFormProps) { placeholder="e.g. VP of Engineering" /> + + mark("identity", "reportsTo", id)} + excludeAgentIds={[props.agent.id]} + chooseLabel="Choose manager…" + /> + void; + disabled?: boolean; + excludeAgentIds?: string[]; + disabledEmptyLabel?: string; + chooseLabel?: string; +}) { + const [open, setOpen] = useState(false); + const exclude = new Set(excludeAgentIds); + const rows = agents.filter( + (a) => a.status !== "terminated" && !exclude.has(a.id), + ); + const current = value ? agents.find((a) => a.id === value) : null; + + return ( + + + + {current ? ( + <> + + {`Reports to ${current.name}`} + > + ) : ( + <> + + {disabled ? disabledEmptyLabel : chooseLabel} + > + )} + + + + { + onChange(null); + setOpen(false); + }} + > + No manager + + {rows.map((a) => ( + { + onChange(a.id); + setOpen(false); + }} + > + + {a.name} + {roleLabels[a.role] ?? a.role} + + ))} + + + ); +} diff --git a/ui/src/pages/AgentDetail.tsx b/ui/src/pages/AgentDetail.tsx index 3e238e5e..e922b79b 100644 --- a/ui/src/pages/AgentDetail.tsx +++ b/ui/src/pages/AgentDetail.tsx @@ -17,6 +17,7 @@ import { issuesApi } from "../api/issues"; import { usePanel } from "../context/PanelContext"; import { useSidebar } from "../context/SidebarContext"; import { useCompany } from "../context/CompanyContext"; +import { useToast } from "../context/ToastContext"; import { useDialog } from "../context/DialogContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { queryKeys } from "../lib/queryKeys"; @@ -1356,6 +1357,7 @@ function ConfigurationTab({ updatePermissions: { mutate: (permissions: AgentPermissionUpdate) => void; isPending: boolean }; }) { const queryClient = useQueryClient(); + const { pushToast } = useToast(); const [awaitingRefreshAfterSave, setAwaitingRefreshAfterSave] = useState(false); const lastAgentRef = useRef(agent); @@ -1377,9 +1379,17 @@ function ConfigurationTab({ queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agent.id) }); queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agent.urlKey) }); queryClient.invalidateQueries({ queryKey: queryKeys.agents.configRevisions(agent.id) }); + queryClient.invalidateQueries({ queryKey: queryKeys.agents.list(agent.companyId) }); }, - onError: () => { + onError: (err) => { setAwaitingRefreshAfterSave(false); + const message = + err instanceof ApiError + ? err.message + : err instanceof Error + ? err.message + : "Could not save agent"; + pushToast({ title: "Save failed", body: message, tone: "error" }); }, }); diff --git a/ui/src/pages/NewAgent.tsx b/ui/src/pages/NewAgent.tsx index 364e35a0..e9488ba2 100644 --- a/ui/src/pages/NewAgent.tsx +++ b/ui/src/pages/NewAgent.tsx @@ -12,13 +12,13 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; -import { Shield, User } from "lucide-react"; +import { Shield } from "lucide-react"; import { cn, agentUrl } from "../lib/utils"; import { roleLabels } from "../components/agent-config-primitives"; import { AgentConfigForm, type CreateConfigValues } from "../components/AgentConfigForm"; import { defaultCreateValues } from "../components/agent-config-defaults"; import { getUIAdapter } from "../adapters"; -import { AgentIcon } from "../components/AgentIconPicker"; +import { ReportsToPicker } from "../components/ReportsToPicker"; import { DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX, DEFAULT_CODEX_LOCAL_MODEL, @@ -66,10 +66,9 @@ export function NewAgent() { const [name, setName] = useState(""); const [title, setTitle] = useState(""); const [role, setRole] = useState("general"); - const [reportsTo, setReportsTo] = useState(""); + const [reportsTo, setReportsTo] = useState(null); const [configValues, setConfigValues] = useState(defaultCreateValues); const [roleOpen, setRoleOpen] = useState(false); - const [reportsToOpen, setReportsToOpen] = useState(false); const [formError, setFormError] = useState(null); const { data: agents } = useQuery({ @@ -173,7 +172,7 @@ export function NewAgent() { name: name.trim(), role: effectiveRole, ...(title.trim() ? { title: title.trim() } : {}), - ...(reportsTo ? { reportsTo } : {}), + ...(reportsTo != null ? { reportsTo } : {}), adapterType: configValues.adapterType, adapterConfig: buildAdapterConfig(), runtimeConfig: { @@ -189,8 +188,6 @@ export function NewAgent() { }); } - const currentReportsTo = (agents ?? []).find((a) => a.id === reportsTo); - return ( @@ -253,54 +250,12 @@ export function NewAgent() { - - - - {currentReportsTo ? ( - <> - - {`Reports to ${currentReportsTo.name}`} - > - ) : ( - <> - - {isFirstAgent ? "Reports to: N/A (CEO)" : "Reports to..."} - > - )} - - - - { setReportsTo(""); setReportsToOpen(false); }} - > - No manager - - {(agents ?? []).map((a) => ( - { setReportsTo(a.id); setReportsToOpen(false); }} - > - - {a.name} - {roleLabels[a.role] ?? a.role} - - ))} - - + {/* Shared config form */}