Skip to content

Commit 0c70285

Browse files
authored
✨ feat(fs_write): Add append functionality to fs_write tool (#764)
* ✨ feat(cli): Add append functionality to fs_write tool This commit enhances the fs_write tool by adding a new "append" command that allows users to append content to the end of files without having to read and rewrite the entire file. ## Features - 📝 Add new `Append` variant to the `FsWrite` enum - 🔄 Implement functionality to append content to existing files - 🆕 Auto-create files when appending to non-existent files - 📁 Auto-create parent directories when needed - 🧪 Add comprehensive test coverage for the new functionality ## Implementation Details - Added validation to ensure path and content are not empty - Enhanced user feedback during append operations - Implemented syntax highlighting for appended content - Maintained consistent code patterns with existing commands - Added proper newline handling to ensure clean appends - Updated tool_index.json schema to expose the new command ## Use Cases - Adding new lines to configuration files - Appending log entries - Adding new content to the end of text files - Creating files incrementally This enhancement improves the user experience by providing a more convenient way to add content to files without having to read the entire file first and then use the "create" command to overwrite it. Resolves: #[issue-number] * 🔧 Clean up code formatting in fs_write.rs Standardized code formatting in the fs_write.rs file by: - Fixing inconsistent indentation - Removing unnecessary whitespace - Simplifying multi-line assertions - Maintaining consistent brace style for struct definitions No functional changes, purely cosmetic improvements for better code readability. * 🔄 refactor(fs_write): rename "content" parameter to "new_str" in append command Standardized parameter naming across all fs_write commands by renaming the "content" parameter to "new_str" in the append command to maintain consistency with other commands. Also removed automatic newline insertion when appending content to files, giving more control to the caller. * ✨ fix: Improve file append behavior with automatic newline handling - Added logic to automatically insert newlines when appending content to files - Updated fs_write tool description to document the new behavior - Fixed test case to properly validate the newline insertion behavior This change resolves the pain point of getting whitespace right when appending content to files, ensuring consistent behavior regardless of whether the existing file ends with a newline or the new content starts with one. * Change append behavior to fail when file doesn't exist * Update fs_write tool description to match new append behavior * 🧹 chore: remove commented-out test code in fs_write tool Removed obsolete commented-out test code that was no longer valid after changing the behavior to not create files when appending. This improves code readability and maintenance by eliminating dead code that was already noted as "no longer valid".
1 parent 8885a67 commit 0c70285

File tree

2 files changed

+113
-4
lines changed

2 files changed

+113
-4
lines changed

crates/q_cli/src/cli/chat/tools/fs_write.rs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ pub enum FsWrite {
4545
insert_line: usize,
4646
new_str: String,
4747
},
48+
#[serde(rename = "append")]
49+
Append { path: String, new_str: String },
4850
}
4951

5052
impl FsWrite {
@@ -121,6 +123,38 @@ impl FsWrite {
121123
fs.write(&path, &file).await?;
122124
Ok(Default::default())
123125
},
126+
FsWrite::Append { path, new_str } => {
127+
let path = sanitize_path_tool_arg(ctx, path);
128+
129+
// Return an error if the file doesn't exist
130+
if !fs.exists(&path) {
131+
bail!("The file does not exist: {}", path.display());
132+
}
133+
134+
queue!(
135+
updates,
136+
style::Print("Appending to: "),
137+
style::SetForegroundColor(Color::Green),
138+
style::Print(format_path(cwd, &path)),
139+
style::ResetColor,
140+
style::Print("\n"),
141+
)?;
142+
143+
// Read existing content
144+
let mut file_content = fs.read_to_string(&path).await.unwrap_or_default();
145+
146+
// Check if we need to add a newline before appending
147+
// Only add a newline if the file is not empty and doesn't already end with one
148+
// Also don't add a newline if the new content starts with one
149+
if !file_content.is_empty() && !file_content.ends_with('\n') && !new_str.starts_with('\n') {
150+
file_content.push('\n');
151+
}
152+
153+
// Append the new content
154+
file_content.push_str(new_str);
155+
fs.write(&path, file_content).await?;
156+
Ok(Default::default())
157+
},
124158
}
125159
}
126160

@@ -210,6 +244,21 @@ impl FsWrite {
210244
)?;
211245
Ok(())
212246
},
247+
FsWrite::Append { path, new_str } => {
248+
let relative_path = format_path(cwd, path);
249+
let file = stylize_output_if_able(ctx, &relative_path, new_str, None, Some("+"));
250+
queue!(
251+
updates,
252+
style::Print("Path: "),
253+
style::SetForegroundColor(Color::Green),
254+
style::Print(relative_path),
255+
style::ResetColor,
256+
style::Print("\n\nAppending content:\n"),
257+
style::Print(file),
258+
style::ResetColor,
259+
)?;
260+
Ok(())
261+
},
213262
}
214263
}
215264

