feat(stash): /stash clear subcommand to wipe the stash file (#440 polish)
Pairs with `/stash list` and `/stash pop` so the user can fully manage the stash from inside the TUI without reaching for `rm`. * New `composer_stash::clear_stash()` returns the number of entries dropped so the slash command can report it. Atomic-write replaces the file with empty content; missing / empty files return `Ok(0)` without erroring. * `clear` / `wipe` / `drop` are accepted as the subcommand alias. The "unknown subcommand" hint now lists the three live subcommands explicitly. * CommandInfo usage updated to `/stash [list|pop|clear]` so `/help` and the autocomplete reflect the new option. * 3 new tests in `composer_stash`: returns-0 when file absent, returns-0 when file is empty, drops entries and reports count on a populated stash. No new dependency; reuses `crate::utils::write_atomic` for the truncate-and-rewrite.
This commit is contained in:
@@ -214,6 +214,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
`TuiOptions::initial_input` plumb that any future caller can
|
||||
reuse to drop the model into a session with text already
|
||||
typed.
|
||||
- **`/stash clear` subcommand** (#440 polish) — wipes the
|
||||
entire stash file and reports how many parked drafts were
|
||||
dropped. Pairs with `/stash list` and `/stash pop` so the
|
||||
user can fully manage the stash from inside the TUI without
|
||||
reaching for `rm`.
|
||||
- **RLM tool family** (#512) — `rlm` tool cards map to
|
||||
`ToolFamily::Rlm` and render `rlm`, not `swarm`. Stale "swarm"
|
||||
wording cleaned out of docs / comments / tests.
|
||||
|
||||
@@ -175,7 +175,7 @@ pub const COMMANDS: &[CommandInfo] = &[
|
||||
CommandInfo {
|
||||
name: "stash",
|
||||
aliases: &["park"],
|
||||
usage: "/stash [list|pop]",
|
||||
usage: "/stash [list|pop|clear]",
|
||||
description_id: MessageId::CmdStashDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
|
||||
@@ -16,13 +16,16 @@ use super::CommandResult;
|
||||
/// * `/stash list` — show parked drafts, oldest first.
|
||||
/// * `/stash pop` — restore the most recently parked draft into
|
||||
/// the composer; the popped entry is removed from disk.
|
||||
/// * `/stash clear` — wipe the entire stash file. Reports how many
|
||||
/// entries were dropped so the user knows what they deleted.
|
||||
pub fn stash(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
let sub = arg.map(str::trim).unwrap_or("list").to_ascii_lowercase();
|
||||
match sub.as_str() {
|
||||
"" | "list" | "ls" | "show" => list(),
|
||||
"pop" | "restore" => pop(app),
|
||||
"clear" | "wipe" | "drop" => clear(),
|
||||
other => CommandResult::error(format!(
|
||||
"unknown subcommand `{other}`. Try `/stash list` or `/stash pop`."
|
||||
"unknown subcommand `{other}`. Try `/stash list`, `/stash pop`, or `/stash clear`."
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -49,6 +52,14 @@ fn list() -> CommandResult {
|
||||
CommandResult::message(out)
|
||||
}
|
||||
|
||||
fn clear() -> CommandResult {
|
||||
match composer_stash::clear_stash() {
|
||||
Ok(0) => CommandResult::message("Stash already empty — nothing to clear."),
|
||||
Ok(n) => CommandResult::message(format!("Cleared {n} parked draft(s) from the stash.")),
|
||||
Err(err) => CommandResult::error(format!("Failed to clear stash: {err}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn pop(app: &mut App) -> CommandResult {
|
||||
match composer_stash::pop_stash() {
|
||||
Some(entry) => {
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
//! ambiguity and the timestamp / future fields land cleanly.
|
||||
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
@@ -125,6 +126,29 @@ pub fn pop_stash() -> Option<StashedDraft> {
|
||||
pop_stash_from(&path)
|
||||
}
|
||||
|
||||
/// Wipe the stash file entirely. Returns the number of entries
|
||||
/// that were dropped (so the caller can report it). Returns 0
|
||||
/// when the file doesn't exist or had no entries.
|
||||
pub fn clear_stash() -> io::Result<usize> {
|
||||
let Some(path) = default_stash_path() else {
|
||||
return Ok(0);
|
||||
};
|
||||
clear_stash_at(&path)
|
||||
}
|
||||
|
||||
fn clear_stash_at(path: &Path) -> io::Result<usize> {
|
||||
if !path.exists() {
|
||||
return Ok(0);
|
||||
}
|
||||
let entries = load_stash_from(path);
|
||||
let count = entries.len();
|
||||
if count == 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
crate::utils::write_atomic(path, b"")?;
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
fn pop_stash_from(path: &Path) -> Option<StashedDraft> {
|
||||
let mut entries = load_stash_from(path);
|
||||
let popped = entries.pop()?;
|
||||
@@ -236,6 +260,32 @@ this is not json
|
||||
assert_eq!(entries[1].text, "good two");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_returns_zero_when_file_is_absent() {
|
||||
let (_tmp, path) = temp_stash_path();
|
||||
// Path doesn't exist yet.
|
||||
assert_eq!(clear_stash_at(&path).unwrap(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_returns_zero_when_file_is_empty() {
|
||||
let (_tmp, path) = temp_stash_path();
|
||||
std::fs::write(&path, "").unwrap();
|
||||
assert_eq!(clear_stash_at(&path).unwrap(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_drops_entries_and_reports_count() {
|
||||
let (_tmp, path) = temp_stash_path();
|
||||
push_stash_to(&path, "first");
|
||||
push_stash_to(&path, "second");
|
||||
push_stash_to(&path, "third");
|
||||
let dropped = clear_stash_at(&path).expect("clear succeeds");
|
||||
assert_eq!(dropped, 3);
|
||||
// File still exists but is empty so subsequent loads come back clean.
|
||||
assert!(load_stash_from(&path).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cap_prunes_oldest_at_push_time() {
|
||||
let (_tmp, path) = temp_stash_path();
|
||||
|
||||
Reference in New Issue
Block a user