Skip to content

Commit fd32a88

Browse files
committed
fix: share session watcher ongoing status with picker via AppState
The session watcher has the most accurate ongoing detection (full chunk analysis + subagent tracking). Store its verdict in AppState so that discover_sessions and picker-refresh events always return correct is_ongoing values. This eliminates the frontend syncOngoing effect and the one-render-delay flicker after refreshing the project tree. - Add watched_session_ongoing field to AppState - Session watcher updates AppState after computing ongoing - discover_sessions and picker watcher apply the override - Remove frontend syncOngoing from usePicker and App Entire-Checkpoint: b84cce8c97ad
1 parent 97dc8bd commit fd32a88

7 files changed

Lines changed: 58 additions & 24 deletions

File tree

src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/src/commands/picker.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ pub async fn discover_sessions(
1313
state: State<'_, AppState>,
1414
) -> Result<Vec<SessionInfo>, String> {
1515
let cache = state.session_cache.lock().map_err(|e| e.to_string())?;
16-
cache.discover_all_project_sessions(&project_dirs)
16+
let mut sessions = cache.discover_all_project_sessions(&project_dirs)?;
17+
// The session watcher has the most accurate ongoing detection, so apply
18+
// its verdict over the picker's lightweight metadata scan.
19+
state.apply_watched_ongoing(&mut sessions);
20+
Ok(sessions)
1721
}
1822

1923
/// Start watching project directories for new/changed sessions.

src-tauri/src/commands/session.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use crate::watcher::start_session_watcher;
1111

