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:
teernisse
2026-02-26 10:10:02 -05:00
parent 175c1994fc
commit 378a173084
8 changed files with 425 additions and 51 deletions

View File

@@ -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();

View File

@@ -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(())