qin-chenghan 8234370a6a
feat(artifacts): inline editing for text artifacts in the panel (#4596)
* feat(artifacts): inline editing for text artifacts in the panel

Add a PUT /api/threads/{id}/artifacts/{path} endpoint that atomically
replaces an existing UTF-8 text file under /mnt/user-data/outputs after
verifying its SHA-256 revision. Active runs conflict (409); binary,
symlink, oversized, and non-output paths are rejected.

Frontend: edit/save/discard buttons, draft state with conflict detection,
CodeEditor onChange/onSave, loader SHA-256 from ETag, i18n, beforeunload guard.

Backend: PUT endpoint with thread reservation, atomic temp-file replacement,
sandbox sync for non-mounted providers, rollback on failure, ETag on GET.

Tests: 8 backend + 1 blocking-IO + 3 frontend test files.

* fix(artifacts): scope replacement permissions and release sandboxes

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-08-01 09:15:21 +08:00

55 lines
1.3 KiB
TypeScript

import { fetch } from "@/core/api/fetcher";
import { urlOfArtifact } from "./utils";
export interface ArtifactUpdateResponse {
path: string;
sha256: string;
size: number;
}
export class ArtifactRequestError extends Error {
readonly status: number;
constructor(status: number, message: string) {
super(message);
this.name = "ArtifactRequestError";
this.status = status;
}
}
async function readErrorDetail(response: Response): Promise<string> {
const data = (await response.json().catch(() => ({}))) as {
detail?: string;
};
return data.detail ?? `HTTP ${response.status}: ${response.statusText}`;
}
export async function updateArtifactContent({
threadId,
filepath,
content,
expectedSha256,
}: {
threadId: string;
filepath: string;
content: string;
expectedSha256: string;
}): Promise<ArtifactUpdateResponse> {
const response = await fetch(urlOfArtifact({ filepath, threadId }), {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
content,
expected_sha256: expectedSha256,
}),
});
if (!response.ok) {
throw new ArtifactRequestError(
response.status,
await readErrorDetail(response),
);
}
return response.json() as Promise<ArtifactUpdateResponse>;
}