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 (
onEdit({ model: value, modelPlanId: "" })} /> ({ label: mode.label, value: mode.id }))} pending={detailPending} pendingLabel={labels.assignmentOptionsLoading} tone={permissionModeAssignmentTone(permissionValue)} value={permissionValue} onChange={(value) => onEdit({ permissionModeId: value })} /> onEdit({ reasoningEffort: value })} />
); } function AssignmentSelectTrigger({ fieldLabel, pending = false, pendingLabel, tone, value }: { fieldLabel: string; pending?: boolean; pendingLabel?: string; tone?: "accent" | "success" | "warning" | undefined; value: string | null; }): React.JSX.Element { const placeholder = pending && pendingLabel ? pendingLabel : fieldLabel; return ( {value ?? placeholder} ); } function AssignmentSelectContent({ children }: { children: React.ReactNode; }): React.JSX.Element { return ( {children} ); } function AssignmentSelectItem({ children, value }: { children: React.ReactNode; value: string; }): React.JSX.Element { return ( {children} ); } function AssignmentValueSelect({ disabled, fieldLabel, notSpecifiedLabel, onChange, options, pending, pendingLabel, tone, value }: { disabled: boolean; fieldLabel: string; notSpecifiedLabel: string; onChange(value: string): void; options: readonly { label: string; value: string }[]; pending: boolean; pendingLabel: string; tone?: "accent" | "success" | "warning" | undefined; value: string; }): React.JSX.Element { const known = options.some((option) => option.value === value); const selectedLabel = assignmentOptionLabel(options, value); return ( ); }