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]/components/table-grid/cells/cell-render.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx
index fe6a6bfd3da..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,7 +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".
- if (!isEmpty) return { kind: 'value', text: stringifyValue(value) }
+ // 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
@@ -109,21 +115,7 @@ 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 }
+ return resolveLinkKind(text, currentWorkspaceId) ?? { kind: 'text', text }
}
return { kind: 'text', text: stringifyValue(value) }
}
@@ -134,6 +126,45 @@ function stringifyValue(value: unknown): string {
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,
+ }
+}
+
+/**
+ * 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 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,}$/
function extractUrlInfo(text: string): { href: string; domain: string } | null {
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 ? (