import { CheckCheck, Split } from "lucide-react"; import { Select, SelectContent, SelectItem, SelectTrigger, Switch, cn } from "@tutti-os/ui-system"; import composerStyles from "../../agent-gui/agentGuiNode/AgentGUINode.styles"; import type { TuttiModePlanAssignmentCatalog } from "./useTuttiModePlanPanels"; import type { TuttiModePlanPanelTaskViewModel } from "./tuttiModePlanPanelProjection"; import { effectiveTaskAssignmentValue, effectiveTaskFlag, type TuttiModePlanTaskAssignmentDraft } from "./tuttiModePlanTaskAssignments"; export interface TuttiModePlanTaskAssignmentEditorLabels { agentTarget: string; model: string; permissionMode: string; reasoningEffort: string; parallelizable: string; autoAccept: string; notSpecified: string; assignmentOptionsLoading: string; } const CLEAR_VALUE = "__tutti_plan_assignment_clear__"; function toSelectValue(value: string): string { return value === "" ? CLEAR_VALUE : value; } function fromSelectValue(value: string): string { return value === CLEAR_VALUE ? "" : value; } /** * Mirrors the composer permission-mode trigger tones so the plan panel's * permission controls pick up the same accent/success/warning colors. */ export function permissionModeAssignmentTone( value: string | null | undefined ): "accent" | "success" | "warning" | undefined { switch (value?.trim().toLowerCase().replace(/\s+/g, "-") || undefined) { case "read-only": case "readonly": case "ask-for-approval": return "success"; case "auto": case "default": case "accept-edits": case "acceptedits": return "accent"; case "full-access": case "bypasspermissions": return "warning"; default: return undefined; } } export function assignmentOptionLabel( options: readonly { label: string; value: string }[], value: string | null | undefined ): string | null { const normalizedValue = value?.trim() ?? ""; if (!normalizedValue) return null; return ( options.find((option) => option.value === normalizedValue)?.label ?? normalizedValue ); } /** * Per-task assignment selectors for the single review checkpoint, rendered as * one composer-styled row. Option catalogs are agent-scoped and supplied by * the host; the editor only chooses among them and reports draft edits upward. * * Model plans are deliberately not exposed here: a plan enters orchestration * by configuring an agent target bound to it, so the row offers only agent / * model / permission mode / reasoning effort. An explicit model choice clears * any document-stamped plan pin so the two can never contradict. */ export function TuttiModePlanTaskAssignmentEditor({ catalog, disabled, draft, labels, onEdit, task }: { catalog: TuttiModePlanAssignmentCatalog; disabled: boolean; draft: TuttiModePlanTaskAssignmentDraft; labels: TuttiModePlanTaskAssignmentEditorLabels; onEdit(patch: TuttiModePlanTaskAssignmentDraft): void; task: TuttiModePlanPanelTaskViewModel; }): React.JSX.Element { const agentValue = effectiveTaskAssignmentValue( draft.agentTargetId, task.agentTargetId ); const modelValue = effectiveTaskAssignmentValue(draft.model, task.model); const permissionValue = effectiveTaskAssignmentValue( draft.permissionModeId, task.permissionModeId ); const effortValue = effectiveTaskAssignmentValue( draft.reasoningEffort, task.reasoningEffort ); const parallelizable = effectiveTaskFlag( draft.parallelizable, task.parallelizable ); const autoAccept = effectiveTaskFlag(draft.autoAccept, task.autoAccept); // Options for document-referenced agents are preloaded by the panels hook // alongside the snapshot refresh; user-driven agent changes trigger the // (deduplicated) load from the change handler, so no component effect is // needed here. const agentDetail = agentValue ? (catalog.optionsByAgentId[agentValue] ?? null) : null; const modelOptions = agentDetail?.models ?? []; const detailPending = Boolean(agentValue) && !agentDetail; const agentLabel = agentValue ? ((catalog.agents ?? []).find( (agent) => agent.agentTargetId === agentValue )?.label ?? agentValue) : null; return (