From 33c779aa9894e1347aeaa4c73e536cf842aee684 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Tue, 21 Apr 2026 10:53:31 -0700 Subject: feat: Implement write_file tool with overwrite safety (#3432) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Implements the `write_file` client-side tool — creates new files or overwrites existing ones with an explicit `overwrite` flag for safety. - **Overwrite flag**: Writing to an existing file without `overwrite: true` returns an error directing the LLM to set the flag or use `edit_file` for targeted changes. Prevents accidental overwrites. - **Snapshots**: Existing files are backed up before overwriting (same infrastructure as `edit_file`). - **Content preview**: Completed writes show the first 10 lines in gray with line numbers, plus "+ N more lines" for longer files. - **Atomic writes**: Uses `tempfile` + fsync + rename (same as `edit_file`). - **File tracker update**: After writing, the file is registered in the tracker so subsequent `edit_file` calls work without a separate read. - **Permission**: Shares the `"Write"` rule with `edit_file` — one permission covers both tools. --- crates/atuin-ai/src/diff.rs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) (limited to 'crates/atuin-ai/src/diff.rs') diff --git a/crates/atuin-ai/src/diff.rs b/crates/atuin-ai/src/diff.rs index 663481c0..e704175c 100644 --- a/crates/atuin-ai/src/diff.rs +++ b/crates/atuin-ai/src/diff.rs @@ -101,6 +101,40 @@ impl EditPreview { } } +/// Maximum lines to show in a write preview. +const WRITE_PREVIEW_LINES: usize = 10; + +/// A content preview for a write_file operation. +/// +/// Shows the first N lines of the written content plus a count of +/// remaining lines if truncated. +#[derive(Debug, Clone)] +pub(crate) struct WritePreview { + /// First lines of content (up to WRITE_PREVIEW_LINES). + pub lines: Vec, + /// Total number of lines in the written file. + pub total_lines: usize, +} + +impl WritePreview { + /// Create a preview from file content. + pub fn from_content(content: &str) -> Self { + let all_lines: Vec<&str> = content.lines().collect(); + let total_lines = all_lines.len(); + let lines = all_lines + .into_iter() + .take(WRITE_PREVIEW_LINES) + .map(String::from) + .collect(); + WritePreview { lines, total_lines } + } + + /// Number of lines not shown in the preview. + pub fn remaining_lines(&self) -> usize { + self.total_lines.saturating_sub(self.lines.len()) + } +} + /// Build a single DiffHunk from a group of adjacent raw hunks. fn build_hunk(group: &[&imara_diff::Hunk], input: &InternedInput<&str>) -> DiffHunk { let first = group.first().unwrap(); -- cgit v1.3.1