Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/staged/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
},
"dependencies": {
"@builderbot/diff-viewer": "workspace:*",
"@milkdown/crepe": "^7.22.1",
"@milkdown/kit": "^7.22.1",
"@tauri-apps/api": "^2.11.1",
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
"@tauri-apps/plugin-dialog": "^2.7.1",
Expand Down
4 changes: 4 additions & 0 deletions apps/staged/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,9 @@ pub struct NoteTimelineItem {
pub completed_at: Option<i64>,
pub suggested_next_commit_step: Option<String>,
pub suggested_next_note_step: Option<String>,
/// `None` for session-produced notes, `"written"` for user-authored ones —
/// the frontend routes the latter to the editor instead of the viewer.
pub subtype: Option<String>,
}

/// Review with session status resolved.
Expand Down Expand Up @@ -2315,6 +2318,7 @@ pub fn run() {
timeline::reset_branch_to_remote,
// Notes
note_commands::create_note,
note_commands::update_note,
note_commands::delete_note,
note_commands::get_note,
note_commands::list_child_notes,
Expand Down
57 changes: 44 additions & 13 deletions apps/staged/src-tauri/src/note_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,35 +24,66 @@ pub(crate) fn note_to_timeline_item(store: &Store, note: Note) -> NoteTimelineIt
completed_at: note.completed_at,
suggested_next_commit_step: note.suggested_next_commit_step,
suggested_next_note_step: note.suggested_next_note_step,
subtype: note.subtype,
}
}

/// Build the timeline item for a note that has no session by construction, so
/// there is no session status to resolve. Shared with the web-server dispatch.
pub(crate) fn standalone_note_to_timeline_item(note: Note) -> NoteTimelineItem {
NoteTimelineItem {
id: note.id,
title: note.title,
content: note.content,
session_id: None,
session_status: None,
completion_reason: None,
created_at: note.created_at,
updated_at: note.updated_at,
completed_at: note.completed_at,
suggested_next_commit_step: None,
suggested_next_note_step: None,
subtype: note.subtype,
}
}

/// Create a standalone note (no session) for a branch.
///
/// `subtype` is `"written"` when the user authored the note in the editor
/// dialog; the drag-drop and save-action-output paths leave it unset.
#[tauri::command(rename_all = "camelCase")]
pub fn create_note(
store: tauri::State<'_, Mutex<Option<Arc<Store>>>>,
branch_id: String,
title: String,
content: String,
subtype: Option<String>,
) -> Result<NoteTimelineItem, String> {
let store = crate::get_store(&store)?;
let mut note = crate::store::models::Note::new(&branch_id, &title, &content);
note.subtype = subtype;
store
.create_note_with_unique_title(&mut note)
.map_err(|e| e.to_string())?;
Ok(NoteTimelineItem {
id: note.id,
title: note.title,
content: note.content,
session_id: None,
session_status: None,
completion_reason: None,
created_at: note.created_at,
updated_at: note.updated_at,
completed_at: note.completed_at,
suggested_next_commit_step: None,
suggested_next_note_step: None,
})
Ok(standalone_note_to_timeline_item(note))
}

/// Save an edit to a user-authored ("written") note.
///
/// Rejects notes an agent session produced — their content is owned by that
/// session and would be overwritten on its next turn.
#[tauri::command(rename_all = "camelCase")]
pub fn update_note(
store: tauri::State<'_, Mutex<Option<Arc<Store>>>>,
note_id: String,
title: String,
content: String,
) -> Result<NoteTimelineItem, String> {
let store = crate::get_store(&store)?;
let note = store
.update_written_note(&note_id, &title, &content)
.map_err(|e| e.to_string())?;
Ok(standalone_note_to_timeline_item(note))
}

/// Delete a note and optionally its linked session.
Expand Down
83 changes: 71 additions & 12 deletions apps/staged/src-tauri/src/store/migration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ fn test_store_bootstraps_fresh_database_with_baseline_migration() {
)
.unwrap();

