aboutsummaryrefslogtreecommitdiffstats
path: root/crates/atuin-ai/src/tui/view
diff options
context:
space:
mode:
authorMichelle Tilley <michelle@michelletilley.net>2026-04-21 10:53:31 -0700
committerGitHub <noreply@github.com>2026-04-21 10:53:31 -0700
commit33c779aa9894e1347aeaa4c73e536cf842aee684 (patch)
treebfe0f60252518798ccf4621c7bea06021e64d2f4 /crates/atuin-ai/src/tui/view
parentfeat: AI tool rendering overhaul + edit_file tool (#3423) (diff)
downloadatuin-33c779aa9894e1347aeaa4c73e536cf842aee684.zip
feat: Implement write_file tool with overwrite safety (#3432)
## 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.
Diffstat (limited to 'crates/atuin-ai/src/tui/view')
-rw-r--r--crates/atuin-ai/src/tui/view/mod.rs68
-rw-r--r--crates/atuin-ai/src/tui/view/turn.rs6
2 files changed, 64 insertions, 10 deletions
diff --git a/crates/atuin-ai/src/tui/view/mod.rs b/crates/atuin-ai/src/tui/view/mod.rs
index bdbece9c..6e13e406 100644
--- a/crates/atuin-ai/src/tui/view/mod.rs
+++ b/crates/atuin-ai/src/tui/view/mod.rs
@@ -175,7 +175,7 @@ fn tool_call_view(tool_call: &TrackedTool, in_git_project: bool) -> Elements {
/// keep the standard set.
fn permission_options_for_tool(tool: &ClientToolCall, in_git_project: bool) -> Vec<SelectOption> {
match tool {
- ClientToolCall::Edit(_) => vec![
+ ClientToolCall::Edit(_) | ClientToolCall::Write(_) => vec![
SelectOption::builder()
.label("Allow")
.value(PermissionResult::Allow.as_value_str())
@@ -296,8 +296,8 @@ fn agent_turn_view(events: &[turn::UiEvent], busy: bool) -> Elements {
turn::ToolRenderData::FileEdit { path, preview } => {
file_edit_tool_view(&tool_key, &details.status, path, preview.as_ref())
},
- turn::ToolRenderData::FileWrite { path } => {
- file_write_tool_view(&details.status, path)
+ turn::ToolRenderData::FileWrite { path, preview } => {
+ file_write_tool_view(&tool_key, &details.status, path, preview.as_ref())
},
turn::ToolRenderData::Remote => {
tool_status_view(&details.name, &details.status)
@@ -577,10 +577,16 @@ fn file_edit_tool_view(
}
}
-/// Render a file write tool call status with the target path.
-fn file_write_tool_view(status: &turn::ToolResultStatus, path: &std::path::Path) -> Elements {
- let display_path = path.display();
- match status {
+/// Render a file write tool call with content preview.
+fn file_write_tool_view(
+ key: &str,
+ status: &turn::ToolResultStatus,
+ path: &std::path::Path,
+ preview: Option<&crate::diff::WritePreview>,
+) -> Elements {
+ let display_path = format_path_for_display(path);
+
+ let status_line = match status {
turn::ToolResultStatus::Pending => {
element! {
Spinner(
@@ -591,18 +597,62 @@ fn file_write_tool_view(status: &turn::ToolResultStatus, path: &std::path::Path)
}
}
turn::ToolResultStatus::Success => {
+ let line_info = preview
+ .map(|p| format!(" ({} lines)", p.total_lines))
+ .unwrap_or_default();
element! {
- Spinner(label: format!("Wrote: {display_path}"), done: true)
+ Spinner(label: format!("Wrote: {display_path}{line_info}"), done: true)
}
}
turn::ToolResultStatus::Error => {
element! {
Text {
Span(text: "✗ ", style: Style::default().fg(Color::Red))
- Span(text: format!("Write {display_path}: denied"), style: Style::default().fg(Color::Red))
+ Span(text: format!("Write {display_path}: failed"), style: Style::default().fg(Color::Red))
}
}
}
+ };
+
+ let Some(preview) = preview else {
+ return status_line;
+ };
+ if preview.lines.is_empty() {
+ return status_line;
+ }
+
+ let gutter_width = preview.total_lines.to_string().len().max(2) as u16 + 1;
+ let remaining = preview.remaining_lines();
+
+ element! {
+ View(key: key.to_string()) {
+ #(status_line)
+
+ View(key: format!("{key}-content"), padding_left: Cells::from(2)) {
+ #(for (idx, line) in preview.lines.iter().enumerate() {
+ HStack(key: format!("{key}-line-{idx}")) {
+ View(width: WidthConstraint::Fixed(gutter_width)) {
+ Text { Span(
+ text: format!("{:>width$}", idx + 1, width = (gutter_width - 1) as usize),
+ style: Style::default().fg(Color::DarkGray)
+ ) }
+ }
+ View {
+ Text { Span(text: line, style: Style::default().fg(Color::DarkGray)) }
+ }
+ }
+ })
+
+ #(if remaining > 0 {
+ Text {
+ Span(
+ text: format!(" ... +{remaining} more lines"),
+ style: Style::default().fg(Color::DarkGray)
+ )
+ }
+ })
+ }
+ }
}
}
diff --git a/crates/atuin-ai/src/tui/view/turn.rs b/crates/atuin-ai/src/tui/view/turn.rs
index 1c19a6b2..6c3d5c29 100644
--- a/crates/atuin-ai/src/tui/view/turn.rs
+++ b/crates/atuin-ai/src/tui/view/turn.rs
@@ -141,7 +141,10 @@ pub(crate) enum ToolRenderData {
preview: Option<crate::diff::EditPreview>,
},
/// File write/create operation.
- FileWrite { path: PathBuf },
+ FileWrite {
+ path: PathBuf,
+ preview: Option<crate::diff::WritePreview>,
+ },
/// Atuin history search.
HistorySearch {
query: String,
@@ -449,6 +452,7 @@ impl<'a> TurnBuilder<'a> {
},
ClientToolCall::Write(write) => ToolRenderData::FileWrite {
path: write.path.clone(),
+ preview: tracked.write_preview.clone(),
},
ClientToolCall::AtuinHistory(history) => ToolRenderData::HistorySearch {
query: history.query.clone(),