@@ -225,6 +225,43 @@ async function fetchTableRunState(tableId: string, signal?: AbortSignal): Promis
225225 }
226226}
227227
228+ /** Count groups flipped to in-flight (`pending`) by an optimistic schedule that
229+ * weren't in-flight before — the delta to add to the run-state counter. */
230+ function countNewlyInFlight ( before : RowExecutions , after : RowExecutions ) : number {
231+ let n = 0
232+ for ( const gid of Object . keys ( after ) ) {
233+ if ( after [ gid ] ?. status === 'pending' && ! isExecInFlight ( before [ gid ] ) ) n ++
234+ }
235+ return n
236+ }
237+
238+ /** Add optimistically-stamped cells to the run-state counter so the "X running"
239+ * badge + per-row gutter Stop reflect them instantly (the optimistic stamp
240+ * eats the dispatcher's `pending` SSE, so `applyCell` never bumps the count).
241+ * Returns the prior snapshot for rollback, or `null` when nothing was bumped. */
242+ function bumpRunState (
243+ queryClient : ReturnType < typeof useQueryClient > ,
244+ tableId : string ,
245+ stampedByRow : Record < string , number >
246+ ) : { snapshot : TableRunState | undefined } | null {
247+ const total = Object . values ( stampedByRow ) . reduce ( ( s , n ) => s + n , 0 )
248+ if ( total === 0 ) return null
249+ const snapshot = queryClient . getQueryData < TableRunState > ( tableKeys . activeDispatches ( tableId ) )
250+ queryClient . setQueryData < TableRunState > ( tableKeys . activeDispatches ( tableId ) , ( prev ) => {
251+ const base = prev ?? { dispatches : [ ] , runningCellCount : 0 , runningByRowId : { } }
252+ const nextByRow = { ...base . runningByRowId }
253+ for ( const [ rid , n ] of Object . entries ( stampedByRow ) ) {
254+ nextByRow [ rid ] = ( nextByRow [ rid ] ?? 0 ) + n
255+ }
256+ return {
257+ ...base ,
258+ runningCellCount : base . runningCellCount + total ,
259+ runningByRowId : nextByRow ,
260+ }
261+ } )
262+ return { snapshot }
263+ }
264+
228265/**
229266 * Aggregate live state for a table: active dispatches (drives the "about to
230267 * run" overlay), the running-cell count (top-right counter), and per-row
@@ -453,6 +490,11 @@ export function useCreateTableRow({ workspaceId, tableId }: RowMutationContext)
453490 . workflowGroups ?? [ ]
454491 const stamped = withOptimisticAutoFireExec ( groups , row )
455492 reconcileCreatedRow ( queryClient , tableId , stamped )
493+ // Bump the run-state counter for any auto-fire groups stamped pending so
494+ // the "X running" badge + gutter Stop show immediately (the row had no
495+ // prior executions, so the stamped set is the full delta).
496+ const stampedCount = countNewlyInFlight ( { } , stamped . executions ?? { } )
497+ if ( stampedCount > 0 ) bumpRunState ( queryClient , tableId , { [ row . id ] : stampedCount } )
456498 } ,
457499 onError : ( error ) => {
458500 if ( isValidationError ( error ) ) return
@@ -618,18 +660,27 @@ export function useUpdateTableRow({ workspaceId, tableId }: RowMutationContext)
618660 queryClient . getQueryData < TableDefinition > ( tableKeys . detail ( tableId ) ) ?. schema
619661 . workflowGroups ?? [ ]
620662
663+ const stampedByRow : Record < string , number > = { }
621664 patchCachedRows ( queryClient , tableId , ( row ) => {
622665 if ( row . id !== rowId ) return row
623666 const patch = data as Partial < RowData >
624667 const nextExecutions = optimisticallyScheduleNewlyEligibleGroups ( groups , row , patch )
668+ if ( nextExecutions ) {
669+ stampedByRow [ row . id ] = countNewlyInFlight ( row . executions ?? { } , nextExecutions )
670+ }
625671 return {
626672 ...row ,
627673 data : { ...row . data , ...patch } as RowData ,
628674 ...( nextExecutions ? { executions : nextExecutions } : { } ) ,
629675 }
630676 } )
631677
632- return { previousQueries }
678+ const bumped = bumpRunState ( queryClient , tableId , stampedByRow )
679+ return {
680+ previousQueries,
681+ runStateSnapshot : bumped ?. snapshot ,
682+ didBumpRunState : bumped !== null ,
683+ }
633684 } ,
634685 onSuccess : ( response , { rowId, data : mutatedData } ) => {
635686 const serverRow = response . data . row
@@ -655,6 +706,9 @@ export function useUpdateTableRow({ workspaceId, tableId }: RowMutationContext)
655706 queryClient . setQueryData ( queryKey , data )
656707 }
657708 }
709+ if ( context ?. didBumpRunState ) {
710+ queryClient . setQueryData ( tableKeys . activeDispatches ( tableId ) , context . runStateSnapshot )
711+ }
658712 if ( isValidationError ( error ) ) return
659713 toast . error ( error . message , { duration : 5000 } )
660714 } ,
@@ -694,26 +748,38 @@ export function useBatchUpdateTableRows({ workspaceId, tableId }: RowMutationCon
694748 queryClient . getQueryData < TableDefinition > ( tableKeys . detail ( tableId ) ) ?. schema
695749 . workflowGroups ?? [ ]
696750
751+ const stampedByRow : Record < string , number > = { }
697752 patchCachedRows ( queryClient , tableId , ( row ) => {
698753 const raw = updateMap . get ( row . id )
699754 if ( ! raw ) return row
700755 const patch = raw as Partial < RowData >
701756 const nextExecutions = optimisticallyScheduleNewlyEligibleGroups ( groups , row , patch )
757+ if ( nextExecutions ) {
758+ stampedByRow [ row . id ] = countNewlyInFlight ( row . executions ?? { } , nextExecutions )
759+ }
702760 return {
703761 ...row ,
704762 data : { ...row . data , ...patch } as RowData ,
705763 ...( nextExecutions ? { executions : nextExecutions } : { } ) ,
706764 }
707765 } )
708766
709- return { previousQueries }
767+ const bumped = bumpRunState ( queryClient , tableId , stampedByRow )
768+ return {
769+ previousQueries,
770+ runStateSnapshot : bumped ?. snapshot ,
771+ didBumpRunState : bumped !== null ,
772+ }
710773 } ,
711774 onError : ( error , _vars , context ) => {
712775 if ( context ?. previousQueries ) {
713776 for ( const [ queryKey , data ] of context . previousQueries ) {
714777 queryClient . setQueryData ( queryKey , data )
715778 }
716779 }
780+ if ( context ?. didBumpRunState ) {
781+ queryClient . setQueryData ( tableKeys . activeDispatches ( tableId ) , context . runStateSnapshot )
782+ }
717783 if ( isValidationError ( error ) ) return
718784 toast . error ( error . message , { duration : 5000 } )
719785 } ,
@@ -1376,29 +1442,8 @@ export function useRunColumn({ workspaceId, tableId }: RowMutationContext) {
13761442 return { ...r , data : nextData , executions : next }
13771443 } )
13781444
1379- // Bump the counter to match the stamped cells. Without it the "X running"
1380- // badge + gutter Stop stay at zero until a refetch: the optimistic stamp
1381- // already marks the cell in-flight, so the dispatcher's `pending` SSE
1382- // sees no `wasInFlight` transition and never bumps the counter.
1383- const runStateSnapshot = queryClient . getQueryData < TableRunState > (
1384- tableKeys . activeDispatches ( tableId )
1385- )
1386- const totalStamped = Object . values ( stampedByRow ) . reduce ( ( s , n ) => s + n , 0 )
1387- if ( totalStamped > 0 ) {
1388- queryClient . setQueryData < TableRunState > ( tableKeys . activeDispatches ( tableId ) , ( prev ) => {
1389- const base = prev ?? { dispatches : [ ] , runningCellCount : 0 , runningByRowId : { } }
1390- const nextByRow = { ...base . runningByRowId }
1391- for ( const [ rid , n ] of Object . entries ( stampedByRow ) ) {
1392- nextByRow [ rid ] = ( nextByRow [ rid ] ?? 0 ) + n
1393- }
1394- return {
1395- ...base ,
1396- runningCellCount : base . runningCellCount + totalStamped ,
1397- runningByRowId : nextByRow ,
1398- }
1399- } )
1400- }
1401- return { snapshots, runStateSnapshot, didBumpRunState : totalStamped > 0 }
1445+ const bumped = bumpRunState ( queryClient , tableId , stampedByRow )
1446+ return { snapshots, runStateSnapshot : bumped ?. snapshot , didBumpRunState : bumped !== null }
14021447 } ,
14031448 onError : ( _err , _variables , context ) => {
14041449 if ( context ?. snapshots ) restoreCachedWorkflowCells ( queryClient , context . snapshots )
0 commit comments