assert_eq!(version, 26);
assert_eq!(version, 27);
assert_eq!(app_version, super::APP_VERSION);
assert!(table_exists(&conn, "projects"));
assert!(table_exists(&conn, "project_notes"));
Expand Down Expand Up @@ -207,7 +207,7 @@ fn test_store_repairs_github_comment_tracking_user_version() {
created_at INTEGER NOT NULL,
image_ids TEXT DEFAULT NULL
);
CREATE TABLE notes (id TEXT PRIMARY KEY);
CREATE TABLE notes (id TEXT PRIMARY KEY, session_id TEXT);
-- Only the table/column the 0026 auto-review cleanup targets.
CREATE TABLE reviews (
id TEXT PRIMARY KEY,
Expand Down Expand Up @@ -245,7 +245,7 @@ fn test_store_repairs_github_comment_tracking_user_version() {
let version: i64 = conn
.query_row("PRAGMA user_version", [], |row| row.get(0))
.unwrap();
assert_eq!(version, 26);
assert_eq!(version, 27);
assert!(column_exists(&conn, "sessions", "pipeline"));
assert!(column_exists(&conn, "sessions", "acp_config_selection"));
assert!(column_exists(&conn, "sessions", "acp_title"));
Expand Down Expand Up @@ -287,7 +287,7 @@ fn test_store_repairs_pipeline_user_version() {
created_at INTEGER NOT NULL,
image_ids TEXT DEFAULT NULL
);
CREATE TABLE notes (id TEXT PRIMARY KEY);
CREATE TABLE notes (id TEXT PRIMARY KEY, session_id TEXT);
-- Only the table/column the 0026 auto-review cleanup targets.
CREATE TABLE reviews (
id TEXT PRIMARY KEY,
Expand Down Expand Up @@ -320,7 +320,7 @@ fn test_store_repairs_pipeline_user_version() {
let version: i64 = conn
.query_row("PRAGMA user_version", [], |row| row.get(0))
.unwrap();
assert_eq!(version, 26);
assert_eq!(version, 27);
assert!(column_exists(&conn, "comments", "github_comment_id"));
assert!(column_exists(&conn, "comments", "github_comment_type"));
assert!(column_exists(&conn, "comments", "github_comment_stale"));
Expand Down Expand Up @@ -366,8 +366,8 @@ fn test_completion_effects_migration_backfills_finished_pipeline_sessions() {
id TEXT PRIMARY KEY,
detecting_actions INTEGER NOT NULL DEFAULT 0
);
-- Only the table the 0025 column add targets.
CREATE TABLE notes (id TEXT PRIMARY KEY);
-- Only the table the 0025/0027 note column adds target.
CREATE TABLE notes (id TEXT PRIMARY KEY, session_id TEXT);
-- Only the table/column the 0026 auto-review cleanup targets.
CREATE TABLE reviews (
id TEXT PRIMARY KEY,
Expand All @@ -385,7 +385,7 @@ fn test_completion_effects_migration_backfills_finished_pipeline_sessions() {
let version: i64 = conn
.query_row("PRAGMA user_version", [], |row| row.get(0))
.unwrap();
assert_eq!(version, 26);
assert_eq!(version, 27);
assert!(column_exists(&conn, "sessions", "completion_effects_at"));

let marker = |id: &str| -> Option<i64> {
Expand Down Expand Up @@ -427,6 +427,8 @@ fn test_auto_review_removal_migration_deletes_auto_reviews_and_drops_flag() {
INSERT INTO reviews (id, is_auto) VALUES
('user-review', 0),
('auto-review', 1);
-- Only the table the 0027 note column add targets.
CREATE TABLE notes (id TEXT PRIMARY KEY, session_id TEXT);
",
)
.unwrap();
Expand All @@ -439,7 +441,7 @@ fn test_auto_review_removal_migration_deletes_auto_reviews_and_drops_flag() {
let version: i64 = conn
.query_row("PRAGMA user_version", [], |row| row.get(0))
.unwrap();
assert_eq!(version, 26);
assert_eq!(version, 27);
assert!(!column_exists(&conn, "reviews", "is_auto"));

// Reviews the removed auto-review feature created in the background are
Expand Down Expand Up @@ -475,8 +477,8 @@ fn test_detecting_pid_migration_clears_orphaned_detection_flags() {
INSERT INTO action_contexts (id, detecting_actions) VALUES
('wedged', 1),
('idle', 0);
-- Only the table the 0025 column add targets.
CREATE TABLE notes (id TEXT PRIMARY KEY);
-- Only the table the 0025/0027 note column adds target.
CREATE TABLE notes (id TEXT PRIMARY KEY, session_id TEXT);
-- Only the table/column the 0026 auto-review cleanup targets.
CREATE TABLE reviews (
id TEXT PRIMARY KEY,
Expand All @@ -494,7 +496,7 @@ fn test_detecting_pid_migration_clears_orphaned_detection_flags() {
let version: i64 = conn
.query_row("PRAGMA user_version", [], |row| row.get(0))
.unwrap();
assert_eq!(version, 26);
assert_eq!(version, 27);
assert!(column_exists(&conn, "action_contexts", "detecting_pid"));

// No shipped build ever cleared the flag from outside the process that set
Expand All @@ -511,3 +513,60 @@ fn test_detecting_pid_migration_clears_orphaned_detection_flags() {

cleanup_db(&path);
}

#[test]
fn test_note_subtype_migration_backfills_session_less_notes() {
let path = temp_db_path("note-subtype-backfill");
let conn = Connection::open(&path).unwrap();
conn.execute_batch(
"
PRAGMA user_version = 25;
CREATE TABLE app_metadata (
id INTEGER PRIMARY KEY CHECK (id = 1),
app_version TEXT NOT NULL
);
INSERT INTO app_metadata (id, app_version) VALUES (1, '0.2.9');
CREATE TABLE notes (
id TEXT PRIMARY KEY,
session_id TEXT
);
INSERT INTO notes (id, session_id) VALUES
('dropped', NULL),
('agent', 'session-1');
-- Only the table/column the 0026 auto-review cleanup targets.
CREATE TABLE reviews (
id TEXT PRIMARY KEY,
is_auto INTEGER NOT NULL DEFAULT 0
);
",
)
.unwrap();
drop(conn);

let store = Store::new(&path).unwrap();
drop(store);

let conn = Connection::open(&path).unwrap();
let version: i64 = conn
.query_row("PRAGMA user_version", [], |row| row.get(0))
.unwrap();
assert_eq!(version, 27);
assert!(column_exists(&conn, "notes", "subtype"));

let subtype = |id: &str| -> Option<String> {
conn.query_row(
"SELECT subtype FROM notes WHERE id = ?1",
params![id],
|row| row.get(0),
)
.unwrap()
};

// Drag-dropped files and saved action output are already user-authored
// notes with no owning session, so they become editable alongside newly
// written ones. Notes an agent produced stay untagged.
assert_eq!(subtype("dropped").as_deref(), Some("written"));
assert_eq!(subtype("agent"), None);

cleanup_db(&path);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-- Distinguishes user-authored notes (written directly in the editor dialog)
-- from agent/session notes. NULL = produced by a session; 'written' = authored
-- by the user and therefore editable in place.
ALTER TABLE notes ADD COLUMN subtype TEXT;

-- Existing session-less notes (drag-dropped files, saved action output) are
-- exactly the user-authored class, so they become editable too.
UPDATE notes SET subtype = 'written' WHERE session_id IS NULL;
18 changes: 18 additions & 0 deletions apps/staged/src-tauri/src/store/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -891,9 +891,17 @@ pub struct Note {
/// rather than a standalone branch note. Children are hidden from the
/// branch timeline and fetched via the parent project-note view.
pub parent_project_note_id: Option<String>,
/// How the note's content came to be. `None` means a session produced it;
/// [`Note::SUBTYPE_WRITTEN`] means the user authored it directly and it can
/// be edited in place.
pub subtype: Option<String>,
}

impl Note {
/// Subtype marking a note the user wrote themselves rather than one an
/// agent session produced. Only these are editable via `update_note`.
pub const SUBTYPE_WRITTEN: &'static str = "written";

pub fn new(branch_id: &str, title: &str, content: &str) -> Self {
let now = now_timestamp();
let has_content = !content.is_empty();
Expand All @@ -909,6 +917,7 @@ impl Note {
suggested_next_commit_step: None,
suggested_next_note_step: None,
parent_project_note_id: None,
subtype: None,
}
}

Expand All @@ -921,6 +930,15 @@ impl Note {
self.parent_project_note_id = Some(id.to_string());
self
}

pub fn with_subtype(mut self, subtype: &str) -> Self {
self.subtype = Some(subtype.to_string());
self
}

pub fn is_written(&self) -> bool {
self.subtype.as_deref() == Some(Self::SUBTYPE_WRITTEN)
}
}

// =============================================================================
Expand Down
Loading