mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 00:53:38 +02:00
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:
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();
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user