From dfe1af2e5faeac0cbb930e71ab88fa1196afaa8c Mon Sep 17 00:00:00 2001 From: shenlvkang-collab Date: Mon, 9 Nov 2026 02:40:13 +0100 Subject: [PATCH] feat(files): delete a file from the Files panel A delete button on each file row of the Files panel (tree and search results) and a DELETE /api/sessions/:id/files?path= route that removes one regular file from the session workspace, refused for remote (SSH) sessions and behind the same sensitive-path, attachment-guard and .git checks as the edit route. Salvaged from #529. --- src/web/public/panels-ui.js | 26 ++++++++++++++++++++++ src/web/public/styles.css | 18 +++++++++++++++ src/web/routes/file-routes.ts | 42 +++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+) diff --git a/src/web/public/panels-ui.js b/src/web/public/panels-ui.js index 30e3ac70..a94194a7 100644 --- a/src/web/public/panels-ui.js +++ b/src/web/public/panels-ui.js @@ -3799,6 +3799,9 @@ Object.assign(CodemanApp.prototype, { const downloadBtn = !isDir ? `⬇` : ''; + const deleteBtn = !isDir + ? `` + : ''; html.push(`
@@ -3807,6 +3810,7 @@ Object.assign(CodemanApp.prototype, { ${escapeHtml(node.name)} ${sizeStr} ${downloadBtn} + ${deleteBtn}
`); @@ -3981,6 +3985,9 @@ Object.assign(CodemanApp.prototype, { const downloadBtn = !isDir ? `⬇` : ''; + const deleteBtn = !isDir + ? `` + : ''; return `
@@ -3988,6 +3995,7 @@ Object.assign(CodemanApp.prototype, { ${escapeHtml(match.name)} ${sizeStr} ${downloadBtn} + ${deleteBtn}
`; }) @@ -4012,6 +4020,24 @@ Object.assign(CodemanApp.prototype, { } }, + async deleteFileFromBrowser(sessionId, filePath) { + if (!sessionId || !filePath) return; + if (!window.confirm(`Delete ${filePath}?`)) return; + try { + const response = await fetch( + `/api/sessions/${encodeURIComponent(sessionId)}/files?path=${encodeURIComponent(filePath)}`, + { method: 'DELETE' }, + ); + const result = await response.json().catch(() => null); + if (!response.ok || !result?.success) throw new Error(result?.error || 'Delete failed'); + this.showToast(`Deleted: ${filePath}`, 'success'); + await this.loadFileBrowser(sessionId, { force: true }); + } catch (error) { + console.error('File browser delete failed:', error); + this.showToast(error?.message || 'Delete failed', 'error'); + } + }, + _findFileBrowserDirectory(nodes, targetPath, ancestors = []) { if (!Array.isArray(nodes)) return null; for (const node of nodes) { diff --git a/src/web/public/styles.css b/src/web/public/styles.css index af663547..f6108e77 100644 --- a/src/web/public/styles.css +++ b/src/web/public/styles.css @@ -12386,6 +12386,24 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat .file-tree-download:hover { color: #4fc3f7; } +.file-tree-delete { + display: none; + margin-left: 2px; + padding: 0 4px; + border: 0; + background: transparent; + color: #888; + font-size: 14px; + line-height: 1; + cursor: pointer; + flex-shrink: 0; +} +.file-tree-item:hover .file-tree-delete { + display: inline; +} +.file-tree-delete:hover { + color: #ef5350; +} /* File tree indent levels */ .file-tree-item[data-depth="0"] { padding-left: 0.5rem; } diff --git a/src/web/routes/file-routes.ts b/src/web/routes/file-routes.ts index 218bc8f5..89978f3c 100644 --- a/src/web/routes/file-routes.ts +++ b/src/web/routes/file-routes.ts @@ -2000,6 +2000,48 @@ export function registerFileRoutes(app: FastifyInstance, ctx: SessionPort & Even }; }); + // Delete a regular file from the current session workspace. The same + // confinement and sensitive-path checks used by previews apply here. + app.delete('/api/sessions/:id/files', async (req, reply) => { + const { id } = req.params as { id: string }; + const { path: filePath } = req.query as { path?: string }; + const session = findSessionOrFail(ctx, id, req); + if (session.remote) { + reply.code(400); + return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Deleting is not supported for remote (SSH) sessions'); + } + if (!filePath) { + reply.code(400); + return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Missing path parameter'); + } + const validated = validateSessionFilePath(session.workingDir, filePath); + if (!validated) { + reply.code(404); + return createErrorResponse(ApiErrorCode.NOT_FOUND, 'File not found'); + } + const { resolvedPath, relativePath } = validated; + const guard = await loadAttachmentGuardConfig(); + if ( + isSensitivePath(resolvedPath) || + isBlockedAttachmentPath(resolvedPath, guard.blockedTrees) || + isDeniedEditRelativePath(relativePath) + ) { + reply.code(403); + return createErrorResponse(ApiErrorCode.FORBIDDEN, 'Deleting this file is blocked'); + } + const stat = await fs.lstat(resolvedPath).catch(() => undefined); + if (!stat) { + reply.code(404); + return createErrorResponse(ApiErrorCode.NOT_FOUND, 'File not found'); + } + if (!stat.isFile()) { + reply.code(400); + return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Only regular files can be deleted'); + } + await fs.unlink(resolvedPath); + return { success: true, data: { path: filePath } }; + }); + // Get file content for preview (File Browser) app.get('/api/sessions/:id/file-content', async (req) => { const { id } = req.params as { id: string };