From 172c183df88e4d55ed66e62c47ccb982732a393f Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Fri, 29 May 2026 19:09:51 -0700 Subject: [PATCH 1/3] fix(tables): right-align run/stop in the embedded table toolbar Add a right-aligned `trailing` slot to ResourceOptionsBar and move the embedded mothership table's run/stop control into it, so Filter + Sort stay left-aligned and run/stop sits opposite on the right. No-op for the search-bearing consumers (logs, resource list), which don't pass `trailing`. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../resource-options-bar/resource-options-bar.tsx | 14 +++++++++++++- .../[workspaceId]/tables/[tableId]/table.tsx | 8 ++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options-bar/resource-options-bar.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options-bar/resource-options-bar.tsx index fc3442c124f..853349ed363 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options-bar/resource-options-bar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options-bar/resource-options-bar.tsx @@ -73,6 +73,10 @@ interface ResourceOptionsBarProps { filterActive?: boolean filterTags?: FilterTag[] extras?: ReactNode + /** Right-aligned slot. Unlike `extras` (which sits with the left controls), + * `trailing` is pushed to the far right via `justify-between` — used for the + * table's run/stop control opposite the left-aligned filter/sort. */ + trailing?: ReactNode } export const ResourceOptionsBar = memo(function ResourceOptionsBar({ @@ -83,9 +87,16 @@ export const ResourceOptionsBar = memo(function ResourceOptionsBar({ filterActive, filterTags, extras, + trailing, }: ResourceOptionsBarProps) { const hasContent = - search || sort || filter || onFilterToggle || extras || (filterTags && filterTags.length > 0) + search || + sort || + filter || + onFilterToggle || + extras || + trailing || + (filterTags && filterTags.length > 0) if (!hasContent) return null return ( @@ -143,6 +154,7 @@ export const ResourceOptionsBar = memo(function ResourceOptionsBar({ ) : null} {sort && } + {trailing &&
{trailing}
} ) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx index 466835d041b..9eb5a8de8e8 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx @@ -478,14 +478,14 @@ export function Table({ } /> )} - {/* 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. */} + {/* Sort + filter render in both modes (left-aligned). In embedded (mothership) + mode there's no ResourceHeader, so the run/stop control rides in the options + bar's right-aligned `trailing` slot — opposite the left-aligned filter/sort. */} setFilterOpen((prev) => !prev)} filterActive={filterOpen || !!queryOptions.filter} - extras={ + trailing={ embedded && selection.totalRunning > 0 ? ( Date: Fri, 29 May 2026 19:09:51 -0700 Subject: [PATCH 2/3] fix(tables): workflow-output cells format values like normal cells Workflow-output columns short-circuited in resolveCellRender and rendered their value as plain text, so a sim-resource URL / external URL / JSON / date produced by a workflow never got the chip, favicon link, or typed formatting a normal cell gets. Factor value formatting into a shared `resolveValueKind` helper used by both the workflow-value branch and the plain-cell branch; the workflow branch keeps the typewriter reveal for plain streaming text via a `typewriter` flag. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../table-grid/cells/cell-render.tsx | 74 +++++++++++++------ 1 file changed, 51 insertions(+), 23 deletions(-) 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 fe6a6bfd3da..e89ca4fcd6f 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 @@ -77,7 +77,9 @@ export function resolveCellRender({ // Value wins over pending-upstream: a finished column stays finished even // while other blocks in the group are still running. An empty string is not // a value — it falls through so a completed enrichment can show "Not found". - if (!isEmpty) return { kind: 'value', text: stringifyValue(value) } + // Format the value exactly like a plain cell (resource chip / URL / JSON / + // date / boolean), keeping the typewriter reveal for plain streaming text. + if (!isEmpty) return resolveValueKind(value, column, currentWorkspaceId, { typewriter: true }) if (inFlight && !(groupHasBlockErrors && !blockRunning)) { // A `pending` cell whose jobId starts with `paused-` is mid-pause @@ -103,35 +105,61 @@ export function resolveCellRender({ return { kind: 'empty' } } + return resolveValueKind(value, column, currentWorkspaceId, { typewriter: false }) +} + +function stringifyValue(value: unknown): string { + if (typeof value === 'string') return value + if (value === null || value === undefined) return '' + return JSON.stringify(value) +} + +/** Returns a `sim-resource` cell kind when `text` is a URL pointing to a + * resource in the current workspace, else null. Shared by plain string cells + * and workflow-output value cells so both surface in-workspace resource links + * as tagged chips. */ +function resolveSimResourceKind( + text: string, + currentWorkspaceId: string | undefined +): Extract | null { + if (!currentWorkspaceId) return null + const resource = extractSimResourceInfo(text) + if (!resource || resource.workspaceId !== currentWorkspaceId) return null + return { + kind: 'sim-resource', + workspaceId: resource.workspaceId, + resourceType: resource.resourceType, + resourceId: resource.resourceId, + href: resource.href, + } +} + +/** + * Maps a present (non-empty) cell value to its render kind based on the column + * type — the shared formatter for plain cells and workflow-output value cells, + * so a workflow output renders booleans, JSON, dates, resource chips and URL + * links exactly like a normal cell. `typewriter` selects the plain-text + * fallback: `value` (animated reveal, for streaming workflow outputs) vs `text` + * (static, for plain cells). + */ +function resolveValueKind( + value: unknown, + column: DisplayColumn, + currentWorkspaceId: string | undefined, + opts: { typewriter: boolean } +): CellRenderKind { if (column.type === 'boolean') return { kind: 'boolean', checked: Boolean(value) } - if (isNull) return { kind: 'empty' } + if (value === null || value === undefined) return { kind: 'empty' } if (column.type === 'json') return { kind: 'json', text: JSON.stringify(value) } if (column.type === 'date') return { kind: 'date', text: String(value) } + const text = stringifyValue(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 simKind = resolveSimResourceKind(text, currentWorkspaceId) + if (simKind) return simKind const urlInfo = extractUrlInfo(text) if (urlInfo) return { kind: 'url', text, href: urlInfo.href, domain: urlInfo.domain } - return { kind: 'text', text } } - return { kind: 'text', text: stringifyValue(value) } -} - -function stringifyValue(value: unknown): string { - if (typeof value === 'string') return value - if (value === null || value === undefined) return '' - return JSON.stringify(value) + return opts.typewriter ? { kind: 'value', text } : { kind: 'text', text } } const BARE_DOMAIN_RE = /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/ From d0ea8cf08053da0acb418609c944e95fb143d50e Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Sat, 30 May 2026 10:13:23 -0700 Subject: [PATCH 3/3] fix(tables): detect resource/URL links on workflow output regardless of column type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Workflow output columns default to `json` (columnTypeForLeaf), so routing their values through the type-based formatter (a) gated chip/URL promotion behind `column.type === 'string'` — a URL produced by a json-typed output never became a chip — and (b) JSON.stringify'd plain string values, adding quotes and losing the typewriter reveal. Detect links (sim-resource chip / favicon URL) on the value string directly for workflow outputs, falling back to the plain `value` kind; plain cells keep the type-based formatting. Addresses Greptile P2 on #4806. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../table-grid/cells/cell-render.tsx | 59 ++++++++++--------- 1 file changed, 31 insertions(+), 28 deletions(-) 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 e89ca4fcd6f..065385a9f05 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 @@ -77,9 +77,13 @@ export function resolveCellRender({ // Value wins over pending-upstream: a finished column stays finished even // while other blocks in the group are still running. An empty string is not // a value — it falls through so a completed enrichment can show "Not found". - // Format the value exactly like a plain cell (resource chip / URL / JSON / - // date / boolean), keeping the typewriter reveal for plain streaming text. - if (!isEmpty) return resolveValueKind(value, column, currentWorkspaceId, { typewriter: true }) + // A value that's wholly a resource/URL string renders as a chip/link (any + // column type — workflow output is free-form); otherwise the plain `value` + // kind keeps the typewriter reveal for streaming text. + if (!isEmpty) { + const text = stringifyValue(value) + return resolveLinkKind(text, currentWorkspaceId) ?? { kind: 'value', text } + } if (inFlight && !(groupHasBlockErrors && !blockRunning)) { // A `pending` cell whose jobId starts with `paused-` is mid-pause @@ -105,7 +109,15 @@ export function resolveCellRender({ return { kind: 'empty' } } - return resolveValueKind(value, column, currentWorkspaceId, { typewriter: false }) + if (column.type === 'boolean') return { kind: 'boolean', checked: Boolean(value) } + if (isNull) return { kind: 'empty' } + if (column.type === 'json') return { kind: 'json', text: JSON.stringify(value) } + if (column.type === 'date') return { kind: 'date', text: String(value) } + if (column.type === 'string') { + const text = stringifyValue(value) + return resolveLinkKind(text, currentWorkspaceId) ?? { kind: 'text', text } + } + return { kind: 'text', text: stringifyValue(value) } } function stringifyValue(value: unknown): string { @@ -135,31 +147,22 @@ function resolveSimResourceKind( } /** - * Maps a present (non-empty) cell value to its render kind based on the column - * type — the shared formatter for plain cells and workflow-output value cells, - * so a workflow output renders booleans, JSON, dates, resource chips and URL - * links exactly like a normal cell. `typewriter` selects the plain-text - * fallback: `value` (animated reveal, for streaming workflow outputs) vs `text` - * (static, for plain cells). + * Promotes a cell value that is wholly a resource/URL string to a chip + * (in-workspace resource) or a favicon link, else null. Shared by plain string + * cells and workflow-output value cells. Workflow outputs apply this regardless + * of `column.type` (their type defaults to `json`, so gating on `string` would + * miss URL outputs); a stringified object never matches the whole-string URL + * check, so it stays JSON/text. */ -function resolveValueKind( - value: unknown, - column: DisplayColumn, - currentWorkspaceId: string | undefined, - opts: { typewriter: boolean } -): CellRenderKind { - if (column.type === 'boolean') return { kind: 'boolean', checked: Boolean(value) } - if (value === null || value === undefined) return { kind: 'empty' } - if (column.type === 'json') return { kind: 'json', text: JSON.stringify(value) } - if (column.type === 'date') return { kind: 'date', text: String(value) } - const text = stringifyValue(value) - if (column.type === 'string') { - const simKind = resolveSimResourceKind(text, currentWorkspaceId) - if (simKind) return simKind - const urlInfo = extractUrlInfo(text) - if (urlInfo) return { kind: 'url', text, href: urlInfo.href, domain: urlInfo.domain } - } - return opts.typewriter ? { kind: 'value', text } : { kind: 'text', text } +function resolveLinkKind( + text: string, + currentWorkspaceId: string | undefined +): Extract | null { + const simKind = resolveSimResourceKind(text, currentWorkspaceId) + if (simKind) return simKind + const urlInfo = extractUrlInfo(text) + if (urlInfo) return { kind: 'url', text, href: urlInfo.href, domain: urlInfo.domain } + return null } const BARE_DOMAIN_RE = /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/