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
1 change: 1 addition & 0 deletions apps/staged/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2325,6 +2325,7 @@ pub fn run() {
session_commands::get_session_messages,
session_commands::get_session_messages_since,
session_commands::get_session_acp_metadata_messages,
session_commands::get_session_acp_metadata_messages_since,
session_commands::get_session_acp_initialization,
session_commands::count_assistant_messages_after,
session_commands::start_session,
Expand Down
12 changes: 12 additions & 0 deletions apps/staged/src-tauri/src/session_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -931,6 +931,18 @@ pub fn get_session_acp_metadata_messages(
.map_err(|e| e.to_string())
}

#[tauri::command]
pub fn get_session_acp_metadata_messages_since(
store: tauri::State<'_, Mutex<Option<Arc<Store>>>>,
session_id: String,
since_id: i64,
refetch_ids: Vec<i64>,
) -> Result<Vec<store::SessionMessage>, String> {
get_store(&store)?
.get_session_acp_metadata_messages_since(&session_id, since_id, &refetch_ids)
.map_err(|e| e.to_string())
}

#[tauri::command]
pub fn get_session_acp_initialization(
store: tauri::State<'_, Mutex<Option<Arc<Store>>>>,
Expand Down
37 changes: 37 additions & 0 deletions apps/staged/src-tauri/src/store/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,43 @@ impl Store {
rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
}

/// Incremental variant of [`Self::get_session_acp_metadata_messages`]:
/// returns metadata rows with `id > since_id`, plus fresh copies of
/// `refetch_ids`. Rows for tool calls that have not reached a terminal
/// status are updated in place by the message writer rather than
/// re-inserted, so callers pass those ids to observe the mutations.
pub fn get_session_acp_metadata_messages_since(
&self,
session_id: &str,
since_id: i64,
refetch_ids: &[i64],
) -> Result<Vec<SessionMessage>, StoreError> {
let conn = self.conn.lock().unwrap();
let refetch_placeholders = refetch_ids
.iter()
.map(|_| "?")
.collect::<Vec<_>>()
.join(", ");
let id_filter = if refetch_ids.is_empty() {
"id > ?".to_string()
} else {
format!("(id > ? OR id IN ({refetch_placeholders}))")
};
let sql = format!(
"SELECT {SESSION_MESSAGE_COLUMNS}
FROM session_messages
WHERE session_id = ? AND acp_event_kind IS NOT NULL AND {id_filter}
ORDER BY id ASC"
);
let mut stmt = conn.prepare(&sql)?;
let mut query_params: Vec<&dyn rusqlite::ToSql> = vec![&session_id, &since_id];
for id in refetch_ids {
query_params.push(id);
}
let rows = stmt.query_map(&query_params[..], session_message_from_row)?;
rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
}

/// Return the latest ACP initialization metadata row for a session.
///
/// This exposes negotiated provider/protocol/capability data without
Expand Down
85 changes: 85 additions & 0 deletions apps/staged/src-tauri/src/store/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1203,6 +1203,91 @@ fn test_acp_metadata_messages_query_includes_hidden_rows() {
assert_eq!(metadata_rows[1].acp.acp_usage.as_ref().unwrap()["used"], 42);
}

#[test]
fn test_acp_metadata_messages_since_returns_new_and_refetched_rows() {
let store = Store::in_memory().unwrap();

let session = Session::new_running("test", Path::new("/tmp"));
store.create_session(&session).unwrap();

// A visible tool_call row whose ACP metadata is later mutated in place —
// the pattern the writer uses for tool status updates.
let tool_id = store
.add_session_message(&session.id, MessageRole::ToolCall, "Run tests")
.unwrap();
store
.update_message_acp_metadata(
tool_id,
&AcpMessageMetadata {
acp_event_kind: Some("tool_call".to_string()),
acp_tool_call_id: Some("tc-1".to_string()),
acp_tool_status: Some("in_progress".to_string()),
..Default::default()
},
)
.unwrap();
let usage_id = store
.add_acp_metadata_message(
&session.id,
&AcpMessageMetadata {
acp_event_kind: Some("usage_update".to_string()),
acp_usage: Some(serde_json::json!({"used": 1})),
..Default::default()
},
)
.unwrap();
// A visible row without ACP metadata never shows up in the metadata query.
store
.add_session_message(&session.id, MessageRole::Assistant, "text")
.unwrap();

// Cursor past everything, no refetch ids: nothing to return.
let none = store
.get_session_acp_metadata_messages_since(&session.id, usage_id, &[])
.unwrap();
assert!(none.is_empty());

// Only rows after the cursor are returned.
let after_tool = store
.get_session_acp_metadata_messages_since(&session.id, tool_id, &[])
.unwrap();
assert_eq!(after_tool.len(), 1);
assert_eq!(after_tool[0].id, usage_id);

// An in-place status update on a row behind the cursor is only observable
// when the caller re-requests that row explicitly.
store
.update_message_acp_metadata(
tool_id,
&AcpMessageMetadata {
acp_tool_status: Some("completed".to_string()),
..Default::default()
},
)
.unwrap();
let refetched = store
.get_session_acp_metadata_messages_since(&session.id, usage_id, &[tool_id])
.unwrap();
assert_eq!(refetched.len(), 1);
assert_eq!(refetched[0].id, tool_id);
assert_eq!(
refetched[0].acp.acp_tool_status.as_deref(),
Some("completed")
);

// The full metadata query and the since-query with a zero cursor agree.
let full = store
.get_session_acp_metadata_messages(&session.id)
.unwrap();
let since_zero = store
.get_session_acp_metadata_messages_since(&session.id, 0, &[])
.unwrap();
assert_eq!(
full.iter().map(|m| m.id).collect::<Vec<_>>(),
since_zero.iter().map(|m| m.id).collect::<Vec<_>>()
);
}

#[test]
fn test_acp_initialization_metadata_is_queryable() {
let store = Store::in_memory().unwrap();
Expand Down
10 changes: 10 additions & 0 deletions apps/staged/src-tauri/src/web_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2828,6 +2828,16 @@ async fn dispatch(command: &str, args: Value, state: &WebAppState) -> Result<Val
.map_err(|e| e.to_string())?;
Ok(serde_json::to_value(messages).unwrap())
}
"get_session_acp_metadata_messages_since" => {
let store = get_store(store_mutex)?;
let session_id: String = arg(&args, "sessionId")?;
let since_id: i64 = arg(&args, "sinceId")?;
let refetch_ids: Vec<i64> = arg(&args, "refetchIds")?;
let messages = store
.get_session_acp_metadata_messages_since(&session_id, since_id, &refetch_ids)
.map_err(|e| e.to_string())?;
Ok(serde_json::to_value(messages).unwrap())
}
"get_session_acp_initialization" => {
let store = get_store(store_mutex)?;
let session_id: String = arg(&args, "sessionId")?;
Expand Down
17 changes: 17 additions & 0 deletions apps/staged/src/lib/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,23 @@ export function getSessionAcpMetadataMessages(sessionId: string): Promise<Sessio
return invokeCommand('get_session_acp_metadata_messages', { sessionId });
}

/**
* Incremental variant of getSessionAcpMetadataMessages: returns metadata rows
* with id > sinceId plus fresh copies of refetchIds — rows the caller knows
* may still be mutated in place (tool calls without a terminal status).
*/
export function getSessionAcpMetadataMessagesSince(
sessionId: string,
sinceId: number,
refetchIds: number[]
): Promise<SessionMessage[]> {
return invokeCommand('get_session_acp_metadata_messages_since', {
sessionId,
sinceId,
refetchIds,
});
}

export function countAssistantMessagesAfter(
sessionId: string,
afterTimestamp: number
Expand Down
Loading