feat(routines): add workspace-aware routine runs

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dotta 2026-04-02 11:38:57 -05:00
parent 36376968af
commit 909e8cd4c8
38 changed files with 15468 additions and 250 deletions

View file

@ -26,7 +26,12 @@ function issueModeForExistingWorkspace(mode: string | null | undefined) {
return "shared_workspace";
}
function shouldPresentExistingWorkspaceSelection(issue: Issue) {
function shouldPresentExistingWorkspaceSelection(issue: {
executionWorkspaceId: string | null;
executionWorkspacePreference: string | null;
executionWorkspaceSettings: Issue["executionWorkspaceSettings"];
currentExecutionWorkspace?: ExecutionWorkspace | null;
}) {
const persistedMode =
issue.currentExecutionWorkspace?.mode
?? issue.executionWorkspaceSettings?.mode
@ -156,19 +161,44 @@ function statusBadge(status: string) {
/* -------------------------------------------------------------------------- */
interface IssueWorkspaceCardProps {
issue: Issue;
issue: Omit<
Pick<
Issue,
| "companyId"
| "projectId"
| "projectWorkspaceId"
| "executionWorkspaceId"
| "executionWorkspacePreference"
| "executionWorkspaceSettings"
>,
"companyId"
> & {
companyId: string | null;
currentExecutionWorkspace?: ExecutionWorkspace | null;
};
project: { id: string; executionWorkspacePolicy?: { enabled?: boolean; defaultMode?: string | null; defaultProjectWorkspaceId?: string | null } | null; workspaces?: Array<{ id: string; isPrimary: boolean }> } | null;
onUpdate: (data: Record<string, unknown>) => void;
initialEditing?: boolean;
livePreview?: boolean;
onDraftChange?: (data: Record<string, unknown>, meta: { canSave: boolean }) => void;
}
export function IssueWorkspaceCard({ issue, project, onUpdate }: IssueWorkspaceCardProps) {
export function IssueWorkspaceCard({
issue,
project,
onUpdate,
initialEditing = false,
livePreview = false,
onDraftChange,
}: IssueWorkspaceCardProps) {
const { selectedCompanyId } = useCompany();
const companyId = issue.companyId ?? selectedCompanyId;
const [editing, setEditing] = useState(false);
const [editing, setEditing] = useState(initialEditing);
const { data: experimentalSettings } = useQuery({
queryKey: queryKeys.instance.experimentalSettings,
queryFn: () => instanceSettingsApi.getExperimental(),
retry: false,
});
const policyEnabled = experimentalSettings?.enableIsolatedWorkspaces === true
@ -209,13 +239,16 @@ export function IssueWorkspaceCard({ issue, project, onUpdate }: IssueWorkspaceC
?? workspace
?? null;
const currentSelection = shouldPresentExistingWorkspaceSelection(issue)
const configuredSelection = shouldPresentExistingWorkspaceSelection(issue)
? "reuse_existing"
: (
issue.executionWorkspacePreference
?? issue.executionWorkspaceSettings?.mode
?? defaultExecutionWorkspaceModeForProject(project)
);
const currentSelection = configuredSelection === "operator_branch" || configuredSelection === "agent_default"
? "shared_workspace"
: configuredSelection;
const [draftSelection, setDraftSelection] = useState(currentSelection);
const [draftExecutionWorkspaceId, setDraftExecutionWorkspaceId] = useState(issue.executionWorkspaceId ?? "");
@ -245,24 +278,33 @@ export function IssueWorkspaceCard({ issue, project, onUpdate }: IssueWorkspaceC
const canSaveWorkspaceConfig = draftSelection !== "reuse_existing" || draftExecutionWorkspaceId.length > 0;
const handleSave = useCallback(() => {
if (!canSaveWorkspaceConfig) return;
onUpdate({
executionWorkspacePreference: draftSelection,
executionWorkspaceId: draftSelection === "reuse_existing" ? draftExecutionWorkspaceId || null : null,
executionWorkspaceSettings: {
mode:
draftSelection === "reuse_existing"
? issueModeForExistingWorkspace(configuredReusableWorkspace?.mode)
: draftSelection,
},
});
setEditing(false);
}, [
canSaveWorkspaceConfig,
const buildWorkspaceDraftUpdate = useCallback(() => ({
executionWorkspacePreference: draftSelection,
executionWorkspaceId: draftSelection === "reuse_existing" ? draftExecutionWorkspaceId || null : null,
executionWorkspaceSettings: {
mode:
draftSelection === "reuse_existing"
? issueModeForExistingWorkspace(configuredReusableWorkspace?.mode)
: draftSelection,
},
}), [
configuredReusableWorkspace?.mode,
draftExecutionWorkspaceId,
draftSelection,
]);
useEffect(() => {
if (!onDraftChange) return;
onDraftChange(buildWorkspaceDraftUpdate(), { canSave: canSaveWorkspaceConfig });
}, [buildWorkspaceDraftUpdate, canSaveWorkspaceConfig, onDraftChange]);
const handleSave = useCallback(() => {
if (!canSaveWorkspaceConfig) return;
onUpdate(buildWorkspaceDraftUpdate());
setEditing(false);
}, [
buildWorkspaceDraftUpdate,
canSaveWorkspaceConfig,
onUpdate,
]);
@ -274,6 +316,8 @@ export function IssueWorkspaceCard({ issue, project, onUpdate }: IssueWorkspaceC
if (!policyEnabled || !project) return null;
const showEditingControls = livePreview || editing;
return (
<div className="rounded-lg border border-border p-3 space-y-2">
{/* Header row */}
@ -286,7 +330,7 @@ export function IssueWorkspaceCard({ issue, project, onUpdate }: IssueWorkspaceC
{workspace ? statusBadge(workspace.status) : statusBadge("idle")}
</div>
<div className="flex items-center gap-1">
{editing ? (
{!livePreview && editing ? (
<>
<Button
variant="ghost"
@ -305,7 +349,7 @@ export function IssueWorkspaceCard({ issue, project, onUpdate }: IssueWorkspaceC
Save
</Button>
</>
) : (
) : !livePreview ? (
<Button
variant="ghost"
size="sm"
@ -314,12 +358,12 @@ export function IssueWorkspaceCard({ issue, project, onUpdate }: IssueWorkspaceC
>
<Pencil className="h-3 w-3 mr-1" />Edit
</Button>
)}
) : null}
</div>
</div>
{/* Read-only info */}
{!editing && (
{!showEditingControls && (
<div className="space-y-1.5 text-xs">
{workspace?.branchName && (
<div className="flex items-center gap-1.5">
@ -377,7 +421,7 @@ export function IssueWorkspaceCard({ issue, project, onUpdate }: IssueWorkspaceC
)}
{/* Editing controls */}
{editing && (
{showEditingControls && (
<div className="space-y-2 pt-1">
<select
className="w-full rounded border border-border bg-transparent px-2 py-1.5 text-xs outline-none"