mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-28 16:27:55 -05:00
improvement(terminal): ui/ux
This commit is contained in:
@@ -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<string, string>
|
||||
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({
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
side='bottom'
|
||||
side='top'
|
||||
align='end'
|
||||
sideOffset={4}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
@@ -103,7 +93,7 @@ export const FilterPopover = memo(function FilterPopover({
|
||||
|
||||
{uniqueBlocks.length > 0 && (
|
||||
<>
|
||||
<PopoverDivider />
|
||||
<PopoverDivider className='my-[4px]' />
|
||||
<PopoverSection className='!mt-0'>Blocks</PopoverSection>
|
||||
<PopoverScrollArea className='max-h-[100px]'>
|
||||
{uniqueBlocks.map((block) => {
|
||||
@@ -125,35 +115,6 @@ export const FilterPopover = memo(function FilterPopover({
|
||||
</PopoverScrollArea>
|
||||
</>
|
||||
)}
|
||||
|
||||
{uniqueRunIds.length > 0 && (
|
||||
<>
|
||||
<PopoverDivider />
|
||||
<PopoverSection className='!mt-0'>Run ID</PopoverSection>
|
||||
<PopoverScrollArea className='max-h-[100px]'>
|
||||
{uniqueRunIds.map((runId) => {
|
||||
const isSelected = filters.runIds.has(runId)
|
||||
const runIdColor = getRunIdColor(runId, executionColorMap)
|
||||
|
||||
return (
|
||||
<PopoverItem
|
||||
key={runId}
|
||||
active={isSelected}
|
||||
showCheck={isSelected}
|
||||
onClick={() => toggleRunId(runId)}
|
||||
>
|
||||
<span
|
||||
className='flex-1 font-mono text-[11px]'
|
||||
style={{ color: runIdColor || '#D2D2D2' }}
|
||||
>
|
||||
{formatRunId(runId)}
|
||||
</span>
|
||||
</PopoverItem>
|
||||
)
|
||||
})}
|
||||
</PopoverScrollArea>
|
||||
</>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 (
|
||||
<Popover
|
||||
@@ -127,34 +120,11 @@ export const LogRowContextMenu = memo(function LogRowContextMenu({
|
||||
>
|
||||
Filter by Status
|
||||
</PopoverItem>
|
||||
{hasRunId && (
|
||||
<PopoverItem
|
||||
showCheck={isRunIdFiltered}
|
||||
onClick={() => {
|
||||
onFilterByRunId(entry.executionId!)
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
Filter by Run ID
|
||||
</PopoverItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Clear filters */}
|
||||
{hasActiveFilters && (
|
||||
<PopoverItem
|
||||
onClick={() => {
|
||||
onClearFilters()
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
Clear All Filters
|
||||
</PopoverItem>
|
||||
)}
|
||||
|
||||
{/* Destructive action */}
|
||||
{(entry || hasActiveFilters) && <PopoverDivider />}
|
||||
{entry && <PopoverDivider />}
|
||||
<PopoverItem
|
||||
onClick={() => {
|
||||
onClearConsole()
|
||||
|
||||
@@ -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<string, string>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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({
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{filteredEntries.length > 0 && (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={handleExportConsole}
|
||||
aria-label='Download console CSV'
|
||||
className='!p-1.5 -m-1.5'
|
||||
>
|
||||
<ArrowDownToLine className='h-3 w-3' />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<span>Download CSV</span>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)}
|
||||
{hasActiveFilters && (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={handleClearFiltersClick}
|
||||
aria-label='Clear filters'
|
||||
className='!p-1.5 -m-1.5'
|
||||
>
|
||||
<FilterX className='h-3 w-3' />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<span>Clear filters</span>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)}
|
||||
{filteredEntries.length > 0 && (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={handleClearConsole}
|
||||
aria-label='Clear console'
|
||||
className='!p-1.5 -m-1.5'
|
||||
>
|
||||
<Trash2 className='h-3 w-3' />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<Tooltip.Shortcut keys='⌘D'>Clear console</Tooltip.Shortcut>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
<>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={handleExportConsole}
|
||||
aria-label='Download console CSV'
|
||||
className='!p-1.5 -m-1.5'
|
||||
>
|
||||
<ArrowDownToLine className='h-3 w-3' />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<span>Download CSV</span>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={handleClearConsole}
|
||||
aria-label='Clear console'
|
||||
className='!p-1.5 -m-1.5'
|
||||
>
|
||||
<Trash2 className='h-3 w-3' />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<Tooltip.Shortcut keys='⌘D'>Clear console</Tooltip.Shortcut>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</>
|
||||
)}
|
||||
<Popover open={outputOptionsOpen} onOpenChange={setOutputOptionsOpen} size='sm'>
|
||||
<PopoverTrigger asChild>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { RunningBadge, StatusDisplay, type StatusDisplayProps } from './status-display'
|
||||
@@ -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 (
|
||||
<Badge variant='green' className={BADGE_STYLE}>
|
||||
Running
|
||||
</Badge>
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* 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 <RunningBadge />
|
||||
}
|
||||
if (isCanceled) {
|
||||
return <>canceled</>
|
||||
}
|
||||
return <>{formattedDuration}</>
|
||||
})
|
||||
@@ -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)
|
||||
|
||||
@@ -15,7 +15,6 @@ export function useTerminalFilters() {
|
||||
const [filters, setFilters] = useState<TerminalFilters>({
|
||||
blockIds: new Set(),
|
||||
statuses: new Set(),
|
||||
runIds: new Set(),
|
||||
})
|
||||
|
||||
const [sortConfig, setSortConfig] = useState<SortConfig>({
|
||||
@@ -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,
|
||||
|
||||
@@ -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 (
|
||||
<Badge variant='green' className={BADGE_STYLES.base}>
|
||||
Running
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
if (isCanceled) {
|
||||
return (
|
||||
<Badge variant='gray' className={BADGE_STYLES.mono}>
|
||||
canceled
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Badge variant={hasError ? 'red' : 'gray'} className={BADGE_STYLES.mono}>
|
||||
{hasError ? 'error' : 'info'}
|
||||
</Badge>
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* 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<string>
|
||||
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 (
|
||||
<div className='flex flex-col px-[6px]'>
|
||||
{/* Execution header */}
|
||||
<div
|
||||
className={clsx(ROW_STYLES.base, 'ml-[4px] h-[24px]', ROW_STYLES.hover)}
|
||||
onClick={onToggle}
|
||||
>
|
||||
<div className='flex min-w-0 flex-1 items-center gap-[8px]'>
|
||||
<span
|
||||
className={clsx(
|
||||
'font-medium text-[13px]',
|
||||
isExpanded
|
||||
? 'text-[var(--text-primary)]'
|
||||
: 'text-[var(--text-tertiary)] group-hover:text-[var(--text-primary)]'
|
||||
)}
|
||||
>
|
||||
Run #{formatRunId(group.executionId)}
|
||||
</span>
|
||||
<StatusBadge
|
||||
hasError={hasError}
|
||||
isRunning={hasRunningEntry}
|
||||
isCanceled={hasCanceledEntry}
|
||||
/>
|
||||
<ChevronDown
|
||||
className={clsx(
|
||||
'h-[8px] w-[8px] flex-shrink-0 text-[var(--text-tertiary)] transition-transform duration-100 group-hover:text-[var(--text-primary)]',
|
||||
!isExpanded && '-rotate-90'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
className={clsx(
|
||||
'flex-shrink-0 font-medium text-[13px]',
|
||||
!hasRunningEntry &&
|
||||
(hasCanceledEntry ? 'text-[var(--text-secondary)]' : 'text-[var(--text-tertiary)]')
|
||||
)}
|
||||
>
|
||||
<StatusDisplay
|
||||
isRunning={hasRunningEntry}
|
||||
isCanceled={hasCanceledEntry}
|
||||
formattedDuration={formatDuration(group.duration)}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Expanded content - Tree structure */}
|
||||
{isExpanded && (
|
||||
<div className='flex flex-col pb-[4px] pl-[10px]'>
|
||||
<div className={ROW_STYLES.nested}>
|
||||
{group.entryTree.map((node) => (
|
||||
<EntryNodeRow
|
||||
key={node.entry.id}
|
||||
node={node}
|
||||
selectedEntryId={selectedEntryId}
|
||||
onSelectEntry={onSelectEntry}
|
||||
expandedNodes={expandedNodes}
|
||||
onToggleNode={onToggleNode}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/* Dashed separator between executions */}
|
||||
{showSeparator && (
|
||||
<div className='mx-[4px] my-[4px] border-[var(--border)] border-t border-dashed' />
|
||||
)}
|
||||
|
||||
{/* Entry tree */}
|
||||
<div className='ml-[4px] flex flex-col gap-[2px] pb-[4px]'>
|
||||
{group.entryTree.map((node) => (
|
||||
<EntryNodeRow
|
||||
key={node.entry.id}
|
||||
node={node}
|
||||
selectedEntryId={selectedEntryId}
|
||||
onSelectEntry={onSelectEntry}
|
||||
expandedNodes={expandedNodes}
|
||||
onToggleNode={onToggleNode}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
@@ -526,7 +438,6 @@ const ExecutionRow = memo(function ExecutionRow({
|
||||
export const Terminal = memo(function Terminal() {
|
||||
const terminalRef = useRef<HTMLElement>(null)
|
||||
const logsContainerRef = useRef<HTMLDivElement>(null)
|
||||
const prevEntriesLengthRef = useRef(0)
|
||||
const prevWorkflowEntriesLengthRef = useRef(0)
|
||||
const isTerminalFocusedRef = useRef(false)
|
||||
const lastExpandedHeightRef = useRef<number>(DEFAULT_EXPANDED_HEIGHT)
|
||||
@@ -543,10 +454,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
|
||||
@@ -565,7 +472,6 @@ export const Terminal = memo(function Terminal() {
|
||||
const exportConsoleCSV = useTerminalConsoleStore((state) => state.exportConsoleCSV)
|
||||
|
||||
const [selectedEntry, setSelectedEntry] = useState<ConsoleEntry | null>(null)
|
||||
const [expandedExecutions, setExpandedExecutions] = useState<Set<string>>(new Set())
|
||||
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set())
|
||||
const [isToggling, setIsToggling] = useState(false)
|
||||
const [showCopySuccess, setShowCopySuccess] = useState(false)
|
||||
@@ -588,7 +494,6 @@ export const Terminal = memo(function Terminal() {
|
||||
sortConfig,
|
||||
toggleBlock,
|
||||
toggleStatus,
|
||||
toggleRunId,
|
||||
toggleSort,
|
||||
clearFilters,
|
||||
filterEntries,
|
||||
@@ -634,7 +539,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(() => {
|
||||
@@ -662,65 +567,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<string>()
|
||||
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<string>()
|
||||
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<string, string>()
|
||||
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
|
||||
*/
|
||||
@@ -784,7 +630,7 @@ export const Terminal = memo(function Terminal() {
|
||||
}, [allWorkflowEntries.length, expandToLastHeight, openOnRun, isExpanded])
|
||||
|
||||
/**
|
||||
* 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(() => {
|
||||
@@ -792,14 +638,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) {
|
||||
@@ -834,35 +672,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
|
||||
*/
|
||||
@@ -912,7 +735,6 @@ export const Terminal = memo(function Terminal() {
|
||||
if (activeWorkflowId) {
|
||||
clearWorkflowConsole(activeWorkflowId)
|
||||
setSelectedEntry(null)
|
||||
setExpandedExecutions(new Set())
|
||||
setExpandedNodes(new Set())
|
||||
}
|
||||
}, [activeWorkflowId, clearWorkflowConsole])
|
||||
@@ -951,14 +773,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)
|
||||
@@ -1053,85 +867,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.
|
||||
@@ -1173,14 +959,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) => {
|
||||
@@ -1370,10 +1148,7 @@ export const Terminal = memo(function Terminal() {
|
||||
filters={filters}
|
||||
toggleStatus={toggleStatus}
|
||||
toggleBlock={toggleBlock}
|
||||
toggleRunId={toggleRunId}
|
||||
uniqueBlocks={uniqueBlocks}
|
||||
uniqueRunIds={uniqueRunIds}
|
||||
executionColorMap={executionColorMap}
|
||||
hasActiveFilters={hasActiveFilters}
|
||||
/>
|
||||
)}
|
||||
@@ -1448,27 +1223,6 @@ export const Terminal = memo(function Terminal() {
|
||||
</Tooltip.Root>
|
||||
)}
|
||||
|
||||
{hasActiveFilters && (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
clearFilters()
|
||||
}}
|
||||
aria-label='Clear filters'
|
||||
className='!p-1.5 -m-1.5'
|
||||
>
|
||||
<FilterX className='h-3 w-3' />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<span>Clear filters</span>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)}
|
||||
|
||||
{filteredEntries.length > 0 && (
|
||||
<>
|
||||
<Tooltip.Root>
|
||||
@@ -1557,12 +1311,11 @@ export const Terminal = memo(function Terminal() {
|
||||
No logs yet
|
||||
</div>
|
||||
) : (
|
||||
executionGroups.map((group) => (
|
||||
<ExecutionRow
|
||||
executionGroups.map((group, index) => (
|
||||
<ExecutionGroupRow
|
||||
key={group.executionId}
|
||||
group={group}
|
||||
isExpanded={expandedExecutions.has(group.executionId)}
|
||||
onToggle={() => handleToggleExecution(group.executionId)}
|
||||
showSeparator={index > 0}
|
||||
selectedEntryId={selectedEntry?.id || null}
|
||||
onSelectEntry={handleSelectEntry}
|
||||
expandedNodes={expandedNodes}
|
||||
@@ -1593,7 +1346,6 @@ export const Terminal = memo(function Terminal() {
|
||||
filteredEntries={filteredEntries}
|
||||
handleExportConsole={handleExportConsole}
|
||||
hasActiveFilters={hasActiveFilters}
|
||||
clearFilters={clearFilters}
|
||||
handleClearConsole={handleClearConsole}
|
||||
shouldShowCodeDisplay={shouldShowCodeDisplay}
|
||||
outputDataStringified={outputDataStringified}
|
||||
@@ -1602,10 +1354,7 @@ export const Terminal = memo(function Terminal() {
|
||||
filters={filters}
|
||||
toggleBlock={toggleBlock}
|
||||
toggleStatus={toggleStatus}
|
||||
toggleRunId={toggleRunId}
|
||||
uniqueBlocks={uniqueBlocks}
|
||||
uniqueRunIds={uniqueRunIds}
|
||||
executionColorMap={executionColorMap}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -1621,15 +1370,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}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import { memo } from 'react'
|
||||
import { Badge } from '@/components/emcn'
|
||||
|
||||
/**
|
||||
* Terminal filter configuration state
|
||||
*/
|
||||
export interface TerminalFilters {
|
||||
blockIds: Set<string>
|
||||
statuses: Set<'error' | 'info'>
|
||||
runIds: Set<string>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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 (
|
||||
<Badge variant='green' className={BADGE_STYLES.base}>
|
||||
Running
|
||||
</Badge>
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* 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 <RunningBadge />
|
||||
}
|
||||
if (isCanceled) {
|
||||
return <>canceled</>
|
||||
}
|
||||
return <>{formattedDuration}</>
|
||||
})
|
||||
export const BADGE_STYLE = 'rounded-[4px] px-[4px] py-[0px] text-[11px]'
|
||||
@@ -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, string>
|
||||
): 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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user