Fix execution workspace close messaging

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dotta 2026-03-28 17:38:34 -05:00
parent 1f1fe9c989
commit 11f08ea5d5
3 changed files with 52 additions and 23 deletions

View file

@ -295,7 +295,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
workspaceId: executionWorkspaceId, workspaceId: executionWorkspaceId,
state: "ready_with_warnings", state: "ready_with_warnings",
isSharedWorkspace: false, isSharedWorkspace: false,
isProjectPrimaryWorkspace: true, isProjectPrimaryWorkspace: false,
isDestructiveCloseAllowed: true, isDestructiveCloseAllowed: true,
git: { git: {
workspacePath: worktreePath, workspacePath: worktreePath,

View file

@ -439,7 +439,15 @@ export function executionWorkspaceService(db: Db) {
const warnings = [...gitWarnings]; const warnings = [...gitWarnings];
const blockingReasons: string[] = []; const blockingReasons: string[] = [];
const isSharedWorkspace = executionWorkspace.mode === "shared_workspace"; const isSharedWorkspace = executionWorkspace.mode === "shared_workspace";
const isProjectPrimaryWorkspace = workspace.projectWorkspaceId != null && workspace.projectWorkspaceId === primaryProjectWorkspace?.id; const workspacePath = readNullableString(executionWorkspace.providerRef) ?? readNullableString(executionWorkspace.cwd);
const resolvedWorkspacePath = workspacePath ? path.resolve(workspacePath) : null;
const resolvedPrimaryWorkspacePath = projectWorkspace?.cwd ? path.resolve(projectWorkspace.cwd) : null;
const isProjectPrimaryWorkspace =
workspace.projectWorkspaceId != null
&& workspace.projectWorkspaceId === primaryProjectWorkspace?.id
&& resolvedWorkspacePath != null
&& resolvedPrimaryWorkspacePath != null
&& resolvedWorkspacePath === resolvedPrimaryWorkspacePath;
const linkedIssueSummaries = linkedIssues.map((issue) => ({ const linkedIssueSummaries = linkedIssues.map((issue) => ({
...issue, ...issue,
@ -546,7 +554,6 @@ export function executionWorkspaceService(db: Db) {
}); });
} }
const workspacePath = readNullableString(executionWorkspace.providerRef) ?? readNullableString(executionWorkspace.cwd);
if (executionWorkspace.providerType === "git_worktree" && workspacePath) { if (executionWorkspace.providerType === "git_worktree" && workspacePath) {
plannedActions.push({ plannedActions.push({
kind: "git_worktree_remove", kind: "git_worktree_remove",

View file

@ -75,6 +75,8 @@ export function ExecutionWorkspaceCloseDialog({
}); });
const readiness = readinessQuery.data ?? null; const readiness = readinessQuery.data ?? null;
const blockingIssues = readiness?.linkedIssues.filter((issue) => !issue.isTerminal) ?? [];
const otherLinkedIssues = readiness?.linkedIssues.filter((issue) => issue.isTerminal) ?? [];
const confirmDisabled = const confirmDisabled =
currentStatus === "archived" || currentStatus === "archived" ||
closeWorkspace.isPending || closeWorkspace.isPending ||
@ -86,10 +88,10 @@ export function ExecutionWorkspaceCloseDialog({
<Dialog open={open} onOpenChange={(nextOpen) => { <Dialog open={open} onOpenChange={(nextOpen) => {
if (!closeWorkspace.isPending) onOpenChange(nextOpen); if (!closeWorkspace.isPending) onOpenChange(nextOpen);
}}> }}>
<DialogContent className="sm:max-w-2xl"> <DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-2xl">
<DialogHeader> <DialogHeader>
<DialogTitle>{actionLabel}</DialogTitle> <DialogTitle>{actionLabel}</DialogTitle>
<DialogDescription> <DialogDescription className="break-words">
Archive <span className="font-medium text-foreground">{workspaceName}</span> and clean up any owned workspace Archive <span className="font-medium text-foreground">{workspaceName}</span> and clean up any owned workspace
artifacts. Paperclip keeps the workspace record and issue history, but removes it from active workspace views. artifacts. Paperclip keeps the workspace record and issue history, but removes it from active workspace views.
</DialogDescription> </DialogDescription>
@ -116,19 +118,39 @@ export function ExecutionWorkspaceCloseDialog({
</div> </div>
<div className="mt-1 text-xs opacity-80"> <div className="mt-1 text-xs opacity-80">
{readiness.isSharedWorkspace {readiness.isSharedWorkspace
? "This workspace is attached to shared project infrastructure." ? "This is the shared project workspace session, so destructive close is blocked."
: readiness.isProjectPrimaryWorkspace : readiness.git?.workspacePath && readiness.git.repoRoot && readiness.git.workspacePath !== readiness.git.repoRoot
? "This workspace is based on the project's primary workspace." ? "This execution workspace has its own checkout path and can be archived independently."
: "This workspace is disposable and can be archived."} : readiness.isProjectPrimaryWorkspace
? "This execution workspace currently points at the project's primary workspace path."
: "This workspace is disposable and can be archived."}
</div> </div>
</div> </div>
{blockingIssues.length > 0 ? (
<section className="space-y-2">
<h3 className="text-sm font-medium">Blocking issues</h3>
<div className="space-y-2">
{blockingIssues.map((issue) => (
<div key={issue.id} className="rounded-xl border border-destructive/20 bg-destructive/5 px-4 py-3 text-sm">
<div className="flex min-w-0 flex-wrap items-center justify-between gap-2">
<Link to={issueUrl(issue)} className="min-w-0 break-words font-medium hover:underline">
{issue.identifier ?? issue.id} · {issue.title}
</Link>
<span className="text-xs text-muted-foreground">{issue.status}</span>
</div>
</div>
))}
</div>
</section>
) : null}
{readiness.blockingReasons.length > 0 ? ( {readiness.blockingReasons.length > 0 ? (
<section className="space-y-2"> <section className="space-y-2">
<h3 className="text-sm font-medium">Blocking reasons</h3> <h3 className="text-sm font-medium">Blocking reasons</h3>
<ul className="space-y-2 text-sm text-muted-foreground"> <ul className="space-y-2 text-sm text-muted-foreground">
{readiness.blockingReasons.map((reason) => ( {readiness.blockingReasons.map((reason) => (
<li key={reason} className="rounded-lg border border-destructive/20 bg-destructive/5 px-3 py-2 text-destructive"> <li key={reason} className="break-words rounded-lg border border-destructive/20 bg-destructive/5 px-3 py-2 text-destructive">
{reason} {reason}
</li> </li>
))} ))}
@ -141,7 +163,7 @@ export function ExecutionWorkspaceCloseDialog({
<h3 className="text-sm font-medium">Warnings</h3> <h3 className="text-sm font-medium">Warnings</h3>
<ul className="space-y-2 text-sm text-muted-foreground"> <ul className="space-y-2 text-sm text-muted-foreground">
{readiness.warnings.map((warning) => ( {readiness.warnings.map((warning) => (
<li key={warning} className="rounded-lg border border-amber-500/20 bg-amber-500/5 px-3 py-2"> <li key={warning} className="break-words rounded-lg border border-amber-500/20 bg-amber-500/5 px-3 py-2">
{warning} {warning}
</li> </li>
))} ))}
@ -185,14 +207,14 @@ export function ExecutionWorkspaceCloseDialog({
</section> </section>
) : null} ) : null}
{readiness.linkedIssues.length > 0 ? ( {otherLinkedIssues.length > 0 ? (
<section className="space-y-2"> <section className="space-y-2">
<h3 className="text-sm font-medium">Linked issues</h3> <h3 className="text-sm font-medium">Other linked issues</h3>
<div className="space-y-2"> <div className="space-y-2">
{readiness.linkedIssues.map((issue) => ( {otherLinkedIssues.map((issue) => (
<div key={issue.id} className="rounded-xl border border-border bg-muted/20 px-4 py-3 text-sm"> <div key={issue.id} className="rounded-xl border border-border bg-muted/20 px-4 py-3 text-sm">
<div className="flex flex-wrap items-center justify-between gap-2"> <div className="flex min-w-0 flex-wrap items-center justify-between gap-2">
<Link to={issueUrl(issue)} className="font-medium hover:underline"> <Link to={issueUrl(issue)} className="min-w-0 break-words font-medium hover:underline">
{issue.identifier ?? issue.id} · {issue.title} {issue.identifier ?? issue.id} · {issue.title}
</Link> </Link>
<span className="text-xs text-muted-foreground">{issue.status}</span> <span className="text-xs text-muted-foreground">{issue.status}</span>
@ -209,11 +231,11 @@ export function ExecutionWorkspaceCloseDialog({
<div className="space-y-2"> <div className="space-y-2">
{readiness.runtimeServices.map((service) => ( {readiness.runtimeServices.map((service) => (
<div key={service.id} className="rounded-xl border border-border bg-muted/20 px-4 py-3 text-sm"> <div key={service.id} className="rounded-xl border border-border bg-muted/20 px-4 py-3 text-sm">
<div className="flex flex-wrap items-center justify-between gap-2"> <div className="flex min-w-0 flex-wrap items-center justify-between gap-2">
<span className="font-medium">{service.serviceName}</span> <span className="font-medium">{service.serviceName}</span>
<span className="text-xs text-muted-foreground">{service.status} · {service.lifecycle}</span> <span className="text-xs text-muted-foreground">{service.status} · {service.lifecycle}</span>
</div> </div>
<div className="mt-1 text-xs text-muted-foreground"> <div className="mt-1 break-words text-xs text-muted-foreground">
{service.url ?? service.command ?? service.cwd ?? "No additional details"} {service.url ?? service.command ?? service.cwd ?? "No additional details"}
</div> </div>
</div> </div>
@ -228,9 +250,9 @@ export function ExecutionWorkspaceCloseDialog({
{readiness.plannedActions.map((action, index) => ( {readiness.plannedActions.map((action, index) => (
<div key={`${action.kind}-${index}`} className="rounded-xl border border-border bg-muted/20 px-4 py-3 text-sm"> <div key={`${action.kind}-${index}`} className="rounded-xl border border-border bg-muted/20 px-4 py-3 text-sm">
<div className="font-medium">{action.label}</div> <div className="font-medium">{action.label}</div>
<div className="mt-1 text-muted-foreground">{action.description}</div> <div className="mt-1 break-words text-muted-foreground">{action.description}</div>
{action.command ? ( {action.command ? (
<pre className="mt-2 overflow-x-auto rounded-lg bg-background px-3 py-2 font-mono text-xs text-foreground"> <pre className="mt-2 whitespace-pre-wrap break-all rounded-lg bg-background px-3 py-2 font-mono text-xs text-foreground">
{action.command} {action.command}
</pre> </pre>
) : null} ) : null}
@ -253,11 +275,11 @@ export function ExecutionWorkspaceCloseDialog({
) : null} ) : null}
{readiness.git?.repoRoot ? ( {readiness.git?.repoRoot ? (
<div className="text-xs text-muted-foreground"> <div className="break-words text-xs text-muted-foreground">
Repo root: <span className="font-mono">{readiness.git.repoRoot}</span> Repo root: <span className="font-mono break-all">{readiness.git.repoRoot}</span>
{readiness.git.workspacePath ? ( {readiness.git.workspacePath ? (
<> <>
{" · "}Workspace path: <span className="font-mono">{readiness.git.workspacePath}</span> {" · "}Workspace path: <span className="font-mono break-all">{readiness.git.workspacePath}</span>
</> </>
) : null} ) : null}
</div> </div>