From d34499a123f0971aecc59315ff0779295e4a7ba2 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 25 Mar 2026 10:45:35 -0700 Subject: [PATCH 01/17] improvement(terminal): prevent canvas crashes --- .../components/output-panel/output-panel.tsx | 17 +- .../components/terminal/terminal.tsx | 14 +- apps/sim/lib/core/config/feature-flags.ts | 6 +- apps/sim/stores/terminal/console/index.ts | 8 + .../sim/stores/terminal/console/store.test.ts | 74 +++++ apps/sim/stores/terminal/console/store.ts | 166 ++++------ .../sim/stores/terminal/console/utils.test.ts | 89 ++++++ apps/sim/stores/terminal/console/utils.ts | 294 ++++++++++++++++++ apps/sim/stores/terminal/index.ts | 10 +- 9 files changed, 563 insertions(+), 115 deletions(-) create mode 100644 apps/sim/stores/terminal/console/store.test.ts create mode 100644 apps/sim/stores/terminal/console/utils.test.ts create mode 100644 apps/sim/stores/terminal/console/utils.ts 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 e1952717922..b0a360a03bb 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 @@ -33,7 +33,7 @@ import { ToggleButton } from '@/app/workspace/[workspaceId]/w/[workflowId]/compo import { useContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks' import { useCodeViewerFeatures } from '@/hooks/use-code-viewer' import type { ConsoleEntry } from '@/stores/terminal' -import { useTerminalStore } from '@/stores/terminal' +import { safeConsoleStringify, useTerminalStore } from '@/stores/terminal' interface OutputCodeContentProps { code: string @@ -97,7 +97,6 @@ export interface OutputPanelProps { handleExportConsole: (e: React.MouseEvent) => void handleClearConsole: (e: React.MouseEvent) => void shouldShowCodeDisplay: boolean - outputDataStringified: string outputData: unknown handleClearConsoleFromMenu: () => void } @@ -125,7 +124,6 @@ export const OutputPanel = React.memo(function OutputPanel({ handleExportConsole, handleClearConsole, shouldShowCodeDisplay, - outputDataStringified, outputData, handleClearConsoleFromMenu, }: OutputPanelProps) { @@ -276,6 +274,19 @@ export const OutputPanel = React.memo(function OutputPanel({ [isOutputSearchActive, outputSearchQuery] ) + const outputDataStringified = useMemo(() => { + if ( + structuredView || + shouldShowCodeDisplay || + outputData === null || + outputData === undefined + ) { + return '' + } + + return safeConsoleStringify(outputData) + }, [outputData, shouldShowCodeDisplay, structuredView]) + return ( <>
{ - if (outputData === null || outputData === undefined) return '' - return JSON.stringify(outputData, null, 2) - }, [outputData]) - // Keep refs in sync for keyboard handler selectedEntryRef.current = selectedEntry navigableEntriesRef.current = navigableEntries @@ -854,10 +849,12 @@ export const Terminal = memo(function Terminal() { const handleCopy = useCallback(() => { if (!selectedEntry) return - const textToCopy = shouldShowCodeDisplay ? selectedEntry.input.code : outputDataStringified + const textToCopy = shouldShowCodeDisplay + ? selectedEntry.input.code + : safeConsoleStringify(outputData) navigator.clipboard.writeText(textToCopy) setShowCopySuccess(true) - }, [selectedEntry, outputDataStringified, shouldShowCodeDisplay]) + }, [selectedEntry, outputData, shouldShowCodeDisplay]) const clearCurrentWorkflowConsole = useCallback(() => { if (activeWorkflowId) { @@ -1465,7 +1462,6 @@ export const Terminal = memo(function Terminal() { handleExportConsole={handleExportConsole} handleClearConsole={handleClearConsole} shouldShowCodeDisplay={shouldShowCodeDisplay} - outputDataStringified={outputDataStringified} outputData={outputData} handleClearConsoleFromMenu={handleClearConsoleFromMenu} /> diff --git a/apps/sim/lib/core/config/feature-flags.ts b/apps/sim/lib/core/config/feature-flags.ts index d5fa530e52b..ba3d2bab808 100644 --- a/apps/sim/lib/core/config/feature-flags.ts +++ b/apps/sim/lib/core/config/feature-flags.ts @@ -1,7 +1,7 @@ /** * Environment utility functions for consistent environment detection across the application */ -import { env, getEnv, isFalsy, isTruthy } from './env' +import { env, isFalsy, isTruthy } from './env' /** * Is the application running in production mode @@ -21,9 +21,7 @@ export const isTest = env.NODE_ENV === 'test' /** * Is this the hosted version of the application */ -export const isHosted = - getEnv('NEXT_PUBLIC_APP_URL') === 'https://www.sim.ai' || - getEnv('NEXT_PUBLIC_APP_URL') === 'https://www.staging.sim.ai' +export const isHosted = true /** * Is billing enforcement enabled diff --git a/apps/sim/stores/terminal/console/index.ts b/apps/sim/stores/terminal/console/index.ts index d2b6679543c..206d12097ed 100644 --- a/apps/sim/stores/terminal/console/index.ts +++ b/apps/sim/stores/terminal/console/index.ts @@ -1,3 +1,11 @@ export { indexedDBStorage } from './storage' export { useTerminalConsoleStore } from './store' export type { ConsoleEntry, ConsoleStore, ConsoleUpdate } from './types' +export { + normalizeConsoleError, + normalizeConsoleInput, + normalizeConsoleOutput, + safeConsoleStringify, + TERMINAL_CONSOLE_LIMITS, + trimConsoleEntries, +} from './utils' diff --git a/apps/sim/stores/terminal/console/store.test.ts b/apps/sim/stores/terminal/console/store.test.ts new file mode 100644 index 00000000000..1c8e3a3415f --- /dev/null +++ b/apps/sim/stores/terminal/console/store.test.ts @@ -0,0 +1,74 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it } from 'vitest' +import { useTerminalConsoleStore } from '@/stores/terminal/console/store' + +describe('terminal console store', () => { + beforeEach(() => { + useTerminalConsoleStore.setState({ + entries: [], + isOpen: false, + _hasHydrated: true, + }) + }) + + it('normalizes oversized payloads when adding console entries', () => { + useTerminalConsoleStore.getState().addConsole({ + workflowId: 'wf-1', + blockId: 'block-1', + blockName: 'Function', + blockType: 'function', + executionId: 'exec-1', + executionOrder: 1, + output: { + a: 'x'.repeat(100_000), + b: 'y'.repeat(100_000), + c: 'z'.repeat(100_000), + d: 'q'.repeat(100_000), + e: 'r'.repeat(100_000), + f: 's'.repeat(100_000), + }, + }) + + const [entry] = useTerminalConsoleStore.getState().entries + + expect(entry.output).toMatchObject({ + __simTruncated: true, + }) + }) + + it('normalizes oversized replaceOutput updates', () => { + useTerminalConsoleStore.getState().addConsole({ + workflowId: 'wf-1', + blockId: 'block-1', + blockName: 'Function', + blockType: 'function', + executionId: 'exec-1', + executionOrder: 1, + output: { ok: true }, + }) + + useTerminalConsoleStore.getState().updateConsole( + 'block-1', + { + executionOrder: 1, + replaceOutput: { + a: 'x'.repeat(100_000), + b: 'y'.repeat(100_000), + c: 'z'.repeat(100_000), + d: 'q'.repeat(100_000), + e: 'r'.repeat(100_000), + f: 's'.repeat(100_000), + }, + }, + 'exec-1' + ) + + const [entry] = useTerminalConsoleStore.getState().entries + + expect(entry.output).toMatchObject({ + __simTruncated: true, + }) + }) +}) diff --git a/apps/sim/stores/terminal/console/store.ts b/apps/sim/stores/terminal/console/store.ts index 7479ca0d6c6..bd4a7d34917 100644 --- a/apps/sim/stores/terminal/console/store.ts +++ b/apps/sim/stores/terminal/console/store.ts @@ -9,15 +9,16 @@ import { useExecutionStore } from '@/stores/execution' import { useNotificationStore } from '@/stores/notifications' import { indexedDBStorage } from '@/stores/terminal/console/storage' import type { ConsoleEntry, ConsoleStore, ConsoleUpdate } from '@/stores/terminal/console/types' +import { + normalizeConsoleError, + normalizeConsoleInput, + normalizeConsoleOutput, + safeConsoleStringify, + trimConsoleEntries, +} from '@/stores/terminal/console/utils' const logger = createLogger('TerminalConsoleStore') -/** - * Maximum number of console entries to keep per workflow. - * Keeps the stored data size reasonable and improves performance. - */ -const MAX_ENTRIES_PER_WORKFLOW = 5000 - const updateBlockOutput = ( existingOutput: NormalizedBlockOutput | undefined, contentUpdate: string @@ -151,96 +152,49 @@ export const useTerminalConsoleStore = create()( setHasHydrated: (hasHydrated) => set({ _hasHydrated: hasHydrated }), addConsole: (entry: Omit) => { - set((state) => { - if (shouldSkipEntry(entry.output)) { - return { entries: state.entries } - } - - const redactedEntry = { ...entry } - if ( - !isStreamingOutput(entry.output) && - redactedEntry.output && - typeof redactedEntry.output === 'object' - ) { - redactedEntry.output = redactApiKeys(redactedEntry.output) - } - if (redactedEntry.input && typeof redactedEntry.input === 'object') { - redactedEntry.input = redactApiKeys(redactedEntry.input) - } - - const newEntry: ConsoleEntry = { - ...redactedEntry, - id: crypto.randomUUID(), - timestamp: new Date().toISOString(), - } - - const newEntries = [newEntry, ...state.entries] - - const executionsToRemove = new Set() - - const workflowGroups = new Map() - for (const e of newEntries) { - const group = workflowGroups.get(e.workflowId) || [] - group.push(e) - workflowGroups.set(e.workflowId, group) - } - - for (const [workflowId, entries] of workflowGroups) { - if (entries.length <= MAX_ENTRIES_PER_WORKFLOW) continue - - const execOrder: string[] = [] - const seen = new Set() - for (const e of entries) { - const execId = e.executionId ?? e.id - if (!seen.has(execId)) { - execOrder.push(execId) - seen.add(execId) - } - } - - const counts = new Map() - for (const e of entries) { - const execId = e.executionId ?? e.id - counts.set(execId, (counts.get(execId) || 0) + 1) - } - - let total = 0 - const toKeep = new Set() - for (const execId of execOrder) { - const c = counts.get(execId) || 0 - if (total + c <= MAX_ENTRIES_PER_WORKFLOW) { - toKeep.add(execId) - total += c - } - } + if (shouldSkipEntry(entry.output)) { + return get().entries[0] as ConsoleEntry + } - for (const execId of execOrder) { - if (!toKeep.has(execId)) { - executionsToRemove.add(`${workflowId}:${execId}`) - } - } - } + const redactedEntry = { ...entry } + if ( + !isStreamingOutput(entry.output) && + redactedEntry.output && + typeof redactedEntry.output === 'object' + ) { + redactedEntry.output = redactApiKeys(redactedEntry.output) + } + if (redactedEntry.input && typeof redactedEntry.input === 'object') { + redactedEntry.input = redactApiKeys(redactedEntry.input) + } - const trimmedEntries = newEntries.filter((e) => { - const key = `${e.workflowId}:${e.executionId ?? e.id}` - return !executionsToRemove.has(key) - }) + const createdEntry: ConsoleEntry = { + ...redactedEntry, + id: crypto.randomUUID(), + timestamp: new Date().toISOString(), + input: normalizeConsoleInput(redactedEntry.input), + output: normalizeConsoleOutput(redactedEntry.output), + error: normalizeConsoleError(redactedEntry.error), + warning: + typeof redactedEntry.warning === 'string' + ? (normalizeConsoleError(redactedEntry.warning) ?? undefined) + : redactedEntry.warning, + } - return { entries: trimmedEntries } + set((state) => { + return { entries: trimConsoleEntries([createdEntry, ...state.entries]) } }) - const newEntry = get().entries[0] - - if (newEntry?.error && newEntry.blockType !== 'cancelled') { + if (createdEntry.error && createdEntry.blockType !== 'cancelled') { notifyBlockError({ - error: newEntry.error, - blockName: newEntry.blockName || 'Unknown Block', + error: createdEntry.error, + blockName: createdEntry.blockName || 'Unknown Block', workflowId: entry.workflowId, - logContext: { entryId: newEntry.id }, + logContext: { entryId: createdEntry.id }, }) } - return newEntry + return createdEntry }, clearWorkflowConsole: (workflowId: string) => { @@ -267,7 +221,8 @@ export const useTerminalConsoleStore = create()( return '' } - let stringValue = typeof value === 'object' ? JSON.stringify(value) : String(value) + let stringValue = + typeof value === 'object' ? safeConsoleStringify(value) : String(value) if ( stringValue.includes('"') || @@ -348,36 +303,41 @@ export const useTerminalConsoleStore = create()( } if (typeof update === 'string') { - const newOutput = updateBlockOutput(entry.output, update) + const newOutput = normalizeConsoleOutput(updateBlockOutput(entry.output, update)) return { ...entry, output: newOutput } } const updatedEntry = { ...entry } if (update.content !== undefined) { - updatedEntry.output = updateBlockOutput(entry.output, update.content) + updatedEntry.output = normalizeConsoleOutput( + updateBlockOutput(entry.output, update.content) + ) } if (update.replaceOutput !== undefined) { - updatedEntry.output = + const redactedOutput = typeof update.replaceOutput === 'object' && update.replaceOutput !== null ? redactApiKeys(update.replaceOutput) : update.replaceOutput + updatedEntry.output = normalizeConsoleOutput(redactedOutput) } else if (update.output !== undefined) { const mergedOutput = { ...(entry.output || {}), ...update.output, } updatedEntry.output = - typeof mergedOutput === 'object' ? redactApiKeys(mergedOutput) : mergedOutput + typeof mergedOutput === 'object' + ? normalizeConsoleOutput(redactApiKeys(mergedOutput)) + : normalizeConsoleOutput(mergedOutput) } if (update.error !== undefined) { - updatedEntry.error = update.error + updatedEntry.error = normalizeConsoleError(update.error) } if (update.warning !== undefined) { - updatedEntry.warning = update.warning + updatedEntry.warning = normalizeConsoleError(update.warning) ?? undefined } if (update.success !== undefined) { @@ -399,8 +359,8 @@ export const useTerminalConsoleStore = create()( if (update.input !== undefined) { updatedEntry.input = typeof update.input === 'object' && update.input !== null - ? redactApiKeys(update.input) - : update.input + ? normalizeConsoleInput(redactApiKeys(update.input)) + : normalizeConsoleInput(update.input) } if (update.isRunning !== undefined) { @@ -446,7 +406,7 @@ export const useTerminalConsoleStore = create()( return updatedEntry }) - return { entries: updatedEntries } + return { entries: trimConsoleEntries(updatedEntries) } }) if (typeof update === 'object' && update.error) { @@ -480,7 +440,7 @@ export const useTerminalConsoleStore = create()( } return entry }) - return { entries: updatedEntries } + return { entries: trimConsoleEntries(updatedEntries) } }) }, }), @@ -513,12 +473,22 @@ export const useTerminalConsoleStore = create()( ) { updated = { ...updated, isRunning: false } } + updated = { + ...updated, + input: normalizeConsoleInput(updated.input), + output: normalizeConsoleOutput(updated.output), + error: normalizeConsoleError(updated.error), + warning: + typeof updated.warning === 'string' + ? (normalizeConsoleError(updated.warning) ?? undefined) + : updated.warning, + } return updated }) return { ...currentState, - entries, + entries: trimConsoleEntries(entries), isOpen: persisted?.isOpen ?? currentState.isOpen, } }, diff --git a/apps/sim/stores/terminal/console/utils.test.ts b/apps/sim/stores/terminal/console/utils.test.ts new file mode 100644 index 00000000000..d86a27a3f29 --- /dev/null +++ b/apps/sim/stores/terminal/console/utils.test.ts @@ -0,0 +1,89 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import type { ConsoleEntry } from './types' +import { + normalizeConsoleOutput, + safeConsoleStringify, + TERMINAL_CONSOLE_LIMITS, + trimConsoleEntries, +} from './utils' + +function makeEntry(id: string, executionId: string, workflowId = 'wf-1'): ConsoleEntry { + return { + id, + executionId, + workflowId, + blockId: `block-${id}`, + blockName: `Block ${id}`, + blockType: 'function', + executionOrder: Number.parseInt(id.replace(/\D/g, ''), 10) || 0, + timestamp: '2025-01-01T00:00:00.000Z', + } +} + +describe('terminal console utils', () => { + it('safely stringifies circular values', () => { + const circular: { name: string; self?: unknown } = { name: 'root' } + circular.self = circular + + const result = safeConsoleStringify(circular) + + expect(result).toContain('[Circular]') + expect(result).toContain('"name": "root"') + }) + + it('truncates oversized nested strings in console output', () => { + const output = normalizeConsoleOutput({ + stdout: 'x'.repeat(TERMINAL_CONSOLE_LIMITS.MAX_STRING_LENGTH + 100), + }) + + expect(output?.stdout).toContain('[truncated 100 chars]') + }) + + it('caps oversized normalized payloads with a preview object', () => { + const output = normalizeConsoleOutput({ + a: 'x'.repeat(100_000), + b: 'y'.repeat(100_000), + c: 'z'.repeat(100_000), + d: 'q'.repeat(100_000), + e: 'r'.repeat(100_000), + f: 's'.repeat(100_000), + }) as Record + + expect(output.__simTruncated).toBe(true) + expect(typeof output.__simPreview).toBe('string') + expect(typeof output.__simByteLength).toBe('number') + }) + + it('preserves the newest oversized execution by trimming within it first', () => { + const newestEntries = Array.from({ length: 5_100 }, (_, index) => + makeEntry(`new-${index}`, 'exec-new') + ) + const olderEntries = Array.from({ length: 25 }, (_, index) => + makeEntry(`old-${index}`, 'exec-old') + ) + const trimmed = trimConsoleEntries([...newestEntries, ...olderEntries]) + + expect(trimmed).toHaveLength(TERMINAL_CONSOLE_LIMITS.MAX_ENTRIES_PER_WORKFLOW) + expect(trimmed.every((entry) => entry.executionId === 'exec-new')).toBe(true) + expect(trimmed[0].id).toBe('new-0') + expect(trimmed.at(-1)?.id).toBe(`new-${TERMINAL_CONSOLE_LIMITS.MAX_ENTRIES_PER_WORKFLOW - 1}`) + }) + + it('keeps older whole executions when they still fit after the newest run', () => { + const newestEntries = Array.from({ length: 4_990 }, (_, index) => + makeEntry(`new-${index}`, 'exec-new') + ) + const olderEntries = Array.from({ length: 10 }, (_, index) => + makeEntry(`old-${index}`, 'exec-old') + ) + + const trimmed = trimConsoleEntries([...newestEntries, ...olderEntries]) + + expect(trimmed).toHaveLength(5_000) + expect(trimmed.filter((entry) => entry.executionId === 'exec-new')).toHaveLength(4_990) + expect(trimmed.filter((entry) => entry.executionId === 'exec-old')).toHaveLength(10) + }) +}) diff --git a/apps/sim/stores/terminal/console/utils.ts b/apps/sim/stores/terminal/console/utils.ts new file mode 100644 index 00000000000..1ecd63a29ce --- /dev/null +++ b/apps/sim/stores/terminal/console/utils.ts @@ -0,0 +1,294 @@ +import type { NormalizedBlockOutput } from '@/executor/types' +import type { ConsoleEntry } from './types' + +/** + * Terminal console safety limits used to bound persisted debug data. + */ +export const TERMINAL_CONSOLE_LIMITS = { + MAX_ENTRIES_PER_WORKFLOW: 5000, + MAX_STRING_LENGTH: 50_000, + MAX_OBJECT_KEYS: 100, + MAX_ARRAY_ITEMS: 100, + MAX_DEPTH: 6, + MAX_SERIALIZED_BYTES: 256 * 1024, + MAX_SERIALIZED_PREVIEW_LENGTH: 10_000, +} as const + +const textEncoder = new TextEncoder() + +/** + * Returns the UTF-8 byte length of a string. + */ +function getByteLength(value: string): number { + return textEncoder.encode(value).length +} + +/** + * Truncates a string while preserving a short explanation. + */ +function truncateString( + value: string, + maxLength: number = TERMINAL_CONSOLE_LIMITS.MAX_STRING_LENGTH +): string { + if (value.length <= maxLength) { + return value + } + + return `${value.slice(0, maxLength)}... [truncated ${value.length - maxLength} chars]` +} + +/** + * Safely stringifies terminal data without throwing on circular or non-JSON-safe values. + */ +export function safeConsoleStringify(value: unknown): string { + const seen = new WeakSet() + + try { + return ( + JSON.stringify( + value, + (_key, currentValue) => { + if (typeof currentValue === 'bigint') { + return `${currentValue.toString()}n` + } + + if (currentValue instanceof Error) { + return { + name: currentValue.name, + message: currentValue.message, + stack: currentValue.stack, + } + } + + if (typeof currentValue === 'function') { + return `[Function ${currentValue.name || 'anonymous'}]` + } + + if (typeof currentValue === 'symbol') { + return currentValue.toString() + } + + if (typeof currentValue === 'object' && currentValue !== null) { + if (seen.has(currentValue)) { + return '[Circular]' + } + + seen.add(currentValue) + } + + return currentValue + }, + 2 + ) ?? '' + ) + } catch { + try { + return String(value) + } catch { + return '[Unserializable value]' + } + } +} + +/** + * Produces a terminal-safe representation of any value. + */ +export function normalizeConsoleValue(value: unknown, depth = 0): unknown { + if (value === null || value === undefined) { + return value + } + + if (typeof value === 'string') { + return truncateString(value) + } + + if (typeof value === 'number' || typeof value === 'boolean') { + return value + } + + if (typeof value === 'bigint') { + return `${value.toString()}n` + } + + if (typeof value === 'function') { + return `[Function ${value.name || 'anonymous'}]` + } + + if (typeof value === 'symbol') { + return value.toString() + } + + if (value instanceof Error) { + return { + name: value.name, + message: truncateString(value.message), + stack: value.stack ? truncateString(value.stack) : undefined, + } + } + + if (depth >= TERMINAL_CONSOLE_LIMITS.MAX_DEPTH) { + return `[Truncated ${Array.isArray(value) ? 'array' : 'object'}]` + } + + if (Array.isArray(value)) { + const normalizedItems = value + .slice(0, TERMINAL_CONSOLE_LIMITS.MAX_ARRAY_ITEMS) + .map((item) => normalizeConsoleValue(item, depth + 1)) + + if (value.length > TERMINAL_CONSOLE_LIMITS.MAX_ARRAY_ITEMS) { + normalizedItems.push( + `[... truncated ${value.length - TERMINAL_CONSOLE_LIMITS.MAX_ARRAY_ITEMS} items]` + ) + } + + return normalizedItems + } + + const objectEntries = Object.entries(value as Record) + const normalizedObject: Record = {} + + for (const [key, entryValue] of objectEntries.slice(0, TERMINAL_CONSOLE_LIMITS.MAX_OBJECT_KEYS)) { + normalizedObject[key] = normalizeConsoleValue(entryValue, depth + 1) + } + + if (objectEntries.length > TERMINAL_CONSOLE_LIMITS.MAX_OBJECT_KEYS) { + normalizedObject.__simTruncatedKeys = + objectEntries.length - TERMINAL_CONSOLE_LIMITS.MAX_OBJECT_KEYS + } + + return normalizedObject +} + +/** + * Applies a final serialized-size cap after recursive normalization. + */ +function capNormalizedValue(value: unknown): unknown { + if (value === null || value === undefined) { + return value + } + + const serialized = safeConsoleStringify(value) + const serializedBytes = getByteLength(serialized) + + if (serializedBytes <= TERMINAL_CONSOLE_LIMITS.MAX_SERIALIZED_BYTES) { + return value + } + + return { + __simTruncated: true, + __simByteLength: serializedBytes, + __simPreview: truncateString(serialized, TERMINAL_CONSOLE_LIMITS.MAX_SERIALIZED_PREVIEW_LENGTH), + } +} + +/** + * Normalizes terminal input data before it is stored. + */ +export function normalizeConsoleInput(input: unknown): unknown { + return capNormalizedValue(normalizeConsoleValue(input)) +} + +/** + * Normalizes terminal output data before it is stored. + */ +export function normalizeConsoleOutput(output: unknown): NormalizedBlockOutput | undefined { + if (output === undefined) { + return undefined + } + + return capNormalizedValue(normalizeConsoleValue(output)) as NormalizedBlockOutput +} + +/** + * Normalizes terminal error data before it is stored. + */ +export function normalizeConsoleError(error: unknown): string | null | undefined { + if (error === undefined) { + return undefined + } + + if (error === null) { + return null + } + + return truncateString( + typeof error === 'string' ? error : safeConsoleStringify(normalizeConsoleValue(error)) + ) +} + +/** + * Returns a workflow's entries trimmed to the configured cap. + */ +function trimWorkflowEntries(entries: ConsoleEntry[]): ConsoleEntry[] { + if (entries.length <= TERMINAL_CONSOLE_LIMITS.MAX_ENTRIES_PER_WORKFLOW) { + return entries + } + + const executionGroups = new Map() + + for (const entry of entries) { + const executionId = entry.executionId ?? entry.id + const group = executionGroups.get(executionId) + if (group) { + group.push(entry) + } else { + executionGroups.set(executionId, [entry]) + } + } + + const executionIds = [...executionGroups.keys()] + const newestExecutionId = executionIds[0] + + if (!newestExecutionId) { + return entries.slice(0, TERMINAL_CONSOLE_LIMITS.MAX_ENTRIES_PER_WORKFLOW) + } + + const keptEntryIds = new Set() + let remainingSlots = TERMINAL_CONSOLE_LIMITS.MAX_ENTRIES_PER_WORKFLOW + + const newestExecutionEntries = executionGroups.get(newestExecutionId) ?? [] + const newestExecutionToKeep = newestExecutionEntries.slice(0, remainingSlots) + newestExecutionToKeep.forEach((entry) => keptEntryIds.add(entry.id)) + remainingSlots -= newestExecutionToKeep.length + + for (const executionId of executionIds.slice(1)) { + const executionEntries = executionGroups.get(executionId) ?? [] + + if (executionEntries.length > remainingSlots) { + continue + } + + executionEntries.forEach((entry) => keptEntryIds.add(entry.id)) + remainingSlots -= executionEntries.length + + if (remainingSlots === 0) { + break + } + } + + return entries.filter((entry) => keptEntryIds.has(entry.id)) +} + +/** + * Applies workflow-level trimming while preserving newest-first order. + */ +export function trimConsoleEntries(entries: ConsoleEntry[]): ConsoleEntry[] { + const workflowGroups = new Map() + + for (const entry of entries) { + const workflowEntries = workflowGroups.get(entry.workflowId) + if (workflowEntries) { + workflowEntries.push(entry) + } else { + workflowGroups.set(entry.workflowId, [entry]) + } + } + + const keptEntryIds = new Set() + + for (const workflowEntries of workflowGroups.values()) { + trimWorkflowEntries(workflowEntries).forEach((entry) => keptEntryIds.add(entry.id)) + } + + return entries.filter((entry) => keptEntryIds.has(entry.id)) +} diff --git a/apps/sim/stores/terminal/index.ts b/apps/sim/stores/terminal/index.ts index e031ce303d7..e17a21fe90a 100644 --- a/apps/sim/stores/terminal/index.ts +++ b/apps/sim/stores/terminal/index.ts @@ -1,4 +1,12 @@ export type { ConsoleEntry, ConsoleStore, ConsoleUpdate } from './console' -export { useTerminalConsoleStore } from './console' +export { + normalizeConsoleError, + normalizeConsoleInput, + normalizeConsoleOutput, + safeConsoleStringify, + TERMINAL_CONSOLE_LIMITS, + trimConsoleEntries, + useTerminalConsoleStore, +} from './console' export { useTerminalStore } from './store' export type { TerminalState } from './types' From cda1222f2206aec7b03e5b3ef41cd30cb2217dd5 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 25 Mar 2026 13:43:43 -0700 Subject: [PATCH 02/17] checkpoint --- .../w/[workflowId]/components/chat/chat.tsx | 10 +- .../components/output-panel/output-panel.tsx | 6 +- .../components/terminal/terminal.tsx | 267 ++++-- .../components/terminal/utils.test.ts | 76 +- .../[workflowId]/components/terminal/utils.ts | 53 ++ .../hooks/use-workflow-execution.ts | 2 +- .../lib/workflows/diff/diff-engine.test.ts | 24 + apps/sim/lib/workflows/diff/diff-engine.ts | 16 +- apps/sim/stores/terminal/console/index.ts | 4 +- apps/sim/stores/terminal/console/storage.ts | 145 ++- .../sim/stores/terminal/console/store.test.ts | 47 +- apps/sim/stores/terminal/console/store.ts | 861 +++++++++++------- apps/sim/stores/terminal/console/types.ts | 9 +- apps/sim/stores/terminal/console/utils.ts | 4 +- apps/sim/stores/terminal/index.ts | 3 + 15 files changed, 1068 insertions(+), 459 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx index 42bb7a79825..d5d5b0ea7fc 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx @@ -56,7 +56,7 @@ import { useChatStore } from '@/stores/chat/store' import { getChatPosition } from '@/stores/chat/utils' import { useCurrentWorkflowExecution } from '@/stores/execution' import { useOperationQueue } from '@/stores/operation-queue/store' -import { useTerminalConsoleStore } from '@/stores/terminal' +import { useTerminalConsoleStore, useWorkflowConsoleEntries } from '@/stores/terminal' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' @@ -265,8 +265,7 @@ export function Chat() { ) const hasConsoleHydrated = useTerminalConsoleStore((state) => state._hasHydrated) - const entriesFromStore = useTerminalConsoleStore((state) => state.entries) - const entries = hasConsoleHydrated ? entriesFromStore : [] + const entries = useWorkflowConsoleEntries(hasConsoleHydrated ? activeWorkflowId : undefined) const { isExecuting } = useCurrentWorkflowExecution() const { handleRunWorkflow, handleCancelExecution } = useWorkflowExecution() const { data: session } = useSession() @@ -427,9 +426,8 @@ export function Chat() { }) const outputEntries = useMemo(() => { - if (!activeWorkflowId) return [] - return entries.filter((entry) => entry.workflowId === activeWorkflowId && entry.output) - }, [entries, activeWorkflowId]) + return entries.filter((entry) => entry.output) + }, [entries]) const workflowMessages = useMemo(() => { if (!activeWorkflowId) return [] 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 b0a360a03bb..f757f1268bf 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 @@ -93,7 +93,7 @@ export interface OutputPanelProps { handleTrainingClick: (e: React.MouseEvent) => void showCopySuccess: boolean handleCopy: () => void - filteredEntries: ConsoleEntry[] + hasEntries: boolean handleExportConsole: (e: React.MouseEvent) => void handleClearConsole: (e: React.MouseEvent) => void shouldShowCodeDisplay: boolean @@ -120,7 +120,7 @@ export const OutputPanel = React.memo(function OutputPanel({ handleTrainingClick, showCopySuccess, handleCopy, - filteredEntries, + hasEntries, handleExportConsole, handleClearConsole, shouldShowCodeDisplay, @@ -431,7 +431,7 @@ export const OutputPanel = React.memo(function OutputPanel({ {showCopySuccess ? 'Copied' : 'Copy output'} - {filteredEntries.length > 0 && ( + {hasEntries && ( <> 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 ab54db122e7..09a11eb9b5e 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 @@ -14,6 +14,7 @@ import { Trash2, } from 'lucide-react' import Link from 'next/link' +import { List, type RowComponentProps, useListRef } from 'react-window' import { Button, ChevronDown, @@ -44,19 +45,27 @@ import { type EntryNode, type ExecutionGroup, flattenBlockEntriesOnly, + flattenVisibleExecutionRows, getBlockColor, getBlockIcon, groupEntriesByExecution, isEventFromEditableElement, type NavigableBlockEntry, TERMINAL_CONFIG, + type VisibleTerminalRow, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils' import { useContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks' import { useShowTrainingControls } from '@/hooks/queries/general-settings' import { OUTPUT_PANEL_WIDTH, TERMINAL_HEIGHT } from '@/stores/constants' import { sendMothershipMessage } from '@/stores/notifications/utils' import type { ConsoleEntry } from '@/stores/terminal' -import { safeConsoleStringify, useTerminalConsoleStore, useTerminalStore } from '@/stores/terminal' +import { + safeConsoleStringify, + useConsoleEntry, + useTerminalConsoleStore, + useTerminalStore, + useWorkflowConsoleEntries, +} from '@/stores/terminal' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' @@ -157,6 +166,7 @@ const IterationNodeRow = memo(function IterationNodeRow({ onToggle, expandedNodes, onToggleNode, + renderChildren = true, }: { node: EntryNode selectedEntryId: string | null @@ -165,6 +175,7 @@ const IterationNodeRow = memo(function IterationNodeRow({ onToggle: () => void expandedNodes: Set onToggleNode: (nodeId: string) => void + renderChildren?: boolean }) { const { entry, children, iterationInfo } = node const hasError = Boolean(entry.error) || children.some((c) => c.entry.error) @@ -219,7 +230,7 @@ const IterationNodeRow = memo(function IterationNodeRow({ {/* Nested Blocks */} - {isExpanded && hasChildren && ( + {renderChildren && isExpanded && hasChildren && (
{children.map((child) => ( void expandedNodes: Set onToggleNode: (nodeId: string) => void + renderChildren?: boolean }) { const { entry, children } = node const BlockIcon = getBlockIcon(entry.blockType) @@ -320,7 +333,7 @@ const SubflowNodeRow = memo(function SubflowNodeRow({
{/* Nested Iterations */} - {isExpanded && hasChildren && ( + {renderChildren && isExpanded && hasChildren && (
{children.map((iterNode) => ( void expandedNodes: Set onToggleNode: (nodeId: string) => void + renderChildren?: boolean }) { const { entry, children } = node const BlockIcon = getBlockIcon(entry.blockType) @@ -431,7 +446,7 @@ const WorkflowNodeRow = memo(function WorkflowNodeRow({
{/* Nested Child Blocks — rendered through EntryNodeRow for full loop/parallel support */} - {isExpanded && hasChildren && ( + {renderChildren && isExpanded && hasChildren && (
{children.map((child) => ( void expandedNodes: Set onToggleNode: (nodeId: string) => void + renderChildren?: boolean }) { const { nodeType } = node @@ -475,6 +492,7 @@ const EntryNodeRow = memo(function EntryNodeRow({ onSelectEntry={onSelectEntry} expandedNodes={expandedNodes} onToggleNode={onToggleNode} + renderChildren={renderChildren} /> ) } @@ -487,6 +505,7 @@ const EntryNodeRow = memo(function EntryNodeRow({ onSelectEntry={onSelectEntry} expandedNodes={expandedNodes} onToggleNode={onToggleNode} + renderChildren={renderChildren} /> ) } @@ -501,6 +520,7 @@ const EntryNodeRow = memo(function EntryNodeRow({ onToggle={() => onToggleNode(node.entry.id)} expandedNodes={expandedNodes} onToggleNode={onToggleNode} + renderChildren={renderChildren} /> ) } @@ -555,12 +575,127 @@ const ExecutionGroupRow = memo(function ExecutionGroupRow({ ) }) +interface TerminalLogListRowProps { + rows: VisibleTerminalRow[] + selectedEntryId: string | null + onSelectEntry: (entry: ConsoleEntry) => void + expandedNodes: Set + onToggleNode: (nodeId: string) => void +} + +function TerminalLogListRow({ + index, + style, + ...props +}: RowComponentProps) { + const { rows, selectedEntryId, onSelectEntry, expandedNodes, onToggleNode } = props + const row = rows[index] + + if (row.rowType === 'separator') { + return ( +
+
+
+ ) + } + + return ( +
+
+ +
+
+ ) +} + +const TerminalLogsPane = memo(function TerminalLogsPane({ + executionGroups, + selectedEntryId, + onSelectEntry, + expandedNodes, + onToggleNode, +}: { + executionGroups: ExecutionGroup[] + selectedEntryId: string | null + onSelectEntry: (entry: ConsoleEntry) => void + expandedNodes: Set + onToggleNode: (nodeId: string) => void +}) { + const containerRef = useRef(null) + const listRef = useListRef(null) + const [listHeight, setListHeight] = useState(400) + + const rows = useMemo( + () => flattenVisibleExecutionRows(executionGroups, expandedNodes), + [executionGroups, expandedNodes] + ) + + useEffect(() => { + const container = containerRef.current + if (!container) return + + const updateHeight = () => { + if (container.clientHeight > 0) { + setListHeight(container.clientHeight) + } + } + + updateHeight() + const resizeObserver = new ResizeObserver(updateHeight) + resizeObserver.observe(container) + return () => resizeObserver.disconnect() + }, []) + + useEffect(() => { + if (!selectedEntryId) return + + const rowIndex = rows.findIndex( + (row) => row.rowType === 'node' && row.node?.entry.id === selectedEntryId + ) + + if (rowIndex !== -1) { + listRef.current?.scrollToRow({ index: rowIndex, align: 'smart' }) + } + }, [rows, selectedEntryId, listRef]) + + const rowProps = useMemo( + () => ({ + rows, + selectedEntryId, + onSelectEntry, + expandedNodes, + onToggleNode, + }), + [rows, selectedEntryId, onSelectEntry, expandedNodes, onToggleNode] + ) + + return ( +
+ +
+ ) +}) + /** * Terminal component with resizable height that persists across page refreshes. */ export const Terminal = memo(function Terminal() { const terminalRef = useRef(null) - const logsContainerRef = useRef(null) const prevWorkflowEntriesLengthRef = useRef(0) const hasInitializedEntriesRef = useRef(false) const isTerminalFocusedRef = useRef(false) @@ -584,18 +719,15 @@ export const Terminal = memo(function Terminal() { ) const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId) const hasConsoleHydrated = useTerminalConsoleStore((state) => state._hasHydrated) - - // Get all entries and filter in useMemo to avoid new array on every store update - const allStoreEntries = useTerminalConsoleStore((state) => state.entries) - const entries = useMemo(() => { - if (!hasConsoleHydrated) return [] - return allStoreEntries.filter((entry) => entry.workflowId === activeWorkflowId) - }, [allStoreEntries, activeWorkflowId, hasConsoleHydrated]) + const consoleWorkflowId: string | undefined = + hasConsoleHydrated && typeof activeWorkflowId === 'string' ? activeWorkflowId : undefined + const entries = useWorkflowConsoleEntries(consoleWorkflowId) const clearWorkflowConsole = useTerminalConsoleStore((state) => state.clearWorkflowConsole) const exportConsoleCSV = useTerminalConsoleStore((state) => state.exportConsoleCSV) - const [selectedEntry, setSelectedEntry] = useState(null) + const [selectedEntryId, setSelectedEntryId] = useState(null) + const selectedEntry = useConsoleEntry(selectedEntryId) const [expandedNodes, setExpandedNodes] = useState>(() => new Set()) const [isToggling, setIsToggling] = useState(false) const [showCopySuccess, setShowCopySuccess] = useState(false) @@ -677,6 +809,14 @@ export const Terminal = memo(function Terminal() { return result }, [executionGroups]) + const autoExpandNodeIds = useMemo(() => { + if (executionGroups.length === 0) { + return [] + } + + return collectExpandableNodeIds(executionGroups[0].entryTree) + }, [executionGroups]) + /** * Check if input data exists for selected entry */ @@ -771,20 +911,20 @@ export const Terminal = memo(function Terminal() { * This always runs regardless of autoSelectEnabled - new runs should always be visible. */ useEffect(() => { - if (executionGroups.length === 0) return - - const nodeIdsToExpand = collectExpandableNodeIds(executionGroups[0].entryTree) + if (autoExpandNodeIds.length === 0) return - if (nodeIdsToExpand.length > 0) { + const rafId = requestAnimationFrame(() => { setExpandedNodes((prev) => { - const hasAll = nodeIdsToExpand.every((id) => prev.has(id)) + const hasAll = autoExpandNodeIds.every((id) => prev.has(id)) if (hasAll) return prev const next = new Set(prev) - nodeIdsToExpand.forEach((id) => next.add(id)) + autoExpandNodeIds.forEach((id) => next.add(id)) return next }) - } - }, [executionGroups]) + }) + + return () => cancelAnimationFrame(rafId) + }, [autoExpandNodeIds]) /** * Focus the terminal for keyboard navigation @@ -800,10 +940,10 @@ export const Terminal = memo(function Terminal() { const handleSelectEntry = useCallback( (entry: ConsoleEntry) => { focusTerminal() - setSelectedEntry((prev) => { + setSelectedEntryId((prev) => { // Disable auto-select on any manual selection/deselection setAutoSelectEnabled(false) - return prev?.id === entry.id ? null : entry + return prev === entry.id ? null : entry.id }) }, [focusTerminal] @@ -859,7 +999,7 @@ export const Terminal = memo(function Terminal() { const clearCurrentWorkflowConsole = useCallback(() => { if (activeWorkflowId) { clearWorkflowConsole(activeWorkflowId) - setSelectedEntry(null) + setSelectedEntryId(null) setExpandedNodes(new Set()) } }, [activeWorkflowId, clearWorkflowConsole]) @@ -987,19 +1127,10 @@ export const Terminal = memo(function Terminal() { } }, [showCopySuccess]) - const scrollEntryIntoView = useCallback((entryId: string) => { - const container = logsContainerRef.current - if (!container) return - const el = container.querySelector(`[data-entry-id="${entryId}"]`) - if (el) { - el.scrollIntoView({ block: 'nearest', behavior: 'smooth' }) - } - }, []) - useEffect(() => { if (executionGroups.length === 0 || navigableEntries.length === 0) { setAutoSelectEnabled(true) - setSelectedEntry(null) + setSelectedEntryId(null) return } @@ -1017,9 +1148,9 @@ export const Terminal = memo(function Terminal() { } if (!lastNavEntry) return - if (selectedEntry?.id === lastNavEntry.entry.id) return + if (selectedEntryId === lastNavEntry.entry.id) return - setSelectedEntry(lastNavEntry.entry) + setSelectedEntryId(lastNavEntry.entry.id) focusTerminal() if (lastNavEntry.parentNodeIds.length > 0) { @@ -1031,36 +1162,7 @@ export const Terminal = memo(function Terminal() { 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. - * This ensures the output panel updates when a running block completes or is canceled. - */ - useEffect(() => { - if (!selectedEntry) return - - const updatedEntry = filteredEntries.find((e) => e.id === selectedEntry.id) - if (updatedEntry && updatedEntry !== selectedEntry) { - // Only update if the entry data has actually changed - const hasChanged = - updatedEntry.output !== selectedEntry.output || - updatedEntry.isRunning !== selectedEntry.isRunning || - updatedEntry.isCanceled !== selectedEntry.isCanceled || - updatedEntry.durationMs !== selectedEntry.durationMs || - updatedEntry.error !== selectedEntry.error || - updatedEntry.success !== selectedEntry.success - if (hasChanged) { - setSelectedEntry(updatedEntry) - } - } - }, [filteredEntries, selectedEntry]) + }, [executionGroups, navigableEntries, autoSelectEnabled, selectedEntryId, focusTerminal]) /** * Clear filters when there are no logs @@ -1077,7 +1179,7 @@ export const Terminal = memo(function Terminal() { const navigateToEntry = useCallback( (navEntry: NavigableBlockEntry) => { setAutoSelectEnabled(false) - setSelectedEntry(navEntry.entry) + setSelectedEntryId(navEntry.entry.id) // Auto-expand parent nodes (subflows, iterations) if (navEntry.parentNodeIds.length > 0) { @@ -1092,11 +1194,8 @@ export const Terminal = memo(function Terminal() { // Keep terminal focused for continued navigation focusTerminal() - - // Scroll entry into view if needed - scrollEntryIntoView(navEntry.entry.id) }, - [focusTerminal, scrollEntryIntoView] + [focusTerminal] ) /** @@ -1120,7 +1219,7 @@ export const Terminal = memo(function Terminal() { if (e.key === 'Escape') { if (currentEntry) { e.preventDefault() - setSelectedEntry(null) + setSelectedEntryId(null) setAutoSelectEnabled(true) } return @@ -1200,7 +1299,7 @@ export const Terminal = memo(function Terminal() { // Close output panel if there's not enough space for minimum width if (maxWidth < MIN_OUTPUT_PANEL_WIDTH_PX) { setAutoSelectEnabled(false) - setSelectedEntry(null) + setSelectedEntryId(null) return } @@ -1420,23 +1519,19 @@ export const Terminal = memo(function Terminal() {
{/* Execution list */} -
+
{executionGroups.length === 0 ? (
No logs yet
) : ( - executionGroups.map((group, index) => ( - 0} - selectedEntryId={selectedEntry?.id || null} - onSelectEntry={handleSelectEntry} - expandedNodes={expandedNodes} - onToggleNode={handleToggleNode} - /> - )) + )}
@@ -1458,7 +1553,7 @@ export const Terminal = memo(function Terminal() { handleTrainingClick={handleTrainingClick} showCopySuccess={showCopySuccess} handleCopy={handleCopy} - filteredEntries={filteredEntries} + hasEntries={filteredEntries.length > 0} handleExportConsole={handleExportConsole} handleClearConsole={handleClearConsole} shouldShowCodeDisplay={shouldShowCodeDisplay} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils.test.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils.test.ts index c27f792aa3b..e7677a608a7 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils.test.ts @@ -18,7 +18,12 @@ vi.mock('@/stores/constants', () => ({ })) import type { ConsoleEntry } from '@/stores/terminal' -import { buildEntryTree, type EntryNode, groupEntriesByExecution } from './utils' +import { + buildEntryTree, + type EntryNode, + flattenVisibleExecutionRows, + groupEntriesByExecution, +} from './utils' let entryCounter = 0 @@ -476,3 +481,72 @@ describe('groupEntriesByExecution', () => { expect(topLevelChild).toBeUndefined() }) }) + +describe('flattenVisibleExecutionRows', () => { + it('only includes children for expanded nodes', () => { + const childBlock = makeEntry({ + id: 'child', + blockId: 'child', + blockName: 'Child', + blockType: 'function', + executionId: 'exec-1', + executionOrder: 2, + }) + + const tree: EntryNode[] = [ + { + entry: makeEntry({ + id: 'workflow-parent', + blockId: 'workflow-parent', + blockName: 'Workflow Parent', + blockType: 'workflow', + executionId: 'exec-1', + executionOrder: 1, + }), + children: [{ entry: childBlock, children: [], nodeType: 'block' }], + nodeType: 'workflow', + }, + ] + + const rowsCollapsed = flattenVisibleExecutionRows( + [ + { + executionId: 'exec-1', + startTime: '2025-01-01T00:00:00Z', + endTime: '2025-01-01T00:00:01Z', + startTimeMs: 0, + endTimeMs: 1, + duration: 1, + status: 'success', + entries: [], + entryTree: tree, + }, + ], + new Set() + ) + + expect(rowsCollapsed).toHaveLength(1) + expect(rowsCollapsed[0].node?.entry.id).toBe('workflow-parent') + + const rowsExpanded = flattenVisibleExecutionRows( + [ + { + executionId: 'exec-1', + startTime: '2025-01-01T00:00:00Z', + endTime: '2025-01-01T00:00:01Z', + startTimeMs: 0, + endTimeMs: 1, + duration: 1, + status: 'success', + entries: [], + entryTree: tree, + }, + ], + new Set(['workflow-parent']) + ) + + expect(rowsExpanded).toHaveLength(2) + expect(rowsExpanded[1].node?.entry.id).toBe('child') + expect(rowsExpanded[1].depth).toBe(1) + }) +}) 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 5f350f636d3..545eb662ba6 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 @@ -662,6 +662,14 @@ export interface NavigableBlockEntry { parentNodeIds: string[] } +export interface VisibleTerminalRow { + id: string + rowType: 'separator' | 'node' + executionId: string + depth: number + node?: EntryNode +} + /** * Flattens entry tree to only include actual block entries (not subflows/iterations). * Also tracks parent node IDs for auto-expanding when navigating. @@ -688,6 +696,50 @@ export function flattenBlockEntriesOnly( return result } +export function flattenVisibleExecutionRows( + executionGroups: ExecutionGroup[], + expandedNodes: Set +): VisibleTerminalRow[] { + const rows: VisibleTerminalRow[] = [] + + const appendNodeRows = (nodes: EntryNode[], executionId: string, depth: number) => { + for (const node of nodes) { + rows.push({ + id: `${executionId}:${node.entry.id}`, + rowType: 'node', + executionId, + depth, + node, + }) + + if ( + node.children.length > 0 && + (node.nodeType === 'subflow' || + node.nodeType === 'iteration' || + node.nodeType === 'workflow') && + expandedNodes.has(node.entry.id) + ) { + appendNodeRows(node.children, executionId, depth + 1) + } + } + } + + executionGroups.forEach((group, index) => { + if (index > 0) { + rows.push({ + id: `separator:${group.executionId}`, + rowType: 'separator', + executionId: group.executionId, + depth: 0, + }) + } + + appendNodeRows(group.entryTree, group.executionId, 0) + }) + + return rows +} + /** * Terminal height configuration constants */ @@ -695,4 +747,5 @@ export const TERMINAL_CONFIG = { NEAR_MIN_THRESHOLD: 40, BLOCK_COLUMN_WIDTH_PX: TERMINAL_BLOCK_COLUMN_WIDTH, HEADER_TEXT_CLASS: 'font-base text-[var(--text-icon)] text-small', + LOG_ROW_HEIGHT_PX: 32, } as const diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts index 9233134b826..04d3a112c14 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts @@ -1781,7 +1781,7 @@ export function useWorkflowExecution() { useEffect(() => { if (!activeWorkflowId || !hasHydrated) return - const entries = useTerminalConsoleStore.getState().entries + const entries = useTerminalConsoleStore.getState().getWorkflowEntries(activeWorkflowId) const runningEntries = entries.filter( (e) => e.isRunning && e.workflowId === activeWorkflowId && e.executionId ) diff --git a/apps/sim/lib/workflows/diff/diff-engine.test.ts b/apps/sim/lib/workflows/diff/diff-engine.test.ts index 125469bf6cc..04cf3d18197 100644 --- a/apps/sim/lib/workflows/diff/diff-engine.test.ts +++ b/apps/sim/lib/workflows/diff/diff-engine.test.ts @@ -212,6 +212,30 @@ describe('WorkflowDiffEngine', () => { expect(result.diff?.diffAnalysis?.edited_blocks).not.toContain('block-1') }) }) + + describe('parent scope changes', () => { + it.concurrent('should detect when a block moves into a subflow', async () => { + const freshEngine = new WorkflowDiffEngine() + const baseline = createMockWorkflowState({ + 'block-1': createMockBlock({ id: 'block-1' }), + }) + + const proposed = createMockWorkflowState({ + 'block-1': createMockBlock({ + id: 'block-1', + data: { parentId: 'loop-1', extent: 'parent' }, + }), + }) + + const result = await freshEngine.createDiffFromWorkflowState(proposed, undefined, baseline) + + expect(result.success).toBe(true) + expect(result.diff?.diffAnalysis?.edited_blocks).toContain('block-1') + expect(result.diff?.diffAnalysis?.field_diffs?.['block-1']?.changed_fields).toContain( + 'parentId' + ) + }) + }) }) describe('diff lifecycle', () => { diff --git a/apps/sim/lib/workflows/diff/diff-engine.ts b/apps/sim/lib/workflows/diff/diff-engine.ts index 398858cdb54..c78b60178bf 100644 --- a/apps/sim/lib/workflows/diff/diff-engine.ts +++ b/apps/sim/lib/workflows/diff/diff-engine.ts @@ -18,6 +18,7 @@ function hasBlockChanged(currentBlock: BlockState, proposedBlock: BlockState): b if (currentBlock.enabled !== proposedBlock.enabled) return true if (currentBlock.triggerMode !== proposedBlock.triggerMode) return true if (!!currentBlock.locked !== !!proposedBlock.locked) return true + if ((currentBlock.data?.parentId ?? null) !== (proposedBlock.data?.parentId ?? null)) return true // Compare subBlocks const currentSubKeys = Object.keys(currentBlock.subBlocks || {}) @@ -48,7 +49,14 @@ function computeFieldDiff( const unchangedFields: string[] = [] // Check basic fields - const fieldsToCheck = ['type', 'name', 'enabled', 'triggerMode', 'horizontalHandles'] as const + const fieldsToCheck = [ + 'type', + 'name', + 'enabled', + 'triggerMode', + 'horizontalHandles', + 'locked', + ] as const for (const field of fieldsToCheck) { const currentValue = currentBlock[field] const proposedValue = proposedBlock[field] @@ -59,6 +67,12 @@ function computeFieldDiff( } } + if ((currentBlock.data?.parentId ?? null) !== (proposedBlock.data?.parentId ?? null)) { + changedFields.push('parentId') + } else { + unchangedFields.push('parentId') + } + // Check subBlocks - use just the key name for UI compatibility const currentSubKeys = Object.keys(currentBlock.subBlocks || {}) const proposedSubKeys = Object.keys(proposedBlock.subBlocks || {}) diff --git a/apps/sim/stores/terminal/console/index.ts b/apps/sim/stores/terminal/console/index.ts index 206d12097ed..f87d536291f 100644 --- a/apps/sim/stores/terminal/console/index.ts +++ b/apps/sim/stores/terminal/console/index.ts @@ -1,5 +1,4 @@ -export { indexedDBStorage } from './storage' -export { useTerminalConsoleStore } from './store' +export { useConsoleEntry, useTerminalConsoleStore, useWorkflowConsoleEntries } from './store' export type { ConsoleEntry, ConsoleStore, ConsoleUpdate } from './types' export { normalizeConsoleError, @@ -8,4 +7,5 @@ export { safeConsoleStringify, TERMINAL_CONSOLE_LIMITS, trimConsoleEntries, + trimWorkflowConsoleEntries, } from './utils' diff --git a/apps/sim/stores/terminal/console/storage.ts b/apps/sim/stores/terminal/console/storage.ts index 1a809648f88..f85ff9284a0 100644 --- a/apps/sim/stores/terminal/console/storage.ts +++ b/apps/sim/stores/terminal/console/storage.ts @@ -1,22 +1,23 @@ import { createLogger } from '@sim/logger' import { del, get, set } from 'idb-keyval' -import type { StateStorage } from 'zustand/middleware' +import type { ConsoleEntry } from './types' const logger = createLogger('ConsoleStorage') const STORE_KEY = 'terminal-console-store' const MIGRATION_KEY = 'terminal-console-store-migrated' +const WRITE_DEBOUNCE_MS = 750 /** - * Promise that resolves when migration is complete. - * Used to ensure getItem waits for migration before reading. + * Shape of terminal console data persisted to IndexedDB. */ +export interface PersistedConsoleData { + workflowEntries: Record + isOpen: boolean +} + let migrationPromise: Promise | null = null -/** - * Migrates existing console data from localStorage to IndexedDB. - * Runs once on first load, then marks migration as complete. - */ async function migrateFromLocalStorage(): Promise { if (typeof window === 'undefined') return @@ -43,39 +44,107 @@ if (typeof window !== 'undefined') { }) } -export const indexedDBStorage: StateStorage = { - getItem: async (name: string): Promise => { - if (typeof window === 'undefined') return null +/** + * Loads persisted console data from IndexedDB. + * Handles three historical storage formats: + * 1. Zustand persist wrapper: `{ state: { entries: [...] }, version }` (original flat format) + * 2. Zustand persist wrapper: `{ state: { workflowEntries: {...} }, version }` (refactored format) + * 3. Raw data: `{ workflowEntries: {...}, isOpen }` (current format) + */ +export async function loadConsoleData(): Promise { + if (typeof window === 'undefined') return null - // Ensure migration completes before reading - if (migrationPromise) { - await migrationPromise - } + if (migrationPromise) { + await migrationPromise + } - try { - const value = await get(name) - return value ?? null - } catch (error) { - logger.warn('IndexedDB read failed', { name, error }) - return null - } - }, - - setItem: async (name: string, value: string): Promise => { - if (typeof window === 'undefined') return - try { - await set(name, value) - } catch (error) { - logger.warn('IndexedDB write failed', { name, error }) + try { + const raw = await get(STORE_KEY) + if (!raw) return null + + const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw + if (!parsed || typeof parsed !== 'object') return null + + const data = parsed.state ?? parsed + + if (Array.isArray(data.entries) && !data.workflowEntries) { + const workflowEntries: Record = {} + for (const entry of data.entries) { + if (!entry?.workflowId) continue + const wfId = entry.workflowId + if (!workflowEntries[wfId]) workflowEntries[wfId] = [] + workflowEntries[wfId].push(entry) + } + return { workflowEntries, isOpen: Boolean(data.isOpen) } } - }, - - removeItem: async (name: string): Promise => { - if (typeof window === 'undefined') return - try { - await del(name) - } catch (error) { - logger.warn('IndexedDB delete failed', { name, error }) + + return { + workflowEntries: data.workflowEntries ?? {}, + isOpen: Boolean(data.isOpen), } - }, + } catch (error) { + logger.warn('Failed to load console data from IndexedDB', { error }) + return null + } +} + +let pendingData: PersistedConsoleData | null = null +let writeTimer: ReturnType | null = null + +function executeWrite(): void { + writeTimer = null + const data = pendingData + pendingData = null + if (!data) return + + try { + const serialized = JSON.stringify(data) + set(STORE_KEY, serialized).catch((error) => { + logger.warn('IndexedDB write failed', { error }) + }) + } catch (error) { + logger.warn('Failed to serialize console data for persistence', { error }) + } +} + +/** + * Schedules a debounced write of console data to IndexedDB. + * Only stores a reference until the timer fires, so no serialization + * happens on the calling thread. + */ +export function scheduleConsolePersist(data: PersistedConsoleData): void { + if (typeof window === 'undefined') return + pendingData = data + if (writeTimer !== null) return + writeTimer = setTimeout(executeWrite, WRITE_DEBOUNCE_MS) +} + +/** + * Immediately flushes any pending console data to IndexedDB. + * Used on page hide to avoid data loss. + */ +export function flushConsolePersist(): void { + if (writeTimer !== null) { + clearTimeout(writeTimer) + } + executeWrite() +} + +/** + * Removes all persisted console data from IndexedDB. + */ +export async function clearPersistedConsoleData(): Promise { + if (typeof window === 'undefined') return + + if (writeTimer !== null) { + clearTimeout(writeTimer) + writeTimer = null + } + pendingData = null + + try { + await del(STORE_KEY) + } catch (error) { + logger.warn('IndexedDB delete failed', { error }) + } } diff --git a/apps/sim/stores/terminal/console/store.test.ts b/apps/sim/stores/terminal/console/store.test.ts index 1c8e3a3415f..50b836a7283 100644 --- a/apps/sim/stores/terminal/console/store.test.ts +++ b/apps/sim/stores/terminal/console/store.test.ts @@ -7,7 +7,9 @@ import { useTerminalConsoleStore } from '@/stores/terminal/console/store' describe('terminal console store', () => { beforeEach(() => { useTerminalConsoleStore.setState({ - entries: [], + workflowEntries: {}, + entryIdsByBlockExecution: {}, + entryLocationById: {}, isOpen: false, _hasHydrated: true, }) @@ -31,7 +33,7 @@ describe('terminal console store', () => { }, }) - const [entry] = useTerminalConsoleStore.getState().entries + const [entry] = useTerminalConsoleStore.getState().getWorkflowEntries('wf-1') expect(entry.output).toMatchObject({ __simTruncated: true, @@ -65,10 +67,49 @@ describe('terminal console store', () => { 'exec-1' ) - const [entry] = useTerminalConsoleStore.getState().entries + const [entry] = useTerminalConsoleStore.getState().getWorkflowEntries('wf-1') expect(entry.output).toMatchObject({ __simTruncated: true, }) }) + + it('updates one workflow without replacing unrelated workflow arrays', () => { + useTerminalConsoleStore.getState().addConsole({ + workflowId: 'wf-1', + blockId: 'block-1', + blockName: 'Function', + blockType: 'function', + executionId: 'exec-1', + executionOrder: 1, + output: { ok: true }, + }) + + useTerminalConsoleStore.getState().addConsole({ + workflowId: 'wf-2', + blockId: 'block-2', + blockName: 'Function', + blockType: 'function', + executionId: 'exec-2', + executionOrder: 1, + output: { ok: true }, + }) + + const before = useTerminalConsoleStore.getState() + const workflowTwoEntries = before.workflowEntries['wf-2'] + + useTerminalConsoleStore.getState().updateConsole( + 'block-1', + { + executionOrder: 1, + replaceOutput: { status: 'updated' }, + }, + 'exec-1' + ) + + const after = useTerminalConsoleStore.getState() + + expect(after.workflowEntries['wf-2']).toBe(workflowTwoEntries) + expect(after.getWorkflowEntries('wf-1')[0].output).toMatchObject({ status: 'updated' }) + }) }) diff --git a/apps/sim/stores/terminal/console/store.ts b/apps/sim/stores/terminal/console/store.ts index bd4a7d34917..de0c96895df 100644 --- a/apps/sim/stores/terminal/console/store.ts +++ b/apps/sim/stores/terminal/console/store.ts @@ -1,23 +1,34 @@ import { createLogger } from '@sim/logger' import { create } from 'zustand' -import { createJSONStorage, devtools, persist } from 'zustand/middleware' +import { devtools } from 'zustand/middleware' +import { useShallow } from 'zustand/react/shallow' import { redactApiKeys } from '@/lib/core/security/redaction' import { getQueryClient } from '@/app/_shell/providers/query-provider' import type { NormalizedBlockOutput } from '@/executor/types' import { type GeneralSettings, generalSettingsKeys } from '@/hooks/queries/general-settings' import { useExecutionStore } from '@/stores/execution' import { useNotificationStore } from '@/stores/notifications' -import { indexedDBStorage } from '@/stores/terminal/console/storage' -import type { ConsoleEntry, ConsoleStore, ConsoleUpdate } from '@/stores/terminal/console/types' +import { + flushConsolePersist, + loadConsoleData, + scheduleConsolePersist, +} from '@/stores/terminal/console/storage' +import type { + ConsoleEntry, + ConsoleEntryLocation, + ConsoleStore, + ConsoleUpdate, +} from '@/stores/terminal/console/types' import { normalizeConsoleError, normalizeConsoleInput, normalizeConsoleOutput, safeConsoleStringify, - trimConsoleEntries, + trimWorkflowConsoleEntries, } from '@/stores/terminal/console/utils' const logger = createLogger('TerminalConsoleStore') +const EMPTY_CONSOLE_ENTRIES: ConsoleEntry[] = [] const updateBlockOutput = ( existingOutput: NormalizedBlockOutput | undefined, @@ -63,6 +74,9 @@ const shouldSkipEntry = (output: any): boolean => { return false } +const getBlockExecutionKey = (blockId: string, executionId?: string): string => + `${executionId ?? 'no-execution'}:${blockId}` + const matchesEntryForUpdate = ( entry: ConsoleEntry, blockId: string, @@ -102,6 +116,140 @@ const matchesEntryForUpdate = ( return true } +function cloneWorkflowEntries( + workflowEntries: Record +): Record { + return { ...workflowEntries } +} + +function removeWorkflowIndexes( + workflowId: string, + entries: ConsoleEntry[], + entryIdsByBlockExecution: Record, + entryLocationById: Record +): void { + for (const entry of entries) { + delete entryLocationById[entry.id] + const blockExecutionKey = getBlockExecutionKey(entry.blockId, entry.executionId) + const existingIds = entryIdsByBlockExecution[blockExecutionKey] + if (!existingIds) { + continue + } + + const nextIds = existingIds.filter((entryId) => entryId !== entry.id) + if (nextIds.length === 0) { + delete entryIdsByBlockExecution[blockExecutionKey] + } else { + entryIdsByBlockExecution[blockExecutionKey] = nextIds + } + } +} + +function indexWorkflowEntries( + workflowId: string, + entries: ConsoleEntry[], + entryIdsByBlockExecution: Record, + entryLocationById: Record +): void { + entries.forEach((entry, index) => { + entryLocationById[entry.id] = { workflowId, index } + const blockExecutionKey = getBlockExecutionKey(entry.blockId, entry.executionId) + const existingIds = entryIdsByBlockExecution[blockExecutionKey] + if (existingIds) { + existingIds.push(entry.id) + } else { + entryIdsByBlockExecution[blockExecutionKey] = [entry.id] + } + }) +} + +function rebuildWorkflowStateMaps(workflowEntries: Record) { + const entryIdsByBlockExecution: Record = {} + const entryLocationById: Record = {} + + Object.entries(workflowEntries).forEach(([workflowId, entries]) => { + indexWorkflowEntries(workflowId, entries, entryIdsByBlockExecution, entryLocationById) + }) + + return { entryIdsByBlockExecution, entryLocationById } +} + +function replaceWorkflowEntries( + state: ConsoleStore, + workflowId: string, + nextEntries: ConsoleEntry[] +): Pick { + const workflowEntries = cloneWorkflowEntries(state.workflowEntries) + const entryIdsByBlockExecution = { ...state.entryIdsByBlockExecution } + const entryLocationById = { ...state.entryLocationById } + const previousEntries = workflowEntries[workflowId] ?? EMPTY_CONSOLE_ENTRIES + + removeWorkflowIndexes(workflowId, previousEntries, entryIdsByBlockExecution, entryLocationById) + + if (nextEntries.length === 0) { + delete workflowEntries[workflowId] + } else { + workflowEntries[workflowId] = nextEntries + indexWorkflowEntries(workflowId, nextEntries, entryIdsByBlockExecution, entryLocationById) + } + + return { workflowEntries, entryIdsByBlockExecution, entryLocationById } +} + +function patchWorkflowEntry( + state: ConsoleStore, + workflowId: string, + entryIndex: number, + updatedEntry: ConsoleEntry +): Pick { + const workflowEntries = cloneWorkflowEntries(state.workflowEntries) + const currentEntries = workflowEntries[workflowId] + if (!currentEntries) { + return { + workflowEntries, + entryIdsByBlockExecution: state.entryIdsByBlockExecution, + entryLocationById: state.entryLocationById, + } + } + + const nextEntries = [...currentEntries] + nextEntries[entryIndex] = updatedEntry + workflowEntries[workflowId] = nextEntries + + return { + workflowEntries, + entryIdsByBlockExecution: state.entryIdsByBlockExecution, + entryLocationById: state.entryLocationById, + } +} + +function appendWorkflowEntry( + state: ConsoleStore, + workflowId: string, + newEntry: ConsoleEntry, + trimmedEntries: ConsoleEntry[] +): Pick { + const workflowEntries = cloneWorkflowEntries(state.workflowEntries) + workflowEntries[workflowId] = trimmedEntries + + const entryLocationById = { ...state.entryLocationById } + const entryIdsByBlockExecution = { ...state.entryIdsByBlockExecution } + + trimmedEntries.forEach((entry, index) => { + entryLocationById[entry.id] = { workflowId, index } + }) + + const blockExecutionKey = getBlockExecutionKey(newEntry.blockId, newEntry.executionId) + const existingIds = entryIdsByBlockExecution[blockExecutionKey] + if (existingIds) { + entryIdsByBlockExecution[blockExecutionKey] = [...existingIds, newEntry.id] + } else { + entryIdsByBlockExecution[blockExecutionKey] = [newEntry.id] + } + + return { workflowEntries, entryIdsByBlockExecution, entryLocationById } +} + interface NotifyBlockErrorParams { error: unknown blockName: string @@ -109,9 +257,6 @@ interface NotifyBlockErrorParams { logContext: Record } -/** - * Sends an error notification for a block failure if error notifications are enabled. - */ const notifyBlockError = ({ error, blockName, workflowId, logContext }: NotifyBlockErrorParams) => { const settings = getQueryClient().getQueryData(generalSettingsKeys.settings()) const isErrorNotificationsEnabled = settings?.errorNotificationsEnabled ?? true @@ -142,326 +287,377 @@ const notifyBlockError = ({ error, blockName, workflowId, logContext }: NotifyBl } export const useTerminalConsoleStore = create()( - devtools( - persist( - (set, get) => ({ - entries: [], - isOpen: false, - _hasHydrated: false, - - setHasHydrated: (hasHydrated) => set({ _hasHydrated: hasHydrated }), - - addConsole: (entry: Omit) => { - if (shouldSkipEntry(entry.output)) { - return get().entries[0] as ConsoleEntry + devtools((set, get) => ({ + workflowEntries: {}, + entryIdsByBlockExecution: {}, + entryLocationById: {}, + isOpen: false, + _hasHydrated: false, + + setHasHydrated: (hasHydrated) => set({ _hasHydrated: hasHydrated }), + + addConsole: (entry: Omit) => { + if (shouldSkipEntry(entry.output)) { + return get().getWorkflowEntries(entry.workflowId)[0] as ConsoleEntry + } + + const redactedEntry = { ...entry } + if ( + !isStreamingOutput(entry.output) && + redactedEntry.output && + typeof redactedEntry.output === 'object' + ) { + redactedEntry.output = redactApiKeys(redactedEntry.output) + } + if (redactedEntry.input && typeof redactedEntry.input === 'object') { + redactedEntry.input = redactApiKeys(redactedEntry.input) + } + + const createdEntry: ConsoleEntry = { + ...redactedEntry, + id: crypto.randomUUID(), + timestamp: new Date().toISOString(), + input: normalizeConsoleInput(redactedEntry.input), + output: normalizeConsoleOutput(redactedEntry.output), + error: normalizeConsoleError(redactedEntry.error), + warning: + typeof redactedEntry.warning === 'string' + ? (normalizeConsoleError(redactedEntry.warning) ?? undefined) + : redactedEntry.warning, + } + + set((state) => { + const workflowEntries = state.workflowEntries[entry.workflowId] ?? EMPTY_CONSOLE_ENTRIES + const nextWorkflowEntries = trimWorkflowConsoleEntries([createdEntry, ...workflowEntries]) + return appendWorkflowEntry(state, entry.workflowId, createdEntry, nextWorkflowEntries) + }) + + if (createdEntry.error && createdEntry.blockType !== 'cancelled') { + notifyBlockError({ + error: createdEntry.error, + blockName: createdEntry.blockName || 'Unknown Block', + workflowId: entry.workflowId, + logContext: { entryId: createdEntry.id }, + }) + } + + return createdEntry + }, + + clearWorkflowConsole: (workflowId: string) => { + set((state) => replaceWorkflowEntries(state, workflowId, EMPTY_CONSOLE_ENTRIES)) + useExecutionStore.getState().clearRunPath(workflowId) + }, + + clearExecutionEntries: (executionId: string) => + set((state) => { + const nextWorkflowEntries = cloneWorkflowEntries(state.workflowEntries) + let didChange = false + + Object.entries(nextWorkflowEntries).forEach(([workflowId, entries]) => { + const filteredEntries = entries.filter((entry) => entry.executionId !== executionId) + if (filteredEntries.length !== entries.length) { + nextWorkflowEntries[workflowId] = filteredEntries + didChange = true + } + }) + + if (!didChange) { + return state + } + + const normalizedEntries = Object.fromEntries( + Object.entries(nextWorkflowEntries).filter(([, entries]) => entries.length > 0) + ) + + return { + workflowEntries: normalizedEntries, + ...rebuildWorkflowStateMaps(normalizedEntries), + } + }), + + exportConsoleCSV: (workflowId: string) => { + const entries = get().getWorkflowEntries(workflowId) + + if (entries.length === 0) { + return + } + + const formatCSVValue = (value: any): string => { + if (value === null || value === undefined) { + return '' + } + + let stringValue = typeof value === 'object' ? safeConsoleStringify(value) : String(value) + + if (stringValue.includes('"') || stringValue.includes(',') || stringValue.includes('\n')) { + stringValue = `"${stringValue.replace(/"/g, '""')}"` + } + + return stringValue + } + + const headers = [ + 'timestamp', + 'blockName', + 'blockType', + 'startedAt', + 'endedAt', + 'durationMs', + 'success', + 'input', + 'output', + 'error', + 'warning', + ] + + const csvRows = [ + headers.join(','), + ...entries.map((entry) => + [ + formatCSVValue(entry.timestamp), + formatCSVValue(entry.blockName), + formatCSVValue(entry.blockType), + formatCSVValue(entry.startedAt), + formatCSVValue(entry.endedAt), + formatCSVValue(entry.durationMs), + formatCSVValue(entry.success), + formatCSVValue(entry.input), + formatCSVValue(entry.output), + formatCSVValue(entry.error), + formatCSVValue(entry.warning), + ].join(',') + ), + ] + + const csvContent = csvRows.join('\n') + const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19) + const filename = `terminal-console-${workflowId}-${timestamp}.csv` + + const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }) + const link = document.createElement('a') + + if (link.download !== undefined) { + const url = URL.createObjectURL(blob) + link.setAttribute('href', url) + link.setAttribute('download', filename) + link.style.visibility = 'hidden' + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + URL.revokeObjectURL(url) + } + }, + + getWorkflowEntries: (workflowId) => { + return get().workflowEntries[workflowId] ?? EMPTY_CONSOLE_ENTRIES + }, + + toggleConsole: () => { + set((state) => ({ isOpen: !state.isOpen })) + }, + + updateConsole: (blockId: string, update: string | ConsoleUpdate, executionId?: string) => { + set((state) => { + const candidateIds = + state.entryIdsByBlockExecution[getBlockExecutionKey(blockId, executionId)] ?? [] + if (candidateIds.length === 0) { + return state + } + + const workflowId = state.entryLocationById[candidateIds[0]]?.workflowId + if (!workflowId) { + return state + } + + const workflowEntries = state.workflowEntries[workflowId] ?? EMPTY_CONSOLE_ENTRIES + + for (const candidateId of candidateIds) { + const location = state.entryLocationById[candidateId] + if (!location || location.workflowId !== workflowId) continue + + const entry = workflowEntries[location.index] + if (!entry || entry.id !== candidateId) continue + if (!matchesEntryForUpdate(entry, blockId, executionId, update)) continue + + if (typeof update === 'string') { + const newOutput = normalizeConsoleOutput(updateBlockOutput(entry.output, update)) + return patchWorkflowEntry(state, workflowId, location.index, { + ...entry, + output: newOutput, + }) + } + + const updatedEntry = { ...entry } + + if (update.content !== undefined) { + updatedEntry.output = normalizeConsoleOutput( + updateBlockOutput(entry.output, update.content) + ) } - const redactedEntry = { ...entry } - if ( - !isStreamingOutput(entry.output) && - redactedEntry.output && - typeof redactedEntry.output === 'object' - ) { - redactedEntry.output = redactApiKeys(redactedEntry.output) + if (update.replaceOutput !== undefined) { + const redactedOutput = + typeof update.replaceOutput === 'object' && update.replaceOutput !== null + ? redactApiKeys(update.replaceOutput) + : update.replaceOutput + updatedEntry.output = normalizeConsoleOutput(redactedOutput) + } else if (update.output !== undefined) { + const mergedOutput = { + ...(entry.output || {}), + ...update.output, + } + updatedEntry.output = + typeof mergedOutput === 'object' + ? normalizeConsoleOutput(redactApiKeys(mergedOutput)) + : normalizeConsoleOutput(mergedOutput) } - if (redactedEntry.input && typeof redactedEntry.input === 'object') { - redactedEntry.input = redactApiKeys(redactedEntry.input) + + if (update.error !== undefined) { + updatedEntry.error = normalizeConsoleError(update.error) } - const createdEntry: ConsoleEntry = { - ...redactedEntry, - id: crypto.randomUUID(), - timestamp: new Date().toISOString(), - input: normalizeConsoleInput(redactedEntry.input), - output: normalizeConsoleOutput(redactedEntry.output), - error: normalizeConsoleError(redactedEntry.error), - warning: - typeof redactedEntry.warning === 'string' - ? (normalizeConsoleError(redactedEntry.warning) ?? undefined) - : redactedEntry.warning, + if (update.warning !== undefined) { + updatedEntry.warning = normalizeConsoleError(update.warning) ?? undefined } - set((state) => { - return { entries: trimConsoleEntries([createdEntry, ...state.entries]) } - }) + if (update.success !== undefined) { + updatedEntry.success = update.success + } - if (createdEntry.error && createdEntry.blockType !== 'cancelled') { - notifyBlockError({ - error: createdEntry.error, - blockName: createdEntry.blockName || 'Unknown Block', - workflowId: entry.workflowId, - logContext: { entryId: createdEntry.id }, - }) + if (update.startedAt !== undefined) { + updatedEntry.startedAt = update.startedAt } - return createdEntry - }, + if (update.endedAt !== undefined) { + updatedEntry.endedAt = update.endedAt + } - clearWorkflowConsole: (workflowId: string) => { - set((state) => ({ - entries: state.entries.filter((entry) => entry.workflowId !== workflowId), - })) - useExecutionStore.getState().clearRunPath(workflowId) - }, + if (update.durationMs !== undefined) { + updatedEntry.durationMs = update.durationMs + } - clearExecutionEntries: (executionId: string) => - set((state) => ({ - entries: state.entries.filter((e) => e.executionId !== executionId), - })), + if (update.input !== undefined) { + updatedEntry.input = + typeof update.input === 'object' && update.input !== null + ? normalizeConsoleInput(redactApiKeys(update.input)) + : normalizeConsoleInput(update.input) + } - exportConsoleCSV: (workflowId: string) => { - const entries = get().entries.filter((entry) => entry.workflowId === workflowId) + if (update.isRunning !== undefined) { + updatedEntry.isRunning = update.isRunning + } - if (entries.length === 0) { - return + if (update.isCanceled !== undefined) { + updatedEntry.isCanceled = update.isCanceled } - const formatCSVValue = (value: any): string => { - if (value === null || value === undefined) { - return '' - } + if (update.iterationCurrent !== undefined) { + updatedEntry.iterationCurrent = update.iterationCurrent + } - let stringValue = - typeof value === 'object' ? safeConsoleStringify(value) : String(value) + if (update.iterationTotal !== undefined) { + updatedEntry.iterationTotal = update.iterationTotal + } - if ( - stringValue.includes('"') || - stringValue.includes(',') || - stringValue.includes('\n') - ) { - stringValue = `"${stringValue.replace(/"/g, '""')}"` - } + if (update.iterationType !== undefined) { + updatedEntry.iterationType = update.iterationType + } - return stringValue + if (update.iterationContainerId !== undefined) { + updatedEntry.iterationContainerId = update.iterationContainerId } - const headers = [ - 'timestamp', - 'blockName', - 'blockType', - 'startedAt', - 'endedAt', - 'durationMs', - 'success', - 'input', - 'output', - 'error', - 'warning', - ] - - const csvRows = [ - headers.join(','), - ...entries.map((entry) => - [ - formatCSVValue(entry.timestamp), - formatCSVValue(entry.blockName), - formatCSVValue(entry.blockType), - formatCSVValue(entry.startedAt), - formatCSVValue(entry.endedAt), - formatCSVValue(entry.durationMs), - formatCSVValue(entry.success), - formatCSVValue(entry.input), - formatCSVValue(entry.output), - formatCSVValue(entry.error), - formatCSVValue(entry.warning), - ].join(',') - ), - ] - - const csvContent = csvRows.join('\n') - const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19) - const filename = `terminal-console-${workflowId}-${timestamp}.csv` - - const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }) - const link = document.createElement('a') - - if (link.download !== undefined) { - const url = URL.createObjectURL(blob) - link.setAttribute('href', url) - link.setAttribute('download', filename) - link.style.visibility = 'hidden' - document.body.appendChild(link) - link.click() - document.body.removeChild(link) - URL.revokeObjectURL(url) + if (update.parentIterations !== undefined) { + updatedEntry.parentIterations = update.parentIterations } - }, - - getWorkflowEntries: (workflowId) => { - return get().entries.filter((entry) => entry.workflowId === workflowId) - }, - - toggleConsole: () => { - set((state) => ({ isOpen: !state.isOpen })) - }, - - updateConsole: (blockId: string, update: string | ConsoleUpdate, executionId?: string) => { - set((state) => { - const updatedEntries = state.entries.map((entry) => { - if (!matchesEntryForUpdate(entry, blockId, executionId, update)) { - return entry - } - - if (typeof update === 'string') { - const newOutput = normalizeConsoleOutput(updateBlockOutput(entry.output, update)) - return { ...entry, output: newOutput } - } - - const updatedEntry = { ...entry } - - if (update.content !== undefined) { - updatedEntry.output = normalizeConsoleOutput( - updateBlockOutput(entry.output, update.content) - ) - } - - if (update.replaceOutput !== undefined) { - const redactedOutput = - typeof update.replaceOutput === 'object' && update.replaceOutput !== null - ? redactApiKeys(update.replaceOutput) - : update.replaceOutput - updatedEntry.output = normalizeConsoleOutput(redactedOutput) - } else if (update.output !== undefined) { - const mergedOutput = { - ...(entry.output || {}), - ...update.output, - } - updatedEntry.output = - typeof mergedOutput === 'object' - ? normalizeConsoleOutput(redactApiKeys(mergedOutput)) - : normalizeConsoleOutput(mergedOutput) - } - - if (update.error !== undefined) { - updatedEntry.error = normalizeConsoleError(update.error) - } - - if (update.warning !== undefined) { - updatedEntry.warning = normalizeConsoleError(update.warning) ?? undefined - } - - if (update.success !== undefined) { - updatedEntry.success = update.success - } - - if (update.startedAt !== undefined) { - updatedEntry.startedAt = update.startedAt - } - - if (update.endedAt !== undefined) { - updatedEntry.endedAt = update.endedAt - } - - if (update.durationMs !== undefined) { - updatedEntry.durationMs = update.durationMs - } - - if (update.input !== undefined) { - updatedEntry.input = - typeof update.input === 'object' && update.input !== null - ? normalizeConsoleInput(redactApiKeys(update.input)) - : normalizeConsoleInput(update.input) - } - - if (update.isRunning !== undefined) { - updatedEntry.isRunning = update.isRunning - } - - if (update.isCanceled !== undefined) { - updatedEntry.isCanceled = update.isCanceled - } - - if (update.iterationCurrent !== undefined) { - updatedEntry.iterationCurrent = update.iterationCurrent - } - - if (update.iterationTotal !== undefined) { - updatedEntry.iterationTotal = update.iterationTotal - } - - if (update.iterationType !== undefined) { - updatedEntry.iterationType = update.iterationType - } - - if (update.iterationContainerId !== undefined) { - updatedEntry.iterationContainerId = update.iterationContainerId - } - - if (update.parentIterations !== undefined) { - updatedEntry.parentIterations = update.parentIterations - } - - if (update.childWorkflowBlockId !== undefined) { - updatedEntry.childWorkflowBlockId = update.childWorkflowBlockId - } - - if (update.childWorkflowName !== undefined) { - updatedEntry.childWorkflowName = update.childWorkflowName - } - - if (update.childWorkflowInstanceId !== undefined) { - updatedEntry.childWorkflowInstanceId = update.childWorkflowInstanceId - } - - return updatedEntry - }) - return { entries: trimConsoleEntries(updatedEntries) } - }) + if (update.childWorkflowBlockId !== undefined) { + updatedEntry.childWorkflowBlockId = update.childWorkflowBlockId + } - if (typeof update === 'object' && update.error) { - const matchingEntry = get().entries.find( - (e) => e.blockId === blockId && e.executionId === executionId - ) - notifyBlockError({ - error: update.error, - blockName: matchingEntry?.blockName || 'Unknown Block', - workflowId: matchingEntry?.workflowId, - logContext: { blockId }, - }) + if (update.childWorkflowName !== undefined) { + updatedEntry.childWorkflowName = update.childWorkflowName } - }, - - cancelRunningEntries: (workflowId: string) => { - set((state) => { - const now = new Date() - const updatedEntries = state.entries.map((entry) => { - if (entry.workflowId === workflowId && entry.isRunning) { - const durationMs = entry.startedAt - ? now.getTime() - new Date(entry.startedAt).getTime() - : entry.durationMs - return { - ...entry, - isRunning: false, - isCanceled: true, - endedAt: now.toISOString(), - durationMs, - } - } - return entry - }) - return { entries: trimConsoleEntries(updatedEntries) } - }) - }, - }), - { - name: 'terminal-console-store', - storage: createJSONStorage(() => indexedDBStorage), - partialize: (state) => ({ - entries: state.entries, - isOpen: state.isOpen, - }), - onRehydrateStorage: () => (_state, error) => { - if (error) { - logger.error('Failed to rehydrate console store', { error }) + + if (update.childWorkflowInstanceId !== undefined) { + updatedEntry.childWorkflowInstanceId = update.childWorkflowInstanceId + } + + return patchWorkflowEntry(state, workflowId, location.index, updatedEntry) + } + + return state + }) + + if (typeof update === 'object' && update.error) { + const matchingEntry = get() + .getWorkflowEntries( + get().entryLocationById[ + (get().entryIdsByBlockExecution[getBlockExecutionKey(blockId, executionId)] ?? + [])[0] ?? '' + ]?.workflowId ?? '' + ) + .find((entry) => matchesEntryForUpdate(entry, blockId, executionId, update)) + notifyBlockError({ + error: update.error, + blockName: matchingEntry?.blockName || 'Unknown Block', + workflowId: matchingEntry?.workflowId, + logContext: { blockId }, + }) + } + }, + + cancelRunningEntries: (workflowId: string) => { + set((state) => { + const now = new Date() + const workflowEntries = state.workflowEntries[workflowId] ?? EMPTY_CONSOLE_ENTRIES + let didChange = false + const updatedEntries = workflowEntries.map((entry) => { + if (entry.workflowId === workflowId && entry.isRunning) { + didChange = true + const durationMs = entry.startedAt + ? now.getTime() - new Date(entry.startedAt).getTime() + : entry.durationMs + return { + ...entry, + isRunning: false, + isCanceled: true, + endedAt: now.toISOString(), + durationMs, + } } - }, - merge: (persistedState, currentState) => { - const persisted = persistedState as Partial | undefined - const rawEntries = persisted?.entries ?? currentState.entries - const oneHourAgo = Date.now() - 60 * 60 * 1000 + return entry + }) + if (!didChange) { + return state + } + return replaceWorkflowEntries(state, workflowId, updatedEntries) + }) + }, + })) +) - const entries = rawEntries.map((entry, index) => { +/** + * Hydrates the console store from IndexedDB on startup. + * Applies the same normalization and trimming as the old persist merge. + */ +async function hydrateConsoleStore(): Promise { + try { + const data = await loadConsoleData() + + if (!data) { + useTerminalConsoleStore.setState({ _hasHydrated: true }) + return + } + + const oneHourAgo = Date.now() - 60 * 60 * 1000 + + const workflowEntries = Object.fromEntries( + Object.entries(data.workflowEntries).map(([workflowId, entries]) => [ + workflowId, + trimWorkflowConsoleEntries( + entries.map((entry, index) => { let updated = entry if (entry.executionOrder === undefined) { updated = { ...updated, executionOrder: index + 1 } @@ -485,24 +681,59 @@ export const useTerminalConsoleStore = create()( } return updated }) - - return { - ...currentState, - entries: trimConsoleEntries(entries), - isOpen: persisted?.isOpen ?? currentState.isOpen, - } - }, - } + ), + ]) ) - ) -) -if (typeof window !== 'undefined') { - useTerminalConsoleStore.persist.onFinishHydration(() => { + useTerminalConsoleStore.setState({ + workflowEntries, + ...rebuildWorkflowStateMaps(workflowEntries), + isOpen: data.isOpen, + _hasHydrated: true, + }) + } catch (error) { + logger.error('Failed to hydrate console store', { error }) useTerminalConsoleStore.setState({ _hasHydrated: true }) + } +} + +if (typeof window !== 'undefined') { + hydrateConsoleStore() + + useTerminalConsoleStore.subscribe((state) => { + if (!state._hasHydrated) return + scheduleConsolePersist({ + workflowEntries: state.workflowEntries, + isOpen: state.isOpen, + }) }) - if (useTerminalConsoleStore.persist.hasHydrated()) { - useTerminalConsoleStore.setState({ _hasHydrated: true }) - } + window.addEventListener('pagehide', flushConsolePersist) +} + +export function useWorkflowConsoleEntries(workflowId?: string): ConsoleEntry[] { + return useTerminalConsoleStore( + useShallow((state) => { + if (!workflowId) { + return EMPTY_CONSOLE_ENTRIES + } + + return state.workflowEntries[workflowId] ?? EMPTY_CONSOLE_ENTRIES + }) + ) +} + +export function useConsoleEntry(entryId?: string | null): ConsoleEntry | null { + return useTerminalConsoleStore((state) => { + if (!entryId) { + return null + } + + const location = state.entryLocationById[entryId] + if (!location) { + return null + } + + return state.workflowEntries[location.workflowId]?.[location.index] ?? null + }) } diff --git a/apps/sim/stores/terminal/console/types.ts b/apps/sim/stores/terminal/console/types.ts index 80610cc8d82..c9784b91fa4 100644 --- a/apps/sim/stores/terminal/console/types.ts +++ b/apps/sim/stores/terminal/console/types.ts @@ -58,8 +58,15 @@ export interface ConsoleUpdate { childWorkflowInstanceId?: string } +export interface ConsoleEntryLocation { + workflowId: string + index: number +} + export interface ConsoleStore { - entries: ConsoleEntry[] + workflowEntries: Record + entryIdsByBlockExecution: Record + entryLocationById: Record isOpen: boolean addConsole: (entry: Omit) => ConsoleEntry clearWorkflowConsole: (workflowId: string) => void diff --git a/apps/sim/stores/terminal/console/utils.ts b/apps/sim/stores/terminal/console/utils.ts index 1ecd63a29ce..659d0e74aaf 100644 --- a/apps/sim/stores/terminal/console/utils.ts +++ b/apps/sim/stores/terminal/console/utils.ts @@ -219,7 +219,7 @@ export function normalizeConsoleError(error: unknown): string | null | undefined /** * Returns a workflow's entries trimmed to the configured cap. */ -function trimWorkflowEntries(entries: ConsoleEntry[]): ConsoleEntry[] { +export function trimWorkflowConsoleEntries(entries: ConsoleEntry[]): ConsoleEntry[] { if (entries.length <= TERMINAL_CONSOLE_LIMITS.MAX_ENTRIES_PER_WORKFLOW) { return entries } @@ -287,7 +287,7 @@ export function trimConsoleEntries(entries: ConsoleEntry[]): ConsoleEntry[] { const keptEntryIds = new Set() for (const workflowEntries of workflowGroups.values()) { - trimWorkflowEntries(workflowEntries).forEach((entry) => keptEntryIds.add(entry.id)) + trimWorkflowConsoleEntries(workflowEntries).forEach((entry) => keptEntryIds.add(entry.id)) } return entries.filter((entry) => keptEntryIds.has(entry.id)) diff --git a/apps/sim/stores/terminal/index.ts b/apps/sim/stores/terminal/index.ts index e17a21fe90a..37c91bf0b17 100644 --- a/apps/sim/stores/terminal/index.ts +++ b/apps/sim/stores/terminal/index.ts @@ -6,7 +6,10 @@ export { safeConsoleStringify, TERMINAL_CONSOLE_LIMITS, trimConsoleEntries, + trimWorkflowConsoleEntries, + useConsoleEntry, useTerminalConsoleStore, + useWorkflowConsoleEntries, } from './console' export { useTerminalStore } from './store' export type { TerminalState } from './types' From 28801d9a410c31d2f02287cc2644885557c19ddf Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 26 Mar 2026 15:08:10 -0700 Subject: [PATCH 03/17] make persistence execution scoped not debounce --- .../[workspaceId]/home/hooks/use-chat.ts | 3 +- .../hooks/use-workflow-execution.ts | 18 ++- .../copilot/client-sse/run-tool-execution.ts | 3 + apps/sim/stores/index.ts | 10 +- apps/sim/stores/terminal/console/index.ts | 1 + apps/sim/stores/terminal/console/storage.ts | 130 +++++++++++------- apps/sim/stores/terminal/console/store.ts | 44 +++--- apps/sim/stores/terminal/console/types.ts | 1 - apps/sim/stores/terminal/index.ts | 1 + 9 files changed, 143 insertions(+), 68 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts index b2667577992..ebea4c9dc3a 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts @@ -33,7 +33,7 @@ import { useExecutionStream } from '@/hooks/use-execution-stream' import { useExecutionStore } from '@/stores/execution/store' import { useFolderStore } from '@/stores/folders/store' import type { ChatContext } from '@/stores/panel' -import { useTerminalConsoleStore } from '@/stores/terminal' +import { consolePersistence, useTerminalConsoleStore } from '@/stores/terminal' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import type { ChatMessage, @@ -1512,6 +1512,7 @@ export function useChat( }) executionStream.cancel(workflowId) + consolePersistence.executionEnded() execState.setIsExecuting(workflowId, false) execState.setIsDebugging(workflowId, false) execState.setActiveBlocks(workflowId, new Set()) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts index 04d3a112c14..a2bb0f45f84 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts @@ -36,7 +36,7 @@ import { useCurrentWorkflowExecution, useExecutionStore } from '@/stores/executi import { useNotificationStore } from '@/stores/notifications' import { useVariablesStore } from '@/stores/panel' import { useEnvironmentStore } from '@/stores/settings/environment' -import { useTerminalConsoleStore } from '@/stores/terminal' +import { consolePersistence, useTerminalConsoleStore } from '@/stores/terminal' import { useWorkflowDiffStore } from '@/stores/workflow-diff' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { mergeSubblockState } from '@/stores/workflows/utils' @@ -123,7 +123,19 @@ export function useWorkflowExecution() { useCurrentWorkflowExecution() const setCurrentExecutionId = useExecutionStore((s) => s.setCurrentExecutionId) const getCurrentExecutionId = useExecutionStore((s) => s.getCurrentExecutionId) - const setIsExecuting = useExecutionStore((s) => s.setIsExecuting) + const rawSetIsExecuting = useExecutionStore((s) => s.setIsExecuting) + + const setIsExecuting = useCallback( + (workflowId: string, executing: boolean) => { + if (executing) { + consolePersistence.executionStarted() + } else { + consolePersistence.executionEnded() + } + rawSetIsExecuting(workflowId, executing) + }, + [rawSetIsExecuting] + ) const setIsDebugging = useExecutionStore((s) => s.setIsDebugging) const setPendingBlocks = useExecutionStore((s) => s.setPendingBlocks) const setExecutor = useExecutionStore((s) => s.setExecutor) @@ -1804,6 +1816,7 @@ export function useWorkflowExecution() { ) if (otherExecutionIds.size > 0) { cancelRunningEntries(activeWorkflowId) + consolePersistence.persist() } setCurrentExecutionId(activeWorkflowId, executionId) @@ -1965,6 +1978,7 @@ export function useWorkflowExecution() { for (const entry of originalEntries) { addConsole(entry) } + consolePersistence.persist() } } // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/apps/sim/lib/copilot/client-sse/run-tool-execution.ts b/apps/sim/lib/copilot/client-sse/run-tool-execution.ts index 2d28d149beb..13d087cb9cb 100644 --- a/apps/sim/lib/copilot/client-sse/run-tool-execution.ts +++ b/apps/sim/lib/copilot/client-sse/run-tool-execution.ts @@ -4,6 +4,7 @@ import { COPILOT_CONFIRM_API_PATH } from '@/lib/copilot/constants' import { ClientToolCallState } from '@/lib/copilot/tools/client/tool-display-registry' import { executeWorkflowWithFullLogging } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils' import { useExecutionStore } from '@/stores/execution/store' +import { consolePersistence } from '@/stores/terminal' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' const logger = createLogger('CopilotRunToolExecution') @@ -148,6 +149,7 @@ async function doExecuteRunTool( const abortController = new AbortController() activeRunAbortByWorkflowId.set(targetWorkflowId, abortController) + consolePersistence.executionStarted() setIsExecuting(targetWorkflowId, true) const executionId = uuidv4() setCurrentExecutionId(targetWorkflowId, executionId) @@ -241,6 +243,7 @@ async function doExecuteRunTool( } const { setCurrentExecutionId: clearExecId } = useExecutionStore.getState() clearExecId(targetWorkflowId, null) + consolePersistence.executionEnded() setIsExecuting(targetWorkflowId, false) } } diff --git a/apps/sim/stores/index.ts b/apps/sim/stores/index.ts index dbf926c02ed..4ae3c335f07 100644 --- a/apps/sim/stores/index.ts +++ b/apps/sim/stores/index.ts @@ -5,7 +5,7 @@ import { createLogger } from '@sim/logger' import { useExecutionStore } from '@/stores/execution' import { useVariablesStore } from '@/stores/panel' import { useEnvironmentStore } from '@/stores/settings/environment' -import { useTerminalConsoleStore } from '@/stores/terminal' +import { consolePersistence, useTerminalConsoleStore } from '@/stores/terminal' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' @@ -217,7 +217,13 @@ export const resetAllStores = () => { useSubBlockStore.getState().clear() useEnvironmentStore.getState().reset() useExecutionStore.getState().reset() - useTerminalConsoleStore.setState({ entries: [], isOpen: false }) + useTerminalConsoleStore.setState({ + workflowEntries: {}, + entryIdsByBlockExecution: {}, + entryLocationById: {}, + isOpen: false, + }) + consolePersistence.persist() // Custom tools are managed by React Query cache, not a Zustand store // Variables store has no tracking to reset; registry hydrates } diff --git a/apps/sim/stores/terminal/console/index.ts b/apps/sim/stores/terminal/console/index.ts index f87d536291f..83e72289b29 100644 --- a/apps/sim/stores/terminal/console/index.ts +++ b/apps/sim/stores/terminal/console/index.ts @@ -1,3 +1,4 @@ +export { consolePersistence } from './storage' export { useConsoleEntry, useTerminalConsoleStore, useWorkflowConsoleEntries } from './store' export type { ConsoleEntry, ConsoleStore, ConsoleUpdate } from './types' export { diff --git a/apps/sim/stores/terminal/console/storage.ts b/apps/sim/stores/terminal/console/storage.ts index f85ff9284a0..698e9051666 100644 --- a/apps/sim/stores/terminal/console/storage.ts +++ b/apps/sim/stores/terminal/console/storage.ts @@ -1,12 +1,18 @@ import { createLogger } from '@sim/logger' -import { del, get, set } from 'idb-keyval' +import { get, set } from 'idb-keyval' import type { ConsoleEntry } from './types' const logger = createLogger('ConsoleStorage') const STORE_KEY = 'terminal-console-store' const MIGRATION_KEY = 'terminal-console-store-migrated' -const WRITE_DEBOUNCE_MS = 750 + +/** + * Safety-net interval for persisting during very long executions. + * Only fires while an execution is active. Much longer than a debounce + * because intermediate writes during execution are low-value. + */ +const LONG_EXECUTION_PERSIST_INTERVAL_MS = 30_000 /** * Shape of terminal console data persisted to IndexedDB. @@ -88,63 +94,95 @@ export async function loadConsoleData(): Promise { } } -let pendingData: PersistedConsoleData | null = null -let writeTimer: ReturnType | null = null +let writeSequence = 0 +let activeWrite: Promise | null = null -function executeWrite(): void { - writeTimer = null - const data = pendingData - pendingData = null - if (!data) return +function writeToIndexedDB(data: PersistedConsoleData): void { + const seq = ++writeSequence - try { - const serialized = JSON.stringify(data) - set(STORE_KEY, serialized).catch((error) => { + const doWrite = async () => { + try { + const serialized = JSON.stringify(data) + if (seq !== writeSequence) return + await set(STORE_KEY, serialized) + } catch (error) { logger.warn('IndexedDB write failed', { error }) - }) - } catch (error) { - logger.warn('Failed to serialize console data for persistence', { error }) + } } -} -/** - * Schedules a debounced write of console data to IndexedDB. - * Only stores a reference until the timer fires, so no serialization - * happens on the calling thread. - */ -export function scheduleConsolePersist(data: PersistedConsoleData): void { - if (typeof window === 'undefined') return - pendingData = data - if (writeTimer !== null) return - writeTimer = setTimeout(executeWrite, WRITE_DEBOUNCE_MS) + activeWrite = (activeWrite ?? Promise.resolve()).then(doWrite) } /** - * Immediately flushes any pending console data to IndexedDB. - * Used on page hide to avoid data loss. + * Execution-aware persistence manager for the terminal console store. + * + * Writes happen only at meaningful lifecycle boundaries: + * - When an execution ends (success, error, cancel) + * - On explicit user actions (clear console) + * - On page hide (crash safety) + * - Every 30s during very long active executions (safety net) + * + * During normal execution, no serialization or IndexedDB writes occur, + * keeping the hot path completely free of persistence overhead. */ -export function flushConsolePersist(): void { - if (writeTimer !== null) { - clearTimeout(writeTimer) +class ConsolePersistenceManager { + private dataProvider: (() => PersistedConsoleData) | null = null + private safetyTimer: ReturnType | null = null + private activeExecutions = 0 + + /** + * Binds the data provider function used to snapshot current state. + * Called once during store initialization. + */ + bind(provider: () => PersistedConsoleData): void { + this.dataProvider = provider } - executeWrite() -} -/** - * Removes all persisted console data from IndexedDB. - */ -export async function clearPersistedConsoleData(): Promise { - if (typeof window === 'undefined') return + /** + * Signals that a workflow execution has started. + * Starts the long-execution safety-net timer if this is the first active execution. + */ + executionStarted(): void { + this.activeExecutions++ + if (this.activeExecutions === 1) { + this.startSafetyTimer() + } + } - if (writeTimer !== null) { - clearTimeout(writeTimer) - writeTimer = null + /** + * Signals that a workflow execution has ended (success, error, or cancel). + * Triggers an immediate persist and stops the safety timer if no executions remain. + */ + executionEnded(): void { + this.activeExecutions = Math.max(0, this.activeExecutions - 1) + this.persist() + if (this.activeExecutions === 0) { + this.stopSafetyTimer() + } } - pendingData = null - try { - await del(STORE_KEY) - } catch (error) { - logger.warn('IndexedDB delete failed', { error }) + /** + * Triggers an immediate persist. Used for explicit user actions + * like clearing the console, and for page-hide durability. + */ + persist(): void { + if (!this.dataProvider) return + writeToIndexedDB(this.dataProvider()) + } + + private startSafetyTimer(): void { + this.stopSafetyTimer() + this.safetyTimer = setInterval(() => { + this.persist() + }, LONG_EXECUTION_PERSIST_INTERVAL_MS) + } + + private stopSafetyTimer(): void { + if (this.safetyTimer !== null) { + clearInterval(this.safetyTimer) + this.safetyTimer = null + } } } + +export const consolePersistence = new ConsolePersistenceManager() diff --git a/apps/sim/stores/terminal/console/store.ts b/apps/sim/stores/terminal/console/store.ts index de0c96895df..e4989b1c0da 100644 --- a/apps/sim/stores/terminal/console/store.ts +++ b/apps/sim/stores/terminal/console/store.ts @@ -8,11 +8,7 @@ import type { NormalizedBlockOutput } from '@/executor/types' import { type GeneralSettings, generalSettingsKeys } from '@/hooks/queries/general-settings' import { useExecutionStore } from '@/stores/execution' import { useNotificationStore } from '@/stores/notifications' -import { - flushConsolePersist, - loadConsoleData, - scheduleConsolePersist, -} from '@/stores/terminal/console/storage' +import { consolePersistence, loadConsoleData } from '@/stores/terminal/console/storage' import type { ConsoleEntry, ConsoleEntryLocation, @@ -294,8 +290,6 @@ export const useTerminalConsoleStore = create()( isOpen: false, _hasHydrated: false, - setHasHydrated: (hasHydrated) => set({ _hasHydrated: hasHydrated }), - addConsole: (entry: Omit) => { if (shouldSkipEntry(entry.output)) { return get().getWorkflowEntries(entry.workflowId)[0] as ConsoleEntry @@ -347,6 +341,7 @@ export const useTerminalConsoleStore = create()( clearWorkflowConsole: (workflowId: string) => { set((state) => replaceWorkflowEntries(state, workflowId, EMPTY_CONSOLE_ENTRIES)) useExecutionStore.getState().clearRunPath(workflowId) + consolePersistence.persist() }, clearExecutionEntries: (executionId: string) => @@ -685,9 +680,26 @@ async function hydrateConsoleStore(): Promise { ]) ) + const currentState = useTerminalConsoleStore.getState() + const mergedWorkflowEntries = { ...workflowEntries } + + for (const [wfId, currentEntries] of Object.entries(currentState.workflowEntries)) { + if (currentEntries.length > 0) { + const persistedEntries = mergedWorkflowEntries[wfId] ?? [] + const persistedIds = new Set(persistedEntries.map((e) => e.id)) + const newEntries = currentEntries.filter((e) => !persistedIds.has(e.id)) + if (newEntries.length > 0) { + mergedWorkflowEntries[wfId] = trimWorkflowConsoleEntries([ + ...newEntries, + ...persistedEntries, + ]) + } + } + } + useTerminalConsoleStore.setState({ - workflowEntries, - ...rebuildWorkflowStateMaps(workflowEntries), + workflowEntries: mergedWorkflowEntries, + ...rebuildWorkflowStateMaps(mergedWorkflowEntries), isOpen: data.isOpen, _hasHydrated: true, }) @@ -698,17 +710,17 @@ async function hydrateConsoleStore(): Promise { } if (typeof window !== 'undefined') { - hydrateConsoleStore() - - useTerminalConsoleStore.subscribe((state) => { - if (!state._hasHydrated) return - scheduleConsolePersist({ + consolePersistence.bind(() => { + const state = useTerminalConsoleStore.getState() + return { workflowEntries: state.workflowEntries, isOpen: state.isOpen, - }) + } }) - window.addEventListener('pagehide', flushConsolePersist) + hydrateConsoleStore() + + window.addEventListener('pagehide', () => consolePersistence.persist()) } export function useWorkflowConsoleEntries(workflowId?: string): ConsoleEntry[] { diff --git a/apps/sim/stores/terminal/console/types.ts b/apps/sim/stores/terminal/console/types.ts index c9784b91fa4..5e6a107b900 100644 --- a/apps/sim/stores/terminal/console/types.ts +++ b/apps/sim/stores/terminal/console/types.ts @@ -77,5 +77,4 @@ export interface ConsoleStore { updateConsole: (blockId: string, update: string | ConsoleUpdate, executionId?: string) => void cancelRunningEntries: (workflowId: string) => void _hasHydrated: boolean - setHasHydrated: (hasHydrated: boolean) => void } diff --git a/apps/sim/stores/terminal/index.ts b/apps/sim/stores/terminal/index.ts index 37c91bf0b17..d580e7ee65c 100644 --- a/apps/sim/stores/terminal/index.ts +++ b/apps/sim/stores/terminal/index.ts @@ -1,5 +1,6 @@ export type { ConsoleEntry, ConsoleStore, ConsoleUpdate } from './console' export { + consolePersistence, normalizeConsoleError, normalizeConsoleInput, normalizeConsoleOutput, From 09dd870b3b8b228d4fd85cd6e1d8b0bf3e02ad8a Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 26 Mar 2026 15:39:53 -0700 Subject: [PATCH 04/17] revert feature flags --- apps/sim/lib/core/config/feature-flags.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/sim/lib/core/config/feature-flags.ts b/apps/sim/lib/core/config/feature-flags.ts index ba3d2bab808..d5fa530e52b 100644 --- a/apps/sim/lib/core/config/feature-flags.ts +++ b/apps/sim/lib/core/config/feature-flags.ts @@ -1,7 +1,7 @@ /** * Environment utility functions for consistent environment detection across the application */ -import { env, isFalsy, isTruthy } from './env' +import { env, getEnv, isFalsy, isTruthy } from './env' /** * Is the application running in production mode @@ -21,7 +21,9 @@ export const isTest = env.NODE_ENV === 'test' /** * Is this the hosted version of the application */ -export const isHosted = true +export const isHosted = + getEnv('NEXT_PUBLIC_APP_URL') === 'https://www.sim.ai' || + getEnv('NEXT_PUBLIC_APP_URL') === 'https://www.staging.sim.ai' /** * Is billing enforcement enabled From 21971134ed3e65a0b4aba39e296974bbfb0c69a6 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 26 Mar 2026 15:43:26 -0700 Subject: [PATCH 05/17] address bugbot comments --- apps/sim/stores/terminal/console/store.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/apps/sim/stores/terminal/console/store.ts b/apps/sim/stores/terminal/console/store.ts index e4989b1c0da..7e0d2e48131 100644 --- a/apps/sim/stores/terminal/console/store.ts +++ b/apps/sim/stores/terminal/console/store.ts @@ -226,11 +226,18 @@ function appendWorkflowEntry( trimmedEntries: ConsoleEntry[] ): Pick { const workflowEntries = cloneWorkflowEntries(state.workflowEntries) + const previousEntries = workflowEntries[workflowId] ?? EMPTY_CONSOLE_ENTRIES workflowEntries[workflowId] = trimmedEntries const entryLocationById = { ...state.entryLocationById } const entryIdsByBlockExecution = { ...state.entryIdsByBlockExecution } + const survivingIds = new Set(trimmedEntries.map((e) => e.id)) + const droppedEntries = previousEntries.filter((e) => !survivingIds.has(e.id)) + if (droppedEntries.length > 0) { + removeWorkflowIndexes(workflowId, droppedEntries, entryIdsByBlockExecution, entryLocationById) + } + trimmedEntries.forEach((entry, index) => { entryLocationById[entry.id] = { workflowId, index } }) @@ -238,7 +245,9 @@ function appendWorkflowEntry( const blockExecutionKey = getBlockExecutionKey(newEntry.blockId, newEntry.executionId) const existingIds = entryIdsByBlockExecution[blockExecutionKey] if (existingIds) { - entryIdsByBlockExecution[blockExecutionKey] = [...existingIds, newEntry.id] + if (!existingIds.includes(newEntry.id)) { + entryIdsByBlockExecution[blockExecutionKey] = [...existingIds, newEntry.id] + } } else { entryIdsByBlockExecution[blockExecutionKey] = [newEntry.id] } @@ -746,6 +755,11 @@ export function useConsoleEntry(entryId?: string | null): ConsoleEntry | null { return null } - return state.workflowEntries[location.workflowId]?.[location.index] ?? null + const entry = state.workflowEntries[location.workflowId]?.[location.index] + if (!entry || entry.id !== entryId) { + return null + } + + return entry }) } From 35f75fa93300798bcc782feeb1c1748192b09d84 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 26 Mar 2026 15:51:19 -0700 Subject: [PATCH 06/17] fix test --- .../sim/lib/copilot/client-sse/run-tool-execution.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/sim/lib/copilot/client-sse/run-tool-execution.test.ts b/apps/sim/lib/copilot/client-sse/run-tool-execution.test.ts index 00c306ac235..e87f8ba185c 100644 --- a/apps/sim/lib/copilot/client-sse/run-tool-execution.test.ts +++ b/apps/sim/lib/copilot/client-sse/run-tool-execution.test.ts @@ -35,6 +35,14 @@ vi.mock('@/stores/workflows/registry/store', () => ({ }, })) +vi.mock('@/stores/terminal', () => ({ + consolePersistence: { + executionStarted: vi.fn(), + executionEnded: vi.fn(), + persist: vi.fn(), + }, +})) + import { cancelRunToolExecution, executeRunToolOnClient, From a00773c7a9e3bb79beb0d7d9dc977cfda2fb136a Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 26 Mar 2026 15:55:08 -0700 Subject: [PATCH 07/17] fix --- apps/sim/stores/terminal/console/store.ts | 32 ++++++++++++++++------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/apps/sim/stores/terminal/console/store.ts b/apps/sim/stores/terminal/console/store.ts index 7e0d2e48131..030c5981b10 100644 --- a/apps/sim/stores/terminal/console/store.ts +++ b/apps/sim/stores/terminal/console/store.ts @@ -152,7 +152,7 @@ function indexWorkflowEntries( const blockExecutionKey = getBlockExecutionKey(entry.blockId, entry.executionId) const existingIds = entryIdsByBlockExecution[blockExecutionKey] if (existingIds) { - existingIds.push(entry.id) + entryIdsByBlockExecution[blockExecutionKey] = [...existingIds, entry.id] } else { entryIdsByBlockExecution[blockExecutionKey] = [entry.id] } @@ -474,22 +474,26 @@ export const useTerminalConsoleStore = create()( return state } - const workflowEntries = state.workflowEntries[workflowId] ?? EMPTY_CONSOLE_ENTRIES + const currentEntries = state.workflowEntries[workflowId] ?? EMPTY_CONSOLE_ENTRIES + let nextEntries: ConsoleEntry[] | null = null for (const candidateId of candidateIds) { const location = state.entryLocationById[candidateId] if (!location || location.workflowId !== workflowId) continue - const entry = workflowEntries[location.index] + const source = nextEntries ?? currentEntries + const entry = source[location.index] if (!entry || entry.id !== candidateId) continue if (!matchesEntryForUpdate(entry, blockId, executionId, update)) continue + if (!nextEntries) { + nextEntries = [...currentEntries] + } + if (typeof update === 'string') { const newOutput = normalizeConsoleOutput(updateBlockOutput(entry.output, update)) - return patchWorkflowEntry(state, workflowId, location.index, { - ...entry, - output: newOutput, - }) + nextEntries[location.index] = { ...entry, output: newOutput } + continue } const updatedEntry = { ...entry } @@ -588,10 +592,20 @@ export const useTerminalConsoleStore = create()( updatedEntry.childWorkflowInstanceId = update.childWorkflowInstanceId } - return patchWorkflowEntry(state, workflowId, location.index, updatedEntry) + nextEntries[location.index] = updatedEntry } - return state + if (!nextEntries) { + return state + } + + const workflowEntriesClone = cloneWorkflowEntries(state.workflowEntries) + workflowEntriesClone[workflowId] = nextEntries + return { + workflowEntries: workflowEntriesClone, + entryIdsByBlockExecution: state.entryIdsByBlockExecution, + entryLocationById: state.entryLocationById, + } }) if (typeof update === 'object' && update.error) { From 1cce03a0ad8994d7f64e496bb1f1095bcc2510ef Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 26 Mar 2026 16:03:22 -0700 Subject: [PATCH 08/17] fix type --- .cursor/debug-f357a1.log | 348 ++++++++++++++++++++++ apps/sim/stores/terminal/console/store.ts | 29 +- apps/sim/stores/terminal/console/types.ts | 2 +- 3 files changed, 350 insertions(+), 29 deletions(-) create mode 100644 .cursor/debug-f357a1.log diff --git a/.cursor/debug-f357a1.log b/.cursor/debug-f357a1.log new file mode 100644 index 00000000000..714154d0b60 --- /dev/null +++ b/.cursor/debug-f357a1.log @@ -0,0 +1,348 @@ +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":5000,"ms":29.3},"timestamp":1774564640460,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":5000,"ms":25.3},"timestamp":1774564640485,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":5011},"timestamp":1774564640498,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":5011},"timestamp":1774564640499,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":5000,"newId":"1b477670"},"timestamp":1774564640717,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":10011,"rowIndex":10010,"selectedId":"1b477670"},"timestamp":1774564640736,"runId":"post-fix","hypothesisId":"E"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":24},"timestamp":1774564646564,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":14,"newId":"9d64f60a"},"timestamp":1774564646842,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":38,"rowIndex":37,"selectedId":"9d64f60a"},"timestamp":1774564646873,"runId":"post-fix","hypothesisId":"E"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":48},"timestamp":1774564647008,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":38,"newId":"4176354d"},"timestamp":1774564647268,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":86,"rowIndex":85,"selectedId":"4176354d"},"timestamp":1774564647276,"runId":"post-fix","hypothesisId":"E"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":78},"timestamp":1774564647571,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":68,"newId":"53341e22"},"timestamp":1774564647802,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":146,"rowIndex":145,"selectedId":"53341e22"},"timestamp":1774564647832,"runId":"post-fix","hypothesisId":"E"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":103},"timestamp":1774564648043,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":93,"newId":"33d04e6b"},"timestamp":1774564648358,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":196,"rowIndex":195,"selectedId":"33d04e6b"},"timestamp":1774564648384,"runId":"post-fix","hypothesisId":"E"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":126},"timestamp":1774564648515,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":116,"newId":"152181cc"},"timestamp":1774564648715,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":242,"rowIndex":241,"selectedId":"152181cc"},"timestamp":1774564648720,"runId":"post-fix","hypothesisId":"E"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":148},"timestamp":1774564650045,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":138,"newId":"a46c9ca7"},"timestamp":1774564650325,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":286,"rowIndex":285,"selectedId":"a46c9ca7"},"timestamp":1774564650329,"runId":"post-fix","hypothesisId":"E"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":182},"timestamp":1774564650591,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":172,"newId":"6c107ded"},"timestamp":1774564650971,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":354,"rowIndex":353,"selectedId":"6c107ded"},"timestamp":1774564650996,"runId":"post-fix","hypothesisId":"E"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":217},"timestamp":1774564651238,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":207,"newId":"517f37ed"},"timestamp":1774564652086,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":424,"rowIndex":423,"selectedId":"517f37ed"},"timestamp":1774564652115,"runId":"post-fix","hypothesisId":"E"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":279},"timestamp":1774564652250,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":269,"newId":"e65cc8ba"},"timestamp":1774564653506,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":548,"rowIndex":547,"selectedId":"e65cc8ba"},"timestamp":1774564653529,"runId":"post-fix","hypothesisId":"E"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":350,"ms":2.1},"timestamp":1774564653608,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":360},"timestamp":1774564653653,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":350,"newId":"c0f4ae54"},"timestamp":1774564654744,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":710,"rowIndex":709,"selectedId":"c0f4ae54"},"timestamp":1774564654809,"runId":"post-fix","hypothesisId":"E"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":437,"ms":2.3},"timestamp":1774564655051,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":437,"ms":2.1},"timestamp":1774564655053,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":447},"timestamp":1774564655103,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":437,"newId":"3ae12526"},"timestamp":1774564656529,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":884,"rowIndex":883,"selectedId":"3ae12526"},"timestamp":1774564656551,"runId":"post-fix","hypothesisId":"E"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":527,"ms":2.7},"timestamp":1774564656558,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":527,"ms":2.5},"timestamp":1774564656561,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":537},"timestamp":1774564656592,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":527,"newId":"2b5b589b"},"timestamp":1774564657436,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":582,"ms":2.9},"timestamp":1774564657442,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":582,"ms":2.7},"timestamp":1774564657445,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":592},"timestamp":1774564657518,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":592,"ms":2.9},"timestamp":1774564657639,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":592,"ms":2.8},"timestamp":1774564657642,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":602},"timestamp":1774564657690,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":592,"newId":"b38c5ae1"},"timestamp":1774564658101,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":1194,"rowIndex":1193,"selectedId":"b38c5ae1"},"timestamp":1774564658135,"runId":"post-fix","hypothesisId":"E"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":629,"ms":3.3},"timestamp":1774564658247,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":629,"ms":5},"timestamp":1774564658252,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":639},"timestamp":1774564658285,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":629,"newId":"1f058c09"},"timestamp":1774564658543,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":647,"ms":3.5},"timestamp":1774564658548,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":647,"ms":3},"timestamp":1774564658551,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":657},"timestamp":1774564658582,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":647,"newId":"07b6858a"},"timestamp":1774564659021,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":1304,"rowIndex":1303,"selectedId":"07b6858a"},"timestamp":1774564659049,"runId":"post-fix","hypothesisId":"E"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":692,"ms":3.8},"timestamp":1774564659249,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":692,"ms":3.3},"timestamp":1774564659252,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":702},"timestamp":1774564659290,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":692,"newId":"822ed7de"},"timestamp":1774564659814,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":722,"ms":3.5},"timestamp":1774564659849,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":722,"ms":3.4},"timestamp":1774564659853,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":732},"timestamp":1774564659974,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":760,"ms":3.6},"timestamp":1774564660427,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":760,"ms":3.3},"timestamp":1774564660430,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":770},"timestamp":1774564660523,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":760,"newId":"94142cca"},"timestamp":1774564660977,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":794,"ms":3.7},"timestamp":1774564660984,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":794,"ms":3.4},"timestamp":1774564660988,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":804},"timestamp":1774564661033,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":794,"newId":"7644db3c"},"timestamp":1774564661302,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":1598,"rowIndex":1597,"selectedId":"7644db3c"},"timestamp":1774564661387,"runId":"post-fix","hypothesisId":"E"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":831,"ms":3.9},"timestamp":1774564661572,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":831,"ms":3.5},"timestamp":1774564661575,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":841},"timestamp":1774564661606,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":831,"newId":"a981da86"},"timestamp":1774564661982,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":1672,"rowIndex":1671,"selectedId":"a981da86"},"timestamp":1774564662018,"runId":"post-fix","hypothesisId":"E"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":861,"ms":3.8},"timestamp":1774564662076,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":861,"ms":3.6},"timestamp":1774564662080,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":871},"timestamp":1774564662121,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":861,"newId":"800bb7b4"},"timestamp":1774564662386,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":1732,"rowIndex":1731,"selectedId":"800bb7b4"},"timestamp":1774564662393,"runId":"post-fix","hypothesisId":"E"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":891,"ms":4},"timestamp":1774564662558,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":891,"ms":3.6},"timestamp":1774564662562,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":901},"timestamp":1774564662601,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":891,"newId":"dbcc8bb8"},"timestamp":1774564662977,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":1792,"rowIndex":1791,"selectedId":"dbcc8bb8"},"timestamp":1774564663014,"runId":"post-fix","hypothesisId":"E"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":921,"ms":4.2},"timestamp":1774564663073,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":921,"ms":3.7},"timestamp":1774564663077,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":931},"timestamp":1774564663121,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":921,"newId":"30649f0c"},"timestamp":1774564663417,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":1852,"rowIndex":1851,"selectedId":"30649f0c"},"timestamp":1774564663425,"runId":"post-fix","hypothesisId":"E"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":941,"ms":4.2},"timestamp":1774564663523,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":941,"ms":5.5},"timestamp":1774564663529,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":951},"timestamp":1774564663573,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":941,"newId":"7aab4dff"},"timestamp":1774564663774,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":1892,"rowIndex":1891,"selectedId":"7aab4dff"},"timestamp":1774564663778,"runId":"post-fix","hypothesisId":"E"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":943,"ms":6},"timestamp":1774564664006,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":943,"ms":4},"timestamp":1774564664010,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":953},"timestamp":1774564664097,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":943,"newId":"d0662825"},"timestamp":1774564664491,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":1896,"rowIndex":1895,"selectedId":"d0662825"},"timestamp":1774564664526,"runId":"post-fix","hypothesisId":"E"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":966,"ms":4.3},"timestamp":1774564664663,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":966,"ms":4.1},"timestamp":1774564664668,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":976},"timestamp":1774564664771,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":966,"newId":"87f206aa"},"timestamp":1774564665277,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":1942,"rowIndex":1941,"selectedId":"87f206aa"},"timestamp":1774564665326,"runId":"post-fix","hypothesisId":"E"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1006,"ms":4.2},"timestamp":1774564665356,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1006,"ms":4.1},"timestamp":1774564665360,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1016},"timestamp":1774564665419,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1034,"ms":4.3},"timestamp":1774564665738,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1034,"ms":4.1},"timestamp":1774564665743,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1044},"timestamp":1774564665779,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":1034,"newId":"0b6515ee"},"timestamp":1774564666319,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1068,"ms":4.4},"timestamp":1774564666340,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1068,"ms":4.3},"timestamp":1774564666345,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1078},"timestamp":1774564666508,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1119,"ms":5.8},"timestamp":1774564667167,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1119,"ms":5.1},"timestamp":1774564667172,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1129},"timestamp":1774564667291,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":1119,"newId":"1968ad06"},"timestamp":1774564667666,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1151,"ms":5.6},"timestamp":1774564667673,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1151,"ms":5.4},"timestamp":1774564667678,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1161},"timestamp":1774564667839,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":1151,"newId":"f36b36ec"},"timestamp":1774564668151,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1181,"ms":5.8},"timestamp":1774564668160,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1181,"ms":5.4},"timestamp":1774564668166,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1191},"timestamp":1774564668301,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":1181,"newId":"08f520af"},"timestamp":1774564668700,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1211,"ms":5.7},"timestamp":1774564668727,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1211,"ms":5.8},"timestamp":1774564668733,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1221},"timestamp":1774564668821,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1248,"ms":6.3},"timestamp":1774564669327,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1248,"ms":5.8},"timestamp":1774564669333,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1258},"timestamp":1774564669419,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1286,"ms":9.5},"timestamp":1774564669997,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1286,"ms":6.2},"timestamp":1774564670004,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1296},"timestamp":1774564670200,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1337,"ms":6.5},"timestamp":1774564670772,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1337,"ms":5.7},"timestamp":1774564670778,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1347},"timestamp":1774564670987,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1410,"ms":6.2},"timestamp":1774564672047,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1410,"ms":8.2},"timestamp":1774564672055,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1420},"timestamp":1774564672272,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":1410,"newId":"8e957aa3"},"timestamp":1774564673899,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":2830,"rowIndex":2829,"selectedId":"8e957aa3"},"timestamp":1774564673945,"runId":"post-fix","hypothesisId":"E"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1509,"ms":11.9},"timestamp":1774564673978,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1509,"ms":8.8},"timestamp":1774564673988,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1519},"timestamp":1774564674213,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":1509,"newId":"19dac108"},"timestamp":1774564675166,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1578,"ms":7.7},"timestamp":1774564675174,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1578,"ms":7.6},"timestamp":1774564675182,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1588},"timestamp":1774564675385,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1623,"ms":8.8},"timestamp":1774564676100,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1623,"ms":8.4},"timestamp":1774564676109,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1633},"timestamp":1774564676309,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":1623,"newId":"cc605af7"},"timestamp":1774564677678,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1711,"ms":8.5},"timestamp":1774564677687,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1711,"ms":7.5},"timestamp":1774564677695,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1721},"timestamp":1774564677782,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":1711,"newId":"ca144406"},"timestamp":1774564678179,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1736,"ms":7.5},"timestamp":1774564678188,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1736,"ms":7.4},"timestamp":1774564678196,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1746},"timestamp":1774564678334,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":1736,"newId":"3f95fabf"},"timestamp":1774564678657,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1738,"ms":8.2},"timestamp":1774564678666,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1738,"ms":7.2},"timestamp":1774564678674,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":3484,"rowIndex":3483,"selectedId":"3f95fabf"},"timestamp":1774564678904,"runId":"post-fix","hypothesisId":"E"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1748},"timestamp":1774564678905,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1746,"ms":7.8},"timestamp":1774564679052,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1746,"ms":7.3},"timestamp":1774564679060,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1756},"timestamp":1774564679130,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":1746,"newId":"5839f7b5"},"timestamp":1774564679605,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":3502,"rowIndex":3501,"selectedId":"5839f7b5"},"timestamp":1774564679617,"runId":"post-fix","hypothesisId":"E"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1792,"ms":7.6},"timestamp":1774564679838,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1792,"ms":7.4},"timestamp":1774564679846,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1802},"timestamp":1774564680026,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1823,"ms":7.7},"timestamp":1774564680351,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1823,"ms":7.5},"timestamp":1774564680358,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1833},"timestamp":1774564680610,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1866,"ms":9.7},"timestamp":1774564681097,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1866,"ms":7.5},"timestamp":1774564681105,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1876},"timestamp":1774564681221,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":1866,"newId":"2195bd34"},"timestamp":1774564681489,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":3742,"rowIndex":3741,"selectedId":"2195bd34"},"timestamp":1774564681496,"runId":"post-fix","hypothesisId":"E"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1894,"ms":7.9},"timestamp":1774564681590,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1894,"ms":7.7},"timestamp":1774564681598,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1904},"timestamp":1774564681797,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":1894,"newId":"28a1a0e9"},"timestamp":1774564682126,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1930,"ms":8.2},"timestamp":1774564682135,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1930,"ms":9.9},"timestamp":1774564682145,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1940},"timestamp":1774564682244,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":1930,"newId":"babbdb00"},"timestamp":1774564683066,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":3870,"rowIndex":3869,"selectedId":"babbdb00"},"timestamp":1774564683089,"runId":"post-fix","hypothesisId":"E"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1990,"ms":8.4},"timestamp":1774564683154,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1990,"ms":8},"timestamp":1774564683163,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":2000},"timestamp":1774564683256,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":1990,"newId":"88c273c3"},"timestamp":1774564684232,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":3990,"rowIndex":3989,"selectedId":"88c273c3"},"timestamp":1774564684264,"runId":"post-fix","hypothesisId":"E"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2059,"ms":8.3},"timestamp":1774564684289,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2059,"ms":10.7},"timestamp":1774564684300,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":2069},"timestamp":1774564684607,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2141,"ms":12.9},"timestamp":1774564685671,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2141,"ms":9.9},"timestamp":1774564685681,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":2151},"timestamp":1774564685995,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2249,"ms":10.6},"timestamp":1774564687542,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2249,"ms":13.3},"timestamp":1774564687555,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":2259},"timestamp":1774564687874,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":2249,"newId":"8dbe730c"},"timestamp":1774564689289,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2355,"ms":18.7},"timestamp":1774564689309,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2355,"ms":10.9},"timestamp":1774564689320,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":2365},"timestamp":1774564689634,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":2355,"newId":"2e527a90"},"timestamp":1774564690491,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2430,"ms":19.2},"timestamp":1774564690511,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2430,"ms":10.6},"timestamp":1774564690522,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":2440},"timestamp":1774564690657,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":2430,"newId":"b57b0aff"},"timestamp":1774564691463,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2490,"ms":17.6},"timestamp":1774564691481,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2490,"ms":10.6},"timestamp":1774564691492,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":2500},"timestamp":1774564691662,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":2490,"newId":"db42a9e2"},"timestamp":1774564692497,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2550,"ms":24.1},"timestamp":1774564692523,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2550,"ms":25.1},"timestamp":1774564692548,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":2560},"timestamp":1774564692867,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":2550,"newId":"561b0202"},"timestamp":1774564693575,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2570,"ms":25.6},"timestamp":1774564693616,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2570,"ms":23.7},"timestamp":1774564693640,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":2580},"timestamp":1774564694016,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":2570,"newId":"aa5a90c8"},"timestamp":1774564694936,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2650,"ms":19},"timestamp":1774564694981,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2650,"ms":19.9},"timestamp":1774564695001,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":2660},"timestamp":1774564695442,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2742,"ms":17.6},"timestamp":1774564696611,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2742,"ms":16.6},"timestamp":1774564696628,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":2752},"timestamp":1774564697399,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2914,"ms":24.1},"timestamp":1774564699476,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2914,"ms":27.9},"timestamp":1774564699505,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":2924},"timestamp":1774564700060,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":2914,"newId":"3bc830c5"},"timestamp":1774564703783,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3164,"ms":29.7},"timestamp":1774564703814,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3164,"ms":38},"timestamp":1774564703852,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":3174},"timestamp":1774564704398,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":3164,"newId":"4f76af26"},"timestamp":1774564707355,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3361,"ms":19.7},"timestamp":1774564707379,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3361,"ms":19.1},"timestamp":1774564707399,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":3371},"timestamp":1774564707423,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3361,"ms":19},"timestamp":1774564707533,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3361,"ms":17.5},"timestamp":1774564707550,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":3371},"timestamp":1774564707912,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":3361,"newId":"79f2412c"},"timestamp":1774564708648,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3378,"ms":16.5},"timestamp":1774564708713,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3378,"ms":19.1},"timestamp":1774564708733,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":3388},"timestamp":1774564709275,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3442,"ms":16.5},"timestamp":1774564709719,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3442,"ms":19.3},"timestamp":1774564709738,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":3452},"timestamp":1774564709927,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3484,"ms":15.7},"timestamp":1774564710354,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3484,"ms":16.8},"timestamp":1774564710371,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":3494},"timestamp":1774564710896,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3564,"ms":15.2},"timestamp":1774564711660,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3564,"ms":17.2},"timestamp":1774564711678,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":3574},"timestamp":1774564711871,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3639,"ms":15.7},"timestamp":1774564712915,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3639,"ms":17.9},"timestamp":1774564712934,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":3649},"timestamp":1774564713126,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":3639,"newId":"5f391d31"},"timestamp":1774564713626,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3684,"ms":17.5},"timestamp":1774564713646,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3684,"ms":15},"timestamp":1774564713661,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":3694},"timestamp":1774564713892,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3718,"ms":18.1},"timestamp":1774564714319,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3718,"ms":20.3},"timestamp":1774564714340,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":3728},"timestamp":1774564714759,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3769,"ms":18},"timestamp":1774564715066,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3769,"ms":17.8},"timestamp":1774564715084,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":3779},"timestamp":1774564715293,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":3769,"newId":"7be768e8"},"timestamp":1774564715646,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":7548,"rowIndex":7547,"selectedId":"7be768e8"},"timestamp":1774564716197,"runId":"post-fix","hypothesisId":"E"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3805,"ms":18.5},"timestamp":1774564716393,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3805,"ms":16.3},"timestamp":1774564716409,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":3815},"timestamp":1774564716806,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":3805,"newId":"5c46ded9"},"timestamp":1774564717248,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3904,"ms":17.3},"timestamp":1774564717269,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3904,"ms":18.7},"timestamp":1774564717288,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":3914},"timestamp":1774564717503,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3934,"ms":20.1},"timestamp":1774564717853,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3934,"ms":16.6},"timestamp":1774564717870,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":3944},"timestamp":1774564718193,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":3934,"newId":"828df2a2"},"timestamp":1774564718450,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3974,"ms":18.8},"timestamp":1774564718470,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3974,"ms":16.1},"timestamp":1774564718486,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":3984},"timestamp":1774564719150,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4014,"ms":20.6},"timestamp":1774564719507,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4014,"ms":26.4},"timestamp":1774564719534,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":4024},"timestamp":1774564720010,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":4014,"newId":"ccab4bb2"},"timestamp":1774564720691,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4101,"ms":19.3},"timestamp":1774564720725,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4101,"ms":27.9},"timestamp":1774564720754,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":4111},"timestamp":1774564721430,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4170,"ms":18.3},"timestamp":1774564721905,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4170,"ms":25.6},"timestamp":1774564721931,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":4180},"timestamp":1774564722554,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4172,"ms":26},"timestamp":1774564722649,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4172,"ms":18},"timestamp":1774564722667,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":4182},"timestamp":1774564723337,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4227,"ms":17.4},"timestamp":1774564723785,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4227,"ms":25.6},"timestamp":1774564723811,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":4237},"timestamp":1774564724378,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4312,"ms":37},"timestamp":1774564725182,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4312,"ms":36.1},"timestamp":1774564725219,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":4322},"timestamp":1774564725813,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":4312,"newId":"02206f85"},"timestamp":1774564727667,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4466,"ms":35.4},"timestamp":1774564727723,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4466,"ms":47},"timestamp":1774564727771,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":4476},"timestamp":1774564728667,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4708,"ms":48.7},"timestamp":1774564731733,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4708,"ms":49.5},"timestamp":1774564731783,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":4718},"timestamp":1774564733196,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":4708,"newId":"7a5712c7"},"timestamp":1774564736498,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4977,"ms":34.7},"timestamp":1774564736542,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4977,"ms":24.9},"timestamp":1774564736567,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":4987},"timestamp":1774564737275,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4994,"ms":25.8},"timestamp":1774564737613,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4994,"ms":29.1},"timestamp":1774564737642,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":5004},"timestamp":1774564738020,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":4994,"newId":"e7166ef8"},"timestamp":1774564738441,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":9998,"rowIndex":9997,"selectedId":"e7166ef8"},"timestamp":1774564739150,"runId":"post-fix","hypothesisId":"E"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4994,"ms":44.7},"timestamp":1774564742488,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4994,"ms":43.7},"timestamp":1774564742532,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":5004},"timestamp":1774564743532,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":0,"ms":0.1},"timestamp":1774564773409,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":0,"ms":0},"timestamp":1774564773595,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":0,"ms":0},"timestamp":1774564773752,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":0,"ms":0},"timestamp":1774564773892,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":0,"ms":0},"timestamp":1774564774228,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":0,"ms":0.1},"timestamp":1774564783661,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":0,"ms":0},"timestamp":1774564783662,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4994,"ms":49},"timestamp":1774564786955,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4994,"ms":55.7},"timestamp":1774564787012,"runId":"post-fix-3","hypothesisId":"A"} +{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":5004},"timestamp":1774564787065,"hypothesisId":"D"} +{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":4994,"newId":"e7166ef8"},"timestamp":1774564787459,"runId":"post-fix","hypothesisId":"C"} +{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":9998,"rowIndex":9997,"selectedId":"e7166ef8"},"timestamp":1774564787473,"runId":"post-fix","hypothesisId":"E"} diff --git a/apps/sim/stores/terminal/console/store.ts b/apps/sim/stores/terminal/console/store.ts index 030c5981b10..52565934fb2 100644 --- a/apps/sim/stores/terminal/console/store.ts +++ b/apps/sim/stores/terminal/console/store.ts @@ -192,33 +192,6 @@ function replaceWorkflowEntries( return { workflowEntries, entryIdsByBlockExecution, entryLocationById } } -function patchWorkflowEntry( - state: ConsoleStore, - workflowId: string, - entryIndex: number, - updatedEntry: ConsoleEntry -): Pick { - const workflowEntries = cloneWorkflowEntries(state.workflowEntries) - const currentEntries = workflowEntries[workflowId] - if (!currentEntries) { - return { - workflowEntries, - entryIdsByBlockExecution: state.entryIdsByBlockExecution, - entryLocationById: state.entryLocationById, - } - } - - const nextEntries = [...currentEntries] - nextEntries[entryIndex] = updatedEntry - workflowEntries[workflowId] = nextEntries - - return { - workflowEntries, - entryIdsByBlockExecution: state.entryIdsByBlockExecution, - entryLocationById: state.entryLocationById, - } -} - function appendWorkflowEntry( state: ConsoleStore, workflowId: string, @@ -301,7 +274,7 @@ export const useTerminalConsoleStore = create()( addConsole: (entry: Omit) => { if (shouldSkipEntry(entry.output)) { - return get().getWorkflowEntries(entry.workflowId)[0] as ConsoleEntry + return get().getWorkflowEntries(entry.workflowId)[0] as ConsoleEntry | undefined } const redactedEntry = { ...entry } diff --git a/apps/sim/stores/terminal/console/types.ts b/apps/sim/stores/terminal/console/types.ts index 5e6a107b900..8aa342d7a11 100644 --- a/apps/sim/stores/terminal/console/types.ts +++ b/apps/sim/stores/terminal/console/types.ts @@ -68,7 +68,7 @@ export interface ConsoleStore { entryIdsByBlockExecution: Record entryLocationById: Record isOpen: boolean - addConsole: (entry: Omit) => ConsoleEntry + addConsole: (entry: Omit) => ConsoleEntry | undefined clearWorkflowConsole: (workflowId: string) => void clearExecutionEntries: (executionId: string) => void exportConsoleCSV: (workflowId: string) => void From 8d64cf4534cc8f35dbfbf1a9293f83a26a53b27a Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 26 Mar 2026 16:16:35 -0700 Subject: [PATCH 09/17] fix abortion of manual run --- apps/sim/stores/terminal/console/storage.ts | 23 ++++++++++++++++----- apps/sim/stores/terminal/console/store.ts | 4 ++++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/apps/sim/stores/terminal/console/storage.ts b/apps/sim/stores/terminal/console/storage.ts index 698e9051666..3a490338609 100644 --- a/apps/sim/stores/terminal/console/storage.ts +++ b/apps/sim/stores/terminal/console/storage.ts @@ -8,11 +8,11 @@ const STORE_KEY = 'terminal-console-store' const MIGRATION_KEY = 'terminal-console-store-migrated' /** - * Safety-net interval for persisting during very long executions. - * Only fires while an execution is active. Much longer than a debounce - * because intermediate writes during execution are low-value. + * Interval for persisting terminal state during active executions. + * Kept short enough that a hard refresh during execution still has + * recent running entries persisted for the reconnect flow to find. */ -const LONG_EXECUTION_PERSIST_INTERVAL_MS = 30_000 +const EXECUTION_PERSIST_INTERVAL_MS = 5_000 /** * Shape of terminal console data persisted to IndexedDB. @@ -129,6 +129,7 @@ class ConsolePersistenceManager { private dataProvider: (() => PersistedConsoleData) | null = null private safetyTimer: ReturnType | null = null private activeExecutions = 0 + private needsInitialPersist = false /** * Binds the data provider function used to snapshot current state. @@ -144,11 +145,23 @@ class ConsolePersistenceManager { */ executionStarted(): void { this.activeExecutions++ + this.needsInitialPersist = true if (this.activeExecutions === 1) { this.startSafetyTimer() } } + /** + * Called by the store when a running entry is added during an active execution. + * Triggers one immediate persist so the reconnect flow can find running entries + * after a page refresh, then disables until the next execution starts. + */ + onRunningEntryAdded(): void { + if (!this.needsInitialPersist) return + this.needsInitialPersist = false + this.persist() + } + /** * Signals that a workflow execution has ended (success, error, or cancel). * Triggers an immediate persist and stops the safety timer if no executions remain. @@ -174,7 +187,7 @@ class ConsolePersistenceManager { this.stopSafetyTimer() this.safetyTimer = setInterval(() => { this.persist() - }, LONG_EXECUTION_PERSIST_INTERVAL_MS) + }, EXECUTION_PERSIST_INTERVAL_MS) } private stopSafetyTimer(): void { diff --git a/apps/sim/stores/terminal/console/store.ts b/apps/sim/stores/terminal/console/store.ts index 52565934fb2..55f388b465b 100644 --- a/apps/sim/stores/terminal/console/store.ts +++ b/apps/sim/stores/terminal/console/store.ts @@ -308,6 +308,10 @@ export const useTerminalConsoleStore = create()( return appendWorkflowEntry(state, entry.workflowId, createdEntry, nextWorkflowEntries) }) + if (createdEntry.isRunning) { + consolePersistence.onRunningEntryAdded() + } + if (createdEntry.error && createdEntry.blockType !== 'cancelled') { notifyBlockError({ error: createdEntry.error, From faaa4a50f7a0cd0465b28fceaaad473c1a8e48f4 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 26 Mar 2026 16:22:38 -0700 Subject: [PATCH 10/17] fix type errors --- .cursor/debug-f357a1.log | 348 ------------------ .../w/[workflowId]/components/chat/chat.tsx | 4 +- .../components/terminal/terminal.tsx | 40 -- .../utils/workflow-execution-utils.ts | 4 +- 4 files changed, 5 insertions(+), 391 deletions(-) delete mode 100644 .cursor/debug-f357a1.log diff --git a/.cursor/debug-f357a1.log b/.cursor/debug-f357a1.log deleted file mode 100644 index 714154d0b60..00000000000 --- a/.cursor/debug-f357a1.log +++ /dev/null @@ -1,348 +0,0 @@ -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":5000,"ms":29.3},"timestamp":1774564640460,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":5000,"ms":25.3},"timestamp":1774564640485,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":5011},"timestamp":1774564640498,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":5011},"timestamp":1774564640499,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":5000,"newId":"1b477670"},"timestamp":1774564640717,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":10011,"rowIndex":10010,"selectedId":"1b477670"},"timestamp":1774564640736,"runId":"post-fix","hypothesisId":"E"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":24},"timestamp":1774564646564,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":14,"newId":"9d64f60a"},"timestamp":1774564646842,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":38,"rowIndex":37,"selectedId":"9d64f60a"},"timestamp":1774564646873,"runId":"post-fix","hypothesisId":"E"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":48},"timestamp":1774564647008,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":38,"newId":"4176354d"},"timestamp":1774564647268,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":86,"rowIndex":85,"selectedId":"4176354d"},"timestamp":1774564647276,"runId":"post-fix","hypothesisId":"E"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":78},"timestamp":1774564647571,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":68,"newId":"53341e22"},"timestamp":1774564647802,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":146,"rowIndex":145,"selectedId":"53341e22"},"timestamp":1774564647832,"runId":"post-fix","hypothesisId":"E"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":103},"timestamp":1774564648043,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":93,"newId":"33d04e6b"},"timestamp":1774564648358,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":196,"rowIndex":195,"selectedId":"33d04e6b"},"timestamp":1774564648384,"runId":"post-fix","hypothesisId":"E"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":126},"timestamp":1774564648515,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":116,"newId":"152181cc"},"timestamp":1774564648715,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":242,"rowIndex":241,"selectedId":"152181cc"},"timestamp":1774564648720,"runId":"post-fix","hypothesisId":"E"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":148},"timestamp":1774564650045,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":138,"newId":"a46c9ca7"},"timestamp":1774564650325,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":286,"rowIndex":285,"selectedId":"a46c9ca7"},"timestamp":1774564650329,"runId":"post-fix","hypothesisId":"E"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":182},"timestamp":1774564650591,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":172,"newId":"6c107ded"},"timestamp":1774564650971,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":354,"rowIndex":353,"selectedId":"6c107ded"},"timestamp":1774564650996,"runId":"post-fix","hypothesisId":"E"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":217},"timestamp":1774564651238,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":207,"newId":"517f37ed"},"timestamp":1774564652086,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":424,"rowIndex":423,"selectedId":"517f37ed"},"timestamp":1774564652115,"runId":"post-fix","hypothesisId":"E"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":279},"timestamp":1774564652250,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":269,"newId":"e65cc8ba"},"timestamp":1774564653506,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":548,"rowIndex":547,"selectedId":"e65cc8ba"},"timestamp":1774564653529,"runId":"post-fix","hypothesisId":"E"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":350,"ms":2.1},"timestamp":1774564653608,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":360},"timestamp":1774564653653,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":350,"newId":"c0f4ae54"},"timestamp":1774564654744,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":710,"rowIndex":709,"selectedId":"c0f4ae54"},"timestamp":1774564654809,"runId":"post-fix","hypothesisId":"E"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":437,"ms":2.3},"timestamp":1774564655051,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":437,"ms":2.1},"timestamp":1774564655053,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":447},"timestamp":1774564655103,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":437,"newId":"3ae12526"},"timestamp":1774564656529,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":884,"rowIndex":883,"selectedId":"3ae12526"},"timestamp":1774564656551,"runId":"post-fix","hypothesisId":"E"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":527,"ms":2.7},"timestamp":1774564656558,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":527,"ms":2.5},"timestamp":1774564656561,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":537},"timestamp":1774564656592,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":527,"newId":"2b5b589b"},"timestamp":1774564657436,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":582,"ms":2.9},"timestamp":1774564657442,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":582,"ms":2.7},"timestamp":1774564657445,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":592},"timestamp":1774564657518,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":592,"ms":2.9},"timestamp":1774564657639,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":592,"ms":2.8},"timestamp":1774564657642,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":602},"timestamp":1774564657690,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":592,"newId":"b38c5ae1"},"timestamp":1774564658101,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":1194,"rowIndex":1193,"selectedId":"b38c5ae1"},"timestamp":1774564658135,"runId":"post-fix","hypothesisId":"E"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":629,"ms":3.3},"timestamp":1774564658247,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":629,"ms":5},"timestamp":1774564658252,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":639},"timestamp":1774564658285,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":629,"newId":"1f058c09"},"timestamp":1774564658543,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":647,"ms":3.5},"timestamp":1774564658548,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":647,"ms":3},"timestamp":1774564658551,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":657},"timestamp":1774564658582,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":647,"newId":"07b6858a"},"timestamp":1774564659021,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":1304,"rowIndex":1303,"selectedId":"07b6858a"},"timestamp":1774564659049,"runId":"post-fix","hypothesisId":"E"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":692,"ms":3.8},"timestamp":1774564659249,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":692,"ms":3.3},"timestamp":1774564659252,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":702},"timestamp":1774564659290,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":692,"newId":"822ed7de"},"timestamp":1774564659814,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":722,"ms":3.5},"timestamp":1774564659849,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":722,"ms":3.4},"timestamp":1774564659853,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":732},"timestamp":1774564659974,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":760,"ms":3.6},"timestamp":1774564660427,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":760,"ms":3.3},"timestamp":1774564660430,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":770},"timestamp":1774564660523,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":760,"newId":"94142cca"},"timestamp":1774564660977,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":794,"ms":3.7},"timestamp":1774564660984,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":794,"ms":3.4},"timestamp":1774564660988,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":804},"timestamp":1774564661033,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":794,"newId":"7644db3c"},"timestamp":1774564661302,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":1598,"rowIndex":1597,"selectedId":"7644db3c"},"timestamp":1774564661387,"runId":"post-fix","hypothesisId":"E"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":831,"ms":3.9},"timestamp":1774564661572,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":831,"ms":3.5},"timestamp":1774564661575,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":841},"timestamp":1774564661606,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":831,"newId":"a981da86"},"timestamp":1774564661982,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":1672,"rowIndex":1671,"selectedId":"a981da86"},"timestamp":1774564662018,"runId":"post-fix","hypothesisId":"E"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":861,"ms":3.8},"timestamp":1774564662076,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":861,"ms":3.6},"timestamp":1774564662080,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":871},"timestamp":1774564662121,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":861,"newId":"800bb7b4"},"timestamp":1774564662386,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":1732,"rowIndex":1731,"selectedId":"800bb7b4"},"timestamp":1774564662393,"runId":"post-fix","hypothesisId":"E"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":891,"ms":4},"timestamp":1774564662558,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":891,"ms":3.6},"timestamp":1774564662562,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":901},"timestamp":1774564662601,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":891,"newId":"dbcc8bb8"},"timestamp":1774564662977,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":1792,"rowIndex":1791,"selectedId":"dbcc8bb8"},"timestamp":1774564663014,"runId":"post-fix","hypothesisId":"E"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":921,"ms":4.2},"timestamp":1774564663073,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":921,"ms":3.7},"timestamp":1774564663077,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":931},"timestamp":1774564663121,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":921,"newId":"30649f0c"},"timestamp":1774564663417,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":1852,"rowIndex":1851,"selectedId":"30649f0c"},"timestamp":1774564663425,"runId":"post-fix","hypothesisId":"E"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":941,"ms":4.2},"timestamp":1774564663523,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":941,"ms":5.5},"timestamp":1774564663529,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":951},"timestamp":1774564663573,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":941,"newId":"7aab4dff"},"timestamp":1774564663774,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":1892,"rowIndex":1891,"selectedId":"7aab4dff"},"timestamp":1774564663778,"runId":"post-fix","hypothesisId":"E"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":943,"ms":6},"timestamp":1774564664006,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":943,"ms":4},"timestamp":1774564664010,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":953},"timestamp":1774564664097,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":943,"newId":"d0662825"},"timestamp":1774564664491,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":1896,"rowIndex":1895,"selectedId":"d0662825"},"timestamp":1774564664526,"runId":"post-fix","hypothesisId":"E"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":966,"ms":4.3},"timestamp":1774564664663,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":966,"ms":4.1},"timestamp":1774564664668,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":976},"timestamp":1774564664771,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":966,"newId":"87f206aa"},"timestamp":1774564665277,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":1942,"rowIndex":1941,"selectedId":"87f206aa"},"timestamp":1774564665326,"runId":"post-fix","hypothesisId":"E"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1006,"ms":4.2},"timestamp":1774564665356,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1006,"ms":4.1},"timestamp":1774564665360,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1016},"timestamp":1774564665419,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1034,"ms":4.3},"timestamp":1774564665738,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1034,"ms":4.1},"timestamp":1774564665743,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1044},"timestamp":1774564665779,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":1034,"newId":"0b6515ee"},"timestamp":1774564666319,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1068,"ms":4.4},"timestamp":1774564666340,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1068,"ms":4.3},"timestamp":1774564666345,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1078},"timestamp":1774564666508,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1119,"ms":5.8},"timestamp":1774564667167,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1119,"ms":5.1},"timestamp":1774564667172,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1129},"timestamp":1774564667291,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":1119,"newId":"1968ad06"},"timestamp":1774564667666,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1151,"ms":5.6},"timestamp":1774564667673,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1151,"ms":5.4},"timestamp":1774564667678,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1161},"timestamp":1774564667839,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":1151,"newId":"f36b36ec"},"timestamp":1774564668151,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1181,"ms":5.8},"timestamp":1774564668160,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1181,"ms":5.4},"timestamp":1774564668166,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1191},"timestamp":1774564668301,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":1181,"newId":"08f520af"},"timestamp":1774564668700,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1211,"ms":5.7},"timestamp":1774564668727,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1211,"ms":5.8},"timestamp":1774564668733,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1221},"timestamp":1774564668821,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1248,"ms":6.3},"timestamp":1774564669327,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1248,"ms":5.8},"timestamp":1774564669333,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1258},"timestamp":1774564669419,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1286,"ms":9.5},"timestamp":1774564669997,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1286,"ms":6.2},"timestamp":1774564670004,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1296},"timestamp":1774564670200,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1337,"ms":6.5},"timestamp":1774564670772,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1337,"ms":5.7},"timestamp":1774564670778,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1347},"timestamp":1774564670987,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1410,"ms":6.2},"timestamp":1774564672047,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1410,"ms":8.2},"timestamp":1774564672055,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1420},"timestamp":1774564672272,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":1410,"newId":"8e957aa3"},"timestamp":1774564673899,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":2830,"rowIndex":2829,"selectedId":"8e957aa3"},"timestamp":1774564673945,"runId":"post-fix","hypothesisId":"E"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1509,"ms":11.9},"timestamp":1774564673978,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1509,"ms":8.8},"timestamp":1774564673988,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1519},"timestamp":1774564674213,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":1509,"newId":"19dac108"},"timestamp":1774564675166,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1578,"ms":7.7},"timestamp":1774564675174,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1578,"ms":7.6},"timestamp":1774564675182,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1588},"timestamp":1774564675385,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1623,"ms":8.8},"timestamp":1774564676100,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1623,"ms":8.4},"timestamp":1774564676109,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1633},"timestamp":1774564676309,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":1623,"newId":"cc605af7"},"timestamp":1774564677678,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1711,"ms":8.5},"timestamp":1774564677687,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1711,"ms":7.5},"timestamp":1774564677695,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1721},"timestamp":1774564677782,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":1711,"newId":"ca144406"},"timestamp":1774564678179,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1736,"ms":7.5},"timestamp":1774564678188,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1736,"ms":7.4},"timestamp":1774564678196,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1746},"timestamp":1774564678334,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":1736,"newId":"3f95fabf"},"timestamp":1774564678657,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1738,"ms":8.2},"timestamp":1774564678666,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1738,"ms":7.2},"timestamp":1774564678674,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":3484,"rowIndex":3483,"selectedId":"3f95fabf"},"timestamp":1774564678904,"runId":"post-fix","hypothesisId":"E"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1748},"timestamp":1774564678905,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1746,"ms":7.8},"timestamp":1774564679052,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1746,"ms":7.3},"timestamp":1774564679060,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1756},"timestamp":1774564679130,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":1746,"newId":"5839f7b5"},"timestamp":1774564679605,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":3502,"rowIndex":3501,"selectedId":"5839f7b5"},"timestamp":1774564679617,"runId":"post-fix","hypothesisId":"E"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1792,"ms":7.6},"timestamp":1774564679838,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1792,"ms":7.4},"timestamp":1774564679846,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1802},"timestamp":1774564680026,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1823,"ms":7.7},"timestamp":1774564680351,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1823,"ms":7.5},"timestamp":1774564680358,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1833},"timestamp":1774564680610,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1866,"ms":9.7},"timestamp":1774564681097,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1866,"ms":7.5},"timestamp":1774564681105,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1876},"timestamp":1774564681221,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":1866,"newId":"2195bd34"},"timestamp":1774564681489,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":3742,"rowIndex":3741,"selectedId":"2195bd34"},"timestamp":1774564681496,"runId":"post-fix","hypothesisId":"E"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1894,"ms":7.9},"timestamp":1774564681590,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1894,"ms":7.7},"timestamp":1774564681598,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1904},"timestamp":1774564681797,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":1894,"newId":"28a1a0e9"},"timestamp":1774564682126,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1930,"ms":8.2},"timestamp":1774564682135,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1930,"ms":9.9},"timestamp":1774564682145,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":1940},"timestamp":1774564682244,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":1930,"newId":"babbdb00"},"timestamp":1774564683066,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":3870,"rowIndex":3869,"selectedId":"babbdb00"},"timestamp":1774564683089,"runId":"post-fix","hypothesisId":"E"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1990,"ms":8.4},"timestamp":1774564683154,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":1990,"ms":8},"timestamp":1774564683163,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":2000},"timestamp":1774564683256,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":1990,"newId":"88c273c3"},"timestamp":1774564684232,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":3990,"rowIndex":3989,"selectedId":"88c273c3"},"timestamp":1774564684264,"runId":"post-fix","hypothesisId":"E"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2059,"ms":8.3},"timestamp":1774564684289,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2059,"ms":10.7},"timestamp":1774564684300,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":2069},"timestamp":1774564684607,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2141,"ms":12.9},"timestamp":1774564685671,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2141,"ms":9.9},"timestamp":1774564685681,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":2151},"timestamp":1774564685995,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2249,"ms":10.6},"timestamp":1774564687542,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2249,"ms":13.3},"timestamp":1774564687555,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":2259},"timestamp":1774564687874,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":2249,"newId":"8dbe730c"},"timestamp":1774564689289,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2355,"ms":18.7},"timestamp":1774564689309,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2355,"ms":10.9},"timestamp":1774564689320,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":2365},"timestamp":1774564689634,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":2355,"newId":"2e527a90"},"timestamp":1774564690491,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2430,"ms":19.2},"timestamp":1774564690511,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2430,"ms":10.6},"timestamp":1774564690522,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":2440},"timestamp":1774564690657,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":2430,"newId":"b57b0aff"},"timestamp":1774564691463,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2490,"ms":17.6},"timestamp":1774564691481,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2490,"ms":10.6},"timestamp":1774564691492,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":2500},"timestamp":1774564691662,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":2490,"newId":"db42a9e2"},"timestamp":1774564692497,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2550,"ms":24.1},"timestamp":1774564692523,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2550,"ms":25.1},"timestamp":1774564692548,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":2560},"timestamp":1774564692867,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":2550,"newId":"561b0202"},"timestamp":1774564693575,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2570,"ms":25.6},"timestamp":1774564693616,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2570,"ms":23.7},"timestamp":1774564693640,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":2580},"timestamp":1774564694016,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":2570,"newId":"aa5a90c8"},"timestamp":1774564694936,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2650,"ms":19},"timestamp":1774564694981,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2650,"ms":19.9},"timestamp":1774564695001,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":2660},"timestamp":1774564695442,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2742,"ms":17.6},"timestamp":1774564696611,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2742,"ms":16.6},"timestamp":1774564696628,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":2752},"timestamp":1774564697399,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2914,"ms":24.1},"timestamp":1774564699476,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":2914,"ms":27.9},"timestamp":1774564699505,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":2924},"timestamp":1774564700060,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":2914,"newId":"3bc830c5"},"timestamp":1774564703783,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3164,"ms":29.7},"timestamp":1774564703814,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3164,"ms":38},"timestamp":1774564703852,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":3174},"timestamp":1774564704398,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":3164,"newId":"4f76af26"},"timestamp":1774564707355,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3361,"ms":19.7},"timestamp":1774564707379,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3361,"ms":19.1},"timestamp":1774564707399,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":3371},"timestamp":1774564707423,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3361,"ms":19},"timestamp":1774564707533,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3361,"ms":17.5},"timestamp":1774564707550,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":3371},"timestamp":1774564707912,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":3361,"newId":"79f2412c"},"timestamp":1774564708648,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3378,"ms":16.5},"timestamp":1774564708713,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3378,"ms":19.1},"timestamp":1774564708733,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":3388},"timestamp":1774564709275,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3442,"ms":16.5},"timestamp":1774564709719,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3442,"ms":19.3},"timestamp":1774564709738,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":3452},"timestamp":1774564709927,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3484,"ms":15.7},"timestamp":1774564710354,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3484,"ms":16.8},"timestamp":1774564710371,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":3494},"timestamp":1774564710896,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3564,"ms":15.2},"timestamp":1774564711660,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3564,"ms":17.2},"timestamp":1774564711678,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":3574},"timestamp":1774564711871,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3639,"ms":15.7},"timestamp":1774564712915,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3639,"ms":17.9},"timestamp":1774564712934,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":3649},"timestamp":1774564713126,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":3639,"newId":"5f391d31"},"timestamp":1774564713626,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3684,"ms":17.5},"timestamp":1774564713646,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3684,"ms":15},"timestamp":1774564713661,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":3694},"timestamp":1774564713892,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3718,"ms":18.1},"timestamp":1774564714319,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3718,"ms":20.3},"timestamp":1774564714340,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":3728},"timestamp":1774564714759,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3769,"ms":18},"timestamp":1774564715066,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3769,"ms":17.8},"timestamp":1774564715084,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":3779},"timestamp":1774564715293,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":3769,"newId":"7be768e8"},"timestamp":1774564715646,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":7548,"rowIndex":7547,"selectedId":"7be768e8"},"timestamp":1774564716197,"runId":"post-fix","hypothesisId":"E"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3805,"ms":18.5},"timestamp":1774564716393,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3805,"ms":16.3},"timestamp":1774564716409,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":3815},"timestamp":1774564716806,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":3805,"newId":"5c46ded9"},"timestamp":1774564717248,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3904,"ms":17.3},"timestamp":1774564717269,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3904,"ms":18.7},"timestamp":1774564717288,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":3914},"timestamp":1774564717503,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3934,"ms":20.1},"timestamp":1774564717853,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3934,"ms":16.6},"timestamp":1774564717870,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":3944},"timestamp":1774564718193,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":3934,"newId":"828df2a2"},"timestamp":1774564718450,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3974,"ms":18.8},"timestamp":1774564718470,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":3974,"ms":16.1},"timestamp":1774564718486,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":3984},"timestamp":1774564719150,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4014,"ms":20.6},"timestamp":1774564719507,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4014,"ms":26.4},"timestamp":1774564719534,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":4024},"timestamp":1774564720010,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":4014,"newId":"ccab4bb2"},"timestamp":1774564720691,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4101,"ms":19.3},"timestamp":1774564720725,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4101,"ms":27.9},"timestamp":1774564720754,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":4111},"timestamp":1774564721430,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4170,"ms":18.3},"timestamp":1774564721905,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4170,"ms":25.6},"timestamp":1774564721931,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":4180},"timestamp":1774564722554,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4172,"ms":26},"timestamp":1774564722649,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4172,"ms":18},"timestamp":1774564722667,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":4182},"timestamp":1774564723337,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4227,"ms":17.4},"timestamp":1774564723785,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4227,"ms":25.6},"timestamp":1774564723811,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":4237},"timestamp":1774564724378,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4312,"ms":37},"timestamp":1774564725182,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4312,"ms":36.1},"timestamp":1774564725219,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":4322},"timestamp":1774564725813,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":4312,"newId":"02206f85"},"timestamp":1774564727667,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4466,"ms":35.4},"timestamp":1774564727723,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4466,"ms":47},"timestamp":1774564727771,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":4476},"timestamp":1774564728667,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4708,"ms":48.7},"timestamp":1774564731733,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4708,"ms":49.5},"timestamp":1774564731783,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":4718},"timestamp":1774564733196,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":4708,"newId":"7a5712c7"},"timestamp":1774564736498,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4977,"ms":34.7},"timestamp":1774564736542,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4977,"ms":24.9},"timestamp":1774564736567,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":4987},"timestamp":1774564737275,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4994,"ms":25.8},"timestamp":1774564737613,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4994,"ms":29.1},"timestamp":1774564737642,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":5004},"timestamp":1774564738020,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":4994,"newId":"e7166ef8"},"timestamp":1774564738441,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":9998,"rowIndex":9997,"selectedId":"e7166ef8"},"timestamp":1774564739150,"runId":"post-fix","hypothesisId":"E"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4994,"ms":44.7},"timestamp":1774564742488,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4994,"ms":43.7},"timestamp":1774564742532,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":5004},"timestamp":1774564743532,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":0,"ms":0.1},"timestamp":1774564773409,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":0,"ms":0},"timestamp":1774564773595,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":0,"ms":0},"timestamp":1774564773752,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":0,"ms":0},"timestamp":1774564773892,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":0,"ms":0},"timestamp":1774564774228,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":0,"ms":0.1},"timestamp":1774564783661,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":0,"ms":0},"timestamp":1774564783662,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4994,"ms":49},"timestamp":1774564786955,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:executionGroups","message":"tree rebuild","data":{"entryCount":4994,"ms":55.7},"timestamp":1774564787012,"runId":"post-fix-3","hypothesisId":"A"} -{"sessionId":"f357a1","location":"terminal.tsx:autoExpand","message":"auto-expand firing","data":{"nodeCount":5004},"timestamp":1774564787065,"hypothesisId":"D"} -{"sessionId":"f357a1","location":"terminal.tsx:autoSelect","message":"auto-select firing","data":{"entryCount":4994,"newId":"e7166ef8"},"timestamp":1774564787459,"runId":"post-fix","hypothesisId":"C"} -{"sessionId":"f357a1","location":"terminal.tsx:scrollToRow","message":"scroll effect","data":{"rowCount":9998,"rowIndex":9997,"selectedId":"e7166ef8"},"timestamp":1774564787473,"runId":"post-fix","hypothesisId":"E"} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx index d5d5b0ea7fc..3fead285c1f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx @@ -265,7 +265,9 @@ export function Chat() { ) const hasConsoleHydrated = useTerminalConsoleStore((state) => state._hasHydrated) - const entries = useWorkflowConsoleEntries(hasConsoleHydrated ? activeWorkflowId : undefined) + const entries = useWorkflowConsoleEntries( + hasConsoleHydrated && typeof activeWorkflowId === 'string' ? activeWorkflowId : undefined + ) const { isExecuting } = useCurrentWorkflowExecution() const { handleRunWorkflow, handleCancelExecution } = useWorkflowExecution() const { data: session } = useSession() 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 09a11eb9b5e..f363576a4d1 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 @@ -535,46 +535,6 @@ const EntryNodeRow = memo(function EntryNodeRow({ ) }) -/** - * Execution group row component with dashed separator - */ -const ExecutionGroupRow = memo(function ExecutionGroupRow({ - group, - showSeparator, - selectedEntryId, - onSelectEntry, - expandedNodes, - onToggleNode, -}: { - group: ExecutionGroup - showSeparator: boolean - selectedEntryId: string | null - onSelectEntry: (entry: ConsoleEntry) => void - expandedNodes: Set - onToggleNode: (nodeId: string) => void -}) { - return ( -
- {/* Separator between executions */} - {showSeparator &&
} - - {/* Entry tree */} -
- {group.entryTree.map((node) => ( - - ))} -
-
- ) -}) - interface TerminalLogListRowProps { rows: VisibleTerminalRow[] selectedEntryId: string | null diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.ts index c63345a1c88..01fd3c2adb8 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.ts @@ -118,7 +118,7 @@ export interface BlockEventHandlerConfig { } export interface BlockEventHandlerDeps { - addConsole: (entry: Omit) => ConsoleEntry + addConsole: (entry: Omit) => ConsoleEntry | undefined updateConsole: (blockId: string, update: string | ConsoleUpdate, executionId?: string) => void setActiveBlocks: (workflowId: string, blocks: Set) => void setBlockRunStatus: (workflowId: string, blockId: string, status: 'success' | 'error') => void @@ -407,7 +407,7 @@ export function createBlockEventHandlers( return { onBlockStarted, onBlockCompleted, onBlockError, onBlockChildWorkflowStarted } } -type AddConsoleFn = (entry: Omit) => ConsoleEntry +type AddConsoleFn = (entry: Omit) => ConsoleEntry | undefined type CancelRunningEntriesFn = (workflowId: string) => void export interface ExecutionTimingFields { From 98500a88c325e375b9fa23724c80ddb95f8f283b Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 26 Mar 2026 16:58:07 -0700 Subject: [PATCH 11/17] fix diff comment --- apps/sim/lib/workflows/diff/diff-engine.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/sim/lib/workflows/diff/diff-engine.ts b/apps/sim/lib/workflows/diff/diff-engine.ts index c78b60178bf..9a600e90d88 100644 --- a/apps/sim/lib/workflows/diff/diff-engine.ts +++ b/apps/sim/lib/workflows/diff/diff-engine.ts @@ -60,7 +60,11 @@ function computeFieldDiff( for (const field of fieldsToCheck) { const currentValue = currentBlock[field] const proposedValue = proposedBlock[field] - if (JSON.stringify(currentValue) !== JSON.stringify(proposedValue)) { + if ( + field === 'locked' + ? !!currentValue !== !!proposedValue + : JSON.stringify(currentValue) !== JSON.stringify(proposedValue) + ) { changedFields.push(field) } else if (currentValue !== undefined) { unchangedFields.push(field) From f56f6410a88f17613fc8e52ffa6dfcf341918b29 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 26 Mar 2026 17:33:20 -0700 Subject: [PATCH 12/17] fix chat history query --- apps/sim/hooks/queries/tasks.ts | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/apps/sim/hooks/queries/tasks.ts b/apps/sim/hooks/queries/tasks.ts index e667393f588..ea2a6cef4e3 100644 --- a/apps/sim/hooks/queries/tasks.ts +++ b/apps/sim/hooks/queries/tasks.ts @@ -133,13 +133,29 @@ export async function fetchChatHistory( chatId: string, signal?: AbortSignal ): Promise { - const response = await fetch(`/api/mothership/chats/${chatId}`, { signal }) + const mothershipRes = await fetch(`/api/mothership/chats/${chatId}`, { signal }) + + if (mothershipRes.ok) { + const { chat } = await mothershipRes.json() + return { + id: chat.id, + title: chat.title, + messages: Array.isArray(chat.messages) ? chat.messages : [], + activeStreamId: chat.conversationId || null, + resources: Array.isArray(chat.resources) ? chat.resources : [], + streamSnapshot: chat.streamSnapshot || null, + } + } - if (!response.ok) { + const copilotRes = await fetch(`/api/copilot/chat?chatId=${encodeURIComponent(chatId)}`, { + signal, + }) + + if (!copilotRes.ok) { throw new Error('Failed to load chat') } - const { chat } = await response.json() + const { chat } = await copilotRes.json() return { id: chat.id, title: chat.title, From dcf7121da095a805a0651038a59344403f16e357 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 26 Mar 2026 17:35:06 -0700 Subject: [PATCH 13/17] fix comment --- .../w/[workflowId]/hooks/use-workflow-execution.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts index a2bb0f45f84..25d1682839e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts @@ -1978,7 +1978,10 @@ export function useWorkflowExecution() { for (const entry of originalEntries) { addConsole(entry) } - consolePersistence.persist() + } + + if (!reconnectionComplete) { + consolePersistence.executionEnded() } } // eslint-disable-next-line react-hooks/exhaustive-deps From 8f89b77a68b1c3bcbf7da845f4e51be0a8c96f2b Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Thu, 26 Mar 2026 18:07:17 -0700 Subject: [PATCH 14/17] Ignore previously executed tool call ids --- .../[workspaceId]/home/hooks/use-chat.ts | 18 +++++++++++++++++- apps/sim/lib/copilot/client-sse/handlers.ts | 5 ----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts index 8f9f466a336..dde5ca739f2 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts @@ -147,6 +147,22 @@ function isActiveStreamConflictError(input: unknown): boolean { return input.includes('A response is already in progress for this chat') } +/** + * Extracts tool call IDs from snapshot events so that replayed client-executable + * tool calls are not re-executed after a page refresh. + */ +function extractToolCallIdsFromSnapshot(snapshot?: StreamSnapshot | null): Set { + const ids = new Set() + if (!snapshot?.events) return ids + for (const entry of snapshot.events) { + const event = entry.event + if (event.type === 'tool_call' && typeof event.toolCallId === 'string') { + ids.add(event.toolCallId) + } + } + return ids +} + function buildReplayStream(events: StreamEventEnvelope[]): ReadableStream { const encoder = new TextEncoder() return new ReadableStream({ @@ -860,7 +876,7 @@ export function useChat( sendingRef.current = true streamingContentRef.current = '' streamingBlocksRef.current = [] - clientExecutionStartedRef.current.clear() + clientExecutionStartedRef.current = extractToolCallIdsFromSnapshot(snapshot) const assistantId = crypto.randomUUID() diff --git a/apps/sim/lib/copilot/client-sse/handlers.ts b/apps/sim/lib/copilot/client-sse/handlers.ts index 2bb6bc3e318..e812311d5f3 100644 --- a/apps/sim/lib/copilot/client-sse/handlers.ts +++ b/apps/sim/lib/copilot/client-sse/handlers.ts @@ -21,7 +21,6 @@ import { useWorkflowDiffStore } from '@/stores/workflow-diff/store' import { captureBaselineSnapshot } from '@/stores/workflow-diff/utils' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import type { WorkflowState } from '@/stores/workflows/workflow/types' -import { executeRunToolOnClient } from './run-tool-execution' import type { ClientContentBlock, ClientStreamingContext } from './types' const logger = createLogger('CopilotClientSseHandlers') @@ -988,10 +987,6 @@ export const sseHandlers: Record = { return } - if (clientExecutable && initialState === ClientToolCallState.executing) { - executeRunToolOnClient(id, toolName, args || existing?.params || {}) - } - if (toolName === 'oauth_request_access' && args && typeof window !== 'undefined') { try { window.dispatchEvent( From bd5321e0ba185fd0093c8c5be53e94127e5323dd Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 26 Mar 2026 18:20:47 -0700 Subject: [PATCH 15/17] fix eval input color --- .../components/sub-block/components/eval-input/eval-input.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/eval-input/eval-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/eval-input/eval-input.tsx index 447752868e7..d8c9f8a84d6 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/eval-input/eval-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/eval-input/eval-input.tsx @@ -216,7 +216,7 @@ export function EvalInput({ placeholder='How accurate is the response?' disabled={isPreview || disabled} className={cn( - 'min-h-[80px] whitespace-pre-wrap text-transparent caret-white' + 'min-h-[80px] whitespace-pre-wrap text-transparent caret-foreground' )} rows={3} /> From 6328abd960782e6ea4c57dfc761715ded7df3977 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 26 Mar 2026 19:14:41 -0700 Subject: [PATCH 16/17] fix copilot run workflow --- .../app/api/workflows/[id]/execute/route.ts | 8 +- .../executions/[executionId]/stream/route.ts | 5 +- .../hooks/use-workflow-execution.ts | 362 +++++++++--------- .../utils/workflow-execution-utils.ts | 13 +- apps/sim/hooks/use-execution-stream.ts | 5 + apps/sim/lib/copilot/chat-streaming.ts | 12 +- .../copilot/client-sse/run-tool-execution.ts | 27 +- .../orchestrator/sse/handlers/handlers.ts | 25 +- apps/sim/lib/copilot/orchestrator/types.ts | 6 + .../workflows/executor/execution-events.ts | 1 + apps/sim/stores/terminal/console/index.ts | 8 +- apps/sim/stores/terminal/console/storage.ts | 37 ++ apps/sim/stores/terminal/index.ts | 4 + 13 files changed, 333 insertions(+), 180 deletions(-) diff --git a/apps/sim/app/api/workflows/[id]/execute/route.ts b/apps/sim/app/api/workflows/[id]/execute/route.ts index 8cee947272f..fa837f43a9e 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.ts @@ -865,7 +865,13 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: registerManualExecutionAborter(executionId, timeoutController.abort) isManualAbortRegistered = true + let localEventSeq = 0 const sendEvent = (event: ExecutionEvent) => { + const isBuffered = event.type !== 'stream:chunk' && event.type !== 'stream:done' + if (isBuffered) { + localEventSeq++ + event.eventId = localEventSeq + } if (!isStreamClosed) { try { controller.enqueue(encodeSSEEvent(event)) @@ -873,7 +879,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: isStreamClosed = true } } - if (event.type !== 'stream:chunk' && event.type !== 'stream:done') { + if (isBuffered) { eventWriter.write(event).catch(() => {}) } } diff --git a/apps/sim/app/api/workflows/[id]/executions/[executionId]/stream/route.ts b/apps/sim/app/api/workflows/[id]/executions/[executionId]/stream/route.ts index 745c5b7d44e..0893209c961 100644 --- a/apps/sim/app/api/workflows/[id]/executions/[executionId]/stream/route.ts +++ b/apps/sim/app/api/workflows/[id]/executions/[executionId]/stream/route.ts @@ -13,7 +13,7 @@ import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' const logger = createLogger('ExecutionStreamReconnectAPI') const POLL_INTERVAL_MS = 500 -const MAX_POLL_DURATION_MS = 10 * 60 * 1000 // 10 minutes +const MAX_POLL_DURATION_MS = 55 * 60 * 1000 // 55 minutes (just under Redis 1hr TTL) function isTerminalStatus(status: ExecutionStreamStatus): boolean { return status === 'complete' || status === 'error' || status === 'cancelled' @@ -101,6 +101,7 @@ export async function GET( const events = await readExecutionEvents(executionId, lastEventId) for (const entry of events) { if (closed) return + entry.event.eventId = entry.eventId enqueue(formatSSEEvent(entry.event)) lastEventId = entry.eventId } @@ -119,6 +120,7 @@ export async function GET( const newEvents = await readExecutionEvents(executionId, lastEventId) for (const entry of newEvents) { if (closed) return + entry.event.eventId = entry.eventId enqueue(formatSSEEvent(entry.event)) lastEventId = entry.eventId } @@ -128,6 +130,7 @@ export async function GET( const finalEvents = await readExecutionEvents(executionId, lastEventId) for (const entry of finalEvents) { if (closed) return + entry.event.eventId = entry.eventId enqueue(formatSSEEvent(entry.event)) lastEventId = entry.eventId } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts index 25d1682839e..75c119565f2 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts @@ -36,7 +36,13 @@ import { useCurrentWorkflowExecution, useExecutionStore } from '@/stores/executi import { useNotificationStore } from '@/stores/notifications' import { useVariablesStore } from '@/stores/panel' import { useEnvironmentStore } from '@/stores/settings/environment' -import { consolePersistence, useTerminalConsoleStore } from '@/stores/terminal' +import { + clearExecutionPointer, + consolePersistence, + loadExecutionPointer, + saveExecutionPointer, + useTerminalConsoleStore, +} from '@/stores/terminal' import { useWorkflowDiffStore } from '@/stores/workflow-diff' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { mergeSubblockState } from '@/stores/workflows/utils' @@ -131,6 +137,7 @@ export function useWorkflowExecution() { consolePersistence.executionStarted() } else { consolePersistence.executionEnded() + clearExecutionPointer(workflowId) } rawSetIsExecuting(workflowId, executing) }, @@ -149,6 +156,7 @@ export function useWorkflowExecution() { const [executionResult, setExecutionResult] = useState(null) const executionStream = useExecutionStream() const currentChatExecutionIdRef = useRef(null) + const lastSeenEventIdRef = useRef(0) const isViewingDiff = useWorkflowDiffStore((state) => state.isShowingDiff) const addNotification = useNotificationStore((state) => state.addNotification) @@ -1022,8 +1030,17 @@ export function useWorkflowExecution() { onExecutionId: (id) => { executionIdRef.current = id setCurrentExecutionId(activeWorkflowId, id) + saveExecutionPointer({ + workflowId: activeWorkflowId, + executionId: id, + lastEventId: 0, + }) }, callbacks: { + onEventId: (eventId) => { + lastSeenEventIdRef.current = eventId + }, + onExecutionStarted: (data) => { logger.info('Server execution started:', data) }, @@ -1792,197 +1809,198 @@ export function useWorkflowExecution() { useEffect(() => { if (!activeWorkflowId || !hasHydrated) return - - const entries = useTerminalConsoleStore.getState().getWorkflowEntries(activeWorkflowId) - const runningEntries = entries.filter( - (e) => e.isRunning && e.workflowId === activeWorkflowId && e.executionId - ) - if (runningEntries.length === 0) return - if (activeReconnections.has(activeWorkflowId)) return - activeReconnections.add(activeWorkflowId) - executionStream.cancel(activeWorkflowId) + let cleanupRan = false + let reconnectionComplete = false + const reconnectWorkflowId = activeWorkflowId - const sorted = [...runningEntries].sort((a, b) => { - const aTime = a.startedAt ? new Date(a.startedAt).getTime() : 0 - const bTime = b.startedAt ? new Date(b.startedAt).getTime() : 0 - return bTime - aTime - }) - const executionId = sorted[0].executionId! + const runReconnect = async () => { + let executionId: string | undefined + let fromEventId = 0 - const otherExecutionIds = new Set( - sorted.filter((e) => e.executionId !== executionId).map((e) => e.executionId!) - ) - if (otherExecutionIds.size > 0) { - cancelRunningEntries(activeWorkflowId) - consolePersistence.persist() - } + try { + const pointer = await loadExecutionPointer(reconnectWorkflowId) + if (cleanupRan) return + if (pointer && pointer.executionId) { + executionId = pointer.executionId + fromEventId = pointer.lastEventId + } + } catch { + // fall through to console entries + } - setCurrentExecutionId(activeWorkflowId, executionId) - setIsExecuting(activeWorkflowId, true) - - const workflowEdges = useWorkflowStore.getState().edges - const activeBlocksSet = new Set() - const activeBlockRefCounts = new Map() - const accumulatedBlockLogs: BlockLog[] = [] - const accumulatedBlockStates = new Map() - const executedBlockIds = new Set() - - const executionIdRef = { current: executionId } - - const handlers = buildBlockEventHandlers({ - workflowId: activeWorkflowId, - executionIdRef, - workflowEdges, - activeBlocksSet, - activeBlockRefCounts, - accumulatedBlockLogs, - accumulatedBlockStates, - executedBlockIds, - consoleMode: 'update', - includeStartConsoleEntry: true, - }) + if (!executionId) { + const entries = useTerminalConsoleStore.getState().getWorkflowEntries(reconnectWorkflowId) + const runningEntries = entries.filter( + (e) => e.isRunning && e.workflowId === reconnectWorkflowId && e.executionId + ) + if (runningEntries.length === 0) return - const originalEntries = entries - .filter((e) => e.executionId === executionId) - .map((e) => ({ ...e })) + const sorted = [...runningEntries].sort((a, b) => { + const aTime = a.startedAt ? new Date(a.startedAt).getTime() : 0 + const bTime = b.startedAt ? new Date(b.startedAt).getTime() : 0 + return bTime - aTime + }) + executionId = sorted[0].executionId! - let cleared = false - let reconnectionComplete = false - let cleanupRan = false - const clearOnce = () => { - if (!cleared) { - cleared = true - clearExecutionEntries(executionId) + const otherExecutionIds = new Set( + sorted.filter((e) => e.executionId !== executionId).map((e) => e.executionId!) + ) + if (otherExecutionIds.size > 0) { + cancelRunningEntries(reconnectWorkflowId) + consolePersistence.persist() + } } - } - const reconnectWorkflowId = activeWorkflowId + if (!executionId || cleanupRan) return + if (activeReconnections.has(reconnectWorkflowId)) return + activeReconnections.add(reconnectWorkflowId) + + executionStream.cancel(reconnectWorkflowId) + setCurrentExecutionId(reconnectWorkflowId, executionId) + setIsExecuting(reconnectWorkflowId, true) + + const workflowEdges = useWorkflowStore.getState().edges + const activeBlocksSet = new Set() + const activeBlockRefCounts = new Map() + const accumulatedBlockLogs: BlockLog[] = [] + const accumulatedBlockStates = new Map() + const executedBlockIds = new Set() + const executionIdRef = { current: executionId } - executionStream - .reconnect({ + const handlers = buildBlockEventHandlers({ workflowId: reconnectWorkflowId, - executionId, - callbacks: { - onBlockStarted: (data) => { - clearOnce() - handlers.onBlockStarted(data) - }, - onBlockCompleted: (data) => { - clearOnce() - handlers.onBlockCompleted(data) - }, - onBlockError: (data) => { - clearOnce() - handlers.onBlockError(data) - }, - onBlockChildWorkflowStarted: (data) => { - clearOnce() - handlers.onBlockChildWorkflowStarted(data) - }, - onExecutionCompleted: () => { - const currentId = useExecutionStore - .getState() - .getCurrentExecutionId(reconnectWorkflowId) - if (currentId !== executionId) { - reconnectionComplete = true - activeReconnections.delete(reconnectWorkflowId) - return - } - clearOnce() - reconnectionComplete = true - activeReconnections.delete(reconnectWorkflowId) - setCurrentExecutionId(reconnectWorkflowId, null) - setIsExecuting(reconnectWorkflowId, false) - setActiveBlocks(reconnectWorkflowId, new Set()) - }, - onExecutionError: (data) => { - const currentId = useExecutionStore - .getState() - .getCurrentExecutionId(reconnectWorkflowId) - if (currentId !== executionId) { - reconnectionComplete = true - activeReconnections.delete(reconnectWorkflowId) - return - } - clearOnce() - reconnectionComplete = true - activeReconnections.delete(reconnectWorkflowId) - setCurrentExecutionId(reconnectWorkflowId, null) - setIsExecuting(reconnectWorkflowId, false) - setActiveBlocks(reconnectWorkflowId, new Set()) - handleExecutionErrorConsole({ - workflowId: reconnectWorkflowId, - executionId, - error: data.error, - blockLogs: accumulatedBlockLogs, - }) - }, - onExecutionCancelled: () => { - const currentId = useExecutionStore - .getState() - .getCurrentExecutionId(reconnectWorkflowId) - if (currentId !== executionId) { - reconnectionComplete = true - activeReconnections.delete(reconnectWorkflowId) - return - } - clearOnce() - reconnectionComplete = true - activeReconnections.delete(reconnectWorkflowId) + executionIdRef, + workflowEdges, + activeBlocksSet, + activeBlockRefCounts, + accumulatedBlockLogs, + accumulatedBlockStates, + executedBlockIds, + consoleMode: 'update', + includeStartConsoleEntry: true, + }) + + clearExecutionEntries(executionId) + + const capturedExecutionId = executionId + const MAX_ATTEMPTS = 5 + const BASE_DELAY_MS = 1000 + const MAX_DELAY_MS = 15000 + + const attemptReconnect = async (attempt: number): Promise => { + if (cleanupRan || reconnectionComplete) return + + if (attempt > 0) { + const delay = Math.min(BASE_DELAY_MS * 2 ** (attempt - 1), MAX_DELAY_MS) + await new Promise((resolve) => setTimeout(resolve, delay)) + if (cleanupRan || reconnectionComplete) return + } + + try { + await executionStream.reconnect({ + workflowId: reconnectWorkflowId, + executionId: capturedExecutionId, + fromEventId, + callbacks: { + onEventId: (eid) => { + fromEventId = eid + }, + onBlockStarted: handlers.onBlockStarted, + onBlockCompleted: handlers.onBlockCompleted, + onBlockError: handlers.onBlockError, + onBlockChildWorkflowStarted: handlers.onBlockChildWorkflowStarted, + onExecutionCompleted: () => { + const currentId = useExecutionStore + .getState() + .getCurrentExecutionId(reconnectWorkflowId) + if (currentId !== capturedExecutionId) { + reconnectionComplete = true + activeReconnections.delete(reconnectWorkflowId) + return + } + reconnectionComplete = true + activeReconnections.delete(reconnectWorkflowId) + setCurrentExecutionId(reconnectWorkflowId, null) + setIsExecuting(reconnectWorkflowId, false) + setActiveBlocks(reconnectWorkflowId, new Set()) + }, + onExecutionError: (data) => { + const currentId = useExecutionStore + .getState() + .getCurrentExecutionId(reconnectWorkflowId) + if (currentId !== capturedExecutionId) { + reconnectionComplete = true + activeReconnections.delete(reconnectWorkflowId) + return + } + reconnectionComplete = true + activeReconnections.delete(reconnectWorkflowId) + setCurrentExecutionId(reconnectWorkflowId, null) + setIsExecuting(reconnectWorkflowId, false) + setActiveBlocks(reconnectWorkflowId, new Set()) + handleExecutionErrorConsole({ + workflowId: reconnectWorkflowId, + executionId: capturedExecutionId, + error: data.error, + blockLogs: accumulatedBlockLogs, + }) + }, + onExecutionCancelled: () => { + const currentId = useExecutionStore + .getState() + .getCurrentExecutionId(reconnectWorkflowId) + if (currentId !== capturedExecutionId) { + reconnectionComplete = true + activeReconnections.delete(reconnectWorkflowId) + return + } + reconnectionComplete = true + activeReconnections.delete(reconnectWorkflowId) + setCurrentExecutionId(reconnectWorkflowId, null) + setIsExecuting(reconnectWorkflowId, false) + setActiveBlocks(reconnectWorkflowId, new Set()) + handleExecutionCancelledConsole({ + workflowId: reconnectWorkflowId, + executionId: capturedExecutionId, + }) + }, + }, + }) + } catch (error) { + logger.warn('Execution reconnection attempt failed', { + executionId: capturedExecutionId, + attempt, + error, + }) + if (!cleanupRan && !reconnectionComplete && attempt < MAX_ATTEMPTS) { + return attemptReconnect(attempt + 1) + } + } + + if (!reconnectionComplete && !cleanupRan) { + reconnectionComplete = true + activeReconnections.delete(reconnectWorkflowId) + const currentId = useExecutionStore.getState().getCurrentExecutionId(reconnectWorkflowId) + if (currentId === capturedExecutionId) { + cancelRunningEntries(reconnectWorkflowId) setCurrentExecutionId(reconnectWorkflowId, null) setIsExecuting(reconnectWorkflowId, false) setActiveBlocks(reconnectWorkflowId, new Set()) - handleExecutionCancelledConsole({ - workflowId: reconnectWorkflowId, - executionId, - }) - }, - }, - }) - .catch((error) => { - logger.warn('Execution reconnection failed', { executionId, error }) - }) - .finally(() => { - if (reconnectionComplete || cleanupRan) return - const currentId = useExecutionStore.getState().getCurrentExecutionId(reconnectWorkflowId) - if (currentId !== executionId) return - reconnectionComplete = true - activeReconnections.delete(reconnectWorkflowId) - clearExecutionEntries(executionId) - for (const entry of originalEntries) { - addConsole({ - workflowId: entry.workflowId, - blockId: entry.blockId, - blockName: entry.blockName, - blockType: entry.blockType, - executionId: entry.executionId, - executionOrder: entry.executionOrder, - isRunning: false, - warning: 'Execution result unavailable — check the logs page', - }) + } } - setCurrentExecutionId(reconnectWorkflowId, null) - setIsExecuting(reconnectWorkflowId, false) - setActiveBlocks(reconnectWorkflowId, new Set()) - }) + } + + await attemptReconnect(0) + } + + runReconnect() return () => { cleanupRan = true executionStream.cancel(reconnectWorkflowId) activeReconnections.delete(reconnectWorkflowId) - - if (cleared && !reconnectionComplete) { - clearExecutionEntries(executionId) - for (const entry of originalEntries) { - addConsole(entry) - } - } - - if (!reconnectionComplete) { - consolePersistence.executionEnded() - } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [activeWorkflowId, hasHydrated]) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.ts index 01fd3c2adb8..3f27004a04e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.ts @@ -19,7 +19,7 @@ const logger = createLogger('workflow-execution-utils') import { useExecutionStore } from '@/stores/execution' import type { ConsoleEntry, ConsoleUpdate } from '@/stores/terminal' -import { useTerminalConsoleStore } from '@/stores/terminal' +import { saveExecutionPointer, useTerminalConsoleStore } from '@/stores/terminal' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' @@ -667,6 +667,7 @@ export async function executeWorkflowWithFullLogging( if (serverExecutionId) { executionIdRef.current = serverExecutionId setCurrentExecutionId(wfId, serverExecutionId) + saveExecutionPointer({ workflowId: wfId, executionId: serverExecutionId, lastEventId: 0 }) } let executionResult: ExecutionResult = { @@ -679,6 +680,16 @@ export async function executeWorkflowWithFullLogging( await processSSEStream( response.body.getReader(), { + onEventId: (eventId) => { + if (wfId && executionIdRef.current && eventId % 5 === 0) { + saveExecutionPointer({ + workflowId: wfId, + executionId: executionIdRef.current, + lastEventId: eventId, + }) + } + }, + onExecutionStarted: (data) => { logger.info('Execution started', { startTime: data.startTime }) }, diff --git a/apps/sim/hooks/use-execution-stream.ts b/apps/sim/hooks/use-execution-stream.ts index 36fd801db63..1198a179bea 100644 --- a/apps/sim/hooks/use-execution-stream.ts +++ b/apps/sim/hooks/use-execution-stream.ts @@ -63,6 +63,10 @@ export async function processSSEStream( try { const event = JSON.parse(data) as ExecutionEvent + if (event.eventId != null) { + callbacks.onEventId?.(event.eventId) + } + switch (event.type) { case 'execution:started': callbacks.onExecutionStarted?.(event.data) @@ -118,6 +122,7 @@ export interface ExecutionStreamCallbacks { onBlockChildWorkflowStarted?: (data: BlockChildWorkflowStartedData) => void onStreamChunk?: (data: StreamChunkData) => void onStreamDone?: (data: StreamDoneData) => void + onEventId?: (eventId: number) => void } export interface ExecuteStreamOptions { diff --git a/apps/sim/lib/copilot/chat-streaming.ts b/apps/sim/lib/copilot/chat-streaming.ts index 46d9ff758fc..76ae305a9f8 100644 --- a/apps/sim/lib/copilot/chat-streaming.ts +++ b/apps/sim/lib/copilot/chat-streaming.ts @@ -290,6 +290,7 @@ export function createSSEStream(params: StreamingOrchestrationParams): ReadableS let clientDisconnected = false const abortController = new AbortController() const userStopController = new AbortController() + const clientDisconnectedController = new AbortController() activeStreams.set(streamId, { abortController, userStopController }) if (chatId && !pendingChatStreamAlreadyRegistered) { @@ -302,6 +303,9 @@ export function createSSEStream(params: StreamingOrchestrationParams): ReadableS const markClientDisconnected = (reason: string) => { if (clientDisconnected) return clientDisconnected = true + if (!clientDisconnectedController.signal.aborted) { + clientDisconnectedController.abort() + } logger.info( appendCopilotLogContext('Client disconnected from live SSE stream', { requestId, @@ -456,6 +460,7 @@ export function createSSEStream(params: StreamingOrchestrationParams): ReadableS runId, abortSignal: abortController.signal, userStopSignal: userStopController.signal, + clientDisconnectedSignal: clientDisconnectedController.signal, onEvent: async (event) => { await pushEvent(event) }, @@ -616,7 +621,12 @@ export function createSSEStream(params: StreamingOrchestrationParams): ReadableS runId, } ) - clientDisconnected = true + if (!clientDisconnected) { + clientDisconnected = true + if (!clientDisconnectedController.signal.aborted) { + clientDisconnectedController.abort() + } + } if (eventWriter) { eventWriter.flush().catch(() => {}) } diff --git a/apps/sim/lib/copilot/client-sse/run-tool-execution.ts b/apps/sim/lib/copilot/client-sse/run-tool-execution.ts index 13d087cb9cb..c289202d505 100644 --- a/apps/sim/lib/copilot/client-sse/run-tool-execution.ts +++ b/apps/sim/lib/copilot/client-sse/run-tool-execution.ts @@ -4,7 +4,7 @@ import { COPILOT_CONFIRM_API_PATH } from '@/lib/copilot/constants' import { ClientToolCallState } from '@/lib/copilot/tools/client/tool-display-registry' import { executeWorkflowWithFullLogging } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils' import { useExecutionStore } from '@/stores/execution/store' -import { consolePersistence } from '@/stores/terminal' +import { clearExecutionPointer, consolePersistence, saveExecutionPointer } from '@/stores/terminal' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' const logger = createLogger('CopilotRunToolExecution') @@ -153,8 +153,29 @@ async function doExecuteRunTool( setIsExecuting(targetWorkflowId, true) const executionId = uuidv4() setCurrentExecutionId(targetWorkflowId, executionId) + saveExecutionPointer({ workflowId: targetWorkflowId, executionId, lastEventId: 0 }) const executionStartTime = new Date().toISOString() + const onPageHide = () => { + if (manuallyStoppedToolCallIds.has(toolCallId)) return + navigator.sendBeacon( + COPILOT_CONFIRM_API_PATH, + new Blob( + [ + JSON.stringify({ + toolCallId, + status: 'background', + message: 'Client disconnected, execution continuing server-side', + }), + ], + { type: 'application/json' } + ) + ) + } + if (typeof window !== 'undefined') { + window.addEventListener('pagehide', onPageHide) + } + logger.info('[RunTool] Starting client-side workflow execution', { toolCallId, toolName, @@ -232,6 +253,9 @@ async function doExecuteRunTool( await reportCompletion(toolCallId, 'error', msg) } } finally { + if (typeof window !== 'undefined') { + window.removeEventListener('pagehide', onPageHide) + } manuallyStoppedToolCallIds.delete(toolCallId) const activeToolCallId = activeRunToolByWorkflowId.get(targetWorkflowId) if (activeToolCallId === toolCallId) { @@ -243,6 +267,7 @@ async function doExecuteRunTool( } const { setCurrentExecutionId: clearExecId } = useExecutionStore.getState() clearExecId(targetWorkflowId, null) + clearExecutionPointer(targetWorkflowId) consolePersistence.executionEnded() setIsExecuting(targetWorkflowId, false) } diff --git a/apps/sim/lib/copilot/orchestrator/sse/handlers/handlers.ts b/apps/sim/lib/copilot/orchestrator/sse/handlers/handlers.ts index 1b242eef77a..3732fed983e 100644 --- a/apps/sim/lib/copilot/orchestrator/sse/handlers/handlers.ts +++ b/apps/sim/lib/copilot/orchestrator/sse/handlers/handlers.ts @@ -25,6 +25,25 @@ import { executeToolAndReport, waitForToolCompletion } from './tool-execution' const logger = createLogger('CopilotSseHandlers') +/** + * Builds an AbortSignal that fires when either the main abort signal OR + * the client-disconnect signal fires. Used for client-executable tool waits + * so the orchestrator doesn't block for the full timeout when the browser dies. + */ +function buildClientToolAbortSignal(options: OrchestratorOptions): AbortSignal | undefined { + const { abortSignal, clientDisconnectedSignal } = options + if (!clientDisconnectedSignal || clientDisconnectedSignal.aborted) { + return clientDisconnectedSignal?.aborted ? AbortSignal.abort() : abortSignal + } + if (!abortSignal) return clientDisconnectedSignal + + const combined = new AbortController() + const fire = () => combined.abort() + abortSignal.addEventListener('abort', fire, { once: true }) + clientDisconnectedSignal.addEventListener('abort', fire, { once: true }) + return combined.signal +} + function registerPendingToolPromise( context: StreamingContext, toolCallId: string, @@ -538,10 +557,11 @@ export const sseHandlers: Record = { } ) }) + const clientWaitSignal = buildClientToolAbortSignal(options) const completion = await waitForToolCompletion( toolCallId, options.timeout || STREAM_TIMEOUT_MS, - options.abortSignal + clientWaitSignal ) handleClientCompletion(toolCall, toolCallId, completion, context) await emitSyntheticToolResult(toolCallId, toolCall.name, completion, options, context) @@ -794,10 +814,11 @@ export const subAgentHandlers: Record = { } ) }) + const clientWaitSignal = buildClientToolAbortSignal(options) const completion = await waitForToolCompletion( toolCallId, options.timeout || STREAM_TIMEOUT_MS, - options.abortSignal + clientWaitSignal ) handleClientCompletion(toolCall, toolCallId, completion, context) await emitSyntheticToolResult(toolCallId, toolCall.name, completion, options, context) diff --git a/apps/sim/lib/copilot/orchestrator/types.ts b/apps/sim/lib/copilot/orchestrator/types.ts index 55cc2ba6331..b5a9e412f8e 100644 --- a/apps/sim/lib/copilot/orchestrator/types.ts +++ b/apps/sim/lib/copilot/orchestrator/types.ts @@ -166,6 +166,12 @@ export interface OrchestratorOptions { abortSignal?: AbortSignal /** Fires only on explicit user stop, never on passive transport disconnect. */ userStopSignal?: AbortSignal + /** + * Fires when the SSE client disconnects (tab close, navigation, etc.). + * Used to short-circuit `waitForToolCompletion` for client-executable tools + * so the orchestrator doesn't block for the full 60-min timeout. + */ + clientDisconnectedSignal?: AbortSignal interactive?: boolean } diff --git a/apps/sim/lib/workflows/executor/execution-events.ts b/apps/sim/lib/workflows/executor/execution-events.ts index 2a2c06d4016..e87f4d5971f 100644 --- a/apps/sim/lib/workflows/executor/execution-events.ts +++ b/apps/sim/lib/workflows/executor/execution-events.ts @@ -24,6 +24,7 @@ export interface BaseExecutionEvent { type: ExecutionEventType timestamp: string executionId: string + eventId?: number } /** diff --git a/apps/sim/stores/terminal/console/index.ts b/apps/sim/stores/terminal/console/index.ts index 83e72289b29..af22e59bb0c 100644 --- a/apps/sim/stores/terminal/console/index.ts +++ b/apps/sim/stores/terminal/console/index.ts @@ -1,4 +1,10 @@ -export { consolePersistence } from './storage' +export { + clearExecutionPointer, + consolePersistence, + type ExecutionPointer, + loadExecutionPointer, + saveExecutionPointer, +} from './storage' export { useConsoleEntry, useTerminalConsoleStore, useWorkflowConsoleEntries } from './store' export type { ConsoleEntry, ConsoleStore, ConsoleUpdate } from './types' export { diff --git a/apps/sim/stores/terminal/console/storage.ts b/apps/sim/stores/terminal/console/storage.ts index 3a490338609..35f3eb91167 100644 --- a/apps/sim/stores/terminal/console/storage.ts +++ b/apps/sim/stores/terminal/console/storage.ts @@ -199,3 +199,40 @@ class ConsolePersistenceManager { } export const consolePersistence = new ConsolePersistenceManager() + +const EXEC_POINTER_PREFIX = 'terminal-active-execution:' + +/** + * Lightweight pointer to an in-flight execution, persisted immediately on + * execution start so the reconnect flow can find it even if no console + * entries have been written yet. Keyed per-workflow so multiple tabs + * running different workflows don't overwrite each other. + */ +export interface ExecutionPointer { + workflowId: string + executionId: string + lastEventId: number +} + +export async function loadExecutionPointer(workflowId: string): Promise { + if (typeof window === 'undefined') return null + try { + const raw = await get(`${EXEC_POINTER_PREFIX}${workflowId}`) + if (!raw) return null + const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw + if (!parsed?.executionId) return null + return parsed as ExecutionPointer + } catch { + return null + } +} + +export function saveExecutionPointer(pointer: ExecutionPointer): void { + if (typeof window === 'undefined') return + set(`${EXEC_POINTER_PREFIX}${pointer.workflowId}`, JSON.stringify(pointer)).catch(() => {}) +} + +export function clearExecutionPointer(workflowId: string): void { + if (typeof window === 'undefined') return + set(`${EXEC_POINTER_PREFIX}${workflowId}`, '').catch(() => {}) +} diff --git a/apps/sim/stores/terminal/index.ts b/apps/sim/stores/terminal/index.ts index d580e7ee65c..42854c0854c 100644 --- a/apps/sim/stores/terminal/index.ts +++ b/apps/sim/stores/terminal/index.ts @@ -1,10 +1,14 @@ export type { ConsoleEntry, ConsoleStore, ConsoleUpdate } from './console' export { + clearExecutionPointer, consolePersistence, + type ExecutionPointer, + loadExecutionPointer, normalizeConsoleError, normalizeConsoleInput, normalizeConsoleOutput, safeConsoleStringify, + saveExecutionPointer, TERMINAL_CONSOLE_LIMITS, trimConsoleEntries, trimWorkflowConsoleEntries, From 27a0e22533c56501d19fa30a686c048c8b8ac218 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 26 Mar 2026 19:21:41 -0700 Subject: [PATCH 17/17] fix tests --- apps/sim/lib/copilot/client-sse/run-tool-execution.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/sim/lib/copilot/client-sse/run-tool-execution.test.ts b/apps/sim/lib/copilot/client-sse/run-tool-execution.test.ts index e87f8ba185c..395dd25741e 100644 --- a/apps/sim/lib/copilot/client-sse/run-tool-execution.test.ts +++ b/apps/sim/lib/copilot/client-sse/run-tool-execution.test.ts @@ -41,6 +41,8 @@ vi.mock('@/stores/terminal', () => ({ executionEnded: vi.fn(), persist: vi.fn(), }, + saveExecutionPointer: vi.fn(), + clearExecutionPointer: vi.fn(), })) import {