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 };