Expose notes over IPC and drop them with the project

This commit is contained in:
2026-09-01 12:34:35 -07:00
parent cc767bd544
commit 60abff1717
5 changed files with 58 additions and 0 deletions
+1
View File
@@ -8,6 +8,7 @@ pub mod help_commands;
pub mod inspect_commands;
pub mod install_helper_commands;
pub mod migration_commands;
pub mod notes_commands;
pub mod project_commands;
pub mod settings_commands;
pub mod settings_export_commands;
@@ -0,0 +1,33 @@
use crate::models::Note;
use crate::storage::notes_store;
/// Every project's notes, oldest concept first: pinned notes, then most
/// recently edited.
///
/// Sorted here rather than in the webview so the dock and the tab — two views
/// of the same list — cannot drift into two different orders.
#[tauri::command]
pub async fn list_notes(project_id: String) -> Result<Vec<Note>, String> {
let mut notes = notes_store::load(&project_id)?;
notes.sort_by(|a, b| {
b.pinned
.cmp(&a.pinned)
.then_with(|| b.updated_at.cmp(&a.updated_at))
});
Ok(notes)
}
/// Insert or replace one note.
///
/// There is deliberately no whole-list setter. A bulk write is exactly the
/// clobbering this store's per-project file exists to avoid, and every caller
/// here is editing one note.
#[tauri::command]
pub async fn save_note(project_id: String, note: Note) -> Result<Note, String> {
notes_store::upsert(&project_id, note)
}
#[tauri::command]
pub async fn delete_note(project_id: String, note_id: String) -> Result<(), String> {
notes_store::delete(&project_id, &note_id)
}
@@ -722,6 +722,15 @@ pub async fn remove_project(
// holding an entire snapshot image that nothing will ever reference again.
crate::commands::migration_commands::purge_migration_artifacts(&project_id).await;
// A project's notes are the one piece of its state that is purely the
// user's prose, so removal takes them with it rather than leaving an
// orphan file keyed by an id nothing will ever look up again. Logged and
// not propagated: an orphaned notes file is harmless, and a project that
// cannot be removed is not.
if let Err(e) = crate::storage::notes_store::clear(&project_id) {
log::warn!("Could not remove notes for project {}: {}", project_id, e);
}
// Stop and remove container if it exists. Everything named in `report`
// below is what will be unreachable the moment this function drops the
// project record — see [`ProjectRemovalReport`] and
+4
View File
@@ -470,6 +470,10 @@ pub fn run() {
commands::project_commands::stop_project_container,
commands::project_commands::rebuild_project_container,
commands::project_commands::reconcile_project_statuses,
// Notes
commands::notes_commands::list_notes,
commands::notes_commands::save_note,
commands::notes_commands::delete_note,
// Container base-image migration
commands::migration_commands::get_container_staleness,
commands::migration_commands::migrate_project_to_base,
+11
View File
@@ -150,6 +150,7 @@ fn delete_in(dir: &Path, project_id: &str, note_id: &str) -> Result<(), String>
}
fn clear_in(dir: &Path, project_id: &str) -> Result<(), String> {
let _guard = write_lock().lock().unwrap_or_else(|e| e.into_inner());
let path = notes_path_in(dir, project_id);
match fs::remove_file(&path) {
Ok(()) => Ok(()),
@@ -334,4 +335,14 @@ mod tests {
clear_in(&dir, "p1").unwrap(); // idempotent
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn clearing_is_what_project_removal_calls_and_it_never_fails_on_absence() {
// `remove_project` must not be able to fail because a project simply
// never had any notes — an orphaned notes file is harmless, a project
// that cannot be removed is not.
let dir = temp_dir("removal");
assert!(clear_in(&dir, "never-had-notes").is_ok());
std::fs::remove_dir_all(&dir).ok();
}
}