Phase 14: multi-host file browser (0.15.0)
The Files page gained a host switcher: when agents are registered, a Host
dropdown switches the whole browser between the local host and any online agent
(switching resets path + clipboard). Every file operation is sandboxed by the
selected agent's own ALLOWED_BROWSE_ROOTS/HOST_ROOT_PREFIX.
- agent_app.py: /agent/files/* (list/read/download/write/mkdir/touch/rename/
copy/move/delete/upload) reusing file_service + device_service; BrowseError
-> HTTP 400.
- routers/agents.py: proxy routes at /api/agents/{id}/files/* (audit-logged
mutations); download streams via download_to_file, upload via upload_file.
Reuses the WriteBody/NameBody/RenameBody/TransferBody models from routers.files.
- Frontend: filesApi methods take an optional trailing agentId; Files.tsx tracks
a host and threads it through every call, query key, and the editor/dialogs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
012614f5fb
commit
c5f591749f
+26
-22
@@ -20,52 +20,56 @@ function triggerDownload(blob: Blob, filename: string) {
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// Base path for the local host or, when agentId is given, a remote agent.
|
||||
const base = (agentId?: number) =>
|
||||
agentId != null ? `/api/agents/${agentId}/files` : "/api/files";
|
||||
|
||||
export const filesApi = {
|
||||
list: (path: string, showHidden = false) =>
|
||||
list: (path: string, showHidden = false, agentId?: number) =>
|
||||
api
|
||||
.get<HostPathResult>("/api/files/list", { params: { path, show_hidden: showHidden } })
|
||||
.get<HostPathResult>(`${base(agentId)}/list`, { params: { path, show_hidden: showHidden } })
|
||||
.then((r) => r.data),
|
||||
|
||||
read: (path: string) =>
|
||||
api.get<FileContent>("/api/files/read", { params: { path } }).then((r) => r.data),
|
||||
read: (path: string, agentId?: number) =>
|
||||
api.get<FileContent>(`${base(agentId)}/read`, { params: { path } }).then((r) => r.data),
|
||||
|
||||
write: (path: string, content: string) =>
|
||||
api.put<{ path: string; size: number }>("/api/files/write", { path, content }).then((r) => r.data),
|
||||
write: (path: string, content: string, agentId?: number) =>
|
||||
api.put<{ path: string; size: number }>(`${base(agentId)}/write`, { path, content }).then((r) => r.data),
|
||||
|
||||
mkdir: (path: string, name: string) =>
|
||||
api.post<{ path: string }>("/api/files/mkdir", { path, name }).then((r) => r.data),
|
||||
mkdir: (path: string, name: string, agentId?: number) =>
|
||||
api.post<{ path: string }>(`${base(agentId)}/mkdir`, { path, name }).then((r) => r.data),
|
||||
|
||||
touch: (path: string, name: string) =>
|
||||
api.post<{ path: string }>("/api/files/touch", { path, name }).then((r) => r.data),
|
||||
touch: (path: string, name: string, agentId?: number) =>
|
||||
api.post<{ path: string }>(`${base(agentId)}/touch`, { path, name }).then((r) => r.data),
|
||||
|
||||
rename: (path: string, newName: string) =>
|
||||
api.post<{ path: string }>("/api/files/rename", { path, new_name: newName }).then((r) => r.data),
|
||||
rename: (path: string, newName: string, agentId?: number) =>
|
||||
api.post<{ path: string }>(`${base(agentId)}/rename`, { path, new_name: newName }).then((r) => r.data),
|
||||
|
||||
remove: (path: string, recursive = false) =>
|
||||
api.delete("/api/files", { params: { path, recursive } }).then((r) => r.data),
|
||||
remove: (path: string, recursive = false, agentId?: number) =>
|
||||
api.delete(base(agentId), { params: { path, recursive } }).then((r) => r.data),
|
||||
|
||||
copy: (src: string, destDir: string, overwrite = false) =>
|
||||
copy: (src: string, destDir: string, overwrite = false, agentId?: number) =>
|
||||
api
|
||||
.post<{ path: string }>("/api/files/copy", { src, dest_dir: destDir, overwrite })
|
||||
.post<{ path: string }>(`${base(agentId)}/copy`, { src, dest_dir: destDir, overwrite })
|
||||
.then((r) => r.data),
|
||||
|
||||
move: (src: string, destDir: string, overwrite = false) =>
|
||||
move: (src: string, destDir: string, overwrite = false, agentId?: number) =>
|
||||
api
|
||||
.post<{ path: string }>("/api/files/move", { src, dest_dir: destDir, overwrite })
|
||||
.post<{ path: string }>(`${base(agentId)}/move`, { src, dest_dir: destDir, overwrite })
|
||||
.then((r) => r.data),
|
||||
|
||||
download: async (path: string, filename: string) => {
|
||||
const res = await api.get("/api/files/download", { params: { path }, responseType: "blob" });
|
||||
download: async (path: string, filename: string, agentId?: number) => {
|
||||
const res = await api.get(`${base(agentId)}/download`, { params: { path }, responseType: "blob" });
|
||||
triggerDownload(res.data as Blob, filename);
|
||||
},
|
||||
|
||||
upload: async (path: string, file: File, overwrite = false, relPath = "") => {
|
||||
upload: async (path: string, file: File, overwrite = false, relPath = "", agentId?: number) => {
|
||||
const form = new FormData();
|
||||
form.append("path", path);
|
||||
form.append("overwrite", String(overwrite));
|
||||
if (relPath) form.append("rel_path", relPath);
|
||||
form.append("file", file);
|
||||
const res = await api.post<{ ok: boolean; name: string }>("/api/files/upload", form);
|
||||
const res = await api.post<{ ok: boolean; name: string }>(`${base(agentId)}/upload`, form);
|
||||
return res.data;
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user