From ab0d04ff7aa85ec9ef8b9c2c56ade9ecc2943e81 Mon Sep 17 00:00:00 2001 From: dotta Date: Thu, 26 Mar 2026 11:53:25 -0500 Subject: [PATCH] fix(ui): address workspace card review feedback - restore pre-run workspace configuration visibility - require explicit save/cancel for workspace edits - stabilize debounced issue search callback Co-Authored-By: Paperclip --- ui/src/components/IssueWorkspaceCard.tsx | 191 +++++++++++++++++------ ui/src/components/IssuesList.tsx | 9 +- 2 files changed, 148 insertions(+), 52 deletions(-) diff --git a/ui/src/components/IssueWorkspaceCard.tsx b/ui/src/components/IssueWorkspaceCard.tsx index bccf42ee..56484cab 100644 --- a/ui/src/components/IssueWorkspaceCard.tsx +++ b/ui/src/components/IssueWorkspaceCard.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Link } from "@/lib/router"; import type { Issue, ExecutionWorkspace } from "@paperclipai/shared"; import { useQuery } from "@tanstack/react-query"; @@ -98,6 +98,22 @@ function workspaceModeLabel(mode: string | null | undefined) { } } +function configuredWorkspaceLabel( + selection: string | null | undefined, + reusableWorkspace: ExecutionWorkspace | null, +) { + switch (selection) { + case "isolated_workspace": + return "New isolated workspace"; + case "reuse_existing": + return reusableWorkspace?.mode === "isolated_workspace" + ? "Existing isolated workspace" + : "Reuse existing workspace"; + default: + return "Project default"; + } +} + function statusBadge(status: string) { const colors: Record = { active: "bg-green-500/15 text-green-700 dark:text-green-400", @@ -137,9 +153,6 @@ export function IssueWorkspaceCard({ issue, project, onUpdate }: IssueWorkspaceC const workspace = issue.currentExecutionWorkspace as ExecutionWorkspace | null | undefined; - // Only show this card for non-default workspaces - const isNonDefault = workspace && workspace.mode !== "shared_workspace"; - const { data: reusableExecutionWorkspaces } = useQuery({ queryKey: queryKeys.executionWorkspaces.list(companyId!, { projectId: issue.projectId ?? undefined, @@ -181,8 +194,51 @@ export function IssueWorkspaceCard({ issue, project, onUpdate }: IssueWorkspaceC ?? defaultExecutionWorkspaceModeForProject(project) ); - // Don't render if feature is off or workspace is default/absent - if (!policyEnabled || !isNonDefault) return null; + const [draftSelection, setDraftSelection] = useState(currentSelection); + const [draftExecutionWorkspaceId, setDraftExecutionWorkspaceId] = useState(issue.executionWorkspaceId ?? ""); + + useEffect(() => { + if (editing) return; + setDraftSelection(currentSelection); + setDraftExecutionWorkspaceId(issue.executionWorkspaceId ?? ""); + }, [currentSelection, editing, issue.executionWorkspaceId]); + + const activeNonDefaultWorkspace = Boolean(workspace && workspace.mode !== "shared_workspace"); + + const configuredReusableWorkspace = + deduplicatedReusableWorkspaces.find((w) => w.id === draftExecutionWorkspaceId) + ?? (draftExecutionWorkspaceId === issue.executionWorkspaceId ? selectedReusableExecutionWorkspace : null); + + 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, + configuredReusableWorkspace?.mode, + draftExecutionWorkspaceId, + draftSelection, + onUpdate, + ]); + + const handleCancel = useCallback(() => { + setDraftSelection(currentSelection); + setDraftExecutionWorkspaceId(issue.executionWorkspaceId ?? ""); + setEditing(false); + }, [currentSelection, issue.executionWorkspaceId]); + + if (!policyEnabled || !project) return null; return (
@@ -190,48 +246,95 @@ export function IssueWorkspaceCard({ issue, project, onUpdate }: IssueWorkspaceC
- {workspaceModeLabel(workspace.mode)} - {statusBadge(workspace.status)} + {activeNonDefaultWorkspace && workspace + ? workspaceModeLabel(workspace.mode) + : configuredWorkspaceLabel(currentSelection, selectedReusableExecutionWorkspace)} + {workspace ? statusBadge(workspace.status) : statusBadge("idle")} +
+
+ {editing ? ( + <> + + + + ) : ( + + )}
-
{/* Read-only info */} {!editing && (
- {workspace.branchName && ( + {workspace?.branchName && (
)} - {workspace.cwd && ( + {workspace?.cwd && (
)} - {workspace.repoUrl && ( + {workspace?.repoUrl && (
Repo:
)} -
- - View workspace details → - -
+ {!workspace && ( +
+ {currentSelection === "isolated_workspace" + ? "A fresh isolated workspace will be created when this issue runs." + : currentSelection === "reuse_existing" + ? "This issue will reuse an existing workspace when it runs." + : "This issue will use the project default workspace configuration when it runs."} +
+ )} + {currentSelection === "reuse_existing" && selectedReusableExecutionWorkspace && ( +
+ Reusing:{" "} + + + +
+ )} + {workspace && ( +
+ + View workspace details → + +
+ )}
)} @@ -240,44 +343,32 @@ export function IssueWorkspaceCard({ issue, project, onUpdate }: IssueWorkspaceC
- {currentSelection === "reuse_existing" && ( + {draftSelection === "reuse_existing" && (