From 921bcfd247345578819c0c754dd3090cc5adafd5 Mon Sep 17 00:00:00 2001 From: leokun Date: Fri, 28 Aug 2026 19:46:37 +0800 Subject: [PATCH] feat: implement automatic call refresh on CallsPage - Added a refreshCalls function to appStore to fetch updated call data. - Integrated useEffect in CallsPage to periodically refresh calls every 2 seconds and on visibility change. --- apps/desktop/src/pages/CallsPage.tsx | 24 ++++++++++++++++++++ apps/desktop/src/store/appStore.ts | 8 +++++++ server/src/cursor/request/background.rs | 29 ++++++++++++++++++++----- 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/pages/CallsPage.tsx b/apps/desktop/src/pages/CallsPage.tsx index 07d8cfb..7953797 100644 --- a/apps/desktop/src/pages/CallsPage.tsx +++ b/apps/desktop/src/pages/CallsPage.tsx @@ -1,10 +1,34 @@ +import { useEffect } from "react"; import { CallTable } from "../components/CallTable"; import { PageContent } from "../components/layout/PageContent"; import { appStore, useAppStore } from "../store/appStore"; import styles from "./CallsPage.module.scss"; +const CALL_REFRESH_INTERVAL_MS = 2_000; + export function CallsPage() { const { calls } = useAppStore(); + + useEffect(() => { + let disposed = false; + const refreshCalls = () => { + if (!disposed && document.visibilityState === "visible") { + void appStore.refreshCalls(); + } + }; + + refreshCalls(); + const interval = window.setInterval(refreshCalls, CALL_REFRESH_INTERVAL_MS); + window.addEventListener("focus", refreshCalls); + document.addEventListener("visibilitychange", refreshCalls); + return () => { + disposed = true; + window.clearInterval(interval); + window.removeEventListener("focus", refreshCalls); + document.removeEventListener("visibilitychange", refreshCalls); + }; + }, []); + const content =
void appStore.openCallDetails(call.call_id)} />
; return ; } diff --git a/apps/desktop/src/store/appStore.ts b/apps/desktop/src/store/appStore.ts index e8dde89..9a6f192 100644 --- a/apps/desktop/src/store/appStore.ts +++ b/apps/desktop/src/store/appStore.ts @@ -177,6 +177,14 @@ export const appStore = { } }, + async refreshCalls() { + try { + update({ calls: await api.calls() }); + } catch (cause) { + update({ error: cause instanceof Error ? cause.message : String(cause) }); + } + }, + async openCallDetails(callId: string) { await perform(() => api.openCallDetails(callId)); }, diff --git a/server/src/cursor/request/background.rs b/server/src/cursor/request/background.rs index 12a0b65..0fa5710 100644 --- a/server/src/cursor/request/background.rs +++ b/server/src/cursor/request/background.rs @@ -56,10 +56,9 @@ pub(super) fn project( )) })?; if reason != pb::BackgroundTaskCompletionReason::TaskFinished { - return Err(Error::Protocol(format!( - "background task notification is not a finished task: {}", - reason.as_str_name() - ))); + // Progress and reparenting notifications are informational; the + // client batches them together with the real finish notification. + continue; } if completion.task_id.is_empty() || completion.title.is_empty() { return Err(Error::Protocol( @@ -110,7 +109,7 @@ pub(super) fn project( let (first, _) = completions .values() .next() - .expect("background completion action was validated as non-empty"); + .ok_or_else(|| Error::Protocol("background task notification contains no finished task".into()))?; let text = match (has_shell, has_subagent) { (true, false) => SHELL_FOLLOW_UP.into(), (false, true) => FOLLOW_UP.into(), @@ -362,7 +361,25 @@ mod tests { ) .unwrap_err() .to_string() - .contains("not a finished task")); + .contains("no finished task")); + } + + #[test] + fn progress_notifications_batched_with_a_finish_are_ignored() { + let mut progress = completion(); + progress.task_id = "child-id:task_progress:1".into(); + progress.reason = pb::BackgroundTaskCompletionReason::TaskProgress as i32; + let projection = project( + &pb::BackgroundTaskCompletionAction { + completions: vec![progress, completion()], + }, + pb::AgentMode::Agent as i32, + ) + .unwrap(); + + assert!(projection.context.contains("agent_id: child-id")); + assert!(!projection.context.contains("task_progress")); + assert_eq!(projection.turn_user.text, FOLLOW_UP); } fn completion() -> pb::BackgroundTaskCompletion {