fix(tables): right-align run/stop in embedded toolbar; workflow cells format like normal cells (#4806)

* 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) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>

* fix(tables): detect resource/URL links on workflow output regardless of column type

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) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Theodore Li
2026-05-30 13:27:00 -04:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 640b7e12c5
commit b4787dd173
3 changed files with 64 additions and 21 deletions
@@ -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 && <SortDropdown config={sort} />}
</div>
{trailing && <div className='flex shrink-0 items-center gap-1.5'>{trailing}</div>}
</div>
</div>
)
@@ -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<CellRenderKind, { kind: 'sim-resource' }> | 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<CellRenderKind, { kind: 'sim-resource' } | { kind: 'url' }> | 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 {
@@ -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. */}
<ResourceOptionsBar
sort={sortConfig}
onFilterToggle={() => setFilterOpen((prev) => !prev)}
filterActive={filterOpen || !!queryOptions.filter}
extras={
trailing={
embedded && selection.totalRunning > 0 ? (
<RunStatusControl
running={selection.totalRunning}