fix(terminal-colors): change algo to compute colors based on hash of execution id and pointer from bottom (#2817)

This commit is contained in:
Waleed
2026-01-14 12:06:02 -08:00
committed by GitHub
parent d7e0d9ba43
commit 468ec2ea81
2 changed files with 79 additions and 54 deletions

View File

@@ -36,6 +36,7 @@ import {
Tooltip,
} from '@/components/emcn'
import { getEnv, isTruthy } from '@/lib/core/config/env'
import { formatTimeWithSeconds } from '@/lib/core/utils/formatting'
import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
import { createCommands } from '@/app/workspace/[workspaceId]/utils/commands-utils'
import {
@@ -82,18 +83,6 @@ const COLUMN_WIDTHS = {
OUTPUT_PANEL: 'w-[400px]',
} as const
/**
* Color palette for run IDs - matching code syntax highlighting colors
*/
const RUN_ID_COLORS = [
{ text: '#4ADE80' }, // Green
{ text: '#F472B6' }, // Pink
{ text: '#60C5FF' }, // Blue
{ text: '#FF8533' }, // Orange
{ text: '#C084FC' }, // Purple
{ text: '#FCD34D' }, // Yellow
] as const
/**
* Shared styling constants
*/
@@ -183,22 +172,6 @@ const ToggleButton = ({
</Button>
)
/**
* Formats timestamp to H:MM:SS AM/PM TZ format
*/
const formatTimestamp = (timestamp: string): string => {
const date = new Date(timestamp)
const fullString = date.toLocaleTimeString('en-US', {
hour: 'numeric',
minute: '2-digit',
second: '2-digit',
hour12: true,
timeZoneName: 'short',
})
// Format: "5:54:55 PM PST" - return as is
return fullString
}
/**
* Truncates execution ID for display as run ID
*/
@@ -208,16 +181,25 @@ const formatRunId = (executionId?: string): string => {
}
/**
* Gets color for a run ID based on its index in the execution ID order map
* Run ID colors
*/
const getRunIdColor = (
executionId: string | undefined,
executionIdOrderMap: Map<string, number>
) => {
const RUN_ID_COLORS = [
'#4ADE80', // Green
'#F472B6', // Pink
'#60C5FF', // Blue
'#FF8533', // Orange
'#C084FC', // Purple
'#EAB308', // Yellow
'#2DD4BF', // Teal
'#FB7185', // Rose
] as const
/**
* Gets color for a run ID from the precomputed color map.
*/
const getRunIdColor = (executionId: string | undefined, colorMap: Map<string, string>) => {
if (!executionId) return null
const colorIndex = executionIdOrderMap.get(executionId)
if (colorIndex === undefined) return null
return RUN_ID_COLORS[colorIndex % RUN_ID_COLORS.length]
return colorMap.get(executionId) ?? null
}
/**
@@ -464,25 +446,52 @@ export function Terminal() {
}, [allWorkflowEntries])
/**
* Create stable execution ID to color index mapping based on order of first appearance.
* Once an execution ID is assigned a color index, it keeps that index.
* Uses all workflow entries to maintain consistent colors regardless of active filters.
* Track color offset - increments when old executions are trimmed
* so remaining executions keep their colors.
*/
const executionIdOrderMap = useMemo(() => {
const orderMap = new Map<string, number>()
let colorIndex = 0
const colorStateRef = useRef<{ executionIds: string[]; offset: number }>({
executionIds: [],
offset: 0,
})
// Process entries in reverse order (oldest first) since entries array is newest-first
// Use allWorkflowEntries to ensure colors remain consistent when filters change
/**
* Compute colors for each execution ID using sequential assignment.
* Colors cycle through RUN_ID_COLORS based on position + offset.
* When old executions are trimmed, offset increments to preserve colors.
*/
const executionColorMap = useMemo(() => {
const currentIds: string[] = []
const seen = new Set<string>()
for (let i = allWorkflowEntries.length - 1; i >= 0; i--) {
const entry = allWorkflowEntries[i]
if (entry.executionId && !orderMap.has(entry.executionId)) {
orderMap.set(entry.executionId, colorIndex)
colorIndex++
const execId = allWorkflowEntries[i].executionId
if (execId && !seen.has(execId)) {
currentIds.push(execId)
seen.add(execId)
}
}
return orderMap
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])
/**
@@ -1128,7 +1137,7 @@ export function Terminal() {
<PopoverScrollArea style={{ maxHeight: '140px' }}>
{uniqueRunIds.map((runId, index) => {
const isSelected = filters.runIds.has(runId)
const runIdColor = getRunIdColor(runId, executionIdOrderMap)
const runIdColor = getRunIdColor(runId, executionColorMap)
return (
<PopoverItem
@@ -1139,7 +1148,7 @@ export function Terminal() {
>
<span
className='flex-1 font-mono text-[12px]'
style={{ color: runIdColor?.text || '#D2D2D2' }}
style={{ color: runIdColor || '#D2D2D2' }}
>
{formatRunId(runId)}
</span>
@@ -1335,7 +1344,7 @@ export function Terminal() {
const statusInfo = getStatusInfo(entry.success, entry.error)
const isSelected = selectedEntry?.id === entry.id
const BlockIcon = getBlockIcon(entry.blockType)
const runIdColor = getRunIdColor(entry.executionId, executionIdOrderMap)
const runIdColor = getRunIdColor(entry.executionId, executionColorMap)
return (
<div
@@ -1385,7 +1394,7 @@ export function Terminal() {
COLUMN_BASE_CLASS,
'truncate font-medium font-mono text-[12px]'
)}
style={{ color: runIdColor?.text || '#D2D2D2' }}
style={{ color: runIdColor || '#D2D2D2' }}
>
{formatRunId(entry.executionId)}
</span>
@@ -1411,7 +1420,7 @@ export function Terminal() {
ROW_TEXT_CLASS
)}
>
{formatTimestamp(entry.timestamp)}
{formatTimeWithSeconds(new Date(entry.timestamp))}
</span>
</div>
)

View File

@@ -102,6 +102,22 @@ export function formatTime(date: Date): string {
})
}
/**
* Format a time with seconds and timezone
* @param date - The date to format
* @param includeTimezone - Whether to include the timezone abbreviation
* @returns A formatted time string in the format "h:mm:ss AM/PM TZ"
*/
export function formatTimeWithSeconds(date: Date, includeTimezone = true): string {
return date.toLocaleTimeString('en-US', {
hour: 'numeric',
minute: '2-digit',
second: '2-digit',
hour12: true,
timeZoneName: includeTimezone ? 'short' : undefined,
})
}
/**
* Format an ISO timestamp into a compact format for UI display
* @param iso - ISO timestamp string