diff --git a/packages/core/types/autopilot.ts b/packages/core/types/autopilot.ts index 4e0a0022b..79b85934e 100644 --- a/packages/core/types/autopilot.ts +++ b/packages/core/types/autopilot.ts @@ -2,6 +2,12 @@ export type AutopilotStatus = "active" | "paused" | "archived"; export type AutopilotExecutionMode = "create_issue" | "run_only"; +// `assignee_type` selects which polymorphic actor backs the autopilot: +// "agent" → assignee_id references agent(id); "squad" → assignee_id references +// squad(id) and dispatch resolves to squad.leader_id at run time (MUL-2429, +// Path A). Older servers omit this field — callers should default to "agent". +export type AutopilotAssigneeType = "agent" | "squad"; + export type AutopilotTriggerKind = "schedule" | "webhook" | "api"; // `skipped` is emitted by the backend pre-flight admission check @@ -22,6 +28,7 @@ export interface Autopilot { workspace_id: string; title: string; description: string | null; + assignee_type: AutopilotAssigneeType; assignee_id: string; status: AutopilotStatus; execution_mode: AutopilotExecutionMode; @@ -75,6 +82,9 @@ export interface AutopilotRun { export interface CreateAutopilotRequest { title: string; description?: string; + // Optional on the wire — when omitted the server defaults to "agent" so + // older clients keep working. + assignee_type?: AutopilotAssigneeType; assignee_id: string; execution_mode: AutopilotExecutionMode; issue_title_template?: string; @@ -83,6 +93,9 @@ export interface CreateAutopilotRequest { export interface UpdateAutopilotRequest { title?: string; description?: string | null; + // Send `assignee_type` together with `assignee_id` whenever you change the + // assignee — the server requires both for a type swap. + assignee_type?: AutopilotAssigneeType; assignee_id?: string; status?: AutopilotStatus; execution_mode?: AutopilotExecutionMode; diff --git a/packages/core/types/index.ts b/packages/core/types/index.ts index c8dbb3e67..d5dcd0045 100644 --- a/packages/core/types/index.ts +++ b/packages/core/types/index.ts @@ -90,6 +90,7 @@ export type { Autopilot, AutopilotStatus, AutopilotExecutionMode, + AutopilotAssigneeType, AutopilotTrigger, AutopilotTriggerKind, AutopilotRun, diff --git a/packages/views/autopilots/components/autopilot-detail-page.tsx b/packages/views/autopilots/components/autopilot-detail-page.tsx index e9e4aa095..9232ce75d 100644 --- a/packages/views/autopilots/components/autopilot-detail-page.tsx +++ b/packages/views/autopilots/components/autopilot-detail-page.tsx @@ -724,8 +724,16 @@ export function AutopilotDetailPage({ autopilotId }: { autopilotId: string }) {
- - {getActorName("agent", autopilot.assignee_id)} + + + {getActorName(autopilot.assignee_type, autopilot.assignee_id)} +
@@ -796,7 +804,7 @@ export function AutopilotDetailPage({ autopilotId }: { autopilotId: string }) { )} @@ -828,6 +836,7 @@ export function AutopilotDetailPage({ autopilotId }: { autopilotId: string }) { initial={{ title: autopilot.title, description: autopilot.description ?? "", + assignee_type: autopilot.assignee_type, assignee_id: autopilot.assignee_id, execution_mode: autopilot.execution_mode as AutopilotExecutionMode, }} diff --git a/packages/views/autopilots/components/autopilot-dialog.tsx b/packages/views/autopilots/components/autopilot-dialog.tsx index 82faa655f..ccb2b5263 100644 --- a/packages/views/autopilots/components/autopilot-dialog.tsx +++ b/packages/views/autopilots/components/autopilot-dialog.tsx @@ -37,7 +37,7 @@ import { TimeInput } from "@multica/ui/components/ui/time-input"; import { TimezonePicker } from "./pickers/timezone-picker"; import { useCurrentWorkspace } from "@multica/core/paths"; import { useWorkspaceId } from "@multica/core/hooks"; -import { agentListOptions } from "@multica/core/workspace/queries"; +import { agentListOptions, squadListOptions } from "@multica/core/workspace/queries"; import { useCreateAutopilot, useCreateAutopilotTrigger, @@ -47,12 +47,13 @@ import { import { buildAutopilotWebhookUrl } from "@multica/core/autopilots"; import { api } from "@multica/core/api"; import type { + AutopilotAssigneeType, AutopilotExecutionMode, AutopilotTrigger, } from "@multica/core/types"; import { TitleEditor, ContentEditor } from "../../editor"; import { ActorAvatar } from "../../common/actor-avatar"; -import { AgentPicker } from "./pickers/agent-picker"; +import { AgentPicker, type AssigneeSelection } from "./pickers/agent-picker"; import { getDefaultTriggerConfig, getLocalTimezone, @@ -71,6 +72,7 @@ import { formatSchedulePartialFailureToast } from "./autopilot-dialog-toast"; export interface AutopilotInitial { title: string; description: string; + assignee_type: AutopilotAssigneeType; assignee_id: string; execution_mode: AutopilotExecutionMode; } @@ -242,6 +244,7 @@ export function AutopilotDialog(props: AutopilotDialogProps) { const workspaceName = useCurrentWorkspace()?.name; const wsId = useWorkspaceId(); const { data: agents = [] } = useQuery(agentListOptions(wsId)); + const { data: squads = [] } = useQuery(squadListOptions(wsId)); const [isExpanded, setIsExpanded] = useState(false); const isCreate = props.mode === "create"; @@ -251,6 +254,9 @@ export function AutopilotDialog(props: AutopilotDialogProps) { const [title, setTitle] = useState(initial.title ?? ""); const [description, setDescription] = useState(initial.description ?? ""); + const [assigneeType, setAssigneeType] = useState( + initial.assignee_type ?? "agent", + ); const [assigneeId, setAssigneeId] = useState(initial.assignee_id ?? ""); const [executionMode, setExecutionMode] = useState( initial.execution_mode ?? "create_issue", @@ -296,10 +302,20 @@ export function AutopilotDialog(props: AutopilotDialogProps) { const triggerCount = isCreate ? 0 : props.triggers.length; const schedulePillDisabled = !isCreate && triggerCount >= 2; - const selectedAgent = useMemo( - () => agents.find((a) => a.id === assigneeId) ?? null, - [agents, assigneeId], - ); + const selectedAssignee = useMemo(() => { + if (!assigneeId) return null; + if (assigneeType === "squad") { + const squad = squads.find((s) => s.id === assigneeId); + return squad ? { name: squad.name, description: squad.description } : null; + } + const agent = agents.find((a) => a.id === assigneeId); + return agent ? { name: agent.name, description: agent.description } : null; + }, [agents, squads, assigneeId, assigneeType]); + + const handleAssigneeChange = (next: AssigneeSelection) => { + setAssigneeType(next.type); + setAssigneeId(next.id); + }; const createAutopilot = useCreateAutopilot(); const createTrigger = useCreateAutopilotTrigger(); @@ -324,6 +340,7 @@ export function AutopilotDialog(props: AutopilotDialogProps) { const autopilot = await createAutopilot.mutateAsync({ title: title.trim(), description: description.trim() || undefined, + assignee_type: assigneeType, assignee_id: assigneeId, execution_mode: executionMode, }); @@ -370,6 +387,7 @@ export function AutopilotDialog(props: AutopilotDialogProps) { id: props.autopilotId, title: title.trim(), description: description.trim() || null, + assignee_type: assigneeType, assignee_id: assigneeId, execution_mode: executionMode, }); @@ -548,10 +566,11 @@ export function AutopilotDialog(props: AutopilotDialogProps) { {/* Right: Configuration */}