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
16 changes: 16 additions & 0 deletions apps/staged/src-tauri/src/session_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
34 changes: 18 additions & 16 deletions apps/staged/src-tauri/src/session_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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);
Expand Down
10 changes: 10 additions & 0 deletions apps/staged/src/lib/features/branches/BranchCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
? {
Expand Down
9 changes: 2 additions & 7 deletions apps/staged/src/lib/listeners/sessionStatusListener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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) {
Expand Down
17 changes: 17 additions & 0 deletions apps/staged/src/lib/stores/sessionRegistry.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down