Skip to content
Merged
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/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ pub struct NoteTimelineItem {
pub completion_reason: Option<String>,
pub created_at: i64,
pub updated_at: i64,
pub completed_at: Option<i64>,
}

/// Review with session status resolved.
Expand All @@ -155,6 +156,7 @@ pub struct ReviewTimelineItem {
pub comment_count: usize,
pub created_at: i64,
pub updated_at: i64,
pub completed_at: Option<i64>,
}

/// Image with session status resolved.
Expand Down
1 change: 1 addition & 0 deletions apps/staged/src-tauri/src/note_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pub fn create_note(
completion_reason: None,
created_at: note.created_at,
updated_at: note.updated_at,
completed_at: note.completed_at,
})
}

Expand Down
8 changes: 4 additions & 4 deletions apps/staged/src-tauri/src/session_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1982,7 +1982,7 @@ fn note_timeline_entries(
format_note_for_context(&note.id, &note.title, &note.content, workspace_name)
{
entries.push(TimelineEntry {
timestamp: note.created_at / 1000,
timestamp: note.completed_at.unwrap_or(note.created_at) / 1000,
order: 0,
content,
});
Expand Down Expand Up @@ -2025,7 +2025,7 @@ fn project_note_timeline_entries(
let content =
format_project_note_for_context(&note.id, &note.title, &note.content, workspace_name);
entries.push(TimelineEntry {
timestamp: note.created_at / 1000,
timestamp: note.completed_at.unwrap_or(note.created_at) / 1000,
order: 0,
content,
});
Expand Down Expand Up @@ -2104,8 +2104,8 @@ fn review_timeline_entries(
continue;
}
let short_sha = &review.commit_sha[..review.commit_sha.len().min(7)];
let review_ts_secs = review.created_at / 1000;
let is_old = max_commit_ts.is_some_and(|ts| review_ts_secs < ts);
let review_ts_secs = review.completed_at.unwrap_or(review.created_at) / 1000;
let is_old = max_commit_ts.is_some_and(|ts| review.created_at / 1000 < ts);

let heading_title = match review.title.as_deref() {
Some(title) => format!("Code review: {} — {}", title, short_sha),
Expand Down
12 changes: 12 additions & 0 deletions apps/staged/src-tauri/src/session_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,13 @@ fn run_post_completion_hooks(
}
} else {
log::warn!("Session {session_id}: {label} session completed but no --- found in assistant output");
let result = match target.kind {
NoteKind::Repo => store.mark_note_completed(&target.id),
NoteKind::Project => store.mark_project_note_completed(&target.id),
};
if let Err(e) = result {
log::error!("Failed to mark {label} completed: {e}");
}
}
}
}
Expand All @@ -699,6 +706,11 @@ fn run_post_completion_hooks(
}
} else {
log::warn!("Session {session_id}: review session completed but no review-title block found");
// Still mark the review as completed so completed_at is set
// for timeline sorting, even without a title.
if let Err(e) = store.mark_review_completed(&review.id) {
log::error!("Failed to mark review completed: {e}");
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion apps/staged/src-tauri/src/store/migration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ fn test_store_bootstraps_fresh_database_with_baseline_migration() {
)
.unwrap();

assert_eq!(version, 6);
assert_eq!(version, 8);
assert_eq!(app_version, super::APP_VERSION);
assert!(table_exists(&conn, "projects"));
assert!(table_exists(&conn, "project_notes"));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-- Add completed_at column to notes and reviews.
-- This records when the AI session finished producing the item, giving us a
-- stable timestamp for timeline sorting that won't shift on later edits
-- (unlike updated_at which bumps on every user interaction).
--
-- Commits don't need this column because their created_at is already set to
-- the git commit timestamp (i.e. the completion time).
--
-- NULL means the item hasn't completed yet (still queued/generating).
-- For existing rows we backfill with updated_at, which is the best
-- approximation we have.

ALTER TABLE notes ADD COLUMN completed_at INTEGER;
UPDATE notes SET completed_at = updated_at WHERE content != '';

ALTER TABLE reviews ADD COLUMN completed_at INTEGER;
UPDATE reviews SET completed_at = updated_at
WHERE title IS NOT NULL
OR id IN (SELECT DISTINCT review_id FROM comments);
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-- Add completed_at to project_notes so queued project notes sort by when the
-- session finished producing them, not when they were queued.
--
-- NULL means the note hasn't completed yet (still queued/generating).
-- For existing rows we backfill with updated_at, which is the best
-- approximation we have.

ALTER TABLE project_notes ADD COLUMN completed_at INTEGER;
UPDATE project_notes SET completed_at = updated_at WHERE content != '';
14 changes: 14 additions & 0 deletions apps/staged/src-tauri/src/store/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -688,11 +688,15 @@ pub struct Note {
pub content: String,
pub created_at: i64,
pub updated_at: i64,
/// When the AI session finished producing this note's content.
/// `None` while the session is still running.
pub completed_at: Option<i64>,
}

impl Note {
pub fn new(branch_id: &str, title: &str, content: &str) -> Self {
let now = now_timestamp();
let has_content = !content.is_empty();
Self {
id: Uuid::new_v4().to_string(),
branch_id: branch_id.to_string(),
Expand All @@ -701,6 +705,7 @@ impl Note {
content: content.to_string(),
created_at: now,
updated_at: now,
completed_at: if has_content { Some(now) } else { None },
}
}

Expand Down Expand Up @@ -729,11 +734,15 @@ pub struct ProjectNote {
pub content: String,
pub created_at: i64,
pub updated_at: i64,
/// When the AI session finished producing this project note's content.
/// `None` while the session is still running.
pub completed_at: Option<i64>,
}

impl ProjectNote {
pub fn new(project_id: &str, title: &str, content: &str) -> Self {
let now = now_timestamp();
let has_content = !content.is_empty();
Self {
id: Uuid::new_v4().to_string(),
project_id: project_id.to_string(),
Expand All @@ -742,6 +751,7 @@ impl ProjectNote {
content: content.to_string(),
created_at: now,
updated_at: now,
completed_at: if has_content { Some(now) } else { None },
}
}

Expand Down Expand Up @@ -983,6 +993,9 @@ pub struct Review {
pub reference_files: Vec<String>,
pub created_at: i64,
pub updated_at: i64,
/// When the AI session finished producing this review.
/// `None` while the session is still running.
pub completed_at: Option<i64>,
}

impl Review {
Expand All @@ -1001,6 +1014,7 @@ impl Review {
reference_files: Vec::new(),
created_at: now,
updated_at: now,
completed_at: None,
}
}

Expand Down
37 changes: 26 additions & 11 deletions apps/staged/src-tauri/src/store/notes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ impl Store {
pub fn create_note(&self, note: &Note) -> Result<(), StoreError> {
let conn = self.conn.lock().unwrap();
conn.execute(
"INSERT INTO notes (id, branch_id, session_id, title, content, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
"INSERT INTO notes (id, branch_id, session_id, title, content, created_at, updated_at, completed_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
params![
note.id,
note.branch_id,
Expand All @@ -19,6 +19,7 @@ impl Store {
note.content,
note.created_at,
note.updated_at,
note.completed_at,
],
)?;
Ok(())
Expand All @@ -27,7 +28,7 @@ impl Store {
pub fn get_note(&self, id: &str) -> Result<Option<Note>, StoreError> {
let conn = self.conn.lock().unwrap();
conn.query_row(
"SELECT id, branch_id, session_id, title, content, created_at, updated_at
"SELECT id, branch_id, session_id, title, content, created_at, updated_at, completed_at
FROM notes WHERE id = ?1",
params![id],
Self::row_to_note,
Expand All @@ -39,8 +40,9 @@ impl Store {
pub fn list_notes_for_branch(&self, branch_id: &str) -> Result<Vec<Note>, StoreError> {
let conn = self.conn.lock().unwrap();
let mut stmt = conn.prepare(
"SELECT id, branch_id, session_id, title, content, created_at, updated_at
FROM notes WHERE branch_id = ?1 ORDER BY created_at DESC",
"SELECT id, branch_id, session_id, title, content, created_at, updated_at, completed_at
FROM notes WHERE branch_id = ?1
ORDER BY COALESCE(completed_at, created_at) DESC, created_at DESC",
)?;
let rows = stmt.query_map(params![branch_id], Self::row_to_note)?;
rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
Expand All @@ -50,7 +52,7 @@ impl Store {
pub fn get_note_by_session(&self, session_id: &str) -> Result<Option<Note>, StoreError> {
let conn = self.conn.lock().unwrap();
conn.query_row(
"SELECT id, branch_id, session_id, title, content, created_at, updated_at
"SELECT id, branch_id, session_id, title, content, created_at, updated_at, completed_at
FROM notes WHERE session_id = ?1",
params![session_id],
Self::row_to_note,
Expand All @@ -63,7 +65,7 @@ impl Store {
pub fn get_empty_note_by_session(&self, session_id: &str) -> Result<Option<Note>, StoreError> {
let conn = self.conn.lock().unwrap();
conn.query_row(
"SELECT id, branch_id, session_id, title, content, created_at, updated_at
"SELECT id, branch_id, session_id, title, content, created_at, updated_at, completed_at
FROM notes WHERE session_id = ?1 AND content = ''",
params![session_id],
Self::row_to_note,
Expand All @@ -80,18 +82,30 @@ impl Store {
content: &str,
) -> Result<(), StoreError> {
let conn = self.conn.lock().unwrap();
let now = now_timestamp();
conn.execute(
"UPDATE notes SET title = ?1, content = ?2, updated_at = ?3 WHERE id = ?4",
params![title, content, now_timestamp(), id],
"UPDATE notes SET title = ?1, content = ?2, updated_at = ?3, completed_at = COALESCE(completed_at, ?4) WHERE id = ?5",
params![title, content, now, now, id],
)?;
Ok(())
}

pub fn update_note_content(&self, id: &str, content: &str) -> Result<(), StoreError> {
let conn = self.conn.lock().unwrap();
let now = now_timestamp();
conn.execute(
"UPDATE notes SET content = ?1, updated_at = ?2 WHERE id = ?3",
params![content, now_timestamp(), id],
"UPDATE notes SET content = ?1, updated_at = ?2, completed_at = COALESCE(completed_at, ?3) WHERE id = ?4",
params![content, now, now, id],
)?;
Ok(())
}

pub fn mark_note_completed(&self, id: &str) -> Result<(), StoreError> {
let conn = self.conn.lock().unwrap();
let now = now_timestamp();
conn.execute(
"UPDATE notes SET completed_at = COALESCE(completed_at, ?1) WHERE id = ?2",
params![now, id],
)?;
Ok(())
}
Expand All @@ -111,6 +125,7 @@ impl Store {
content: row.get(4)?,
created_at: row.get(5)?,
updated_at: row.get(6)?,
completed_at: row.get(7)?,
})
}
}
35 changes: 26 additions & 9 deletions apps/staged/src-tauri/src/store/project_notes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ impl Store {
pub fn create_project_note(&self, note: &ProjectNote) -> Result<(), StoreError> {
let conn = self.conn.lock().unwrap();
conn.execute(
"INSERT INTO project_notes (id, project_id, session_id, title, content, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
"INSERT INTO project_notes (id, project_id, session_id, title, content, created_at, updated_at, completed_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
params![
note.id,
note.project_id,
Expand All @@ -19,6 +19,7 @@ impl Store {
note.content,
note.created_at,
note.updated_at,
note.completed_at,
],
)?;
Ok(())
Expand All @@ -27,7 +28,7 @@ impl Store {
pub fn get_project_note(&self, id: &str) -> Result<Option<ProjectNote>, StoreError> {
let conn = self.conn.lock().unwrap();
conn.query_row(
"SELECT id, project_id, session_id, title, content, created_at, updated_at
"SELECT id, project_id, session_id, title, content, created_at, updated_at, completed_at
FROM project_notes WHERE id = ?1",
params![id],
Self::row_to_project_note,
Expand All @@ -39,8 +40,10 @@ impl Store {
pub fn list_project_notes(&self, project_id: &str) -> Result<Vec<ProjectNote>, StoreError> {
let conn = self.conn.lock().unwrap();
let mut stmt = conn.prepare(
"SELECT id, project_id, session_id, title, content, created_at, updated_at
FROM project_notes WHERE project_id = ?1 ORDER BY created_at DESC",
"SELECT id, project_id, session_id, title, content, created_at, updated_at, completed_at
FROM project_notes
WHERE project_id = ?1
ORDER BY COALESCE(completed_at, created_at) DESC, created_at DESC",
)?;
let rows = stmt.query_map(params![project_id], Self::row_to_project_note)?;
rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
Expand All @@ -53,7 +56,7 @@ impl Store {
) -> Result<Option<ProjectNote>, StoreError> {
let conn = self.conn.lock().unwrap();
conn.query_row(
"SELECT id, project_id, session_id, title, content, created_at, updated_at
"SELECT id, project_id, session_id, title, content, created_at, updated_at, completed_at
FROM project_notes WHERE session_id = ?1",
params![session_id],
Self::row_to_project_note,
Expand All @@ -69,7 +72,7 @@ impl Store {
) -> Result<Option<ProjectNote>, StoreError> {
let conn = self.conn.lock().unwrap();
conn.query_row(
"SELECT id, project_id, session_id, title, content, created_at, updated_at
"SELECT id, project_id, session_id, title, content, created_at, updated_at, completed_at
FROM project_notes WHERE session_id = ?1 AND content = ''",
params![session_id],
Self::row_to_project_note,
Expand All @@ -85,9 +88,22 @@ impl Store {
content: &str,
) -> Result<(), StoreError> {
let conn = self.conn.lock().unwrap();
let now = now_timestamp();
conn.execute(
"UPDATE project_notes SET title = ?1, content = ?2, updated_at = ?3 WHERE id = ?4",
params![title, content, now_timestamp(), id],
"UPDATE project_notes
SET title = ?1, content = ?2, updated_at = ?3, completed_at = COALESCE(completed_at, ?4)
WHERE id = ?5",
params![title, content, now, now, id],
)?;
Ok(())
}

pub fn mark_project_note_completed(&self, id: &str) -> Result<(), StoreError> {
let conn = self.conn.lock().unwrap();
let now = now_timestamp();
conn.execute(
"UPDATE project_notes SET completed_at = COALESCE(completed_at, ?1) WHERE id = ?2",
params![now, id],
)?;
Ok(())
}
Expand All @@ -107,6 +123,7 @@ impl Store {
content: row.get(4)?,
created_at: row.get(5)?,
updated_at: row.get(6)?,
completed_at: row.get(7)?,
})
}
}
Loading