@@ -226,6 +275,14 @@ impl FsWrite {
226275
bail!("The provided path must exist in order to replace or insert contents into it")
227276
}
228277
},
278+
FsWrite::Append { path, new_str } => {
279+
if path.is_empty() {
280+
bail!("Path must not be empty")
281+
};
282+
if new_str.is_empty() {
283+
bail!("Content to append must not be empty")
284+
};
285+
},
229286
}
230287

231288
Ok(())
@@ -350,6 +407,15 @@ mod tests {
350407
});
351408
let fw = serde_json::from_value::<FsWrite>(v).unwrap();
352409
assert!(matches!(fw, FsWrite::Insert { .. }));
410+
411+
// append
412+
let v = serde_json::json!({
413+
"path": path,
414+
"command": "append",
415+
"new_str": "appended content",
416+
});
417+
let fw = serde_json::from_value::<FsWrite>(v).unwrap();
418+
assert!(matches!(fw, FsWrite::Append { .. }));
353419
}
354420

355421
#[tokio::test]
@@ -565,6 +631,49 @@ mod tests {
565631
assert_eq!(actual, format!("{}{}{}", new_str, test_file_contents, new_str),);
566632
}
567633

634+
#[tokio::test]
635+
async fn test_fs_write_tool_append() {
636+
let ctx = setup_test_directory().await;
637+
let mut stdout = std::io::stdout();
638+
639+
// Test appending to existing file
640+
let content_to_append = "\n5: Appended line";
641+
let v = serde_json::json!({
642+
"path": TEST_FILE_PATH,
643+
"command": "append",
644+
"new_str": content_to_append,
645+
});
646+
647+
serde_json::from_value::<FsWrite>(v)
648+
.unwrap()
649+
.invoke(&ctx, &mut stdout)
650+
.await
651+
.unwrap();
652+
653+
let actual = ctx.fs().read_to_string(TEST_FILE_PATH).await.unwrap();
654+
assert_eq!(
655+
actual,
656+
format!("{}{}", TEST_FILE_CONTENTS, content_to_append),
657+
"Content should be appended to the end of the file"
658+
);
659+
660+
// Test appending to non-existent file (should fail)
661+
let new_file_path = "/new_append_file.txt";
662+
let content = "This is a new file created by append";
663+
let v = serde_json::json!({
664+
"path": new_file_path,
665+
"command": "append",
666+
"new_str": content,
667+
});
668+
669+
let result = serde_json::from_value::<FsWrite>(v)
670+
.unwrap()
671+
.invoke(&ctx, &mut stdout)
672+
.await;
673+
674+
assert!(result.is_err(), "Appending to non-existent file should fail");
675+
}
676+
568677
#[test]
569678
fn test_truncate_str() {
570679
let s = "Hello, world!";

crates/q_cli/src/cli/chat/tools/tool_index.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,14 @@
3636
},
3737
"fs_write": {
3838
"name": "fs_write",
39-
"description": "A tool for creating and editing files\n * The `create` command will override the file at `path` if it already exists as a file, and otherwise create a new file\n * Please prefer creating smaller files even when asked otherwise as large file creations are likely to fail.\n Notes for using the `str_replace` command:\n * The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n * If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n * The `new_str` parameter should contain the edited lines that should replace the `old_str`.",
39+
"description": "A tool for creating and editing files\n * The `create` command will override the file at `path` if it already exists as a file, and otherwise create a new file\n * The `append` command will add content to the end of an existing file, automatically adding a newline if the file doesn't end with one. The file must exist.\n Notes for using the `str_replace` command:\n * The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n * If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n * The `new_str` parameter should contain the edited lines that should replace the `old_str`.",
4040
"input_schema": {
4141
"type": "object",
4242
"properties": {
4343
"command": {
4444
"type": "string",
45-
"enum": ["create", "str_replace", "insert"],
46-
"description": "The commands to run. Allowed options are: `create`, `str_replace`, `insert`."
45+
"enum": ["create", "str_replace", "insert", "append"],
46+
"description": "The commands to run. Allowed options are: `create`, `str_replace`, `insert`, `append`."
4747
},
4848
"file_text": {
4949
"description": "Required parameter of `create` command, with the content of the file to be created.",
@@ -54,7 +54,7 @@
5454
"type": "integer"
5555
},
5656
"new_str": {
57-
"description": "Required parameter of `str_replace` command containing the new string. Required parameter of `insert` command containing the string to insert.",
57+
"description": "Required parameter of `str_replace` command containing the new string. Required parameter of `insert` command containing the string to insert. Required parameter of `append` command containing the content to append to the file.",
5858
"type": "string"
5959
},
6060
"old_str": {

0 commit comments

Comments
 (0)