1212
/// Load a session file and return display messages.
1313
#[tauri::command]
14-
pub async fn load_session(path: String) -> Result<LoadResult, String> {
14+
pub async fn load_session(path: String, state: State<'_, AppState>) -> Result<LoadResult, String> {
1515
if path.is_empty() {
1616
return Err("no session path provided".to_string());
1717
}
@@ -27,6 +27,9 @@ pub async fn load_session(path: String) -> Result<LoadResult, String> {
2727

2828
let ongoing = OngoingChecker::new(&chunks, &all_procs, &path).is_ongoing();
2929

30+
// Set initial ongoing status so picker has it immediately.
31+
state.set_watched_ongoing(path.clone(), ongoing);
32+
3033
let teams = reconstruct_teams(&chunks, &all_procs);
3134
let messages = chunks_to_messages(&chunks, &all_procs, &color_map);
3235
let meta = extract_session_meta(&path);
@@ -109,5 +112,6 @@ pub async fn get_project_dirs(state: State<'_, AppState>) -> Result<Vec<String>,
109112
/// Stop watching the current session.
110113
#[tauri::command]
111114
pub async fn unwatch_session(state: State<'_, AppState>) -> Result<(), String> {
115+
state.clear_watched_ongoing();
112116
state.stop_session_watcher()
113117
}

src-tauri/src/state.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ pub struct AppState {
1010
pub picker_watcher: Mutex<Option<WatcherHandle>>,
1111
pub session_cache: Mutex<SessionCache>,
1212
pub settings: Mutex<Settings>,
13+
/// Ongoing status reported by the session watcher for the currently viewed session.
14+
/// (session_path, is_ongoing) — kept in sync by the session watcher loop.
15+
pub watched_session_ongoing: Mutex<Option<(String, bool)>>,
1316
}
1417

1518
impl AppState {
@@ -19,6 +22,7 @@ impl AppState {
1922
picker_watcher: Mutex::new(None),
2023
session_cache: Mutex::new(SessionCache::new()),
2124
settings: Mutex::new(crate::settings::load_settings()),
25+
watched_session_ongoing: Mutex::new(None),
2226
}
2327
}
2428

@@ -53,4 +57,34 @@ impl AppState {
5357
*guard = Some(handle);
5458
Ok(())
5559
}
60+
61+
/// Update the ongoing status for the currently watched session.
62+
pub fn set_watched_ongoing(&self, path: String, ongoing: bool) {
63+
if let Ok(mut guard) = self.watched_session_ongoing.lock() {
64+
*guard = Some((path, ongoing));
65+
}
66+
}
67+
68+
/// Clear the watched session ongoing status (e.g. when unwatching).
69+
pub fn clear_watched_ongoing(&self) {
70+
if let Ok(mut guard) = self.watched_session_ongoing.lock() {
71+
*guard = None;
72+
}
73+
}
74+
75+
/// Apply the session watcher's ongoing status to a list of sessions.
76+
/// The session watcher has the most accurate ongoing detection (full chunk
77+
/// analysis + subagent tracking), so its verdict overrides the picker's
78+
/// lightweight metadata scan.
79+
pub fn apply_watched_ongoing(&self, sessions: &mut [crate::parser::session::SessionInfo]) {
80+
let guard = match self.watched_session_ongoing.lock() {
81+
Ok(g) => g,
82+
Err(_) => return,
83+
};
84+
if let Some((ref path, ongoing)) = *guard {
85+
if let Some(s) = sessions.iter_mut().find(|s| s.path == *path) {
86+
s.is_ongoing = ongoing;
87+
}
88+
}
89+
}
5690
}

src-tauri/src/watcher.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
22
use std::path::Path;
33
use std::time::Duration;
4-
use tauri::{AppHandle, Emitter};
4+
use tauri::{AppHandle, Emitter, Manager};
55
use tokio::sync::mpsc;
66

77
use crate::convert::*;
@@ -150,6 +150,11 @@ pub fn start_session_watcher(
150150

151151
let ongoing = OngoingChecker::new(&chunks, &all_procs, &path_for_rebuild).is_ongoing();
152152

153+
// Share ongoing status with AppState so the picker can use it.
154+
if let Some(state) = app.try_state::<crate::state::AppState>() {
155+
state.set_watched_ongoing(path_for_rebuild.clone(), ongoing);
156+
}
157+
153158
let teams = reconstruct_teams(&chunks, &all_procs);
154159
let messages = chunks_to_messages(&chunks, &all_procs, &color_map);
155160

@@ -244,9 +249,15 @@ pub fn start_picker_watcher(project_dirs: Vec<String>, app: AppHandle) -> Watche
244249
break;
245250
}
246251
Some(()) = signal_rx.recv() => {
247-
let sessions = crate::parser::session::discover_all_project_sessions(&project_dirs)
252+
let mut sessions = crate::parser::session::discover_all_project_sessions(&project_dirs)
248253
.unwrap_or_default();
249254

255+
// Apply the session watcher's ongoing status (more accurate
256+
// than the picker's lightweight metadata scan).
257+
if let Some(state) = app.try_state::<crate::state::AppState>() {
258+
state.apply_watched_ongoing(&mut sessions);
259+
}
260+
250261
let payload = PickerRefreshPayload { sessions };
251262
let _ = app.emit("picker-refresh", payload);
252263
}

src/App.tsx

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,6 @@ export function App() {
6262
return () => clearInterval(id);
6363
}, [session.ongoing]);
6464

65-
// Sync session ongoing state to picker tree
66-
useEffect(() => {
67-
if (session.sessionPath) {
68-
picker.syncOngoing(session.sessionPath, session.ongoing);
69-
}
70-
}, [session.ongoing, session.sessionPath, picker]);
71-
7265
// Shared: fetch project dirs and discover sessions
7366
const loadProjectDirs = useCallback(async () => {
7467
try {

src/hooks/usePicker.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,6 @@ export function usePicker(selectedProject: string | null = null) {
4040
setState((prev) => ({ ...prev, searchQuery: query }));
4141
}, []);
4242

43-
/** Sync ongoing status from the session watcher into the picker list. */
44-
const syncOngoing = useCallback((sessionPath: string, ongoing: boolean) => {
45-
setState((prev) => {
46-
const idx = prev.sessions.findIndex((s) => s.path === sessionPath);
47-
if (idx === -1 || prev.sessions[idx].is_ongoing === ongoing) return prev;
48-
const sessions = [...prev.sessions];
49-
sessions[idx] = { ...sessions[idx], is_ongoing: ongoing };
50-
return { ...prev, sessions };
51-
});
52-
}, []);
53-
5443
// Listen for picker-refresh events (backend already applies staleness)
5544
useTauriEvent<{ sessions: SessionInfo[] }>("picker-refresh", (payload) => {
5645
setState((prev) => ({
@@ -90,6 +79,5 @@ export function usePicker(selectedProject: string | null = null) {
9079
searchQuery: state.searchQuery,
9180
setSearchQuery,
9281
discoverSessions,
93-
syncOngoing,
9482
};
9583
}

0 commit comments

Comments
 (0)