From d31bdf20fe7ffe3e4476e63c7b5ba796a1c6370f Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Tue, 4 Aug 2026 18:42:28 -0700 Subject: [PATCH 1/3] refactor(logs): centralize workflow-id resolution and editor path The logs list, its context menu, and the details panel each resolved a log's workflow id with their own copy of `workflow?.id || workflowId`, and the list disagreed with the details panel on what counts as a deleted workflow. Extract `resolveLogWorkflowId` and `workflowEditorPath` so the three surfaces cannot drift. `resolveLogWorkflowId` also returns null for Sim agent jobs, which have no workflow of their own. Only the context menu's "Open Workflow" item adopts that stricter predicate; cancel and retry keep using the previous `hasWorkflow` check so their gating is unchanged. --- .../log-row-context-menu.tsx | 9 ++- .../app/workspace/[workspaceId]/logs/logs.tsx | 6 +- .../[workspaceId]/logs/utils.test.ts | 55 +++++++++++++++++++ .../app/workspace/[workspaceId]/logs/utils.ts | 23 ++++++++ 4 files changed, 90 insertions(+), 3 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/logs/utils.test.ts diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-row-context-menu/log-row-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-row-context-menu/log-row-context-menu.tsx index 0b2f7c11a2b..d0c8940bd8d 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-row-context-menu/log-row-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-row-context-menu/log-row-context-menu.tsx @@ -16,6 +16,7 @@ import { X, } from '@sim/emcn' import type { WorkflowLogSummary } from '@/lib/api/contracts/logs' +import { resolveLogWorkflowId } from '@/app/workspace/[workspaceId]/logs/utils' interface LogRowContextMenuProps { isOpen: boolean @@ -58,6 +59,12 @@ export const LogRowContextMenu = memo(function LogRowContextMenu({ }: LogRowContextMenuProps) { const hasExecutionId = Boolean(log?.executionId) const hasWorkflow = Boolean(log?.workflow?.id || log?.workflowId) + /** + * "Open Workflow" needs a navigable target, which is stricter than + * `hasWorkflow`: Sim agent jobs have no workflow of their own. Cancel/retry + * keep using `hasWorkflow` so their gating is unchanged. + */ + const hasOpenableWorkflow = Boolean(log && resolveLogWorkflowId(log)) const isCancellable = (log?.status === 'running' || log?.status === 'pending') && hasExecutionId && hasWorkflow const isRetryable = log?.status === 'failed' && hasWorkflow && log?.trigger !== 'mothership' @@ -112,7 +119,7 @@ export const LogRowContextMenu = memo(function LogRowContextMenu({ - + Open Workflow diff --git a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx index 7bf6142e139..5d812699e7c 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx @@ -92,9 +92,11 @@ import { getDisplayStatus, type LogStatus, parseDuration, + resolveLogWorkflowId, STATUS_CONFIG, StatusBadge, TriggerBadge, + workflowEditorPath, } from './utils' const LOGS_PER_PAGE = 50 as const @@ -524,9 +526,9 @@ export default function Logs() { }, [contextMenuLog, workspaceId]) const handleOpenWorkflow = useCallback(() => { - const wfId = contextMenuLog?.workflow?.id || contextMenuLog?.workflowId + const wfId = contextMenuLog ? resolveLogWorkflowId(contextMenuLog) : null if (wfId) { - window.open(`/workspace/${workspaceId}/w/${wfId}`, '_blank') + window.open(workflowEditorPath(workspaceId, wfId), '_blank') } }, [contextMenuLog, workspaceId]) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/utils.test.ts b/apps/sim/app/workspace/[workspaceId]/logs/utils.test.ts new file mode 100644 index 00000000000..4d4cff37ec7 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/logs/utils.test.ts @@ -0,0 +1,55 @@ +/** + * @vitest-environment jsdom + */ +import { describe, expect, it } from 'vitest' +import { resolveLogWorkflowId, workflowEditorPath } from './utils' + +describe('resolveLogWorkflowId', () => { + it('returns the nested workflow id when present', () => { + expect( + resolveLogWorkflowId({ trigger: 'manual', workflowId: 'wf-1', workflow: { id: 'wf-1' } }) + ).toBe('wf-1') + }) + + it('falls back to workflowId when the workflow object is absent', () => { + expect(resolveLogWorkflowId({ trigger: 'api', workflowId: 'wf-2', workflow: null })).toBe( + 'wf-2' + ) + }) + + it('prefers the nested workflow id over workflowId when both are set', () => { + expect( + resolveLogWorkflowId({ trigger: 'manual', workflowId: 'stale', workflow: { id: 'fresh' } }) + ).toBe('fresh') + }) + + it('returns null for Sim agent jobs even when a workflow id exists', () => { + expect( + resolveLogWorkflowId({ + trigger: 'mothership', + workflowId: 'wf-3', + workflow: { id: 'wf-3' }, + }) + ).toBeNull() + }) + + it('returns null for a deleted workflow (both id fields empty)', () => { + expect(resolveLogWorkflowId({ trigger: 'manual', workflowId: null, workflow: null })).toBeNull() + }) + + it('returns null when ids are present but empty strings', () => { + expect( + resolveLogWorkflowId({ trigger: 'manual', workflowId: '', workflow: { id: '' } }) + ).toBeNull() + }) + + it('treats a missing trigger as a normal workflow run', () => { + expect(resolveLogWorkflowId({ workflowId: 'wf-4' })).toBe('wf-4') + }) +}) + +describe('workflowEditorPath', () => { + it('builds the workspace-scoped editor path', () => { + expect(workflowEditorPath('ws-1', 'wf-1')).toBe('/workspace/ws-1/w/wf-1') + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/utils.ts b/apps/sim/app/workspace/[workspaceId]/logs/utils.ts index fbffba81702..05820985af3 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/logs/utils.ts @@ -17,6 +17,29 @@ export const LOG_COLUMNS = { export const DELETED_WORKFLOW_LABEL = 'Deleted Workflow' +/** + * Resolves the workflow a log row points at, or null when there is nowhere to + * navigate. Sim agent jobs have no workflow of their own, and a deleted + * workflow leaves both id fields empty. + * + * Single source of truth for "is this log's workflow reachable" — the list row, + * its context menu, and the details panel must agree, or a row can render as + * "Deleted Workflow" while still linking somewhere. + */ +export function resolveLogWorkflowId(log: { + trigger?: string | null + workflowId?: string | null + workflow?: { id?: string } | null +}): string | null { + if (log.trigger === 'mothership') return null + return log.workflow?.id || log.workflowId || null +} + +/** Path to a workflow in the editor. */ +export function workflowEditorPath(workspaceId: string, workflowId: string): string { + return `/workspace/${workspaceId}/w/${workflowId}` +} + export type LogStatus = | 'error' | 'pending' From 83ea4df5c2cfa80fc73f1c0f8ab44d9f1dba6fb1 Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Tue, 4 Aug 2026 18:42:59 -0700 Subject: [PATCH 2/3] feat(logs): open the workflow from the log details panel The workflow name in a log's details panel was static text, so the only way to reach the workflow was the row's right-click context menu. Make the label a link to the workflow editor, opening in a new tab so the log list keeps its filters, scroll position, and open panel. On hover or keyboard focus the leading workflow icon morphs into SquareArrowUpRight, reusing the grid-stacked cross-fade already used by the resource header breadcrumb. Sim agent jobs and deleted workflows have no reachable workflow and stay static text. Adds a `group-hover-hover` variant so the morph is gated on a real hover-capable pointer, matching the existing `hover-hover` variant and keeping touch devices out of a half-applied hover state. --- .../components/log-details/log-details.tsx | 51 +++++++++++++++---- apps/sim/tailwind.config.ts | 4 ++ 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx index bde89f51df8..e0cb5aa6267 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx @@ -29,11 +29,13 @@ import { ChevronUp, Clipboard, Search, + SquareArrowUpRight, Workflow, Wrench, X, } from '@sim/emcn/icons' import { formatDuration } from '@sim/utils/formatting' +import Link from 'next/link' import { useParams, useRouter } from 'next/navigation' import { useQueryState } from 'nuqs' import { createPortal } from 'react-dom' @@ -59,8 +61,10 @@ import { DELETED_WORKFLOW_LABEL, formatDate, getDisplayStatus, + resolveLogWorkflowId, StatusBadge, TriggerBadge, + workflowEditorPath, } from '@/app/workspace/[workspaceId]/logs/utils' import { useCodeViewerFeatures } from '@/hooks/use-code-viewer' import { usePermissionConfig } from '@/hooks/use-permission-config' @@ -317,6 +321,19 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP const isWorkflowExecutionLog = (log.trigger === 'manual' && !!log.duration) || !!log.executionData?.traceSpans + /** + * The workflow this run belongs to, when it is still reachable. Null for Sim + * agent jobs and deleted workflows, which render their label as static text. + */ + const openableWorkflowId = resolveLogWorkflowId(log) + + const workflowLabel = + log.trigger === 'mothership' + ? log.jobTitle || 'Untitled Job' + : openableWorkflowId + ? log.workflow?.name || 'Unknown' + : DELETED_WORKFLOW_LABEL + const hasCostInfo = !!(isWorkflowExecutionLog && log.cost) const showWorkflowState = isWorkflowExecutionLog && @@ -465,15 +482,31 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP {log.trigger === 'mothership' ? 'Job' : 'Workflow'} -
- - - {log.trigger === 'mothership' - ? log.jobTitle || 'Untitled Job' - : log.workflow?.name || - (!log.workflowId ? DELETED_WORKFLOW_LABEL : 'Unknown')} - -
+ {openableWorkflowId ? ( + + + + + + + {workflowLabel} + + (opens in a new tab) + + ) : ( +
+ + + {workflowLabel} + +
+ )} diff --git a/apps/sim/tailwind.config.ts b/apps/sim/tailwind.config.ts index 576f1528e06..473050430b0 100644 --- a/apps/sim/tailwind.config.ts +++ b/apps/sim/tailwind.config.ts @@ -300,6 +300,10 @@ export default { require('tailwindcss/plugin')( ({ addVariant }: { addVariant: (name: string, definition: string) => void }) => { addVariant('hover-hover', '@media (hover: hover) and (pointer: fine) { &:hover }') + addVariant( + 'group-hover-hover', + '@media (hover: hover) and (pointer: fine) { :merge(.group):hover & }' + ) } ), ], From 7283a7a87af1163ce072e2609dcceaffa811acb8 Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Tue, 4 Aug 2026 21:27:36 -0700 Subject: [PATCH 3/3] refactor(logs): use the existing group-hover variant for the workflow link Drops the group-hover-hover Tailwind variant this branch added and moves the details-panel workflow link to plain group-hover:, matching the variant already used throughout the app. tailwind.config.ts is untouched by the branch again; all colors, radii, and the focus ring come from existing design tokens. --- .../logs/components/log-details/log-details.tsx | 6 +++--- apps/sim/tailwind.config.ts | 4 ---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx index e0cb5aa6267..4990fd6609e 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx @@ -491,10 +491,10 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP className='-mx-1.5 -my-0.5 group flex w-fit min-w-0 max-w-[calc(100%+0.75rem)] items-center gap-1.5 rounded-[5px] px-1.5 py-0.5 transition-colors hover-hover:bg-[var(--surface-active)] focus-visible:bg-[var(--surface-active)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color-mix(in_srgb,var(--text-muted)_30%,transparent)]' > - - + + - + {workflowLabel} (opens in a new tab) diff --git a/apps/sim/tailwind.config.ts b/apps/sim/tailwind.config.ts index 473050430b0..576f1528e06 100644 --- a/apps/sim/tailwind.config.ts +++ b/apps/sim/tailwind.config.ts @@ -300,10 +300,6 @@ export default { require('tailwindcss/plugin')( ({ addVariant }: { addVariant: (name: string, definition: string) => void }) => { addVariant('hover-hover', '@media (hover: hover) and (pointer: fine) { &:hover }') - addVariant( - 'group-hover-hover', - '@media (hover: hover) and (pointer: fine) { :merge(.group):hover & }' - ) } ), ],