diff --git a/apps/staged/src-tauri/src/session_commands.rs b/apps/staged/src-tauri/src/session_commands.rs index d4d5bad81..cf37cce0e 100644 --- a/apps/staged/src-tauri/src/session_commands.rs +++ b/apps/staged/src-tauri/src/session_commands.rs @@ -741,6 +741,22 @@ pub async fn start_branch_session( } store.create_session(&session).map_err(|e| e.to_string())?; + // Emit a "running" event so the frontend can register the session in its + // state stores (project list spinner, unread badges, etc.) regardless of + // which UI surface started the session. + let session_type_str = match session_type { + BranchSessionType::Commit => "commit", + BranchSessionType::Note => "note", + BranchSessionType::Review => "review", + }; + session_runner::emit_session_running( + &app_handle, + &session.id, + &branch_id, + &branch.project_id, + session_type_str, + ); + // Create artifact stub and compute pre-head SHA let (artifact_id, pre_head_sha) = match session_type { BranchSessionType::Note => { diff --git a/apps/staged/src-tauri/src/session_runner.rs b/apps/staged/src-tauri/src/session_runner.rs index 26c9c73b9..8ac6e68f6 100644 --- a/apps/staged/src-tauri/src/session_runner.rs +++ b/apps/staged/src-tauri/src/session_runner.rs @@ -393,15 +393,18 @@ pub fn start_session( ) .unwrap_or(false); - if transitioned { - emit_status( - &app_handle, - &session_id_for_status, - new_status, - error_msg, - Some(&completion_reason), - ); + // Always emit the terminal status event, even if the DB row was already + // deleted (e.g. user deleted the pending commit). This lets the frontend + // clean up sidebar "running" state as a safety net. + emit_status( + &app_handle, + &session_id_for_status, + new_status, + error_msg, + Some(&completion_reason), + ); + if transitioned { let branch_id = store_for_status .get_branch_id_for_session(&session_id_for_status) .ok() @@ -509,15 +512,14 @@ pub fn recover_dead_sessions( Some(&CompletionReason::AppQuit), ) .unwrap_or(false); + emit_status( + &app_handle, + &session.id, + "error", + None, + Some(&CompletionReason::AppQuit), + ); if transitioned { - emit_status( - &app_handle, - &session.id, - "error", - None, - Some(&CompletionReason::AppQuit), - ); - let branch_id = store.get_branch_id_for_session(&session.id).ok().flatten(); if let Some(branch_id) = branch_id { let store_for_follow_up = Arc::clone(&store); diff --git a/apps/staged/src/lib/features/branches/BranchCard.svelte b/apps/staged/src/lib/features/branches/BranchCard.svelte index 1b50253f7..8dc341c05 100644 --- a/apps/staged/src/lib/features/branches/BranchCard.svelte +++ b/apps/staged/src/lib/features/branches/BranchCard.svelte @@ -41,6 +41,7 @@ import RemoteWorkspaceStatusView from './RemoteWorkspaceStatusView.svelte'; import { alerts } from '../../shared/alerts.svelte'; import { timelineToHashtagItems, projectNotesToHashtagItems } from '../sessions/hashtagItems'; + import { sessionRegistry } from '../../stores/sessionRegistry.svelte'; interface Props { branch: Branch; @@ -707,6 +708,9 @@ } } await commands.deleteNote(noteId, !!sessionId); + if (sessionId) { + sessionRegistry.cleanupSession(sessionId); + } loadTimeline(); // Drain the next queued session now that this one has been removed. commands @@ -742,6 +746,9 @@ } } await commands.deleteReview(reviewId, !!sessionId); + if (sessionId) { + sessionRegistry.cleanupSession(sessionId); + } loadTimeline(); // Drain the next queued session now that this one has been removed. commands @@ -776,6 +783,9 @@ } } await commands.deletePendingCommit(commitId, !!sessionId); + if (sessionId) { + sessionRegistry.cleanupSession(sessionId); + } await loadTimeline(); // Drain the next queued session now that this one has been removed. commands diff --git a/apps/staged/src/lib/features/branches/BranchCardSessionManager.svelte.ts b/apps/staged/src/lib/features/branches/BranchCardSessionManager.svelte.ts index aed767613..429555bdb 100644 --- a/apps/staged/src/lib/features/branches/BranchCardSessionManager.svelte.ts +++ b/apps/staged/src/lib/features/branches/BranchCardSessionManager.svelte.ts @@ -276,8 +276,6 @@ export default class BranchCardSessionManager { throw new Error('Failed to start session: no session ID returned'); } - this.registerRunningSession(result.sessionId, branch.projectId, mode, branch.id); - this.pendingSessionItems = this.pendingSessionItems.map((item) => item.key === pendingKey ? { diff --git a/apps/staged/src/lib/listeners/sessionStatusListener.ts b/apps/staged/src/lib/listeners/sessionStatusListener.ts index d9feb5608..f2c101228 100644 --- a/apps/staged/src/lib/listeners/sessionStatusListener.ts +++ b/apps/staged/src/lib/listeners/sessionStatusListener.ts @@ -73,11 +73,6 @@ async function handleSessionEnd(sessionId: string, status: string) { projectStateStore.markAsUnread(sessionProjectId); } - // Always remove the running session from its project. - if (sessionProjectId) { - projectStateStore.removeRunningSession(sessionProjectId, sessionId); - } - if (sessionType === 'pr' && branchId) { await handlePrCompletion(sessionId, branchId, status); prStateStore.clearSessionTracking(branchId); @@ -88,8 +83,8 @@ async function handleSessionEnd(sessionId: string, status: string) { pushStateStore.clearSessionTracking(branchId); } - // Clean up the session from the unified registry (single point of cleanup). - sessionRegistry.unregister(sessionId); + // Remove running state from projectStateStore and unregister from the registry. + sessionRegistry.cleanupSession(sessionId); } async function handlePrCompletion(sessionId: string, branchId: string, status: string) { diff --git a/apps/staged/src/lib/stores/sessionRegistry.svelte.ts b/apps/staged/src/lib/stores/sessionRegistry.svelte.ts index 85faa0ca7..b7089015b 100644 --- a/apps/staged/src/lib/stores/sessionRegistry.svelte.ts +++ b/apps/staged/src/lib/stores/sessionRegistry.svelte.ts @@ -17,6 +17,8 @@ * But they delegate session metadata tracking to this central registry. */ +import { projectStateStore } from './projectState.svelte'; + export type SessionType = 'commit' | 'pr' | 'push' | 'note' | 'review' | 'other'; interface SessionMetadata { @@ -133,6 +135,21 @@ class SessionRegistry { this.version++; } + /** + * Clean up a session's running state from projectStateStore and unregister it. + * + * This is the symmetric counterpart to register() — it removes the session + * from both the project-level running session tracking and this registry. + * Idempotent: safe to call even if the session is already gone. + */ + cleanupSession(sessionId: string): void { + const projectId = this.getProjectId(sessionId); + if (projectId) { + projectStateStore.removeRunningSession(projectId, sessionId); + } + this.unregister(sessionId); + } + /** * Clean up stale sessions to prevent memory leaks * Removes sessions older than SESSION_TTL_MS or beyond MAX_REGISTRY_SIZE