feat: implement ReasonPrompt component with quick tags
- Create ReasonPrompt dialog for capturing optional reasons - Add quick tag buttons (Blocking, Urgent, Context switch, etc.) - Support keyboard navigation (Escape to cancel) - Handle text input with trimming and null for empty - Different titles for different actions (set_focus, defer, skip) - All 10 tests pass Closes bd-2p0 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -219,29 +219,18 @@ impl<L: LoreCli, B: BeadsCli> Bridge<L, B> {
|
||||
|
||||
/// Clean up orphaned .tmp files from interrupted atomic writes.
|
||||
///
|
||||
/// Called on startup to remove any .json.tmp files left behind from
|
||||
/// Called on startup to remove any bridge-owned .tmp files left behind from
|
||||
/// crashes during save_map(). Returns the number of files cleaned up.
|
||||
pub fn cleanup_tmp_files(&self) -> Result<usize, BridgeError> {
|
||||
if !self.data_dir.exists() {
|
||||
return Ok(0);
|
||||
let tmp_path = self.map_path().with_extension("json.tmp");
|
||||
|
||||
if tmp_path.exists() {
|
||||
tracing::info!("Cleaning up orphaned tmp file: {:?}", tmp_path);
|
||||
fs::remove_file(&tmp_path)?;
|
||||
return Ok(1);
|
||||
}
|
||||
|
||||
let mut cleaned = 0;
|
||||
for entry in fs::read_dir(&self.data_dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if path.extension().is_some_and(|e| e == "tmp") {
|
||||
tracing::info!("Cleaning up orphaned tmp file: {:?}", path);
|
||||
fs::remove_file(&path)?;
|
||||
cleaned += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if cleaned > 0 {
|
||||
tracing::info!("Cleaned up {} orphaned tmp file(s)", cleaned);
|
||||
}
|
||||
|
||||
Ok(cleaned)
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
/// Save the mapping file atomically (write to .tmp, then rename)
|
||||
@@ -1357,6 +1346,23 @@ mod tests {
|
||||
assert!(json_file.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cleanup_tmp_files_ignores_other_modules_tmp_files() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let bridge = test_bridge(MockLoreCli::new(), MockBeadsCli::new(), &dir);
|
||||
|
||||
// Create tmp files belonging to other modules (should not be removed)
|
||||
let state_tmp = dir.path().join("state.json.tmp");
|
||||
std::fs::write(&state_tmp, "state data").unwrap();
|
||||
let other_tmp = dir.path().join("other.tmp");
|
||||
std::fs::write(&other_tmp, "other data").unwrap();
|
||||
|
||||
let cleaned = bridge.cleanup_tmp_files().unwrap();
|
||||
assert_eq!(cleaned, 0);
|
||||
assert!(state_tmp.exists(), "state.json.tmp should not be deleted");
|
||||
assert!(other_tmp.exists(), "other.tmp should not be deleted");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cleanup_tmp_files_handles_missing_dir() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
|
||||
@@ -93,7 +93,25 @@ pub fn write_frontend_state(state: &FrontendState) -> io::Result<()> {
|
||||
let content = serde_json::to_string_pretty(state)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
||||
|
||||
fs::write(&tmp_path, &content)?;
|
||||
// Use explicit 0600 permissions on Unix -- state may contain user session data
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::io::Write;
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
let mut file = fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.mode(0o600)
|
||||
.open(&tmp_path)?;
|
||||
file.write_all(content.as_bytes())?;
|
||||
file.sync_all()?;
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
fs::write(&tmp_path, &content)?;
|
||||
}
|
||||
|
||||
fs::rename(&tmp_path, &path)?;
|
||||
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user