Simplify execution workspace chooser and deduplicate reusable workspaces

- Remove "Operator branch" and "Agent default" options from the workspace
  mode dropdown in both NewIssueDialog and IssueProperties, keeping only
  "Project default", "New isolated workspace", and "Reuse existing workspace"
- Deduplicate reusable workspaces by space identity (cwd path) so the
  "choose existing workspace" dropdown shows unique worktrees instead of
  duplicate entries from multiple issues sharing the same local folder

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta
2026-03-17 07:46:40 -05:00
parent 88bf1b23a3
commit ce105d32c3
2 changed files with 30 additions and 10 deletions

View File

@@ -26,8 +26,6 @@ const EXECUTION_WORKSPACE_OPTIONS = [
{ value: "shared_workspace", label: "Project default" }, { value: "shared_workspace", label: "Project default" },
{ value: "isolated_workspace", label: "New isolated workspace" }, { value: "isolated_workspace", label: "New isolated workspace" },
{ value: "reuse_existing", label: "Reuse existing workspace" }, { value: "reuse_existing", label: "Reuse existing workspace" },
{ value: "operator_branch", label: "Operator branch" },
{ value: "agent_default", label: "Agent default" },
] as const; ] as const;
function defaultProjectWorkspaceIdForProject(project: { function defaultProjectWorkspaceIdForProject(project: {
@@ -237,7 +235,19 @@ export function IssueProperties({ issue, onUpdate, inline }: IssuePropertiesProp
}), }),
enabled: Boolean(companyId) && Boolean(issue.projectId), enabled: Boolean(companyId) && Boolean(issue.projectId),
}); });
const selectedReusableExecutionWorkspace = (reusableExecutionWorkspaces ?? []).find( const deduplicatedReusableWorkspaces = useMemo(() => {
const workspaces = reusableExecutionWorkspaces ?? [];
const seen = new Map<string, typeof workspaces[number]>();
for (const ws of workspaces) {
const key = ws.cwd ?? ws.id;
const existing = seen.get(key);
if (!existing || new Date(ws.lastUsedAt) > new Date(existing.lastUsedAt)) {
seen.set(key, ws);
}
}
return Array.from(seen.values());
}, [reusableExecutionWorkspaces]);
const selectedReusableExecutionWorkspace = deduplicatedReusableWorkspaces.find(
(workspace) => workspace.id === issue.executionWorkspaceId, (workspace) => workspace.id === issue.executionWorkspaceId,
); );
const projectLink = (id: string | null) => { const projectLink = (id: string | null) => {
@@ -630,7 +640,7 @@ export function IssueProperties({ issue, onUpdate, inline }: IssuePropertiesProp
value={issue.executionWorkspaceId ?? ""} value={issue.executionWorkspaceId ?? ""}
onChange={(e) => { onChange={(e) => {
const nextExecutionWorkspaceId = e.target.value || null; const nextExecutionWorkspaceId = e.target.value || null;
const nextExecutionWorkspace = (reusableExecutionWorkspaces ?? []).find( const nextExecutionWorkspace = deduplicatedReusableWorkspaces.find(
(workspace) => workspace.id === nextExecutionWorkspaceId, (workspace) => workspace.id === nextExecutionWorkspaceId,
); );
onUpdate({ onUpdate({
@@ -643,7 +653,7 @@ export function IssueProperties({ issue, onUpdate, inline }: IssuePropertiesProp
}} }}
> >
<option value="">Choose an existing workspace</option> <option value="">Choose an existing workspace</option>
{(reusableExecutionWorkspaces ?? []).map((workspace) => ( {deduplicatedReusableWorkspaces.map((workspace) => (
<option key={workspace.id} value={workspace.id}> <option key={workspace.id} value={workspace.id}>
{workspace.name} · {workspace.status} · {workspace.branchName ?? workspace.cwd ?? workspace.id.slice(0, 8)} {workspace.name} · {workspace.status} · {workspace.branchName ?? workspace.cwd ?? workspace.id.slice(0, 8)}
</option> </option>

View File

@@ -242,8 +242,6 @@ const EXECUTION_WORKSPACE_MODES = [
{ value: "shared_workspace", label: "Project default" }, { value: "shared_workspace", label: "Project default" },
{ value: "isolated_workspace", label: "New isolated workspace" }, { value: "isolated_workspace", label: "New isolated workspace" },
{ value: "reuse_existing", label: "Reuse existing workspace" }, { value: "reuse_existing", label: "Reuse existing workspace" },
{ value: "operator_branch", label: "Operator branch" },
{ value: "agent_default", label: "Agent default" },
] as const; ] as const;
function defaultProjectWorkspaceIdForProject(project: { workspaces?: Array<{ id: string; isPrimary: boolean }>; executionWorkspacePolicy?: { defaultProjectWorkspaceId?: string | null } | null } | null | undefined) { function defaultProjectWorkspaceIdForProject(project: { workspaces?: Array<{ id: string; isPrimary: boolean }>; executionWorkspacePolicy?: { defaultProjectWorkspaceId?: string | null } | null } | null | undefined) {
@@ -638,7 +636,7 @@ export function NewIssueDialog() {
}); });
const selectedProject = orderedProjects.find((project) => project.id === projectId); const selectedProject = orderedProjects.find((project) => project.id === projectId);
const executionWorkspacePolicy = selectedProject?.executionWorkspacePolicy ?? null; const executionWorkspacePolicy = selectedProject?.executionWorkspacePolicy ?? null;
const selectedReusableExecutionWorkspace = (reusableExecutionWorkspaces ?? []).find( const selectedReusableExecutionWorkspace = deduplicatedReusableWorkspaces.find(
(workspace) => workspace.id === selectedExecutionWorkspaceId, (workspace) => workspace.id === selectedExecutionWorkspaceId,
); );
const requestedExecutionWorkspaceMode = const requestedExecutionWorkspaceMode =
@@ -747,7 +745,19 @@ export function NewIssueDialog() {
const currentProject = orderedProjects.find((project) => project.id === projectId); const currentProject = orderedProjects.find((project) => project.id === projectId);
const currentProjectExecutionWorkspacePolicy = currentProject?.executionWorkspacePolicy ?? null; const currentProjectExecutionWorkspacePolicy = currentProject?.executionWorkspacePolicy ?? null;
const currentProjectSupportsExecutionWorkspace = Boolean(currentProjectExecutionWorkspacePolicy?.enabled); const currentProjectSupportsExecutionWorkspace = Boolean(currentProjectExecutionWorkspacePolicy?.enabled);
const selectedReusableExecutionWorkspace = (reusableExecutionWorkspaces ?? []).find( const deduplicatedReusableWorkspaces = useMemo(() => {
const workspaces = reusableExecutionWorkspaces ?? [];
const seen = new Map<string, typeof workspaces[number]>();
for (const ws of workspaces) {
const key = ws.cwd ?? ws.id;
const existing = seen.get(key);
if (!existing || new Date(ws.lastUsedAt) > new Date(existing.lastUsedAt)) {
seen.set(key, ws);
}
}
return Array.from(seen.values());
}, [reusableExecutionWorkspaces]);
const selectedReusableExecutionWorkspace = deduplicatedReusableWorkspaces.find(
(workspace) => workspace.id === selectedExecutionWorkspaceId, (workspace) => workspace.id === selectedExecutionWorkspaceId,
); );
const assigneeOptionsTitle = const assigneeOptionsTitle =
@@ -1126,7 +1136,7 @@ export function NewIssueDialog() {
onChange={(e) => setSelectedExecutionWorkspaceId(e.target.value)} onChange={(e) => setSelectedExecutionWorkspaceId(e.target.value)}
> >
<option value="">Choose an existing workspace</option> <option value="">Choose an existing workspace</option>
{(reusableExecutionWorkspaces ?? []).map((workspace) => ( {deduplicatedReusableWorkspaces.map((workspace) => (
<option key={workspace.id} value={workspace.id}> <option key={workspace.id} value={workspace.id}>
{workspace.name} · {workspace.status} · {workspace.branchName ?? workspace.cwd ?? workspace.id.slice(0, 8)} {workspace.name} · {workspace.status} · {workspace.branchName ?? workspace.cwd ?? workspace.id.slice(0, 8)}
</option> </option>