Files
PaperClipAI/ui/src/components/RoutineRunVariablesDialog.tsx
T
DottaandPaperclip 109d81db4f Add task workspace picker to properties pane (#12693)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The task properties pane lets an operator change the settings for
one task.
> - The pane did not let an operator select the execution workspace for
the next run.
> - The existing workspace card had selection rules that another control
could copy by mistake.
> - This pull request adds one shared selection module and one compact
property picker.
> - The picker keeps the current workspace active until the next run.
> - The benefit is a clear workspace choice in the task properties pane
without an API change.

## Linked Issues or Issue Description

**Subsystem affected**

ui/ — React + Vite board UI

**Problem or motivation**

An operator cannot set the execution workspace from the new task
properties pane. The existing task workspace card also owns selection
rules that a second control could copy and change over time.

**Proposed solution**

Add a compact workspace property picker. Put the shared selection and
update rules in one UI module. Show the picker only when isolated
workspaces and the project workspace policy are enabled.

**Alternatives considered**

The existing workspace card could remain the only control. This would
leave the new task interface incomplete. The picker could also copy the
card logic, but that would create two sources of truth.

**Roadmap alignment**

This is a small UI improvement for existing workspace controls. It does
not duplicate a planned item in `ROADMAP.md`.

Related workspace work: Refs #12682. That pull request changes runner
recovery and other workspace controls. It does not add this task
property picker.

## What Changed

- Added shared helpers for the current workspace selection and its issue
update payload.
- Updated the existing workspace card to use the shared helpers without
changing its project-default behavior.
- Added a gated workspace property picker with mode and workspace search
steps.
- Added unit and component tests for visibility, selection payloads,
search, and workspace reuse.
- Rebased onto the upstream native-run teardown fix so CI drains
background heartbeat writes before PostgreSQL cleanup.

## Verification

- `NODE_ENV=test pnpm --filter @paperclipai/ui exec vitest run
src/lib/issue-workspace-selection.test.ts
src/components/IssueProperties.test.tsx
src/components/RoutineRunVariablesDialog.test.tsx` — 74 tests passed.
- `pnpm --filter @paperclipai/ui typecheck` — passed.
- `pnpm check:token-gates` — passed.
- `pnpm -r typecheck` — passed.
- `pnpm build` — passed.
- `NODE_ENV=test pnpm --filter @paperclipai/server exec vitest run
src/services/native-runtime/native-question-bridge.test.ts` — 8 tests
passed on the rebased head. The upstream teardown drain prevents the
prior PostgreSQL cleanup deadlock.

## Risks

- Low risk. This is a gated UI-only change.
- A wrong selection payload could change the next workspace mode. Exact
payload tests cover every mode.
- The old card must keep its existing project-default payload. A
shared-helper test covers that payload.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex with GPT-5. The exact serving revision and context window
are not exposed. The agent used reasoning, tool use, and code execution.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-09-02 08:19:09 -05:00

595 lines
23 KiB
TypeScript

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<string, unknown>,
) {
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 shouldUseDateInput(variable: RoutineVariable) {
return variable.type === "date";
}
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<string, string | number | boolean>;
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<Record<string, unknown>>({});
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<InlineEntityOption[]>(
() =>
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<InlineEntityOption[]>(
() => 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<string | null>(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<string, unknown>) => {
setWorkspaceConfig((current) => applyWorkspaceDraft(current, data));
}, []);
const handleWorkspaceDraftChange = useCallback((
data: Record<string, unknown> | null,
meta: { canSave: boolean; workspaceBranchName?: string | null },
) => {
if (data) {
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 (
<Dialog open={open} onOpenChange={(next) => !isPending && onOpenChange(next)}>
<DialogContent className="flex h-(--sz-calc-18) max-h-(--sz-calc-18) max-w-xl flex-col gap-0 overflow-hidden p-0 sm:h-auto sm:max-h-(--sz-calc-20)">
<DialogHeader className="shrink-0 border-b border-border/60 px-6 pb-4 pr-12 pt-6">
{routineName && (
<p className="text-muted-foreground text-sm">{routineName}</p>
)}
<DialogTitle>Run routine</DialogTitle>
<DialogDescription>
Choose the agent and optional project for this one run. Routine defaults are prefilled and won&apos;t be changed.
</DialogDescription>
</DialogHeader>
<div className="min-h-0 flex-1 space-y-4 overflow-y-auto overscroll-contain px-6 py-4">
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-1.5">
<Label className="text-xs">Agent *</Label>
<InlineEntitySelector
value={selection.assigneeAgentId}
options={assigneeOptions}
recentOptionIds={recentAssigneeIds}
placeholder="Agent"
noneLabel="Select an agent"
searchPlaceholder="Search agents..."
emptyMessage="No agents found."
disablePortal
openOnFocus={false}
onChange={(assigneeAgentId) => {
if (assigneeAgentId) trackRecentAssignee(assigneeAgentId);
setSelection((current) => ({ ...current, assigneeAgentId }));
}}
renderTriggerValue={(option) =>
option ? (
currentAssignee ? (
<>
<AgentIcon icon={currentAssignee.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<span className="truncate">{option.label}</span>
</>
) : (
<span className="truncate">{option.label}</span>
)
) : (
<span className="text-muted-foreground">Select an agent</span>
)
}
renderOption={(option) => {
if (!option.id) return <span className="truncate">{option.label}</span>;
const assignee = agents.find((agent) => agent.id === option.id);
return (
<>
{assignee ? <AgentIcon icon={assignee.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /> : null}
<span className="truncate">{option.label}</span>
</>
);
}}
/>
</div>
<div className="space-y-1.5">
<Label className="text-xs">Project</Label>
<InlineEntitySelector
value={selection.projectId}
options={projectOptions}
recentOptionIds={recentProjectIds}
placeholder="Project"
noneLabel="No project"
searchPlaceholder="Search projects..."
emptyMessage="No projects found."
disablePortal
openOnFocus={false}
onChange={(projectId) => {
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 ? (
<>
<span
className="h-3.5 w-3.5 shrink-0 rounded-sm"
style={{ backgroundColor: selectedProject.color ?? "var(--project-none)" }}
/>
<span className="truncate">{option.label}</span>
</>
) : (
<span className="text-muted-foreground">No project</span>
)
}
renderOption={(option) => {
if (!option.id) return <span className="truncate">{option.label}</span>;
const project = projects.find((entry) => entry.id === option.id);
return (
<>
<span
className="h-3.5 w-3.5 shrink-0 rounded-sm"
style={{ backgroundColor: project?.color ?? "var(--project-none)" }}
/>
<span className="truncate">{option.label}</span>
</>
);
}}
/>
</div>
</div>
{variables.map((variable) => (
<div key={variable.name} className="space-y-1.5">
<Label className="text-xs">
{variable.label || variable.name}
{variable.required ? " *" : ""}
</Label>
{isAutoWorkspaceBranchVariable(variable) ? (
<Input
readOnly
disabled
value={workspaceBranchAutoValue ?? ""}
/>
) : variable.type === "textarea" ? (
<Textarea
rows={4}
value={typeof values[variable.name] === "string" ? values[variable.name] as string : ""}
onChange={(event) => setValues((current) => ({ ...current, [variable.name]: event.target.value }))}
/>
) : variable.type === "boolean" ? (
<Select
value={values[variable.name] === true ? "true" : values[variable.name] === false ? "false" : "__unset__"}
onValueChange={(next) => setValues((current) => ({
...current,
[variable.name]: next === "__unset__" ? "" : next === "true",
}))}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="__unset__">No value</SelectItem>
<SelectItem value="true">True</SelectItem>
<SelectItem value="false">False</SelectItem>
</SelectContent>
</Select>
) : variable.type === "select" ? (
<Select
value={typeof values[variable.name] === "string" && values[variable.name] ? values[variable.name] as string : "__unset__"}
onValueChange={(next) => setValues((current) => ({
...current,
[variable.name]: next === "__unset__" ? "" : next,
}))}
>
<SelectTrigger>
<SelectValue placeholder="Choose a value" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__unset__">No value</SelectItem>
{variable.options.map((option) => (
<SelectItem key={option} value={option}>{option}</SelectItem>
))}
</SelectContent>
</Select>
) : shouldUseDateInput(variable) ? (
<Input
type="date"
value={values[variable.name] == null ? "" : String(values[variable.name])}
onChange={(event) => setValues((current) => ({ ...current, [variable.name]: event.target.value }))}
/>
) : (
<Input
type={variable.type === "number" ? "number" : "text"}
value={values[variable.name] == null ? "" : String(values[variable.name])}
onChange={(event) => setValues((current) => ({ ...current, [variable.name]: event.target.value }))}
/>
)}
</div>
))}
{workspaceSelectionEnabled && selectedProject && companyId ? (
<IssueWorkspaceCard
key={`${open ? "open" : "closed"}:${selectedProject.id}`}
issue={workspaceIssue}
project={selectedProject}
initialEditing
livePreview
onUpdate={handleWorkspaceUpdate}
onDraftChange={handleWorkspaceDraftChange}
/>
) : null}
</div>
<DialogFooter
showCloseButton={false}
className="shrink-0 border-t border-border/60 bg-background px-6 pb-(--sz-calc-19) pt-4"
>
{!selection.assigneeAgentId ? (
<p className="mr-auto text-xs text-amber-600">Default agent required for this run.</p>
) : missingRequired.length > 0 ? (
<p className="mr-auto text-xs text-amber-600">
Missing: {missingRequired.join(", ")}
</p>
) : workspaceSelectionEnabled && !workspaceConfigValid ? (
<p className="mr-auto text-xs text-amber-600">
Choose an existing workspace before running.
</p>
) : (
<span className="mr-auto" />
)}
<Button variant="ghost" onClick={() => onOpenChange(false)} disabled={isPending}>
Cancel
</Button>
<Button
onClick={() => {
const nextVariables: Record<string, string | number | boolean> = {};
for (const variable of variables) {
if (isAutoWorkspaceBranchVariable(variable)) {
nextVariables[variable.name] = workspaceBranchAutoValue!;
continue;
}
const rawValue = values[variable.name];
if (isMissingRequiredValue(rawValue)) continue;
if (variable.type === "number") {
nextVariables[variable.name] = Number(rawValue);
} else if (variable.type === "boolean") {
nextVariables[variable.name] = rawValue === true;
} else {
nextVariables[variable.name] = String(rawValue);
}
}
onSubmit({
variables: nextVariables,
assigneeAgentId: selection.assigneeAgentId,
projectId: selection.projectId || null,
...(workspaceSelectionEnabled
? {
executionWorkspaceId: workspaceConfig.executionWorkspaceId,
executionWorkspacePreference: workspaceConfig.executionWorkspacePreference,
executionWorkspaceSettings: workspaceConfig.executionWorkspaceSettings,
}
: {}),
});
}}
disabled={isPending || !canSubmit}
>
{isPending ? "Running..." : "Run routine"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}