From e5e54df884e4e50165b0dd00f26ee2af31809656 Mon Sep 17 00:00:00 2001 From: Dotta Date: Tue, 8 Sep 2026 13:06:36 -0500 Subject: [PATCH] fix: keep every failed shared-folder run accessible Co-Authored-By: Paperclip --- ui/src/components/WorkFolderBrowser.test.tsx | 13 +++++++++++++ ui/src/components/WorkFolderBrowser.tsx | 17 +++++++++++------ ui/storybook/fixtures/workFolders.ts | 9 +++++++-- ui/storybook/stories/work-folders.stories.tsx | 2 ++ 4 files changed, 33 insertions(+), 8 deletions(-) diff --git a/ui/src/components/WorkFolderBrowser.test.tsx b/ui/src/components/WorkFolderBrowser.test.tsx index b3cd2f264c..d872086dee 100644 --- a/ui/src/components/WorkFolderBrowser.test.tsx +++ b/ui/src/components/WorkFolderBrowser.test.tsx @@ -53,4 +53,17 @@ describe("work folder save feedback", () => { expect(html).toContain("View failed run"); expect(html).toContain("Last agent save"); }); + it("keeps every failed sandbox accessible alongside saved shared files", () => { + const html = render([checkpoint, ...["newer", "earlier"].map((runId) => ({ + ...checkpoint, runId, agentId: "other-agent", state: "failed" as const, + error: `Retained ${runId} working copy`, + }))], null, true); + expect(html).toContain("2 sandbox runs could not save their files."); + for (const runId of ["newer", "earlier"]) { + expect(html).toContain(`href="/STG/agents/other-agent/runs/${runId}"`); + expect(html).toContain(`Retained ${runId} working copy`); + } + expect(html).toContain("Last agent save"); + }); + }); diff --git a/ui/src/components/WorkFolderBrowser.tsx b/ui/src/components/WorkFolderBrowser.tsx index 9ba569258a..2675a58bd3 100644 --- a/ui/src/components/WorkFolderBrowser.tsx +++ b/ui/src/components/WorkFolderBrowser.tsx @@ -70,7 +70,7 @@ export function WorkFolderBrowser({ owner, exampleFiles, readOnly = false, fillH setAnnouncement(action.type === "refresh" ? "Refresh requested for the next safe run boundary." : "Files saved."); }, onSettled: () => queryClient.invalidateQueries({ queryKey: key }) }); const statuses = syncQuery.data ?? []; - const failed = statuses.find((status) => status.state === "failed"); + const failures = statuses.filter((status) => status.state === "failed"); const saving = mutation.isPending || statuses.some((status) => status.state === "saving"); const lastSaved = statuses.map((status) => status.lastSavedAt) .filter((value): value is string => Boolean(value)).sort().at(-1); @@ -89,7 +89,7 @@ export function WorkFolderBrowser({ owner, exampleFiles, readOnly = false, fillH {!trash && canManageTrash && checkedPaths.length > 0 && } {!readOnly && } - {saving ? "Saving…" : mutation.isError ? "Save failed" : failed ? "Run save failed" : "Saved"} + {saving ? "Saving…" : mutation.isError ? "Save failed" : failures.length > 0 ? "Run save failed" : "Saved"} {lastSaved && Last agent save {new Date(lastSaved).toLocaleTimeString()}} {lastOperation && (!lastSaved || lastOperation > lastSaved) && Files updated {new Date(lastOperation).toLocaleTimeString()}} @@ -98,10 +98,15 @@ export function WorkFolderBrowser({ owner, exampleFiles, readOnly = false, fillH } {[filesQuery.error, syncQuery.error, mutation.error].filter(Boolean).map((error, index) =>

{(error as Error).message}

)} - {failed &&

- A sandbox run could not save its files. The files below are saved copies. {failed.error}{" "} - {failed.agentId && View failed run} -

} + {failures.length > 0 &&
+

{failures.length === 1 ? "A sandbox run could not save its files." : `${failures.length} sandbox runs could not save their files.`} The files below are saved copies.

+
    + {failures.map((failure) =>
  • + {failure.error}{" "} + {failure.agentId && View failed run} +
  • )} +
+
}

{announcement}

{trash ?

Deleted cached files are retained here. Restore them to return them to Files.

{files.length === 0 ?

Trash is empty.

: files.map((file) =>
{file.path}{canManageTrash && } diff --git a/ui/storybook/fixtures/workFolders.ts b/ui/storybook/fixtures/workFolders.ts index 4cd3be96e2..f90402e6fe 100644 --- a/ui/storybook/fixtures/workFolders.ts +++ b/ui/storybook/fixtures/workFolders.ts @@ -10,6 +10,7 @@ export type WorkFolderScenario = | "saving" | "failed" | "olderFailure" + | "multipleFailures" | "empty" | "loading" | "unavailable" @@ -163,11 +164,15 @@ export function createWorkFolderFixture( ? "Storage is unavailable. Your working files are retained in the sandbox; retry when storage recovers." : null, }; - return Response.json(scenario === "olderFailure" ? [status, { + const olderFailure = { ...status, runId: "earlier-run-storybook", state: "failed", active: false, lastSavedAt: "2026-09-07T14:00:00.000Z", error: "An earlier sandbox could not reach storage. Its working copy was retained.", - }] : [status]); + }; + return Response.json(scenario === "multipleFailures" ? [status, olderFailure, { + ...olderFailure, runId: "another-failed-run-storybook", + error: "Another sandbox upload was interrupted. Its working copy was retained.", + }] : scenario === "olderFailure" ? [status, olderFailure] : [status]); } if (action === "refresh") return Response.json({ ok: true }); if (!action) { diff --git a/ui/storybook/stories/work-folders.stories.tsx b/ui/storybook/stories/work-folders.stories.tsx index 128bb9efa3..33e3814f5d 100644 --- a/ui/storybook/stories/work-folders.stories.tsx +++ b/ui/storybook/stories/work-folders.stories.tsx @@ -33,6 +33,7 @@ const meta = { "saving", "failed", "olderFailure", + "multipleFailures", "empty", "loading", "unavailable", @@ -102,6 +103,7 @@ export const Loading: Story = { args: { scenario: "loading" } }; export const Saving: Story = { args: { scenario: "saving" } }; export const SaveFailed: Story = { args: { scenario: "failed" } }; export const SharedFolderWithEarlierFailure: Story = { args: { scope: "agent", scenario: "olderFailure" } }; +export const SharedFolderWithMultipleFailures: Story = { args: { scope: "agent", scenario: "multipleFailures" } }; export const StorageUnavailable: Story = { args: { scenario: "unavailable" } }; export const UploadFailed: Story = { args: { scenario: "uploadFailed" },