Skip to content

Commit c191941

Browse files
ndemiancclaude
andcommitted
Atom++ AI agent: delete_file tool (rounds out M4 multi-file refactors)
Adds delete_file to the agent tool loop (remove a file during a refactor; rename/move = write the new path then delete the old). Wired through apply-then-review (reviewSession.applyDelete + deletion-aware undoFile: Keep confirms, Undo recreates from the snapshot) and the per-turn checkpoint (restore recreates). CLAUDE.md: M4 marked done (the tool loop + auto-verify + checkpoints already shipped) and atom-sync/atom-updater/tools added to the layout. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent cd8c71c commit c191941

4 files changed

Lines changed: 72 additions & 5 deletions

File tree

‎CLAUDE.md‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,11 @@ extensions/atom-npp-pack/ Notepad++ power-editing pack (plain JS, no build ste
3737
extensions/atom-ai/ native Claude AI (chat, inline completion, providers, edit-with-diff, LM provider)
3838
extensions/atom-themes/ signature One Dark / One Light themes (JSON, default via configurationDefaults)
3939
extensions/atom-hackability/ user init script + Atom/NPP keymap presets + package generator & hot-reload dev loader
40+
extensions/atom-sync/ 'atompp' auth provider that lights up the built-in Settings Sync (Atom++ Sync, S0)
41+
extensions/atom-updater/ notify-only update checker (polls the update feed; never auto-applies)
4042
patches/atom-core.patch our core source edits, applied on bootstrap
41-
scripts/ bootstrap.sh, apply-branding.mjs, run-dev.sh, build-macos.sh, make-dmg.sh, make-icon.sh
43+
scripts/ bootstrap.sh, apply-branding.mjs, run-dev.sh, build-macos.sh, make-dmg.sh, make-icon.sh; atom (CLI launcher) + install-atom.sh
44+
tools/ dependency-free reference servers: sync-server (/v1 Settings-Sync), update-server (/api/update feed)
4245
vscode/ GITIGNORED upstream Code-OSS checkout (generated)
4346
```
4447

@@ -121,8 +124,12 @@ big-file mode badge. Files: extension.js + fileOps/lineOps/columnOps/encodingEol
121124
(`gatherAutoContext`), chat moved to the right side bar, typewriter streaming, `make-dmg.sh` packaging.
122125
- M3 so far: One Dark/Light themes (`atom-themes`); user init script + Atom/Notepad++ keymap presets +
123126
package generator with live hot-reload (`atom-hackability`). The Atom/NPP keymap also clears the old M1 leftover.
124-
- Not yet built: agentic multi-file tasks (M4); remaining M3 hackability — settings UI, theme studio,
125-
VS Code settings import.
127+
- M4 agentic multi-file tasks: DONE. `agent.js` has the full tool loop — list_files/read_file/search,
128+
update_plan, edit_file/write_file/**delete_file** (apply-then-review with Keep/Undo + per-turn checkpoint
129+
restore via `reviewSession.js`), run_command(+background)/read_command_output, ask_user, use_skill — plus the
130+
M5 auto-verify loop. `delete_file` also enables rename/move (write new path → delete old).
131+
- Atom++ Sync (S0) + notify-only updater (U0): see `extensions/atom-sync` / `extensions/atom-updater` + `tools/`.
132+
- Not yet built: remaining M3 hackability — settings UI, theme studio, VS Code settings import.
126133

127134
## Conventions
128135

‎extensions/atom-ai/agent.js‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const SYSTEM_BASE = [
2424
'- CRITICAL: never end your turn by merely describing an edit ("now let me refactor…"). If you intend to change a file, you MUST call edit_file or write_file in that SAME turn. Words are not edits.',
2525
'- To change an EXISTING file, use edit_file with an exact, unique snippet (old_str) and its replacement (new_str). This is preferred — small and reliable. Read the file first so old_str matches exactly. Prefer SEVERAL small edit_file calls over one huge one (smaller edits are faster and easier to review).',
2626
'- Use write_file ONLY to create a new file (or fully rewrite a short one); it needs the COMPLETE content. Do not rewrite large files — use edit_file repeatedly instead.',
27+
'- Use delete_file to remove an existing file (e.g. during a refactor). To RENAME/move a file, write_file the new path then delete_file the old one. Deletions are reviewable (Keep/Undo) and restorable from the per-turn checkpoint.',
2728
'- Your file edits are APPLIED IMMEDIATELY and the user reviews them afterward in the editor with Keep/Undo — do NOT wait for approval, and do NOT re-edit a file you just edited. Only run_command still needs approval; if the user skips a command, adapt or stop.',
2829
'- Commands that do NOT exit on their own (dev servers, file watchers, tail -f) MUST be run with run_command background:true — it returns immediately so you keep working instead of hanging. After starting one, call read_command_output with the returned id to watch for a readiness/port line (e.g. "listening on :3000") before you test against it. Use a normal foreground run_command for things that finish (builds, installs, tests, git, curl). This pairs with verification: bring the app up in the background, confirm it serves, fix, repeat.',
2930
'- Paths are relative to the workspace root.',
@@ -41,6 +42,7 @@ const TOOLS = [
4142
{ name: 'update_plan', description: 'Declare or update your task checklist for a multi-step goal. Pass the FULL list each time, each item with a status. Call it once up front (all pending), then again to mark an item in_progress when you start it and done when finished. Skip for trivial single-step goals.', input_schema: { type: 'object', properties: { todos: { type: 'array', items: { type: 'object', properties: { title: { type: 'string' }, status: { type: 'string', enum: ['pending', 'in_progress', 'done'] } }, required: ['title', 'status'] } } }, required: ['todos'] } },
4243
{ name: 'edit_file', description: 'Make a targeted edit to an EXISTING file: replace an exact, unique snippet (old_str) with new_str. Applied immediately; the user reviews it with Keep/Undo. old_str must appear exactly once — include enough surrounding context to be unique.', input_schema: { type: 'object', properties: { path: { type: 'string' }, old_str: { type: 'string' }, new_str: { type: 'string' } }, required: ['path', 'old_str', 'new_str'] } },
4344
{ name: 'write_file', description: 'Create a new file (or fully overwrite a short one) with the COMPLETE content. For edits to existing files, prefer edit_file. Applied immediately; the user reviews it with Keep/Undo.', input_schema: { type: 'object', properties: { path: { type: 'string' }, content: { type: 'string' } }, required: ['path', 'content'] } },
45+
{ name: 'delete_file', description: 'Delete an EXISTING workspace file (e.g. removing a file during a refactor). Applied immediately; the user reviews it with Keep/Undo, and the per-turn checkpoint can restore it. To RENAME or move a file: write_file the new path, then delete_file the old one.', input_schema: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] } },
4446
{ name: 'run_command', description: 'Run a shell command in the workspace root. Requires approval. Pass background:true for commands that do not exit on their own (servers, watchers) so the agent is not blocked — it returns immediately and you read progress later with read_command_output.', input_schema: { type: 'object', properties: { command: { type: 'string' }, explanation: { type: 'string' }, background: { type: 'boolean', description: 'true = start it and keep working without waiting (dev servers, watchers, tail -f). Returns immediately with an id; poll read_command_output for its output/status.' } }, required: ['command'] } },
4547
{ name: 'read_command_output', description: 'Read recent output + status of a command started with run_command background:true. Returns a status header ([running on :3000] / [exited 0] / [stopped]) followed by the latest output lines. Poll this to wait for a server to become ready before testing against it.', input_schema: { type: 'object', properties: { id: { type: 'string', description: 'the id returned by a background run_command' }, lines: { type: 'number', description: 'max recent output lines to return (default 80, max 400)' } }, required: ['id'] } },
4648
{ name: 'ask_user', description: 'Ask the user one or more multiple-choice questions when the goal genuinely depends on a decision only they can make (tech stack, scope, where to put files, must-have features). The user picks by CLICKING — do NOT write questions as prose. Ask ONCE up front with all your questions, then proceed with the answers and never re-ask. Prefer sensible defaults over asking; only ask when a wrong guess would waste real work.', input_schema: { type: 'object', properties: { questions: { type: 'array', items: { type: 'object', properties: { header: { type: 'string', description: 'a 1-3 word tag for the question' }, question: { type: 'string' }, multiSelect: { type: 'boolean', description: 'true if several options can be picked at once' }, options: { type: 'array', items: { type: 'object', properties: { label: { type: 'string' }, description: { type: 'string' } }, required: ['label'] } } }, required: ['question', 'options'] } } }, required: ['questions'] } },
@@ -286,6 +288,17 @@ async function runTool(tu, ctx) {
286288
if (ctx.touched) { ctx.touched.add(abs); } // this run's edited files → verified afterward
287289
return 'Applied edit to ' + input.path + ' (pending the user\'s Keep/Undo review).';
288290
}
291+
if (tu.name === 'delete_file') {
292+
const abs = safeJoin(root, input.path || '');
293+
if (!abs) { return 'ERROR: path is outside the workspace'; }
294+
if (!fs.existsSync(abs)) { return 'ERROR: file not found: ' + input.path; }
295+
try { if (fs.statSync(abs).isDirectory()) { return 'ERROR: ' + input.path + ' is a directory — delete_file removes a single file.'; } } catch { /* */ }
296+
const ok = ctx.applyDelete ? await ctx.applyDelete({ path: input.path }) : false;
297+
if (!ok) { return 'ERROR: could not delete ' + input.path + '.'; }
298+
ctx.editCount = (ctx.editCount || 0) + 1;
299+
if (ctx.touched) { ctx.touched.delete(abs); } // it's gone — don't verify a deleted file
300+
return 'Deleted ' + input.path + ' (pending the user\'s Keep/Undo review; the per-turn checkpoint can also restore it).';
301+
}
289302
if (tu.name === 'run_command') {
290303
const cmd = String(input.command || '');
291304
const bg = input.background === true;
@@ -467,6 +480,7 @@ async function runAgent(ctx) {
467480
onToolStart: (name) => {
468481
dbg('tool.start', { name });
469482
const verb = name === 'edit_file' || name === 'write_file' ? 'preparing edit (' + name + ')…'
483+
: name === 'delete_file' ? 'deleting a file…'
470484
: name === 'run_command' ? 'preparing command…' : name === 'update_plan' ? 'planning…' : 'running ' + name + '…';
471485
ctx.post({ type: 'agentStatus', text: verb });
472486
}

‎extensions/atom-ai/extension.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,7 @@ async function agentFlow(text) {
589589
commandRuns: bgRuns, // runId → background-process registry (read_command_output reads it)
590590
commandTimeout: cfg.get('commandTimeout', 120000), // backstop before a command is force-killed (0 = off)
591591
applyEdit: (req) => review.applyEdit(req), // file edits: apply-then-review
592+
applyDelete: (req) => review.applyDelete(req), // file deletions: apply-then-review
592593
verify: verifyCfg, // M5 auto-verify settings
593594
touched: runTouched, // agent.js adds each edited abs path here
594595
getTouchedUris: () => [...runTouched].map((p) => vscode.Uri.file(p)),

‎extensions/atom-ai/reviewSession.js‎

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,45 @@ function registerReview(context, post, dbg, recordTouch) {
173173
return true;
174174
}
175175

176+
/** Delete a file (and save), registering it for review so Keep confirms / Undo recreates it.
177+
* The pre-image is snapshotted (and handed to the per-turn checkpoint) so it is always recoverable. */
178+
async function applyDelete(req) {
179+
const root = workspaceRoot();
180+
if (!root) { return false; }
181+
const abs = path.resolve(root, req.path || '');
182+
if (abs !== root && !abs.startsWith(root + path.sep)) { return false; }
183+
if (!fs.existsSync(abs)) { return false; }
184+
const uri = vscode.Uri.file(abs);
185+
const key = uri.toString();
186+
let snapshot;
187+
if (pending.has(key)) { snapshot = pending.get(key).snapshot; }
188+
else { try { snapshot = fs.readFileSync(abs, 'utf8').replace(/^/, ''); } catch { return false; } }
189+
// Checkpoint hook: the file EXISTED (created=false) → per-turn Restore recreates it from this snapshot.
190+
if (recordTouch) { try { recordTouch(key, snapshot, false); } catch (e) { /* checkpoint capture must never break editing */ } }
191+
const edit = new vscode.WorkspaceEdit();
192+
edit.deleteFile(uri, { ignoreIfNotExists: true });
193+
const prevExpected = expected.get(key);
194+
expected.set(key, ''); // guard the change listener against our own delete
195+
const ok = await vscode.workspace.applyEdit(edit);
196+
dbg('review.delete', { path: req.path, ok });
197+
if (!ok) {
198+
if (pending.has(key)) { expected.set(key, prevExpected != null ? prevExpected : pending.get(key).appliedText); }
199+
else { expected.delete(key); }
200+
return false;
201+
}
202+
const existed = pending.has(key) ? pending.get(key).exists : true;
203+
const diff = makeDiff(snapshot, '');
204+
const { add, del } = countDiff(diff);
205+
const fr = { uri, rel: vscode.workspace.asRelativePath(uri), exists: existed, snapshot, range: new vscode.Range(0, 0, 0, 0), add, del, appliedText: '', deleted: true };
206+
pending.set(key, fr);
207+
persist();
208+
refreshLenses();
209+
post({ type: 'editApplied', id: key, path: fr.rel, exists: existed, add, del, diff, deleted: true });
210+
postState();
211+
updateActiveUi();
212+
return true;
213+
}
214+
176215
/** Serialize pending reviews so Undo survives a window reload (snapshot is the only irreplaceable bit). */
177216
function persist() {
178217
const arr = [...pending.values()].map((fr) => ({ uri: fr.uri.toString(), rel: fr.rel, exists: fr.exists, snapshot: fr.snapshot }));
@@ -200,7 +239,13 @@ function registerReview(context, post, dbg, recordTouch) {
200239
const fr = pending.get(key);
201240
if (!fr) { return; }
202241
const edit = new vscode.WorkspaceEdit();
203-
if (fr.exists) {
242+
const gone = !fs.existsSync(fr.uri.fsPath);
243+
if (fr.exists && gone) {
244+
// the agent DELETED this file → recreate it from the snapshot
245+
edit.createFile(fr.uri, { ignoreIfExists: true });
246+
edit.insert(fr.uri, new vscode.Position(0, 0), fr.snapshot);
247+
expected.set(key, fr.snapshot);
248+
} else if (fr.exists) {
204249
const doc = await vscode.workspace.openTextDocument(fr.uri);
205250
edit.replace(fr.uri, doc.validateRange(new vscode.Range(0, 0, doc.lineCount + 1, 0)), fr.snapshot);
206251
expected.set(key, fr.snapshot);
@@ -337,7 +382,7 @@ function registerReview(context, post, dbg, recordTouch) {
337382
postState();
338383
}
339384

340-
return { applyEdit, keepFile, undoFile, keepAll, undoAll, finalizeAll, restoreOne, openDiff, resync, pendingCount: () => pending.size };
385+
return { applyEdit, applyDelete, keepFile, undoFile, keepAll, undoAll, finalizeAll, restoreOne, openDiff, resync, pendingCount: () => pending.size };
341386
}
342387

343388
module.exports = { registerReview };

0 commit comments

Comments
 (0)