Skip to content

Commit ed56451

Browse files
icecrasher321Theodore Li
andauthored
improvement(terminal): performance improvements (#3796)
* improvement(terminal): prevent canvas crashes * checkpoint * make persistence execution scoped not debounce * revert feature flags * address bugbot comments * fix test * fix * fix type * fix abortion of manual run * fix type errors * fix diff comment * fix chat history query * fix comment * Ignore previously executed tool call ids * fix eval input color * fix copilot run workflow * fix tests --------- Co-authored-by: Theodore Li <theo@sim.ai>
1 parent 50e42c2 commit ed56451

File tree

31 files changed

+2034
-719
lines changed

31 files changed

+2034
-719
lines changed

apps/sim/app/api/workflows/[id]/execute/route.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -865,15 +865,21 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
865865
registerManualExecutionAborter(executionId, timeoutController.abort)
866866
isManualAbortRegistered = true
867867

868+
let localEventSeq = 0
868869
const sendEvent = (event: ExecutionEvent) => {
870+
const isBuffered = event.type !== 'stream:chunk' && event.type !== 'stream:done'
871+
if (isBuffered) {
872+
localEventSeq++
873+
event.eventId = localEventSeq
874+
}
869875
if (!isStreamClosed) {
870876
try {
871877
controller.enqueue(encodeSSEEvent(event))
872878
} catch {
873879
isStreamClosed = true
874880
}
875881
}
876-
if (event.type !== 'stream:chunk' && event.type !== 'stream:done') {
882+
if (isBuffered) {
877883
eventWriter.write(event).catch(() => {})
878884
}
879885
}

apps/sim/app/api/workflows/[id]/executions/[executionId]/stream/route.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
1313
const logger = createLogger('ExecutionStreamReconnectAPI')
1414

1515
const POLL_INTERVAL_MS = 500
16-
const MAX_POLL_DURATION_MS = 10 * 60 * 1000 // 10 minutes
16+
const MAX_POLL_DURATION_MS = 55 * 60 * 1000 // 55 minutes (just under Redis 1hr TTL)
1717

1818
function isTerminalStatus(status: ExecutionStreamStatus): boolean {
1919
return status === 'complete' || status === 'error' || status === 'cancelled'
@@ -101,6 +101,7 @@ export async function GET(
101101
const events = await readExecutionEvents(executionId, lastEventId)
102102
for (const entry of events) {
103103
if (closed) return
104+
entry.event.eventId = entry.eventId
104105
enqueue(formatSSEEvent(entry.event))
105106
lastEventId = entry.eventId
106107
}
@@ -119,6 +120,7 @@ export async function GET(
119120
const newEvents = await readExecutionEvents(executionId, lastEventId)
120121
for (const entry of newEvents) {
121122
if (closed) return
123+
entry.event.eventId = entry.eventId
122124
enqueue(formatSSEEvent(entry.event))
123125
lastEventId = entry.eventId
124126
}
@@ -128,6 +130,7 @@ export async function GET(
128130
const finalEvents = await readExecutionEvents(executionId, lastEventId)
129131
for (const entry of finalEvents) {
130132
if (closed) return
133+
entry.event.eventId = entry.eventId
131134
enqueue(formatSSEEvent(entry.event))
132135
lastEventId = entry.eventId
133136
}

apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import { useExecutionStream } from '@/hooks/use-execution-stream'
3939
import { useExecutionStore } from '@/stores/execution/store'
4040
import { useFolderStore } from '@/stores/folders/store'
4141
import type { ChatContext } from '@/stores/panel'
42-
import { useTerminalConsoleStore } from '@/stores/terminal'
42+
import { consolePersistence, useTerminalConsoleStore } from '@/stores/terminal'
4343
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
4444
import type {
4545
ChatMessage,
@@ -147,6 +147,22 @@ function isActiveStreamConflictError(input: unknown): boolean {
147147
return input.includes('A response is already in progress for this chat')
148148
}
149149

150+
/**
151+
* Extracts tool call IDs from snapshot events so that replayed client-executable
152+
* tool calls are not re-executed after a page refresh.
153+
*/
154+
function extractToolCallIdsFromSnapshot(snapshot?: StreamSnapshot | null): Set<string> {
155+
const ids = new Set<string>()
156+
if (!snapshot?.events) return ids
157+
for (const entry of snapshot.events) {
158+
const event = entry.event
159+
if (event.type === 'tool_call' && typeof event.toolCallId === 'string') {
160+
ids.add(event.toolCallId)
161+
}
162+
}
163+
return ids
164+
}
165+
150166
function buildReplayStream(events: StreamEventEnvelope[]): ReadableStream<Uint8Array> {
151167
const encoder = new TextEncoder()
152168
return new ReadableStream<Uint8Array>({
@@ -860,7 +876,7 @@ export function useChat(
860876
sendingRef.current = true
861877
streamingContentRef.current = ''
862878
streamingBlocksRef.current = []
863-
clientExecutionStartedRef.current.clear()
879+
clientExecutionStartedRef.current = extractToolCallIdsFromSnapshot(snapshot)
864880

865881
const assistantId = crypto.randomUUID()
866882

@@ -2071,6 +2087,7 @@ export function useChat(
20712087
})
20722088

20732089
executionStream.cancel(workflowId)
2090+
consolePersistence.executionEnded()
20742091
execState.setIsExecuting(workflowId, false)
20752092
execState.setIsDebugging(workflowId, false)
20762093
execState.setActiveBlocks(workflowId, new Set())

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ import { useChatStore } from '@/stores/chat/store'
5656
import { getChatPosition } from '@/stores/chat/utils'
5757
import { useCurrentWorkflowExecution } from '@/stores/execution'
5858
import { useOperationQueue } from '@/stores/operation-queue/store'
59-
import { useTerminalConsoleStore } from '@/stores/terminal'
59+
import { useTerminalConsoleStore, useWorkflowConsoleEntries } from '@/stores/terminal'
6060
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
6161
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
6262
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
@@ -265,8 +265,9 @@ export function Chat() {
265265
)
266266

267267
const hasConsoleHydrated = useTerminalConsoleStore((state) => state._hasHydrated)
268-
const entriesFromStore = useTerminalConsoleStore((state) => state.entries)
269-
const entries = hasConsoleHydrated ? entriesFromStore : []
268+
const entries = useWorkflowConsoleEntries(
269+
hasConsoleHydrated && typeof activeWorkflowId === 'string' ? activeWorkflowId : undefined
270+
)
270271
const { isExecuting } = useCurrentWorkflowExecution()
271272
const { handleRunWorkflow, handleCancelExecution } = useWorkflowExecution()
272273
const { data: session } = useSession()
@@ -427,9 +428,8 @@ export function Chat() {
427428
})
428429

429430
const outputEntries = useMemo(() => {
430-
if (!activeWorkflowId) return []
431-
return entries.filter((entry) => entry.workflowId === activeWorkflowId && entry.output)
432-
}, [entries, activeWorkflowId])
431+
return entries.filter((entry) => entry.output)
432+
}, [entries])
433433

434434
const workflowMessages = useMemo(() => {
435435
if (!activeWorkflowId) return []

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/eval-input/eval-input.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ export function EvalInput({
216216
placeholder='How accurate is the response?'
217217
disabled={isPreview || disabled}
218218
className={cn(
219-
'min-h-[80px] whitespace-pre-wrap text-transparent caret-white'
219+
'min-h-[80px] whitespace-pre-wrap text-transparent caret-foreground'
220220
)}
221221
rows={3}
222222
/>

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/output-panel/output-panel.tsx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import { ToggleButton } from '@/app/workspace/[workspaceId]/w/[workflowId]/compo
3333
import { useContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
3434
import { useCodeViewerFeatures } from '@/hooks/use-code-viewer'
3535
import type { ConsoleEntry } from '@/stores/terminal'
36-
import { useTerminalStore } from '@/stores/terminal'
36+
import { safeConsoleStringify, useTerminalStore } from '@/stores/terminal'
3737

3838
interface OutputCodeContentProps {
3939
code: string
@@ -93,11 +93,10 @@ export interface OutputPanelProps {
9393
handleTrainingClick: (e: React.MouseEvent) => void
9494
showCopySuccess: boolean
9595
handleCopy: () => void
96-
filteredEntries: ConsoleEntry[]
96+
hasEntries: boolean
9797
handleExportConsole: (e: React.MouseEvent) => void
9898
handleClearConsole: (e: React.MouseEvent) => void
9999
shouldShowCodeDisplay: boolean
100-
outputDataStringified: string
101100
outputData: unknown
102101
handleClearConsoleFromMenu: () => void
103102
}
@@ -121,11 +120,10 @@ export const OutputPanel = React.memo(function OutputPanel({
121120
handleTrainingClick,
122121
showCopySuccess,
123122
handleCopy,
124-
filteredEntries,
123+
hasEntries,
125124
handleExportConsole,
126125
handleClearConsole,
127126
shouldShowCodeDisplay,
128-
outputDataStringified,
129127
outputData,
130128
handleClearConsoleFromMenu,
131129
}: OutputPanelProps) {
@@ -276,6 +274,19 @@ export const OutputPanel = React.memo(function OutputPanel({
276274
[isOutputSearchActive, outputSearchQuery]
277275
)
278276

277+
const outputDataStringified = useMemo(() => {
278+
if (
279+
structuredView ||
280+
shouldShowCodeDisplay ||
281+
outputData === null ||
282+
outputData === undefined
283+
) {
284+
return ''
285+
}
286+
287+
return safeConsoleStringify(outputData)
288+
}, [outputData, shouldShowCodeDisplay, structuredView])
289+
279290
return (
280291
<>
281292
<div
@@ -420,7 +431,7 @@ export const OutputPanel = React.memo(function OutputPanel({
420431
<span>{showCopySuccess ? 'Copied' : 'Copy output'}</span>
421432
</Tooltip.Content>
422433
</Tooltip.Root>
423-
{filteredEntries.length > 0 && (
434+
{hasEntries && (
424435
<>
425436
<Tooltip.Root>
426437
<Tooltip.Trigger asChild>

0 commit comments

Comments
 (0)