From d0cac346443ec42b6629bf98ca93652fe969e040 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Thu, 28 May 2026 18:08:24 -0700 Subject: [PATCH 1/4] fix(tables): resource-cell icons, embedded filters, run-count + queued fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - table-grid: render in-workspace resource URLs (workflow/table/KB/file) as tagged-resource cells reusing ContextMentionIcon (colored square for workflows), matching @-mention chips; only the matching list is fetched. - table-grid: fix row-number sticky cell overflow — reserve the full run/stop button area (30px, not 16px) so wide row indices don't clip. - table-grid: show an infinite-scroll loading spinner while the next page loads instead of looking like the end of the table. - table: surface sort + filter (and run/stop via the options-bar extras slot) in the embedded mothership table resource view. - table-grid/utils: stop the dispatch overlay from optimistically painting autoRun=false cells Queued for auto-fire dispatches — the dispatcher skips those groups ('autoRun-off'); manual runs still show Queued (manual-bypass). - dispatcher: exclude orphan pre-stamps (pending + executionId null) from countRunningCells so the "X running" badge doesn't stick above zero. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../table-grid/cells/cell-content.tsx | 13 ++- .../table-grid/cells/cell-render.tsx | 72 +++++++++++++ .../table-grid/cells/sim-resource-cell.tsx | 102 ++++++++++++++++++ .../components/table-grid/data-row.tsx | 6 ++ .../components/table-grid/table-grid.tsx | 15 ++- .../[tableId]/components/table-grid/utils.ts | 11 +- .../[workspaceId]/tables/[tableId]/table.tsx | 68 +++++++----- apps/sim/lib/table/dispatcher.ts | 19 +++- 8 files changed, 272 insertions(+), 34 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/sim-resource-cell.tsx diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-content.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-content.tsx index 60c3cc05336..54a2c7f2dea 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-content.tsx @@ -10,6 +10,9 @@ interface CellContentProps { value: unknown exec?: RowExecutionMetadata column: DisplayColumn + /** Current workspace id — lets string cells holding an in-workspace resource + * URL render as a tagged-resource chip instead of a plain external link. */ + workspaceId: string isEditing: boolean initialCharacter?: string | null onSave: (value: unknown, reason: SaveReason) => void @@ -34,6 +37,7 @@ export function CellContent({ value, exec, column, + workspaceId, isEditing, initialCharacter, onSave, @@ -41,7 +45,14 @@ export function CellContent({ waitingOnLabels, isEnrichmentOutput, }: CellContentProps) { - const kind = resolveCellRender({ value, exec, column, waitingOnLabels, isEnrichmentOutput }) + const kind = resolveCellRender({ + value, + exec, + column, + waitingOnLabels, + isEnrichmentOutput, + currentWorkspaceId: workspaceId, + }) return ( <> diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx index 557186b7668..91cabea7994 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx @@ -9,6 +9,7 @@ import type { RowExecutionMetadata } from '@/lib/table' import { StatusBadge } from '@/app/workspace/[workspaceId]/logs/utils' import { storageToDisplay } from '../../../utils' import type { DisplayColumn } from '../types' +import { SimResourceCell, type SimResourceType } from './sim-resource-cell' export type CellRenderKind = // Workflow-output cells @@ -26,6 +27,13 @@ export type CellRenderKind = | { kind: 'json'; text: string } | { kind: 'date'; text: string } | { kind: 'url'; text: string; href: string; domain: string } + | { + kind: 'sim-resource' + workspaceId: string + resourceType: SimResourceType + resourceId: string + href: string + } | { kind: 'text'; text: string } // Universal fallback | { kind: 'empty' } @@ -38,6 +46,9 @@ interface ResolveCellRenderInput { /** Column is an enrichment-group output — a completed-but-empty cell renders * "Not found" rather than a blank, since the enrichment ran and matched nothing. */ isEnrichmentOutput?: boolean + /** Current workspace id — a URL pointing to a resource in this workspace + * renders as a tagged-resource chip rather than a plain external link. */ + currentWorkspaceId?: string } export function resolveCellRender({ @@ -46,6 +57,7 @@ export function resolveCellRender({ column, waitingOnLabels, isEnrichmentOutput, + currentWorkspaceId, }: ResolveCellRenderInput): CellRenderKind { const isNull = value === null || value === undefined const isEmpty = isNull || value === '' @@ -97,6 +109,18 @@ export function resolveCellRender({ if (column.type === 'date') return { kind: 'date', text: String(value) } if (column.type === 'string') { const text = stringifyValue(value) + if (currentWorkspaceId) { + const resource = extractSimResourceInfo(text) + if (resource && resource.workspaceId === currentWorkspaceId) { + return { + kind: 'sim-resource', + workspaceId: resource.workspaceId, + resourceType: resource.resourceType, + resourceId: resource.resourceId, + href: resource.href, + } + } + } const urlInfo = extractUrlInfo(text) if (urlInfo) return { kind: 'url', text, href: urlInfo.href, domain: urlInfo.domain } return { kind: 'text', text } @@ -131,6 +155,43 @@ function extractUrlInfo(text: string): { href: string; domain: string } | null { return null } +/** Maps a workspace route section to the sim resource kind it addresses. */ +const SIM_RESOURCE_SECTIONS: Record = { + w: 'workflow', + tables: 'table', + knowledge: 'knowledge', + files: 'file', +} + +/** + * Recognizes a `/workspace/{id}/{section}/{resourceId}` URL (absolute or + * relative) pointing to a sim resource and returns its descriptor. The href is + * the pathname so the link stays within the current deployment. Returns null + * for anything that isn't a single-segment resource route. + */ +function extractSimResourceInfo( + text: string +): { workspaceId: string; resourceType: SimResourceType; resourceId: string; href: string } | null { + const trimmed = text.trim() + if (!trimmed) return null + let pathname: string + if (/^https?:\/\//i.test(trimmed)) { + try { + pathname = new URL(trimmed).pathname + } catch { + return null + } + } else if (trimmed.startsWith('/')) { + pathname = trimmed.split(/[?#]/)[0] + } else { + return null + } + const match = pathname.match(/^\/workspace\/([^/]+)\/(w|tables|knowledge|files)\/([^/]+)\/?$/) + if (!match) return null + const [, workspaceId, section, resourceId] = match + return { workspaceId, resourceType: SIM_RESOURCE_SECTIONS[section], resourceId, href: pathname } +} + interface CellRenderProps { kind: CellRenderKind isEditing: boolean @@ -270,6 +331,17 @@ export function CellRender({ kind, isEditing }: CellRenderProps): React.ReactEle ) + case 'sim-resource': + return ( + + ) + case 'text': return ( = { + workflow: 'Workflow', + table: 'Table', + knowledge: 'Knowledge base', + file: 'File', +} + +interface SimResourceCellProps { + /** Always the current workspace — the resolver only emits this kind for same-workspace URLs. */ + workspaceId: string + resourceType: SimResourceType + resourceId: string + /** In-app pathname the resource link navigates to. */ + href: string + isEditing: boolean +} + +/** + * Renders a cell whose value is a URL pointing to a sim resource in the current + * workspace as a tagged-resource chip — the same icon (and per-workflow colored + * square) used for @-style resource mentions, plus the resource's name as a link. + * Only the list matching `resourceType` is fetched; the other queries stay + * disabled so a sim-resource cell subscribes to a single shared list. + */ +export function SimResourceCell({ + workspaceId, + resourceType, + resourceId, + href, + isEditing, +}: SimResourceCellProps) { + const { data: workflows = [] } = useWorkflows( + resourceType === 'workflow' ? workspaceId : undefined + ) + const { data: tables = [] } = useTablesList(resourceType === 'table' ? workspaceId : undefined) + const { data: knowledgeBases = [] } = useKnowledgeBasesQuery(workspaceId, { + enabled: resourceType === 'knowledge', + }) + const { data: files = [] } = useWorkspaceFiles(resourceType === 'file' ? workspaceId : '') + + const workflow = + resourceType === 'workflow' ? workflows.find((w) => w.id === resourceId) : undefined + + const name = useMemo(() => { + switch (resourceType) { + case 'workflow': + return workflow?.name + case 'table': + return tables.find((t) => t.id === resourceId)?.name + case 'knowledge': + return knowledgeBases.find((kb) => kb.id === resourceId)?.name + case 'file': + return files.find((f) => f.id === resourceId)?.name + } + }, [resourceType, resourceId, workflow, tables, knowledgeBases, files]) + + const label = name ?? FALLBACK_LABEL[resourceType] + + const context: ChatMessageContext = + resourceType === 'workflow' + ? { kind: 'workflow', label, workflowId: resourceId } + : resourceType === 'table' + ? { kind: 'table', label, tableId: resourceId } + : resourceType === 'knowledge' + ? { kind: 'knowledge', label, knowledgeId: resourceId } + : { kind: 'file', label, fileId: resourceId } + + return ( + + + e.stopPropagation()} + onDoubleClick={(e) => e.stopPropagation()} + > + {label} + + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/data-row.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/data-row.tsx index e228edba84d..8f4433e4b4d 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/data-row.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/data-row.tsx @@ -23,6 +23,9 @@ import { type NormalizedSelection, resolveCellExec } from './utils' export interface DataRowProps { row: TableRowType columns: DisplayColumn[] + /** Current workspace id — forwarded to cells so in-workspace resource URLs + * render as tagged-resource chips. */ + workspaceId: string rowIndex: number isFirstRow: boolean editingColumnName: string | null @@ -94,6 +97,7 @@ function dataRowPropsAreEqual(prev: DataRowProps, next: DataRowProps): boolean { if ( prev.row !== next.row || prev.columns !== next.columns || + prev.workspaceId !== next.workspaceId || prev.rowIndex !== next.rowIndex || prev.isFirstRow !== next.isFirstRow || prev.editingColumnName !== next.editingColumnName || @@ -135,6 +139,7 @@ function dataRowPropsAreEqual(prev: DataRowProps, next: DataRowProps): boolean { export const DataRow = React.memo(function DataRow({ row, columns, + workspaceId, rowIndex, isFirstRow, editingColumnName, @@ -310,6 +315,7 @@ export const DataRow = React.memo(function DataRow({ )}
)} + {isFetchingNextPage && ( + + +
+ +
+ + + )} ) })() diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/utils.ts b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/utils.ts index d66182b70f4..6a7f53b2185 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/utils.ts @@ -49,7 +49,10 @@ export function checkboxColLayout( ): { colWidth: number; numDivWidth: number } { const digits = maxRows > 0 ? Math.floor(Math.log10(maxRows)) + 1 : 1 const numDivWidth = Math.max(20, digits * 8 + 4) - const colWidth = Math.max(32, numDivWidth + 8) + (hasWorkflowCols ? 16 : 0) + // When workflow columns are present a 20px run/stop button sits to the right of + // the number, separated by a 6px gap and a 4px right pad — 30px total. Reserving + // only the button width clipped the number on tables with many (wide) row indices. + const colWidth = Math.max(32, numDivWidth + 8) + (hasWorkflowCols ? 30 : 0) return { colWidth, numDivWidth } } @@ -196,6 +199,12 @@ export function resolveCellExec( // cell SSE) cover the actual rows instead. if (d.limit) continue if (!d.scope.groupIds.includes(group.id)) continue + // Auto-fire dispatches (row writes / schema changes) scope every group but + // the dispatcher honors `autoRun: false` per-cell ('autoRun-off'), so those + // cells never actually run — don't optimistically paint them Queued. Manual + // runs (Run all / Run column) bypass autoRun and DO run them, so keep the + // overlay's Queued there. + if (!d.isManualRun && group.autoRun === false) continue if (d.scope.rowIds && !d.scope.rowIds.includes(row.id)) continue if (row.position <= d.cursor) continue return { diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx index 64fa3c1af0d..d82ec2ce6d5 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx @@ -462,36 +462,46 @@ export function Table({ return (
{!embedded && ( - <> - 0 ? ( - - ) : null - } - /> - setFilterOpen((prev) => !prev)} - filterActive={filterOpen || !!queryOptions.filter} - /> - {filterOpen && ( - setFilterOpen(false)} + 0 ? ( + + ) : null + } + /> + )} + {/* Sort + filter render in both modes. In embedded (mothership) mode there's + no ResourceHeader, so the run/stop control rides in the options bar's + `extras` slot — keeping the bar populated whether or not a run is live. */} + setFilterOpen((prev) => !prev)} + filterActive={filterOpen || !!queryOptions.filter} + extras={ + embedded && selection.totalRunning > 0 ? ( + - )} - + ) : undefined + } + /> + {filterOpen && ( + setFilterOpen(false)} + /> )} Date: Fri, 29 May 2026 10:55:13 -0700 Subject: [PATCH 2/4] fix(tables): single run/stop control, right-aligned row numbers, View-execution guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - table: de-duplicate the run/stop control in the embedded mothership view — drop TableGrid's own embedded run-status bar; it now lives only in the options bar (left-aligned next to Filter + Sort). Removes the orphaned RunStatusControl import + onStopAll/cancelRunsPending props. - data-row: right-align the row number within its box (hugs the right edge, no hover position jump) with a scaled right inset — 2px for ≤3-digit indices, 4px for 4+ so narrow columns don't look over-padded. - table-grid: require a real executionId in the action bar's canViewExecution flag so an error that never produced an execution (enqueue failure → status 'error', executionId null) doesn't offer "View execution". Co-Authored-By: Claude Opus 4.8 (1M context) --- .../components/table-grid/data-row.tsx | 9 ++++++-- .../components/table-grid/table-grid.tsx | 21 ++++--------------- .../[workspaceId]/tables/[tableId]/table.tsx | 2 -- 3 files changed, 11 insertions(+), 21 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/data-row.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/data-row.tsx index 8f4433e4b4d..d8396f8f613 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/data-row.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/data-row.tsx @@ -201,7 +201,12 @@ export const DataRow = React.memo(function DataRow({ tabIndex={0} aria-checked={isRowSelected} aria-label={`Select row ${rowIndex + 1}`} - className='group/checkbox flex h-[20px] shrink-0 items-center justify-center' + className={cn( + 'group/checkbox flex h-[20px] shrink-0 items-center justify-end', + // Lighter right inset for narrow indices (≤3 digits → numDivWidth ≤ 28); + // full 4px once the column widens (4+ digits, numDivWidth ≥ 36). + numDivWidth >= 36 ? 'pr-1' : 'pr-0.5' + )} style={{ width: numDivWidth }} onMouseDown={(e) => { if (e.button !== 0) return @@ -213,7 +218,7 @@ export const DataRow = React.memo(function DataRow({ > diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx index 4705964c36c..3999bbfb920 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx @@ -40,7 +40,6 @@ import { import type { ColumnConfig } from '../column-config-sidebar' import { ContextMenu } from '../context-menu' import { NewColumnDropdown } from '../new-column-dropdown' -import { RunStatusControl } from '../run-status-control' import type { WorkflowConfig } from '../workflow-sidebar' import { ExpandedCellPopover } from './cells' import { ADD_COL_WIDTH, CELL_HEADER_CHECKBOX, COL_WIDTH, SELECTION_TINT_BG } from './constants' @@ -160,10 +159,6 @@ interface TableGridProps { onStopRows: (rowIds: string[]) => void /** Single-row stop for the per-row gutter button. */ onStopRow: (rowId: string) => void - /** Wholesale cancel — page-header "Stop all". */ - onStopAll: () => void - /** Whether `useCancelTableRuns` is currently in flight. */ - cancelRunsPending: boolean /** * Fired whenever the action-bar selection or running-count derivations * change. Wrapper uses this to render . @@ -258,8 +253,6 @@ export function TableGrid({ onRunRows, onStopRows, onStopRow, - onStopAll, - cancelRunsPending, onSelectionChange, queryOptions, columnRenameSinkRef, @@ -2949,8 +2942,12 @@ export function TableGrid({ rowId: row.id, groupId, executionId: exec?.executionId ?? null, + // Requires a real executionId: an error that never produced an execution + // (e.g. enqueue failure → status 'error' with executionId null) has no + // trace to open, so "View execution" must not offer it. canViewExecution: !isEnrichmentGroup && + Boolean(exec?.executionId) && (status === 'completed' || status === 'error' || status === 'running' || isPaused), } }, [normalizedSelection, rows, displayColumns, workflowGroupById]) @@ -3097,16 +3094,6 @@ export function TableGrid({ return (
- {embedded && totalRunning > 0 && ( -
- -
- )} -
Date: Fri, 29 May 2026 11:09:46 -0700 Subject: [PATCH 3/4] =?UTF-8?q?fix(tables):=20address=20review=20=E2=80=94?= =?UTF-8?q?=20drizzle=20operators=20for=20orphan=20filter,=20enabled=20fla?= =?UTF-8?q?g=20for=20files=20query?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - dispatcher: replace the `not(and(...)) as SQL` cast in countRunningCells with `or(ne(status,'pending'), isNotNull(executionId))` — De Morgan equivalent, fully type-checked, no cast and no hand-written raw SQL. - workspace-files: add an `enabled` option to useWorkspaceFiles; sim-resource cell now passes the real workspaceId with `enabled` instead of '' so the query cache isn't polluted with an empty-key entry. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../components/table-grid/cells/sim-resource-cell.tsx | 4 +++- apps/sim/hooks/queries/workspace-files.ts | 8 ++++++-- apps/sim/lib/table/dispatcher.ts | 11 ++++------- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/sim-resource-cell.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/sim-resource-cell.tsx index 9e654d42e06..455dbc72653 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/sim-resource-cell.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/sim-resource-cell.tsx @@ -50,7 +50,9 @@ export function SimResourceCell({ const { data: knowledgeBases = [] } = useKnowledgeBasesQuery(workspaceId, { enabled: resourceType === 'knowledge', }) - const { data: files = [] } = useWorkspaceFiles(resourceType === 'file' ? workspaceId : '') + const { data: files = [] } = useWorkspaceFiles(workspaceId, 'active', { + enabled: resourceType === 'file', + }) const workflow = resourceType === 'workflow' ? workflows.find((w) => w.id === resourceId) : undefined diff --git a/apps/sim/hooks/queries/workspace-files.ts b/apps/sim/hooks/queries/workspace-files.ts index 4eb1f4577c1..492c77286ec 100644 --- a/apps/sim/hooks/queries/workspace-files.ts +++ b/apps/sim/hooks/queries/workspace-files.ts @@ -98,11 +98,15 @@ async function fetchWorkspaceFiles( /** * Hook to fetch workspace files */ -export function useWorkspaceFiles(workspaceId: string, scope: WorkspaceFileQueryScope = 'active') { +export function useWorkspaceFiles( + workspaceId: string, + scope: WorkspaceFileQueryScope = 'active', + options?: { enabled?: boolean } +) { return useQuery({ queryKey: workspaceFilesKeys.list(workspaceId, scope), queryFn: ({ signal }) => fetchWorkspaceFiles(workspaceId, scope, signal), - enabled: !!workspaceId, + enabled: !!workspaceId && (options?.enabled ?? true), staleTime: 30 * 1000, // 30 seconds - files can change frequently placeholderData: keepPreviousData, // Show cached data immediately }) diff --git a/apps/sim/lib/table/dispatcher.ts b/apps/sim/lib/table/dispatcher.ts index 5aeca4d014c..441abda9330 100644 --- a/apps/sim/lib/table/dispatcher.ts +++ b/apps/sim/lib/table/dispatcher.ts @@ -3,7 +3,7 @@ import { tableRowExecutions, tableRunDispatches, userTableRows } from '@sim/db/s import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' -import { and, asc, eq, gt, inArray, isNull, not, type SQL, sql } from 'drizzle-orm' +import { and, asc, eq, gt, inArray, isNotNull, ne, or, type SQL, sql } from 'drizzle-orm' import { getJobQueue } from '@/lib/core/async-jobs/config' import { writeWorkflowGroupState } from '@/lib/table/cell-write' import { appendTableEvent } from '@/lib/table/events' @@ -208,12 +208,9 @@ export async function countRunningCells( and( eq(tableRowExecutions.tableId, tableId), inArray(tableRowExecutions.status, ['queued', 'running', 'pending']), - not( - and( - eq(tableRowExecutions.status, 'pending'), - isNull(tableRowExecutions.executionId) - ) as SQL - ) + // Exclude orphan pre-stamps (`pending` + null executionId). De Morgan of + // NOT(pending AND null) — `status` is NOT NULL so `ne` is well-defined. + or(ne(tableRowExecutions.status, 'pending'), isNotNull(tableRowExecutions.executionId)) ) ) .groupBy(tableRowExecutions.rowId) From 5460850c5b85c0c58bb4de89e0ebdd907260336d Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Fri, 29 May 2026 11:31:18 -0700 Subject: [PATCH 4/4] fix(tables): "X running" badge counts actual in-flight cells, not dispatch scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The badge derived from `runningCellCount` (the dispatch-scope estimate = rows-ahead × groupCount), which over-counts groups that already finished on rows still inside a dispatch's scope — a cascade where 3 of 4 workflow columns completed read "4 running" instead of "1". Derive `totalRunning` from the live `runningByRowId` map instead (the same per-row source the gutter and action-bar selection already sum), so it reflects cells actually in flight and updates per-cell via SSE rather than only on dispatch-window events. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../tables/[tableId]/components/table-grid/table-grid.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx index 3999bbfb920..0232b886b64 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx @@ -323,8 +323,13 @@ export function TableGrid({ const { data: tableRunState } = useTableRunState(tableId) const activeDispatches = tableRunState?.dispatches - const totalRunning = tableRunState?.runningCellCount ?? 0 const runningByRowId = tableRunState?.runningByRowId ?? EMPTY_RUNNING_BY_ROW + // Actual in-flight cell count = sum of the live per-row map (kept current by + // applyCell's SSE deltas, and the same source the per-row gutter uses). The + // dispatch-scope `runningCellCount` over-counts already-completed groups on + // rows still inside a dispatch's scope — e.g. a cascade where 3 of 4 columns + // finished would read "4 running" instead of "1". + const totalRunning = Object.values(runningByRowId).reduce((sum, n) => sum + n, 0) const tableRowCountRef = useRef(tableData?.rowCount ?? 0) tableRowCountRef.current = tableData?.rowCount ?? 0