import { useCallback, useEffect, useMemo, useState } from "react"; import { WORKSPACE_BRANCH_ROUTINE_VARIABLE, type Agent, type ExecutionWorkspace, type ExecutionWorkspaceMode, type IssueExecutionWorkspaceSettings, type Project, type RoutineVariable, } from "@paperclipai/shared"; import { useQuery } from "@tanstack/react-query"; import { instanceSettingsApi } from "../api/instanceSettings"; import { queryKeys } from "../lib/queryKeys"; import { IssueWorkspaceCard } from "./IssueWorkspaceCard"; import { AgentIcon } from "./AgentIconPicker"; import { InlineEntitySelector, type InlineEntityOption } from "./InlineEntitySelector"; import { getRecentAssigneeIds, sortAgentsByRecency, trackRecentAssignee } from "../lib/recent-assignees"; import { getRecentProjectIds, trackRecentProject } from "../lib/recent-projects"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; function buildInitialValues(variables: RoutineVariable[]) { return Object.fromEntries(variables.map((variable) => [variable.name, variable.defaultValue ?? ""])); } function buildInitialRunSelection(input: { defaultAssigneeAgentId?: string | null; defaultProjectId?: string | null; }) { return { assigneeAgentId: input.defaultAssigneeAgentId ?? "", projectId: input.defaultProjectId ?? "", }; } function defaultProjectWorkspaceIdForProject(project: Project | null | undefined) { if (!project) return null; return project.executionWorkspacePolicy?.defaultProjectWorkspaceId ?? project.workspaces?.find((workspace) => workspace.isPrimary)?.id ?? project.workspaces?.[0]?.id ?? null; } function defaultExecutionWorkspaceModeForProject(project: Project | null | undefined): ExecutionWorkspaceMode { const defaultMode = project?.executionWorkspacePolicy?.enabled ? project.executionWorkspacePolicy.defaultMode : null; if ( defaultMode === "isolated_workspace" || defaultMode === "operator_branch" || defaultMode === "adapter_default" ) { return defaultMode === "adapter_default" ? "agent_default" : defaultMode; } return "shared_workspace"; } function issueModeForExistingWorkspace(mode: string | null | undefined): ExecutionWorkspaceMode { if (mode === "isolated_workspace" || mode === "operator_branch" || mode === "shared_workspace") return mode; if (mode === "adapter_managed" || mode === "cloud_sandbox") return "agent_default"; return "shared_workspace"; } function issueWorkspacePreferenceFromDraft(value: unknown, fallback: ExecutionWorkspaceMode): ExecutionWorkspaceMode { if ( value === "inherit" || value === "shared_workspace" || value === "isolated_workspace" || value === "operator_branch" || value === "reuse_existing" || value === "agent_default" ) { return value; } return fallback; } type RoutineRunWorkspaceConfig = { executionWorkspaceId: string | null; executionWorkspacePreference: ExecutionWorkspaceMode; executionWorkspaceSettings: IssueExecutionWorkspaceSettings; projectWorkspaceId: string | null; }; function buildInitialWorkspaceConfig( project: Project | null | undefined, defaultExecutionWorkspace?: ExecutionWorkspace | null, ): RoutineRunWorkspaceConfig { if (defaultExecutionWorkspace && defaultExecutionWorkspace.projectId === project?.id) { return { executionWorkspaceId: defaultExecutionWorkspace.id, executionWorkspacePreference: "reuse_existing", executionWorkspaceSettings: { mode: issueModeForExistingWorkspace(defaultExecutionWorkspace.mode), }, projectWorkspaceId: defaultExecutionWorkspace.projectWorkspaceId ?? defaultProjectWorkspaceIdForProject(project), }; } const defaultMode = defaultExecutionWorkspaceModeForProject(project); return { executionWorkspaceId: null as string | null, executionWorkspacePreference: defaultMode, executionWorkspaceSettings: { mode: defaultMode }, projectWorkspaceId: defaultProjectWorkspaceIdForProject(project), }; } function workspaceConfigEquals( a: RoutineRunWorkspaceConfig, b: RoutineRunWorkspaceConfig, ) { return a.executionWorkspaceId === b.executionWorkspaceId && a.executionWorkspacePreference === b.executionWorkspacePreference && a.projectWorkspaceId === b.projectWorkspaceId && JSON.stringify(a.executionWorkspaceSettings ?? null) === JSON.stringify(b.executionWorkspaceSettings ?? null); } function applyWorkspaceDraft( current: RoutineRunWorkspaceConfig, data: Record, ) { const next = { ...current, executionWorkspaceId: (data.executionWorkspaceId as string | null | undefined) ?? null, executionWorkspacePreference: issueWorkspacePreferenceFromDraft( data.executionWorkspacePreference, current.executionWorkspacePreference, ), executionWorkspaceSettings: (data.executionWorkspaceSettings as IssueExecutionWorkspaceSettings | null | undefined) ?? current.executionWorkspaceSettings, }; return workspaceConfigEquals(current, next) ? current : next; } function isMissingRequiredValue(value: unknown) { return value == null || (typeof value === "string" && value.trim().length === 0); } function supportsRoutineRunWorkspaceSelection( project: Project | null | undefined, isolatedWorkspacesEnabled: boolean, ) { return isolatedWorkspacesEnabled && Boolean(project?.executionWorkspacePolicy?.enabled); } export function routineRunNeedsConfiguration(input: { variables: RoutineVariable[]; project: Project | null | undefined; isolatedWorkspacesEnabled: boolean; }) { return input.variables.length > 0 || supportsRoutineRunWorkspaceSelection(input.project, input.isolatedWorkspacesEnabled); } export interface RoutineRunDialogSubmitData { variables?: Record; assigneeAgentId?: string | null; projectId?: string | null; executionWorkspaceId?: string | null; executionWorkspacePreference?: string | null; executionWorkspaceSettings?: IssueExecutionWorkspaceSettings | null; } export function RoutineRunVariablesDialog({ open, onOpenChange, companyId, routineName, projects, agents, defaultProjectId, defaultAssigneeAgentId, defaultExecutionWorkspace, variables, isPending, onSubmit, }: { open: boolean; onOpenChange: (open: boolean) => void; companyId: string | null | undefined; routineName?: string | null; projects: Project[]; agents: Agent[]; defaultProjectId?: string | null; defaultAssigneeAgentId?: string | null; defaultExecutionWorkspace?: ExecutionWorkspace | null; variables: RoutineVariable[]; isPending: boolean; onSubmit: (data: RoutineRunDialogSubmitData) => void; }) { const [values, setValues] = useState>({}); const [selection, setSelection] = useState(() => buildInitialRunSelection({ defaultAssigneeAgentId, defaultProjectId, })); const selectedProject = useMemo( () => projects.find((project) => project.id === selection.projectId) ?? null, [projects, selection.projectId], ); const recentAssigneeIds = useMemo(() => getRecentAssigneeIds(), [open]); const recentProjectIds = useMemo(() => getRecentProjectIds(), [open]); const assigneeOptions = useMemo( () => sortAgentsByRecency( agents.filter((agent) => agent.status !== "terminated"), recentAssigneeIds, ).map((agent) => ({ id: agent.id, label: agent.name, searchText: `${agent.name} ${agent.role} ${agent.title ?? ""}`, })), [agents, recentAssigneeIds], ); const projectOptions = useMemo( () => projects.map((project) => ({ id: project.id, label: project.name, searchText: project.description ?? "", })), [projects], ); const currentAssignee = selection.assigneeAgentId ? agents.find((agent) => agent.id === selection.assigneeAgentId) ?? null : null; const [workspaceConfig, setWorkspaceConfig] = useState(() => buildInitialWorkspaceConfig(selectedProject, defaultExecutionWorkspace)); const [workspaceConfigValid, setWorkspaceConfigValid] = useState(true); const [workspaceBranchName, setWorkspaceBranchName] = useState(null); const { data: experimentalSettings } = useQuery({ queryKey: queryKeys.instance.experimentalSettings, queryFn: () => instanceSettingsApi.getExperimental(), retry: false, }); const workspaceSelectionEnabled = supportsRoutineRunWorkspaceSelection( selectedProject, experimentalSettings?.enableIsolatedWorkspaces === true, ); useEffect(() => { if (!open) return; setValues(buildInitialValues(variables)); const nextSelection = buildInitialRunSelection({ defaultAssigneeAgentId, defaultProjectId }); setSelection(nextSelection); setWorkspaceConfig(buildInitialWorkspaceConfig( projects.find((project) => project.id === nextSelection.projectId) ?? null, defaultExecutionWorkspace, )); setWorkspaceConfigValid(true); setWorkspaceBranchName(defaultExecutionWorkspace?.branchName ?? null); }, [defaultAssigneeAgentId, defaultExecutionWorkspace, defaultProjectId, open, projects, variables]); const workspaceBranchAutoValue = workspaceSelectionEnabled && workspaceBranchName ? workspaceBranchName : null; const isAutoWorkspaceBranchVariable = useCallback( (variable: RoutineVariable) => variable.name === WORKSPACE_BRANCH_ROUTINE_VARIABLE && Boolean(workspaceBranchAutoValue), [workspaceBranchAutoValue], ); const missingRequired = useMemo( () => variables .filter((variable) => variable.required) .filter((variable) => !isAutoWorkspaceBranchVariable(variable)) .filter((variable) => isMissingRequiredValue(values[variable.name])) .map((variable) => variable.label || variable.name), [isAutoWorkspaceBranchVariable, values, variables], ); const workspaceIssue = useMemo(() => ({ companyId: companyId ?? null, projectId: selectedProject?.id ?? null, projectWorkspaceId: workspaceConfig.projectWorkspaceId, executionWorkspaceId: workspaceConfig.executionWorkspaceId, executionWorkspacePreference: workspaceConfig.executionWorkspacePreference, executionWorkspaceSettings: workspaceConfig.executionWorkspaceSettings, currentExecutionWorkspace: workspaceConfig.executionWorkspaceId && workspaceConfig.executionWorkspaceId === defaultExecutionWorkspace?.id ? defaultExecutionWorkspace : null, }), [ companyId, defaultExecutionWorkspace, selectedProject?.id, workspaceConfig.executionWorkspaceId, workspaceConfig.executionWorkspacePreference, workspaceConfig.executionWorkspaceSettings, workspaceConfig.projectWorkspaceId, ]); const canSubmit = selection.assigneeAgentId.trim().length > 0 && missingRequired.length === 0 && (!workspaceSelectionEnabled || workspaceConfigValid); const handleWorkspaceUpdate = useCallback((data: Record) => { setWorkspaceConfig((current) => applyWorkspaceDraft(current, data)); }, []); const handleWorkspaceDraftChange = useCallback(( data: Record, meta: { canSave: boolean; workspaceBranchName?: string | null }, ) => { setWorkspaceConfig((current) => applyWorkspaceDraft(current, data)); setWorkspaceConfigValid((current) => (current === meta.canSave ? current : meta.canSave)); setWorkspaceBranchName((current) => { const defaultWorkspaceBranchName = defaultExecutionWorkspace?.branchName ?? null; const next = meta.workspaceBranchName ?? (data.executionWorkspaceId === defaultExecutionWorkspace?.id ? defaultWorkspaceBranchName : null) ?? null; return current === next ? current : next; }); }, [defaultExecutionWorkspace]); return ( !isPending && onOpenChange(next)}> {routineName && (

{routineName}

)} Run routine Choose the agent and optional project for this one run. Routine defaults are prefilled and won't be changed.
{ if (assigneeAgentId) trackRecentAssignee(assigneeAgentId); setSelection((current) => ({ ...current, assigneeAgentId })); }} renderTriggerValue={(option) => option ? ( currentAssignee ? ( <> {option.label} ) : ( {option.label} ) ) : ( Select an agent ) } renderOption={(option) => { if (!option.id) return {option.label}; const assignee = agents.find((agent) => agent.id === option.id); return ( <> {assignee ? : null} {option.label} ); }} />
{ const project = projects.find((entry) => entry.id === projectId) ?? null; if (projectId) trackRecentProject(projectId); setSelection((current) => ({ ...current, projectId })); setWorkspaceConfig(buildInitialWorkspaceConfig(project, defaultExecutionWorkspace)); setWorkspaceConfigValid(true); setWorkspaceBranchName( defaultExecutionWorkspace && defaultExecutionWorkspace.projectId === project?.id ? defaultExecutionWorkspace.branchName : null, ); }} renderTriggerValue={(option) => option && selectedProject ? ( <> {option.label} ) : ( No project ) } renderOption={(option) => { if (!option.id) return {option.label}; const project = projects.find((entry) => entry.id === option.id); return ( <> {option.label} ); }} />
{variables.map((variable) => (
{isAutoWorkspaceBranchVariable(variable) ? ( ) : variable.type === "textarea" ? (