@@ -3,6 +3,7 @@ import { invoke } from '../lib/ipc';
33import { IPC } from '../../electron/ipc/channels' ;
44import { store , setStore } from './core' ;
55import type { WorktreeStatus } from '../ipc/types' ;
6+ import type { TaskGitStatusSnapshot } from './types' ;
67import { warn as logWarn } from '../lib/log' ;
78
89// --- Trust-specific patterns (subset of QUESTION_PATTERNS) ---
@@ -754,7 +755,9 @@ export function clearAgentActivity(agentId: string): void {
754755
755756function isTaskReady ( taskId : string ) : boolean {
756757 const git = store . taskGitStatus [ taskId ] ;
757- return Boolean ( git ?. has_committed_changes && ! git ?. has_uncommitted_changes ) ;
758+ return Boolean (
759+ isGitStatusUsable ( git ) && git . has_committed_changes && ! git . has_uncommitted_changes ,
760+ ) ;
758761}
759762
760763function hasTaskAgentError ( taskId : string ) : boolean {
@@ -818,18 +821,121 @@ export function getTaskDotStatus(taskId: string): TaskDotStatus {
818821
819822// --- Git status polling ---
820823
821- async function refreshTaskGitStatus ( taskId : string ) : Promise < void > {
824+ const GIT_STATUS_STALE_MS = 5 * 60_000 ;
825+ const gitRefreshVersions = new Map < string , number > ( ) ;
826+ const gitStatusStaleTimers = new Map < string , ReturnType < typeof setTimeout > > ( ) ;
827+
828+ function gitStatusErrorMessage ( err : unknown ) : string {
829+ return err instanceof Error ? err . message : String ( err ) ;
830+ }
831+
832+ function isGitStatusUsable ( git : TaskGitStatusSnapshot | undefined ) : git is TaskGitStatusSnapshot {
833+ return Boolean (
834+ git &&
835+ ! git . error &&
836+ ! git . refreshing &&
837+ ! git . stale &&
838+ Date . now ( ) - git . refreshedAt <= GIT_STATUS_STALE_MS ,
839+ ) ;
840+ }
841+
842+ function nextGitRefreshVersion ( taskId : string ) : number {
843+ const version = ( gitRefreshVersions . get ( taskId ) ?? 0 ) + 1 ;
844+ gitRefreshVersions . set ( taskId , version ) ;
845+ return version ;
846+ }
847+
848+ function isCurrentGitRefresh ( taskId : string , version : number ) : boolean {
849+ return gitRefreshVersions . get ( taskId ) === version ;
850+ }
851+
852+ function clearGitStatusStaleTimer ( taskId : string ) : void {
853+ const timer = gitStatusStaleTimers . get ( taskId ) ;
854+ if ( timer !== undefined ) {
855+ clearTimeout ( timer ) ;
856+ gitStatusStaleTimers . delete ( taskId ) ;
857+ }
858+ }
859+
860+ function scheduleGitStatusStaleTimer ( taskId : string , refreshedAt : number ) : void {
861+ clearGitStatusStaleTimer ( taskId ) ;
862+ const delay = Math . max ( 0 , refreshedAt + GIT_STATUS_STALE_MS - Date . now ( ) + 1 ) ;
863+ const timer = setTimeout ( ( ) => {
864+ gitStatusStaleTimers . delete ( taskId ) ;
865+ const current = store . taskGitStatus [ taskId ] ;
866+ if ( ! store . tasks [ taskId ] || current ?. refreshedAt !== refreshedAt ) return ;
867+ if ( current . error || current . refreshing || current . stale ) return ;
868+ setStore ( 'taskGitStatus' , taskId , { ...current , stale : true } ) ;
869+ } , delay ) ;
870+ gitStatusStaleTimers . set ( taskId , timer ) ;
871+ }
872+
873+ export function clearTaskGitStatusTracking ( taskId : string ) : void {
874+ clearGitStatusStaleTimer ( taskId ) ;
875+ gitRefreshVersions . delete ( taskId ) ;
876+ }
877+
878+ async function refreshTaskGitStatus (
879+ taskId : string ,
880+ options : { invalidateExisting ?: boolean } = { } ,
881+ ) : Promise < void > {
822882 const task = store . tasks [ taskId ] ;
823883 if ( ! task || task . gitIsolation === 'none' ) return ;
884+ const version = nextGitRefreshVersion ( taskId ) ;
885+
886+ if ( options . invalidateExisting ) {
887+ clearGitStatusStaleTimer ( taskId ) ;
888+ const previous = store . taskGitStatus [ taskId ] ;
889+ setStore (
890+ 'taskGitStatus' ,
891+ taskId ,
892+ previous
893+ ? { ...previous , refreshing : true , stale : false }
894+ : {
895+ has_committed_changes : false ,
896+ has_uncommitted_changes : false ,
897+ current_branch : null ,
898+ refreshedAt : 0 ,
899+ refreshing : true ,
900+ } ,
901+ ) ;
902+ }
824903
825904 try {
826905 const status = await invoke < WorktreeStatus > ( IPC . GetWorktreeStatus , {
827906 worktreePath : task . worktreePath ,
828907 baseBranch : task . baseBranch ,
829908 } ) ;
830- setStore ( 'taskGitStatus' , taskId , status ) ;
831- } catch {
832- // Worktree may not exist yet or was removed — ignore
909+ if ( ! store . tasks [ taskId ] || ! isCurrentGitRefresh ( taskId , version ) ) return ;
910+ const refreshedAt = Date . now ( ) ;
911+ const next : TaskGitStatusSnapshot = {
912+ ...status ,
913+ refreshedAt,
914+ } ;
915+ setStore ( 'taskGitStatus' , taskId , next ) ;
916+ scheduleGitStatusStaleTimer ( taskId , refreshedAt ) ;
917+ } catch ( err ) {
918+ if ( ! store . tasks [ taskId ] || ! isCurrentGitRefresh ( taskId , version ) ) return ;
919+ clearGitStatusStaleTimer ( taskId ) ;
920+ const previous = store . taskGitStatus [ taskId ] ;
921+ setStore (
922+ 'taskGitStatus' ,
923+ taskId ,
924+ previous
925+ ? {
926+ ...previous ,
927+ refreshing : false ,
928+ error : gitStatusErrorMessage ( err ) ,
929+ }
930+ : {
931+ has_committed_changes : false ,
932+ has_uncommitted_changes : false ,
933+ current_branch : null ,
934+ refreshedAt : 0 ,
935+ refreshing : false ,
936+ error : gitStatusErrorMessage ( err ) ,
937+ } ,
938+ ) ;
833939 }
834940}
835941
@@ -877,7 +983,7 @@ async function refreshActiveTaskGitStatus(): Promise<void> {
877983
878984/** Refresh git status for a single task (e.g. after agent exits). */
879985export function refreshTaskStatus ( taskId : string ) : void {
880- refreshTaskGitStatus ( taskId ) ;
986+ refreshTaskGitStatus ( taskId , { invalidateExisting : true } ) ;
881987}
882988
883989let allTasksTimer : ReturnType < typeof setInterval > | null = null ;
0 commit comments