fix: keep every failed shared-folder run accessible

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta 2026-09-08 13:06:36 -05:00
parent 0a9680a8b8
commit e5e54df884
4 changed files with 33 additions and 8 deletions

View File

@ -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");
});
});

View File

@ -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
</TabsList>
{!trash && canManageTrash && checkedPaths.length > 0 && <Button variant="outline" size="sm" disabled={disabled} onClick={() => mutation.mutate({ type: "deleteSelected", paths: checkedPaths })}><Trash2 aria-hidden />Move {checkedPaths.length} {checkedPaths.length === 1 ? "file" : "files"} to trash</Button>}
{!readOnly && <Button variant="outline" size="sm" disabled={disabled || !statuses.some((status) => status.active)} onClick={() => mutation.mutate({ type: "refresh" })}><RefreshCw aria-hidden />Refresh sandbox</Button>}
<span className="text-xs text-muted-foreground" role="status">{saving ? "Saving…" : mutation.isError ? "Save failed" : failed ? "Run save failed" : "Saved"}</span>
<span className="text-xs text-muted-foreground" role="status">{saving ? "Saving…" : mutation.isError ? "Save failed" : failures.length > 0 ? "Run save failed" : "Saved"}</span>
{lastSaved && <span className="text-xs text-muted-foreground">Last agent save {new Date(lastSaved).toLocaleTimeString()}</span>}
{lastOperation && (!lastSaved || lastOperation > lastSaved) && <span className="text-xs text-muted-foreground">Files updated {new Date(lastOperation).toLocaleTimeString()}</span>}
</div>
@ -98,10 +98,15 @@ export function WorkFolderBrowser({ owner, exampleFiles, readOnly = false, fillH
<Button variant="outline" size="sm" disabled={disabled || !directory} onClick={() => mutation.mutate({ type: "mkdir" })}><FolderPlus aria-hidden />Create folder</Button>
</div>}
{[filesQuery.error, syncQuery.error, mutation.error].filter(Boolean).map((error, index) => <p key={index} role="alert" className="text-sm text-destructive">{(error as Error).message}</p>)}
{failed && <p role="alert" className="text-sm text-destructive">
A sandbox run could not save its files. The files below are saved copies. {failed.error}{" "}
{failed.agentId && <Link className="underline" to={`/agents/${encodeURIComponent(failed.agentId)}/runs/${encodeURIComponent(failed.runId)}`}>View failed run</Link>}
</p>}
{failures.length > 0 && <div role="alert" className="text-sm text-destructive">
<p>{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.</p>
<ul className="max-h-24 space-y-1 overflow-auto">
{failures.map((failure) => <li key={failure.runId}>
{failure.error}{" "}
{failure.agentId && <Link className="underline" to={`/agents/${encodeURIComponent(failure.agentId)}/runs/${encodeURIComponent(failure.runId)}`} aria-label={`View failed run ${failure.runId}`}>View failed run</Link>}
</li>)}
</ul>
</div>}
<p className="sr-only" aria-live="polite">{announcement}</p>
{trash ? <TabsContent value="trash" className={cn("overflow-auto", fillHeight ? "min-h-0 flex-1" : "max-h-96")}><p className="mb-3 text-sm text-muted-foreground">Deleted cached files are retained here. Restore them to return them to Files.</p>{files.length === 0 ? <p className="text-sm text-muted-foreground">Trash is empty.</p> : files.map((file) => <div key={file.id} className="flex items-center gap-2 border-b py-2">
<span className="min-w-0 flex-1 truncate text-sm">{file.path}</span>{canManageTrash && <Button size="sm" variant="outline" disabled={disabled} onClick={() => mutation.mutate({ type: "restore", fileId: file.id })}><RotateCcw aria-hidden />Restore</Button>}

View File

@ -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) {

View File

@ -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" },