feat: add ReportsToPicker for agent management

- Introduced ReportsToPicker component in AgentConfigForm and NewAgent pages to allow selection of an agent's manager.
- Updated organizational structure documentation to reflect the ability to change an agent's manager post-creation.
- Enhanced error handling in ConfigurationTab to provide user feedback on save failures.
This commit is contained in:
Daniel Sousa 2026-03-20 20:06:19 +00:00
parent dfdd3784b9
commit 61f53b6471
No known key found for this signature in database
5 changed files with 134 additions and 56 deletions

View file

@ -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" });
},
});

View file

@ -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<string | null>(null);
const [configValues, setConfigValues] = useState<CreateConfigValues>(defaultCreateValues);
const [roleOpen, setRoleOpen] = useState(false);
const [reportsToOpen, setReportsToOpen] = useState(false);
const [formError, setFormError] = useState<string | null>(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 (
<div className="mx-auto max-w-2xl space-y-6">
<div>
@ -253,54 +250,12 @@ export function NewAgent() {
</PopoverContent>
</Popover>
<Popover open={reportsToOpen} onOpenChange={setReportsToOpen}>
<PopoverTrigger asChild>
<button
className={cn(
"inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs hover:bg-accent/50 transition-colors",
isFirstAgent && "opacity-60 cursor-not-allowed"
)}
disabled={isFirstAgent}
>
{currentReportsTo ? (
<>
<AgentIcon icon={currentReportsTo.icon} className="h-3 w-3 text-muted-foreground" />
{`Reports to ${currentReportsTo.name}`}
</>
) : (
<>
<User className="h-3 w-3 text-muted-foreground" />
{isFirstAgent ? "Reports to: N/A (CEO)" : "Reports to..."}
</>
)}
</button>
</PopoverTrigger>
<PopoverContent className="w-48 p-1" align="start">
<button
className={cn(
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
!reportsTo && "bg-accent"
)}
onClick={() => { setReportsTo(""); setReportsToOpen(false); }}
>
No manager
</button>
{(agents ?? []).map((a) => (
<button
key={a.id}
className={cn(
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50 truncate",
a.id === reportsTo && "bg-accent"
)}
onClick={() => { setReportsTo(a.id); setReportsToOpen(false); }}
>
<AgentIcon icon={a.icon} className="shrink-0 h-3 w-3 text-muted-foreground" />
{a.name}
<span className="text-muted-foreground ml-auto">{roleLabels[a.role] ?? a.role}</span>
</button>
))}
</PopoverContent>
</Popover>
<ReportsToPicker
agents={agents ?? []}
value={reportsTo}
onChange={setReportsTo}
disabled={isFirstAgent}
/>
</div>
{/* Shared config form */}