fix(ui): remove execution workspace access card (#13406)

## Thinking Path

> - Paperclip helps people manage AI agents and their work.
> - Execution workspace pages show the tools and state for one isolated
workspace.
> - The workspace access card repeats runtime state that the page
already shows.
> - The card also adds open and repair actions that are not needed on
this page.
> - This pull request removes the complete workspace access card from
execution workspace detail pages.
> - The benefit is a smaller page that keeps attention on workspace
controls and work results.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

The execution workspace detail page shows a workspace access status
card.

**Current behavior**

The page can show Ready, preparation, degraded, failed, or Workspace is
not running states. The card can also show Open workspace and repair
actions.

**Proposed behavior**

The page does not show the workspace access status card or its page-only
actions.

**Reason and benefit**

The card adds status and controls that are not needed on this page. Its
removal makes the isolated workspace page simpler.

**Breaking changes**

The workspace access card and its actions are no longer available from
the execution workspace detail page. Workspace runtime controls remain
available.

## What Changed

- Removed the workspace access status card from execution workspace
detail pages.
- Removed the page-only login handoff and repair mutations that served
the card.
- Added a regression test for the removed card and text.

## Verification

- `pnpm exec vitest run ui/src/pages/ExecutionWorkspaceDetail.test.tsx`
passes with 8 tests.
- `pnpm typecheck` passes.
- `pnpm check:token-gates` passes.
- `pnpm build` passes.
- `pnpm test:run` found unrelated failures in
`server/src/__tests__/workspace-runtime.test.ts` during the local broad
run. The focused changed-area tests pass.
- The complete GitHub Actions matrix passes on the latest run, including
build, typecheck, server tests, runner verification, and e2e tests.

## Risks

- Low risk. The change removes one UI card and the page-only code that
supported it.
- Users must use the remaining workspace runtime controls instead of
this card.

> This change is focused UI cleanup. It does not add a roadmap feature.

## Model Used

- OpenAI Codex based on GPT-5, with reasoning, tool use, and code
execution. The exact context window is not exposed in this environment.

## 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>
This commit is contained in:
DottaandPaperclip authored and GitHub committed 2026-09-14 09:47:10 -05:00
1 parent 4cd7b40255
commit ef84f363b7
2 files changed
+8 -110

No files matched your search

@@ -318,6 +318,14 @@ describe("ExecutionWorkspaceDetail plugin slots", () => {
expect(container.querySelector('[data-testid="summary-slot-card"]')).not.toBeNull();
});
it("does not show a workspace access status card", async () => {
await render();
expect(container.querySelector('[data-testid="workspace-access-card"]')).toBeNull();
expect(container.textContent).not.toContain("Workspace is not running");
expect(container.textContent).not.toContain("Open workspace");
});
it("does not mount plugin slots scoped to other entity types", async () => {
await render();
-110
View File
@@ -13,7 +13,6 @@ import { CopyText } from "../components/CopyText";
import { ExecutionWorkspaceCloseDialog } from "../components/ExecutionWorkspaceCloseDialog";
import { MissingPluginTabPlaceholder } from "../components/MissingPluginTabPlaceholder";
import { agentsApi } from "../api/agents";
import { ApiError } from "../api/client";
import { executionWorkspacesApi } from "../api/execution-workspaces";
import { heartbeatsApi } from "../api/heartbeats";
import { issuesApi } from "../api/issues";
@@ -36,7 +35,6 @@ import {
type WorkspaceRuntimeControlRequest,
} from "../components/WorkspaceRuntimeControls";
import { WorkspaceServiceControlBar } from "../components/WorkspaceServiceControlBar";
import { WorkspaceAccessCard } from "../components/WorkspaceAccessCard";
import { useBreadcrumbs } from "../context/BreadcrumbContext";
import { useCompany } from "../context/CompanyContext";
import { useManagedSandboxOnly } from "../hooks/useManagedSandboxOnly";
@@ -44,10 +42,6 @@ import { useToastActions } from "../context/ToastContext";
import { collectLiveIssueIds } from "../lib/liveIssueIds";
import { queryKeys } from "../lib/queryKeys";
import { cn, formatDateTime, issueUrl, projectRouteRef, projectWorkspaceUrl } from "../lib/utils";
import {
resolveWorkspaceAccessState,
type WorkspaceLoginHandoffFailureInfo,
} from "../lib/workspace-access-state";
import {
getWorkspaceSpecificRoutineVariableNames,
routineHasWorkspaceSpecificVariables,
@@ -423,27 +417,6 @@ export function resolveRuntimeProvisionStatus(input: {
return configured ? { kind: "deferred" } : { kind: "eager" };
}
/**
* Read the structured refusal the login-handoff endpoint returns.
*
* The server keeps a machine `reason` (and, where it probed, the workspace's own
* readiness) on the error body so the UI can name the cause instead of showing a
* bare HTTP failure. Anything else is a genuine transport error.
*/
export function readWorkspaceHandoffFailure(error: unknown): WorkspaceLoginHandoffFailureInfo | null {
if (!(error instanceof ApiError)) return null;
const body = error.body as
| { reason?: unknown; detail?: unknown; readiness?: unknown }
| null
| undefined;
if (!body || typeof body.reason !== "string") return null;
return {
reason: body.reason,
detail: typeof body.detail === "string" ? body.detail : null,
readiness: (body.readiness as WorkspaceLoginHandoffFailureInfo["readiness"]) ?? null,
};
}
function DetailRow({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div className="flex flex-col gap-1.5 py-1.5 sm:flex-row sm:items-start sm:gap-3">
@@ -812,8 +785,6 @@ export function ExecutionWorkspaceDetail() {
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [runtimeActionErrorMessage, setRuntimeActionErrorMessage] = useState<string | null>(null);
const [runtimeActionMessage, setRuntimeActionMessage] = useState<string | null>(null);
const [handoffFailure, setHandoffFailure] = useState<WorkspaceLoginHandoffFailureInfo | null>(null);
const [handoffErrorMessage, setHandoffErrorMessage] = useState<string | null>(null);
const [pendingRuntimeActions, setPendingRuntimeActions] = useState<WorkspaceRuntimeControlRequest[]>([]);
const activeRouteTab = workspaceId ? resolveExecutionWorkspaceTab(location.pathname, workspaceId) : null;
const pluginTabFromSearch = useMemo(() => {
@@ -1003,67 +974,6 @@ export function ExecutionWorkspaceDetail() {
},
});
/**
* Password-independent workspace entry (PAP-17572).
*
* The server answers with a ticket-bearing URL, and the workspace answers *that*
* with a redirect — which is what keeps the ticket out of session history.
*
* The target tab is opened synchronously on click and only pointed at the URL
* once the ticket arrives. Opening it after the request resolves would be a
* popup the browser did not attribute to the click, and Safari and Firefox
* block exactly that. If the tab could not be opened anyway, fall back to
* navigating this one rather than silently doing nothing.
*/
const openWorkspace = useMutation({
mutationFn: async () => {
const target = window.open("about:blank", "_blank", "noopener,noreferrer");
try {
return { ticket: await executionWorkspacesApi.requestLoginHandoff(workspace!.id), target };
} catch (error) {
target?.close();
throw error;
}
},
onSuccess: ({ ticket, target }) => {
setHandoffFailure(null);
setHandoffErrorMessage(null);
if (target && !target.closed) target.location.replace(ticket.url);
else window.location.assign(ticket.url);
},
onError: async (error) => {
// A structured refusal is rendered as workspace state by the access card,
// so only an unrecognized transport error needs its own message line.
const failure = readWorkspaceHandoffFailure(error);
setHandoffFailure(failure);
setHandoffErrorMessage(
failure ? null : error instanceof Error ? error.message : "Failed to open the workspace.",
);
// The refusal reason often comes from an operation that has since advanced,
// so refresh the log the access card derives its state from.
await queryClient.invalidateQueries({
queryKey: queryKeys.executionWorkspaces.workspaceOperations(workspace!.id),
});
},
});
const repairWorkspace = useMutation({
mutationFn: () => executionWorkspacesApi.repair(workspace!.id),
onSuccess: (result) => {
queryClient.setQueryData(queryKeys.executionWorkspaces.detail(result.workspace.id), result.workspace);
queryClient.invalidateQueries({
queryKey: queryKeys.executionWorkspaces.workspaceOperations(result.workspace.id),
});
setHandoffFailure(null);
setHandoffErrorMessage(null);
setRuntimeActionErrorMessage(null);
setRuntimeActionMessage("Workspace database repaired.");
},
onError: (error) => {
setRuntimeActionMessage(null);
setRuntimeActionErrorMessage(error instanceof Error ? error.message : "Failed to repair the workspace.");
},
});
if (workspaceQuery.isLoading) return <p className="text-sm text-muted-foreground">Loading workspace…</p>;
if (workspaceQuery.error) {
return (
@@ -1088,12 +998,6 @@ export function ExecutionWorkspaceDetail() {
runtimeServices: workspace.runtimeServices ?? [],
pendingRequests: pendingRuntimeActions,
});
const workspaceAccess = resolveWorkspaceAccessState({
runtimeServices: workspace.runtimeServices ?? [],
operations: workspaceOperationsQuery.data,
handoffFailure,
});
const pluginSlotContext = {
companyId: workspace.companyId,
projectId: workspace.projectId,
@@ -1163,20 +1067,6 @@ export function ExecutionWorkspaceDetail() {
{runtimeActionErrorMessage ? <p className="text-sm text-destructive">{runtimeActionErrorMessage}</p> : null}
{!runtimeActionErrorMessage && runtimeActionMessage ? <p className="text-sm text-muted-foreground">{runtimeActionMessage}</p> : null}
<WorkspaceAccessCard
access={workspaceAccess}
isBusy={openWorkspace.isPending || repairWorkspace.isPending}
onOpen={() => openWorkspace.mutate()}
onStart={() => {
runRuntimeControlRequests(
resolveWorkspaceServiceControlRequests(runtimeControlSections, "start", null),
);
}}
onRepair={() => repairWorkspace.mutate()}
onViewLogs={() => handleTabChange("runtime_logs")}
errorMessage={handoffErrorMessage}
/>
<PluginSlotOutlet
slotTypes={["toolbarButton", "contextMenuItem"]}
entityType="execution_workspace"