From 3d51849018ebc512c38b9efa49b0deaac434ced6 Mon Sep 17 00:00:00 2001 From: Emir Karabeg Date: Wed, 28 Jan 2026 02:01:46 -0800 Subject: [PATCH] improvement(terminal): ui/ux --- .../filter-popover/filter-popover.tsx | 45 +- .../components/terminal/components/index.ts | 1 + .../log-row-context-menu.tsx | 32 +- .../components/output-panel/output-panel.tsx | 101 ++--- .../components/status-display/index.ts | 1 + .../status-display/status-display.tsx | 43 ++ .../terminal/hooks/use-output-panel-resize.ts | 6 +- .../terminal/hooks/use-terminal-filters.ts | 28 +- .../components/terminal/terminal.tsx | 397 +++--------------- .../terminal/{types.tsx => types.ts} | 49 +-- .../[workflowId]/components/terminal/utils.ts | 40 +- apps/sim/stores/constants.ts | 3 + 12 files changed, 160 insertions(+), 586 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/status-display/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/status-display/status-display.tsx rename apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/{types.tsx => types.ts} (55%) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/filter-popover/filter-popover.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/filter-popover/filter-popover.tsx index a0312bf5f..3362e28de 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/filter-popover/filter-popover.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/filter-popover/filter-popover.tsx @@ -17,11 +17,7 @@ import type { BlockInfo, TerminalFilters, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/types' -import { - formatRunId, - getBlockIcon, - getRunIdColor, -} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils' +import { getBlockIcon } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils' /** * Props for the FilterPopover component @@ -32,10 +28,7 @@ export interface FilterPopoverProps { filters: TerminalFilters toggleStatus: (status: 'error' | 'info') => void toggleBlock: (blockId: string) => void - toggleRunId: (runId: string) => void uniqueBlocks: BlockInfo[] - uniqueRunIds: string[] - executionColorMap: Map hasActiveFilters: boolean } @@ -48,10 +41,7 @@ export const FilterPopover = memo(function FilterPopover({ filters, toggleStatus, toggleBlock, - toggleRunId, uniqueBlocks, - uniqueRunIds, - executionColorMap, hasActiveFilters, }: FilterPopoverProps) { return ( @@ -69,7 +59,7 @@ export const FilterPopover = memo(function FilterPopover({ e.stopPropagation()} @@ -103,7 +93,7 @@ export const FilterPopover = memo(function FilterPopover({ {uniqueBlocks.length > 0 && ( <> - + Blocks {uniqueBlocks.map((block) => { @@ -125,35 +115,6 @@ export const FilterPopover = memo(function FilterPopover({ )} - - {uniqueRunIds.length > 0 && ( - <> - - Run ID - - {uniqueRunIds.map((runId) => { - const isSelected = filters.runIds.has(runId) - const runIdColor = getRunIdColor(runId, executionColorMap) - - return ( - toggleRunId(runId)} - > - - {formatRunId(runId)} - - - ) - })} - - - )} ) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/index.ts index 60a203827..b230b8196 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/index.ts @@ -1,4 +1,5 @@ export { FilterPopover, type FilterPopoverProps } from './filter-popover' export { LogRowContextMenu, type LogRowContextMenuProps } from './log-row-context-menu' export { OutputPanel, type OutputPanelProps } from './output-panel' +export { RunningBadge, StatusDisplay, type StatusDisplayProps } from './status-display' export { ToggleButton, type ToggleButtonProps } from './toggle-button' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/log-row-context-menu/log-row-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/log-row-context-menu/log-row-context-menu.tsx index b65b5ab76..be911a3c0 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/log-row-context-menu/log-row-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/log-row-context-menu/log-row-context-menu.tsx @@ -23,12 +23,9 @@ export interface LogRowContextMenuProps { filters: TerminalFilters onFilterByBlock: (blockId: string) => void onFilterByStatus: (status: 'error' | 'info') => void - onFilterByRunId: (runId: string) => void onCopyRunId: (runId: string) => void - onClearFilters: () => void onClearConsole: () => void onFixInCopilot: (entry: ConsoleEntry) => void - hasActiveFilters: boolean } /** @@ -44,19 +41,15 @@ export const LogRowContextMenu = memo(function LogRowContextMenu({ filters, onFilterByBlock, onFilterByStatus, - onFilterByRunId, onCopyRunId, - onClearFilters, onClearConsole, onFixInCopilot, - hasActiveFilters, }: LogRowContextMenuProps) { const hasRunId = entry?.executionId != null const isBlockFiltered = entry ? filters.blockIds.has(entry.blockId) : false const entryStatus = entry?.success ? 'info' : 'error' const isStatusFiltered = entry ? filters.statuses.has(entryStatus) : false - const isRunIdFiltered = entry?.executionId ? filters.runIds.has(entry.executionId) : false return ( Filter by Status - {hasRunId && ( - { - onFilterByRunId(entry.executionId!) - onClose() - }} - > - Filter by Run ID - - )} )} - {/* Clear filters */} - {hasActiveFilters && ( - { - onClearFilters() - onClose() - }} - > - Clear All Filters - - )} - {/* Destructive action */} - {(entry || hasActiveFilters) && } + {entry && } { onClearConsole() diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/output-panel/output-panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/output-panel/output-panel.tsx index 7fbeb7329..a9292f706 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/output-panel/output-panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/output-panel/output-panel.tsx @@ -9,7 +9,6 @@ import { Check, Clipboard, Database, - FilterX, MoreHorizontal, Palette, Pause, @@ -102,7 +101,6 @@ export interface OutputPanelProps { filteredEntries: ConsoleEntry[] handleExportConsole: (e: React.MouseEvent) => void hasActiveFilters: boolean - clearFilters: () => void handleClearConsole: (e: React.MouseEvent) => void shouldShowCodeDisplay: boolean outputDataStringified: string @@ -111,10 +109,7 @@ export interface OutputPanelProps { filters: TerminalFilters toggleBlock: (blockId: string) => void toggleStatus: (status: 'error' | 'info') => void - toggleRunId: (runId: string) => void uniqueBlocks: BlockInfo[] - uniqueRunIds: string[] - executionColorMap: Map } /** @@ -139,7 +134,6 @@ export const OutputPanel = React.memo(function OutputPanel({ filteredEntries, handleExportConsole, hasActiveFilters, - clearFilters, handleClearConsole, shouldShowCodeDisplay, outputDataStringified, @@ -148,10 +142,7 @@ export const OutputPanel = React.memo(function OutputPanel({ filters, toggleBlock, toggleStatus, - toggleRunId, uniqueBlocks, - uniqueRunIds, - executionColorMap, }: OutputPanelProps) { // Access store-backed settings directly to reduce prop drilling const outputPanelWidth = useTerminalStore((state) => state.outputPanelWidth) @@ -224,14 +215,6 @@ export const OutputPanel = React.memo(function OutputPanel({ setOpenOnRun(!openOnRun) }, [openOnRun, setOpenOnRun]) - const handleClearFiltersClick = useCallback( - (e: React.MouseEvent) => { - e.stopPropagation() - clearFilters() - }, - [clearFilters] - ) - const handleCopyClick = useCallback( (e: React.MouseEvent) => { e.stopPropagation() @@ -364,10 +347,7 @@ export const OutputPanel = React.memo(function OutputPanel({ filters={filters} toggleStatus={toggleStatus} toggleBlock={toggleBlock} - toggleRunId={toggleRunId} uniqueBlocks={uniqueBlocks} - uniqueRunIds={uniqueRunIds} - executionColorMap={executionColorMap} hasActiveFilters={hasActiveFilters} /> )} @@ -470,55 +450,38 @@ export const OutputPanel = React.memo(function OutputPanel({ {filteredEntries.length > 0 && ( - - - - - - Download CSV - - - )} - {hasActiveFilters && ( - - - - - - Clear filters - - - )} - {filteredEntries.length > 0 && ( - - - - - - Clear console - - + <> + + + + + + Download CSV + + + + + + + + Clear console + + + )} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/status-display/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/status-display/index.ts new file mode 100644 index 000000000..0bc435a9a --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/status-display/index.ts @@ -0,0 +1 @@ +export { RunningBadge, StatusDisplay, type StatusDisplayProps } from './status-display' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/status-display/status-display.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/status-display/status-display.tsx new file mode 100644 index 000000000..fa54725e2 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/status-display/status-display.tsx @@ -0,0 +1,43 @@ +'use client' + +import { memo } from 'react' +import { Badge } from '@/components/emcn' +import { BADGE_STYLE } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/types' + +/** + * Running badge component - displays a consistent "Running" indicator + */ +export const RunningBadge = memo(function RunningBadge() { + return ( + + Running + + ) +}) + +/** + * Props for StatusDisplay component + */ +export interface StatusDisplayProps { + isRunning: boolean + isCanceled: boolean + formattedDuration: string +} + +/** + * Reusable status display for terminal rows. + * Shows Running badge, 'canceled' text, or formatted duration. + */ +export const StatusDisplay = memo(function StatusDisplay({ + isRunning, + isCanceled, + formattedDuration, +}: StatusDisplayProps) { + if (isRunning) { + return + } + if (isCanceled) { + return <>canceled + } + return <>{formattedDuration} +}) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/hooks/use-output-panel-resize.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/hooks/use-output-panel-resize.ts index d3d9f59e9..2c5fe4323 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/hooks/use-output-panel-resize.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/hooks/use-output-panel-resize.ts @@ -1,9 +1,7 @@ import { useCallback, useEffect, useState } from 'react' -import { OUTPUT_PANEL_WIDTH } from '@/stores/constants' +import { OUTPUT_PANEL_WIDTH, TERMINAL_BLOCK_COLUMN_WIDTH } from '@/stores/constants' import { useTerminalStore } from '@/stores/terminal' -const BLOCK_COLUMN_WIDTH = 240 - export function useOutputPanelResize() { const setOutputPanelWidth = useTerminalStore((state) => state.setOutputPanelWidth) const [isResizing, setIsResizing] = useState(false) @@ -25,7 +23,7 @@ export function useOutputPanelResize() { const newWidth = window.innerWidth - e.clientX - panelWidth const terminalWidth = window.innerWidth - sidebarWidth - panelWidth - const maxWidth = terminalWidth - BLOCK_COLUMN_WIDTH + const maxWidth = terminalWidth - TERMINAL_BLOCK_COLUMN_WIDTH const clampedWidth = Math.max(OUTPUT_PANEL_WIDTH.MIN, Math.min(newWidth, maxWidth)) setOutputPanelWidth(clampedWidth) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/hooks/use-terminal-filters.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/hooks/use-terminal-filters.ts index e5e611927..1807828f4 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/hooks/use-terminal-filters.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/hooks/use-terminal-filters.ts @@ -15,7 +15,6 @@ export function useTerminalFilters() { const [filters, setFilters] = useState({ blockIds: new Set(), statuses: new Set(), - runIds: new Set(), }) const [sortConfig, setSortConfig] = useState({ @@ -53,21 +52,6 @@ export function useTerminalFilters() { }) }, []) - /** - * Toggles a run ID filter - */ - const toggleRunId = useCallback((runId: string) => { - setFilters((prev) => { - const newRunIds = new Set(prev.runIds) - if (newRunIds.has(runId)) { - newRunIds.delete(runId) - } else { - newRunIds.add(runId) - } - return { ...prev, runIds: newRunIds } - }) - }, []) - /** * Toggles sort direction between ascending and descending */ @@ -85,7 +69,6 @@ export function useTerminalFilters() { setFilters({ blockIds: new Set(), statuses: new Set(), - runIds: new Set(), }) }, []) @@ -93,7 +76,7 @@ export function useTerminalFilters() { * Checks if any filters are active */ const hasActiveFilters = useMemo(() => { - return filters.blockIds.size > 0 || filters.statuses.size > 0 || filters.runIds.size > 0 + return filters.blockIds.size > 0 || filters.statuses.size > 0 }, [filters]) /** @@ -118,14 +101,6 @@ export function useTerminalFilters() { if (!hasStatus) return false } - // Run ID filter - if ( - filters.runIds.size > 0 && - (!entry.executionId || !filters.runIds.has(entry.executionId)) - ) { - return false - } - return true }) } @@ -148,7 +123,6 @@ export function useTerminalFilters() { sortConfig, toggleBlock, toggleStatus, - toggleRunId, toggleSort, clearFilters, hasActiveFilters, diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx index 49b78c9fb..5886adcd2 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx @@ -8,7 +8,6 @@ import { ArrowDownToLine, ArrowUp, Database, - FilterX, MoreHorizontal, Palette, Pause, @@ -16,7 +15,6 @@ import { } from 'lucide-react' import Link from 'next/link' import { - Badge, Button, ChevronDown, Popover, @@ -32,6 +30,7 @@ import { FilterPopover, LogRowContextMenu, OutputPanel, + StatusDisplay, ToggleButton, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components' import { @@ -39,23 +38,17 @@ import { useTerminalFilters, useTerminalResize, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/hooks' -import { - BADGE_STYLES, - ROW_STYLES, - StatusDisplay, -} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/types' +import { ROW_STYLES } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/types' import { type EntryNode, type ExecutionGroup, flattenBlockEntriesOnly, formatDuration, - formatRunId, getBlockColor, getBlockIcon, groupEntriesByExecution, isEventFromEditableElement, type NavigableBlockEntry, - RUN_ID_COLORS, TERMINAL_CONFIG, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils' import { useContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks' @@ -165,7 +158,7 @@ const IterationNodeRow = memo(function IterationNodeRow({ const hasCanceledChild = children.some((c) => c.entry.isCanceled) && !hasRunningChild const iterationLabel = iterationInfo - ? `Iteration ${iterationInfo.current}${iterationInfo.total !== undefined ? ` / ${iterationInfo.total}` : ''}` + ? `Iteration ${iterationInfo.current + 1}${iterationInfo.total !== undefined ? ` / ${iterationInfo.total}` : ''}` : entry.blockName return ( @@ -398,124 +391,43 @@ const EntryNodeRow = memo(function EntryNodeRow({ }) /** - * Status badge component for execution rows + * Execution group row component with dashed separator */ -const StatusBadge = memo(function StatusBadge({ - hasError, - isRunning, - isCanceled, -}: { - hasError: boolean - isRunning: boolean - isCanceled: boolean -}) { - if (isRunning) { - return ( - - Running - - ) - } - if (isCanceled) { - return ( - - canceled - - ) - } - return ( - - {hasError ? 'error' : 'info'} - - ) -}) - -/** - * Execution row component with expand/collapse - */ -const ExecutionRow = memo(function ExecutionRow({ +const ExecutionGroupRow = memo(function ExecutionGroupRow({ group, - isExpanded, - onToggle, + showSeparator, selectedEntryId, onSelectEntry, expandedNodes, onToggleNode, }: { group: ExecutionGroup - isExpanded: boolean - onToggle: () => void + showSeparator: boolean selectedEntryId: string | null onSelectEntry: (entry: ConsoleEntry) => void expandedNodes: Set onToggleNode: (nodeId: string) => void }) { - const hasError = group.status === 'error' - const hasRunningEntry = group.entries.some((entry) => entry.isRunning) - const hasCanceledEntry = group.entries.some((entry) => entry.isCanceled) && !hasRunningEntry - return (
- {/* Execution header */} -
-
- - Run #{formatRunId(group.executionId)} - - - -
- - - -
- - {/* Expanded content - Tree structure */} - {isExpanded && ( -
-
- {group.entryTree.map((node) => ( - - ))} -
-
+ {/* Dashed separator between executions */} + {showSeparator && ( +
)} + + {/* Entry tree */} +
+ {group.entryTree.map((node) => ( + + ))} +
) }) @@ -526,7 +438,6 @@ const ExecutionRow = memo(function ExecutionRow({ export const Terminal = memo(function Terminal() { const terminalRef = useRef(null) const logsContainerRef = useRef(null) - const prevEntriesLengthRef = useRef(0) const prevWorkflowEntriesLengthRef = useRef(0) const hasInitializedEntriesRef = useRef(false) const isTerminalFocusedRef = useRef(false) @@ -544,10 +455,6 @@ export const Terminal = memo(function Terminal() { const setOutputPanelWidth = useTerminalStore((state) => state.setOutputPanelWidth) const openOnRun = useTerminalStore((state) => state.openOnRun) const setOpenOnRun = useTerminalStore((state) => state.setOpenOnRun) - const wrapText = useTerminalStore((state) => state.wrapText) - const setWrapText = useTerminalStore((state) => state.setWrapText) - const structuredView = useTerminalStore((state) => state.structuredView) - const setStructuredView = useTerminalStore((state) => state.setStructuredView) const setHasHydrated = useTerminalStore((state) => state.setHasHydrated) const isExpanded = useTerminalStore( (state) => state.terminalHeight > TERMINAL_CONFIG.NEAR_MIN_THRESHOLD @@ -566,7 +473,6 @@ export const Terminal = memo(function Terminal() { const exportConsoleCSV = useTerminalConsoleStore((state) => state.exportConsoleCSV) const [selectedEntry, setSelectedEntry] = useState(null) - const [expandedExecutions, setExpandedExecutions] = useState>(new Set()) const [expandedNodes, setExpandedNodes] = useState>(new Set()) const [isToggling, setIsToggling] = useState(false) const [showCopySuccess, setShowCopySuccess] = useState(false) @@ -589,7 +495,6 @@ export const Terminal = memo(function Terminal() { sortConfig, toggleBlock, toggleStatus, - toggleRunId, toggleSort, clearFilters, filterEntries, @@ -635,7 +540,7 @@ export const Terminal = memo(function Terminal() { /** * Navigable block entries for keyboard navigation. - * Only includes actual block outputs (not subflows/iterations/headers). + * Only includes actual block outputs (excludes subflow/iteration container nodes). * Includes parent node IDs for auto-expanding when navigating. */ const navigableEntries = useMemo(() => { @@ -663,65 +568,6 @@ export const Terminal = memo(function Terminal() { return Array.from(blocksMap.values()).sort((a, b) => a.blockName.localeCompare(b.blockName)) }, [allWorkflowEntries]) - /** - * Get unique run IDs from all workflow entries - */ - const uniqueRunIds = useMemo(() => { - const runIdsSet = new Set() - allWorkflowEntries.forEach((entry) => { - if (entry.executionId) { - runIdsSet.add(entry.executionId) - } - }) - return Array.from(runIdsSet).sort() - }, [allWorkflowEntries]) - - /** - * Track color offset for run IDs - */ - const colorStateRef = useRef<{ executionIds: string[]; offset: number }>({ - executionIds: [], - offset: 0, - }) - - /** - * Compute colors for each execution ID - */ - const executionColorMap = useMemo(() => { - const currentIds: string[] = [] - const seen = new Set() - for (let i = allWorkflowEntries.length - 1; i >= 0; i--) { - const execId = allWorkflowEntries[i].executionId - if (execId && !seen.has(execId)) { - currentIds.push(execId) - seen.add(execId) - } - } - - const { executionIds: prevIds, offset: prevOffset } = colorStateRef.current - let newOffset = prevOffset - - if (prevIds.length > 0 && currentIds.length > 0) { - const currentOldest = currentIds[0] - if (prevIds[0] !== currentOldest) { - const trimmedCount = prevIds.indexOf(currentOldest) - if (trimmedCount > 0) { - newOffset = (prevOffset + trimmedCount) % RUN_ID_COLORS.length - } - } - } - - const colorMap = new Map() - for (let i = 0; i < currentIds.length; i++) { - const colorIndex = (newOffset + i) % RUN_ID_COLORS.length - colorMap.set(currentIds[i], RUN_ID_COLORS[colorIndex]) - } - - colorStateRef.current = { executionIds: currentIds, offset: newOffset } - - return colorMap - }, [allWorkflowEntries]) - /** * Check if input data exists for selected entry */ @@ -815,7 +661,7 @@ export const Terminal = memo(function Terminal() { ]) /** - * Auto-expand newest execution, subflows, and iterations when new entries arrive. + * Auto-expand subflows and iterations when new entries arrive. * This always runs regardless of autoSelectEnabled - new runs should always be visible. */ useEffect(() => { @@ -823,14 +669,6 @@ export const Terminal = memo(function Terminal() { const newestExec = executionGroups[0] - // Always expand the newest execution group - setExpandedExecutions((prev) => { - if (prev.has(newestExec.executionId)) return prev - const next = new Set(prev) - next.add(newestExec.executionId) - return next - }) - // Collect all node IDs that should be expanded (subflows and their iterations) const nodeIdsToExpand: string[] = [] for (const node of newestExec.entryTree) { @@ -865,35 +703,20 @@ export const Terminal = memo(function Terminal() { }, []) /** - * Handle entry selection + * Handle entry selection - clicking same entry toggles selection off */ const handleSelectEntry = useCallback( (entry: ConsoleEntry) => { focusTerminal() setSelectedEntry((prev) => { - const isDeselecting = prev?.id === entry.id - setAutoSelectEnabled(isDeselecting) - return isDeselecting ? null : entry + // Disable auto-select on any manual selection/deselection + setAutoSelectEnabled(false) + return prev?.id === entry.id ? null : entry }) }, [focusTerminal] ) - /** - * Toggle execution expansion - */ - const handleToggleExecution = useCallback((executionId: string) => { - setExpandedExecutions((prev) => { - const next = new Set(prev) - if (next.has(executionId)) { - next.delete(executionId) - } else { - next.add(executionId) - } - return next - }) - }, []) - /** * Toggle subflow node expansion */ @@ -943,7 +766,6 @@ export const Terminal = memo(function Terminal() { if (activeWorkflowId) { clearWorkflowConsole(activeWorkflowId) setSelectedEntry(null) - setExpandedExecutions(new Set()) setExpandedNodes(new Set()) } }, [activeWorkflowId, clearWorkflowConsole]) @@ -982,14 +804,6 @@ export const Terminal = memo(function Terminal() { [toggleStatus, closeLogRowMenu] ) - const handleFilterByRunId = useCallback( - (runId: string) => { - toggleRunId(runId) - closeLogRowMenu() - }, - [toggleRunId, closeLogRowMenu] - ) - const handleCopyRunId = useCallback( (runId: string) => { navigator.clipboard.writeText(runId) @@ -1084,85 +898,57 @@ export const Terminal = memo(function Terminal() { } }, [showCopySuccess]) - /** - * Scroll the logs container to the bottom. - */ - const scrollToBottom = useCallback(() => { - requestAnimationFrame(() => { - const container = logsContainerRef.current - if (!container) return - container.scrollTop = container.scrollHeight - }) - }, []) - - /** - * Scroll an entry into view (for keyboard navigation). - */ const scrollEntryIntoView = useCallback((entryId: string) => { - requestAnimationFrame(() => { - const container = logsContainerRef.current - if (!container) return - const el = container.querySelector(`[data-entry-id="${entryId}"]`) - if (el) { - el.scrollIntoView({ block: 'nearest', behavior: 'smooth' }) - } - }) + const container = logsContainerRef.current + if (!container) return + const el = container.querySelector(`[data-entry-id="${entryId}"]`) + if (el) { + el.scrollIntoView({ block: 'nearest', behavior: 'smooth' }) + } }, []) - /** - * Auto-select the last entry (bottom of the list) when new logs arrive. - */ useEffect(() => { if (executionGroups.length === 0 || navigableEntries.length === 0) { setAutoSelectEnabled(true) setSelectedEntry(null) - prevEntriesLengthRef.current = 0 return } - if (autoSelectEnabled && navigableEntries.length > prevEntriesLengthRef.current) { - // Get the last entry from the newest execution (it's at the bottom of the list) - const newestExecutionId = executionGroups[0].executionId - let lastNavEntry: NavigableBlockEntry | null = null + if (!autoSelectEnabled) return - for (const navEntry of navigableEntries) { - if (navEntry.executionId === newestExecutionId) { - lastNavEntry = navEntry - } else { - break - } + const newestExecutionId = executionGroups[0].executionId + let lastNavEntry: NavigableBlockEntry | null = null + + for (const navEntry of navigableEntries) { + if (navEntry.executionId === newestExecutionId) { + lastNavEntry = navEntry + } else { + break } - - if (!lastNavEntry) { - prevEntriesLengthRef.current = navigableEntries.length - return - } - - setSelectedEntry(lastNavEntry.entry) - focusTerminal() - - // Expand execution and parent nodes - setExpandedExecutions((prev) => { - if (prev.has(lastNavEntry.executionId)) return prev - const next = new Set(prev) - next.add(lastNavEntry.executionId) - return next - }) - if (lastNavEntry.parentNodeIds.length > 0) { - setExpandedNodes((prev) => { - const hasAll = lastNavEntry.parentNodeIds.every((id) => prev.has(id)) - if (hasAll) return prev - const next = new Set(prev) - lastNavEntry.parentNodeIds.forEach((id) => next.add(id)) - return next - }) - } - - scrollToBottom() } - prevEntriesLengthRef.current = navigableEntries.length - }, [executionGroups, navigableEntries, autoSelectEnabled, focusTerminal, scrollToBottom]) + if (!lastNavEntry) return + if (selectedEntry?.id === lastNavEntry.entry.id) return + + setSelectedEntry(lastNavEntry.entry) + focusTerminal() + + if (lastNavEntry.parentNodeIds.length > 0) { + setExpandedNodes((prev) => { + const hasAll = lastNavEntry.parentNodeIds.every((id) => prev.has(id)) + if (hasAll) return prev + const next = new Set(prev) + lastNavEntry.parentNodeIds.forEach((id) => next.add(id)) + return next + }) + } + }, [executionGroups, navigableEntries, autoSelectEnabled, selectedEntry?.id, focusTerminal]) + + useEffect(() => { + if (selectedEntry) { + scrollEntryIntoView(selectedEntry.id) + } + }, [selectedEntry?.id, scrollEntryIntoView]) /** * Sync selected entry with latest data from store. @@ -1204,14 +990,6 @@ export const Terminal = memo(function Terminal() { setAutoSelectEnabled(false) setSelectedEntry(navEntry.entry) - // Auto-expand the execution group - setExpandedExecutions((prev) => { - if (prev.has(navEntry.executionId)) return prev - const next = new Set(prev) - next.add(navEntry.executionId) - return next - }) - // Auto-expand parent nodes (subflows, iterations) if (navEntry.parentNodeIds.length > 0) { setExpandedNodes((prev) => { @@ -1401,10 +1179,7 @@ export const Terminal = memo(function Terminal() { filters={filters} toggleStatus={toggleStatus} toggleBlock={toggleBlock} - toggleRunId={toggleRunId} uniqueBlocks={uniqueBlocks} - uniqueRunIds={uniqueRunIds} - executionColorMap={executionColorMap} hasActiveFilters={hasActiveFilters} /> )} @@ -1479,27 +1254,6 @@ export const Terminal = memo(function Terminal() { )} - {hasActiveFilters && ( - - - - - - Clear filters - - - )} - {filteredEntries.length > 0 && ( <> @@ -1588,12 +1342,11 @@ export const Terminal = memo(function Terminal() { No logs yet
) : ( - executionGroups.map((group) => ( - ( + handleToggleExecution(group.executionId)} + showSeparator={index > 0} selectedEntryId={selectedEntry?.id || null} onSelectEntry={handleSelectEntry} expandedNodes={expandedNodes} @@ -1624,7 +1377,6 @@ export const Terminal = memo(function Terminal() { filteredEntries={filteredEntries} handleExportConsole={handleExportConsole} hasActiveFilters={hasActiveFilters} - clearFilters={clearFilters} handleClearConsole={handleClearConsole} shouldShowCodeDisplay={shouldShowCodeDisplay} outputDataStringified={outputDataStringified} @@ -1633,10 +1385,7 @@ export const Terminal = memo(function Terminal() { filters={filters} toggleBlock={toggleBlock} toggleStatus={toggleStatus} - toggleRunId={toggleRunId} uniqueBlocks={uniqueBlocks} - uniqueRunIds={uniqueRunIds} - executionColorMap={executionColorMap} /> )} @@ -1652,15 +1401,9 @@ export const Terminal = memo(function Terminal() { filters={filters} onFilterByBlock={handleFilterByBlock} onFilterByStatus={handleFilterByStatus} - onFilterByRunId={handleFilterByRunId} onCopyRunId={handleCopyRunId} - onClearFilters={() => { - clearFilters() - closeLogRowMenu() - }} onClearConsole={handleClearConsoleFromMenu} onFixInCopilot={handleFixInCopilot} - hasActiveFilters={hasActiveFilters} /> ) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/types.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/types.ts similarity index 55% rename from apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/types.tsx rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/types.ts index 35c8607a4..a9029f814 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/types.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/types.ts @@ -1,15 +1,9 @@ -'use client' - -import { memo } from 'react' -import { Badge } from '@/components/emcn' - /** * Terminal filter configuration state */ export interface TerminalFilters { blockIds: Set statuses: Set<'error' | 'info'> - runIds: Set } /** @@ -67,45 +61,4 @@ export const ROW_STYLES = { /** * Common badge styling for status badges */ -export const BADGE_STYLES = { - base: 'rounded-[4px] px-[4px] py-[0px] text-[11px]', - mono: 'rounded-[4px] px-[4px] py-[0px] font-mono text-[11px]', -} as const - -/** - * Running badge component - displays a consistent "Running" indicator - */ -export const RunningBadge = memo(function RunningBadge() { - return ( - - Running - - ) -}) - -/** - * Props for StatusDisplay component - */ -export interface StatusDisplayProps { - isRunning: boolean - isCanceled: boolean - formattedDuration: string -} - -/** - * Reusable status display for terminal rows. - * Shows Running badge, 'canceled' text, or formatted duration. - */ -export const StatusDisplay = memo(function StatusDisplay({ - isRunning, - isCanceled, - formattedDuration, -}: StatusDisplayProps) { - if (isRunning) { - return - } - if (isCanceled) { - return <>canceled - } - return <>{formattedDuration} -}) +export const BADGE_STYLE = 'rounded-[4px] px-[4px] py-[0px] text-[11px]' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils.ts index c0a9dfca2..39d3770b3 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils.ts @@ -1,8 +1,7 @@ -'use client' - import type React from 'react' import { RepeatIcon, SplitIcon } from 'lucide-react' import { getBlock } from '@/blocks' +import { TERMINAL_BLOCK_COLUMN_WIDTH } from '@/stores/constants' import type { ConsoleEntry } from '@/stores/terminal' /** @@ -13,20 +12,6 @@ const SUBFLOW_COLORS = { parallel: '#FEE12B', } as const -/** - * Run ID color palette for visual distinction between executions - */ -export const RUN_ID_COLORS = [ - '#4ADE80', // Green - '#F472B6', // Pink - '#60C5FF', // Blue - '#FF8533', // Orange - '#C084FC', // Purple - '#EAB308', // Yellow - '#2DD4BF', // Teal - '#FB7185', // Rose -] as const - /** * Retrieves the icon component for a given block type */ @@ -77,25 +62,6 @@ export function formatDuration(ms?: number): string { return `${(ms / 1000).toFixed(2)}s` } -/** - * Truncates execution ID for display as run ID - */ -export function formatRunId(executionId?: string): string { - if (!executionId) return '-' - return executionId.slice(0, 8) -} - -/** - * Gets color for a run ID from the precomputed color map - */ -export function getRunIdColor( - executionId: string | undefined, - colorMap: Map -): string | null { - if (!executionId) return null - return colorMap.get(executionId) ?? null -} - /** * Determines if a keyboard event originated from a text-editable element */ @@ -476,13 +442,11 @@ export function flattenBlockEntriesOnly( return result } -// BlockInfo is now in types.ts for shared use across terminal components - /** * Terminal height configuration constants */ export const TERMINAL_CONFIG = { NEAR_MIN_THRESHOLD: 40, - BLOCK_COLUMN_WIDTH_PX: 240, + BLOCK_COLUMN_WIDTH_PX: TERMINAL_BLOCK_COLUMN_WIDTH, HEADER_TEXT_CLASS: 'font-medium text-[var(--text-tertiary)] text-[12px]', } as const diff --git a/apps/sim/stores/constants.ts b/apps/sim/stores/constants.ts index d2eb2bfd3..86b4fc36c 100644 --- a/apps/sim/stores/constants.ts +++ b/apps/sim/stores/constants.ts @@ -61,3 +61,6 @@ export const OUTPUT_PANEL_WIDTH = { DEFAULT: 440, MIN: 440, } as const + +/** Terminal block column width - minimum width for the logs column */ +export const TERMINAL_BLOCK_COLUMN_WIDTH = 240 as const