From 17b2091ad650722e46293974740604c9be6a3ad0 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 26 Jan 2026 17:55:30 -0800 Subject: [PATCH 1/7] feat(code): undo-redo state --- .../sub-block/components/code/code.tsx | 57 ++++- apps/sim/hooks/use-code-undo-redo.ts | 218 ++++++++++++++++++ apps/sim/hooks/use-collaborative-workflow.ts | 6 +- apps/sim/stores/undo-redo/code-storage.ts | 36 +++ apps/sim/stores/undo-redo/code-store.ts | 179 ++++++++++++++ apps/sim/stores/undo-redo/index.ts | 1 + 6 files changed, 485 insertions(+), 12 deletions(-) create mode 100644 apps/sim/hooks/use-code-undo-redo.ts create mode 100644 apps/sim/stores/undo-redo/code-storage.ts create mode 100644 apps/sim/stores/undo-redo/code-store.ts diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/code/code.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/code/code.tsx index c67cb5528f..d758a26746 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/code/code.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/code/code.tsx @@ -39,6 +39,8 @@ import { normalizeName } from '@/executor/constants' import { createEnvVarPattern, createReferencePattern } from '@/executor/utils/reference-validation' import { useTagSelection } from '@/hooks/kb/use-tag-selection' import { createShouldHighlightEnvVar, useAvailableEnvVarKeys } from '@/hooks/use-available-env-vars' +import { useCodeUndoRedo } from '@/hooks/use-code-undo-redo' +import { useWorkflowStore } from '@/stores/workflows/workflow/store' const logger = createLogger('Code') @@ -212,7 +214,6 @@ export const Code = memo(function Code({ const handleStreamStartRef = useRef<() => void>(() => {}) const handleGeneratedContentRef = useRef<(generatedCode: string) => void>(() => {}) const handleStreamChunkRef = useRef<(chunk: string) => void>(() => {}) - const hasEditedSinceFocusRef = useRef(false) const codeRef = useRef(code) codeRef.current = code @@ -220,8 +221,12 @@ export const Code = memo(function Code({ const emitTagSelection = useTagSelection(blockId, subBlockId) const [languageValue] = useSubBlockValue(blockId, 'language') const availableEnvVars = useAvailableEnvVarKeys(workspaceId) + const blockType = useWorkflowStore( + useCallback((state) => state.blocks?.[blockId]?.type, [blockId]) + ) const effectiveLanguage = (languageValue as 'javascript' | 'python' | 'json') || language + const isFunctionCode = blockType === 'function' && subBlockId === 'code' const trimmedCode = code.trim() const containsReferencePlaceholders = @@ -296,6 +301,16 @@ export const Code = memo(function Code({ const updatePromptValue = wandHook?.updatePromptValue || (() => {}) const cancelGeneration = wandHook?.cancelGeneration || (() => {}) + const { recordChange, clearHistory, recordReplace, flushPending, startSession, undo, redo } = + useCodeUndoRedo({ + blockId, + subBlockId, + value: code, + enabled: isFunctionCode, + isReadOnly: readOnly || disabled || isPreview, + isStreaming: isAiStreaming, + }) + const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId, false, { isStreaming: isAiStreaming, onStreamingEnd: () => { @@ -347,9 +362,10 @@ export const Code = memo(function Code({ setCode(generatedCode) if (!isPreview && !disabled) { setStoreValue(generatedCode) + recordReplace(generatedCode) } } - }, [isPreview, disabled, setStoreValue]) + }, [disabled, isPreview, recordReplace, setStoreValue]) useEffect(() => { if (!editorRef.current) return @@ -492,7 +508,7 @@ export const Code = memo(function Code({ setCode(newValue) setStoreValue(newValue) - hasEditedSinceFocusRef.current = true + recordChange(newValue) const newCursorPosition = dropPosition + 1 setCursorPosition(newCursorPosition) @@ -521,7 +537,7 @@ export const Code = memo(function Code({ if (!isPreview && !readOnly) { setCode(newValue) emitTagSelection(newValue) - hasEditedSinceFocusRef.current = true + recordChange(newValue) } setShowTags(false) setActiveSourceBlockId(null) @@ -539,7 +555,7 @@ export const Code = memo(function Code({ if (!isPreview && !readOnly) { setCode(newValue) emitTagSelection(newValue) - hasEditedSinceFocusRef.current = true + recordChange(newValue) } setShowEnvVars(false) @@ -625,9 +641,9 @@ export const Code = memo(function Code({ const handleValueChange = useCallback( (newCode: string) => { if (!isAiStreaming && !isPreview && !disabled && !readOnly) { - hasEditedSinceFocusRef.current = true setCode(newCode) setStoreValue(newCode) + recordChange(newCode) const textarea = editorRef.current?.querySelector('textarea') if (textarea) { @@ -646,7 +662,7 @@ export const Code = memo(function Code({ } } }, - [isAiStreaming, isPreview, disabled, readOnly, setStoreValue] + [isAiStreaming, isPreview, disabled, readOnly, recordChange, setStoreValue] ) const handleKeyDown = useCallback( @@ -657,21 +673,39 @@ export const Code = memo(function Code({ } if (isAiStreaming) { e.preventDefault() + return } - if (e.key === 'z' && (e.metaKey || e.ctrlKey) && !hasEditedSinceFocusRef.current) { + if (!isFunctionCode) return + const isUndo = (e.key === 'z' || e.key === 'Z') && (e.metaKey || e.ctrlKey) && !e.shiftKey + const isRedo = + ((e.key === 'z' || e.key === 'Z') && (e.metaKey || e.ctrlKey) && e.shiftKey) || + (e.key === 'y' && (e.metaKey || e.ctrlKey)) + if (isUndo) { e.preventDefault() + e.stopPropagation() + undo() + return + } + if (isRedo) { + e.preventDefault() + e.stopPropagation() + redo() } }, - [isAiStreaming] + [isAiStreaming, isFunctionCode, redo, undo] ) const handleEditorFocus = useCallback(() => { - hasEditedSinceFocusRef.current = false + startSession(codeRef.current) if (!isPreview && !disabled && !readOnly && codeRef.current.trim() === '') { setShowTags(true) setCursorPosition(0) } - }, [isPreview, disabled, readOnly]) + }, [disabled, isPreview, readOnly, startSession]) + + const handleEditorBlur = useCallback(() => { + flushPending() + }, [flushPending]) /** * Renders the line numbers, aligned with wrapped visual lines and highlighting the active line. @@ -791,6 +825,7 @@ export const Code = memo(function Code({ onValueChange={handleValueChange} onKeyDown={handleKeyDown} onFocus={handleEditorFocus} + onBlur={handleEditorBlur} highlight={highlightCode} {...getCodeEditorProps({ isStreaming: isAiStreaming, isPreview, disabled })} /> diff --git a/apps/sim/hooks/use-code-undo-redo.ts b/apps/sim/hooks/use-code-undo-redo.ts new file mode 100644 index 0000000000..e9da1bb577 --- /dev/null +++ b/apps/sim/hooks/use-code-undo-redo.ts @@ -0,0 +1,218 @@ +import { useCallback, useEffect, useMemo, useRef } from 'react' +import { createLogger } from '@sim/logger' +import { useShallow } from 'zustand/react/shallow' +import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' +import { useCodeUndoRedoStore } from '@/stores/undo-redo' +import { useWorkflowDiffStore } from '@/stores/workflow-diff/store' +import { useWorkflowRegistry } from '@/stores/workflows/registry/store' + +const logger = createLogger('CodeUndoRedo') + +interface UseCodeUndoRedoOptions { + blockId: string + subBlockId: string + value: string + enabled?: boolean + isReadOnly?: boolean + isStreaming?: boolean + debounceMs?: number +} + +export function useCodeUndoRedo({ + blockId, + subBlockId, + value, + enabled = true, + isReadOnly = false, + isStreaming = false, + debounceMs = 300, +}: UseCodeUndoRedoOptions) { + const { collaborativeSetSubblockValue } = useCollaborativeWorkflow() + const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId) + const { isShowingDiff, hasActiveDiff } = useWorkflowDiffStore( + useShallow((state) => ({ + isShowingDiff: state.isShowingDiff, + hasActiveDiff: state.hasActiveDiff, + })) + ) + + const isBaselineView = hasActiveDiff && !isShowingDiff + const isEnabled = useMemo( + () => Boolean(enabled && activeWorkflowId && !isReadOnly && !isStreaming && !isBaselineView), + [enabled, activeWorkflowId, isReadOnly, isStreaming, isBaselineView] + ) + + const lastCommittedValueRef = useRef(value ?? '') + const pendingBeforeRef = useRef(null) + const pendingAfterRef = useRef(null) + const timeoutRef = useRef | null>(null) + const isApplyingRef = useRef(false) + + const clearTimer = useCallback(() => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + timeoutRef.current = null + } + }, []) + + const resetPending = useCallback(() => { + pendingBeforeRef.current = null + pendingAfterRef.current = null + }, []) + + const commitPending = useCallback(() => { + if (!isEnabled || !activeWorkflowId) { + clearTimer() + resetPending() + return + } + + const before = pendingBeforeRef.current + const after = pendingAfterRef.current + if (before === null || after === null) return + + if (before === after) { + lastCommittedValueRef.current = after + clearTimer() + resetPending() + return + } + + useCodeUndoRedoStore.getState().push({ + id: crypto.randomUUID(), + createdAt: Date.now(), + workflowId: activeWorkflowId, + blockId, + subBlockId, + before, + after, + }) + + lastCommittedValueRef.current = after + clearTimer() + resetPending() + }, [activeWorkflowId, blockId, clearTimer, isEnabled, resetPending, subBlockId]) + + const recordChange = useCallback( + (nextValue: string) => { + if (!isEnabled || isApplyingRef.current) return + + if (pendingBeforeRef.current === null) { + pendingBeforeRef.current = lastCommittedValueRef.current ?? '' + } + + pendingAfterRef.current = nextValue + clearTimer() + timeoutRef.current = setTimeout(commitPending, debounceMs) + }, + [clearTimer, commitPending, debounceMs, isEnabled] + ) + + const recordReplace = useCallback( + (nextValue: string) => { + if (!isEnabled || isApplyingRef.current || !activeWorkflowId) return + + if (pendingBeforeRef.current !== null) { + commitPending() + } + + const before = lastCommittedValueRef.current ?? '' + if (before === nextValue) { + lastCommittedValueRef.current = nextValue + resetPending() + return + } + + useCodeUndoRedoStore.getState().push({ + id: crypto.randomUUID(), + createdAt: Date.now(), + workflowId: activeWorkflowId, + blockId, + subBlockId, + before, + after: nextValue, + }) + + lastCommittedValueRef.current = nextValue + clearTimer() + resetPending() + }, + [activeWorkflowId, blockId, clearTimer, commitPending, isEnabled, resetPending, subBlockId] + ) + + const clearHistory = useCallback( + (nextValue?: string) => { + if (!activeWorkflowId) return + clearTimer() + resetPending() + useCodeUndoRedoStore.getState().clear(activeWorkflowId, blockId, subBlockId) + if (nextValue !== undefined) { + lastCommittedValueRef.current = nextValue + } + }, + [activeWorkflowId, blockId, clearTimer, resetPending, subBlockId] + ) + + const flushPending = useCallback(() => { + if (!pendingBeforeRef.current) return + clearTimer() + commitPending() + }, [clearTimer, commitPending]) + + const startSession = useCallback( + (currentValue: string) => { + clearTimer() + resetPending() + lastCommittedValueRef.current = currentValue ?? '' + }, + [clearTimer, resetPending] + ) + + const applyValue = useCallback( + (nextValue: string) => { + if (!isEnabled) return + isApplyingRef.current = true + try { + collaborativeSetSubblockValue(blockId, subBlockId, nextValue) + } finally { + isApplyingRef.current = false + } + lastCommittedValueRef.current = nextValue + clearTimer() + resetPending() + }, + [blockId, clearTimer, collaborativeSetSubblockValue, isEnabled, resetPending, subBlockId] + ) + + const undo = useCallback(() => { + if (!activeWorkflowId || !isEnabled) return + const entry = useCodeUndoRedoStore.getState().undo(activeWorkflowId, blockId, subBlockId) + if (!entry) return + logger.debug('Undo code edit', { blockId, subBlockId }) + applyValue(entry.before) + }, [activeWorkflowId, applyValue, blockId, isEnabled, subBlockId]) + + const redo = useCallback(() => { + if (!activeWorkflowId || !isEnabled) return + const entry = useCodeUndoRedoStore.getState().redo(activeWorkflowId, blockId, subBlockId) + if (!entry) return + logger.debug('Redo code edit', { blockId, subBlockId }) + applyValue(entry.after) + }, [activeWorkflowId, applyValue, blockId, isEnabled, subBlockId]) + + useEffect(() => { + if (!pendingBeforeRef.current && !isApplyingRef.current) { + lastCommittedValueRef.current = value ?? '' + } + }, [value]) + + return { + recordChange, + clearHistory, + recordReplace, + flushPending, + startSession, + undo, + redo, + } +} diff --git a/apps/sim/hooks/use-collaborative-workflow.ts b/apps/sim/hooks/use-collaborative-workflow.ts index 89aff1d12b..5f57215490 100644 --- a/apps/sim/hooks/use-collaborative-workflow.ts +++ b/apps/sim/hooks/use-collaborative-workflow.ts @@ -20,7 +20,7 @@ import { import { useNotificationStore } from '@/stores/notifications' import { registerEmitFunctions, useOperationQueue } from '@/stores/operation-queue/store' import { usePanelEditorStore, useVariablesStore } from '@/stores/panel' -import { useUndoRedoStore } from '@/stores/undo-redo' +import { useCodeUndoRedoStore, useUndoRedoStore } from '@/stores/undo-redo' import { useWorkflowDiffStore } from '@/stores/workflow-diff/store' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' @@ -449,6 +449,10 @@ export function useCollaborativeWorkflow() { try { // The setValue function automatically uses the active workflow ID useSubBlockStore.getState().setValue(blockId, subblockId, value) + const blockType = useWorkflowStore.getState().blocks?.[blockId]?.type + if (activeWorkflowId && blockType === 'function' && subblockId === 'code') { + useCodeUndoRedoStore.getState().clear(activeWorkflowId, blockId, subblockId) + } } catch (error) { logger.error('Error applying remote subblock update:', error) } finally { diff --git a/apps/sim/stores/undo-redo/code-storage.ts b/apps/sim/stores/undo-redo/code-storage.ts new file mode 100644 index 0000000000..05f86c82ba --- /dev/null +++ b/apps/sim/stores/undo-redo/code-storage.ts @@ -0,0 +1,36 @@ +import { createLogger } from '@sim/logger' +import { del, get, set } from 'idb-keyval' +import type { StateStorage } from 'zustand/middleware' + +const logger = createLogger('CodeUndoRedoStorage') + +export const codeUndoRedoStorage: StateStorage = { + getItem: async (name: string): Promise => { + if (typeof window === 'undefined') return null + 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 }) + } + }, + + removeItem: async (name: string): Promise => { + if (typeof window === 'undefined') return + try { + await del(name) + } catch (error) { + logger.warn('IndexedDB delete failed', { name, error }) + } + }, +} diff --git a/apps/sim/stores/undo-redo/code-store.ts b/apps/sim/stores/undo-redo/code-store.ts new file mode 100644 index 0000000000..5b1ebc119d --- /dev/null +++ b/apps/sim/stores/undo-redo/code-store.ts @@ -0,0 +1,179 @@ +import { create } from 'zustand' +import { createJSONStorage, devtools, persist } from 'zustand/middleware' +import { codeUndoRedoStorage } from '@/stores/undo-redo/code-storage' + +interface CodeUndoRedoEntry { + id: string + createdAt: number + workflowId: string + blockId: string + subBlockId: string + before: string + after: string +} + +interface CodeUndoRedoStack { + undo: CodeUndoRedoEntry[] + redo: CodeUndoRedoEntry[] + lastUpdated?: number +} + +interface CodeUndoRedoState { + stacks: Record + capacity: number + push: (entry: CodeUndoRedoEntry) => void + undo: (workflowId: string, blockId: string, subBlockId: string) => CodeUndoRedoEntry | null + redo: (workflowId: string, blockId: string, subBlockId: string) => CodeUndoRedoEntry | null + clear: (workflowId: string, blockId: string, subBlockId: string) => void + clearRedo: (workflowId: string, blockId: string, subBlockId: string) => void + getStackSizes: ( + workflowId: string, + blockId: string, + subBlockId: string + ) => { undoSize: number; redoSize: number } + reset: () => void +} + +const DEFAULT_CAPACITY = 500 +const MAX_STACKS = 50 + +function getStackKey(workflowId: string, blockId: string, subBlockId: string): string { + return `${workflowId}:${blockId}:${subBlockId}` +} + +const initialState = { + stacks: {} as Record, + capacity: DEFAULT_CAPACITY, +} + +export const useCodeUndoRedoStore = create()( + devtools( + persist( + (set, get) => ({ + ...initialState, + push: (entry) => { + if (entry.before === entry.after) return + + const state = get() + const key = getStackKey(entry.workflowId, entry.blockId, entry.subBlockId) + const currentStacks = { ...state.stacks } + + const stackKeys = Object.keys(currentStacks) + if (stackKeys.length >= MAX_STACKS && !currentStacks[key]) { + let oldestKey: string | null = null + let oldestTime = Number.POSITIVE_INFINITY + + for (const stackKey of stackKeys) { + const t = currentStacks[stackKey].lastUpdated ?? 0 + if (t < oldestTime) { + oldestTime = t + oldestKey = stackKey + } + } + + if (oldestKey) { + delete currentStacks[oldestKey] + } + } + + const stack = currentStacks[key] || { undo: [], redo: [] } + + const newUndo = [...stack.undo, entry] + if (newUndo.length > state.capacity) { + newUndo.shift() + } + + currentStacks[key] = { + undo: newUndo, + redo: [], + lastUpdated: Date.now(), + } + + set({ stacks: currentStacks }) + }, + undo: (workflowId, blockId, subBlockId) => { + const key = getStackKey(workflowId, blockId, subBlockId) + const state = get() + const stack = state.stacks[key] + if (!stack || stack.undo.length === 0) return null + + const entry = stack.undo[stack.undo.length - 1] + const newUndo = stack.undo.slice(0, -1) + const newRedo = [...stack.redo, entry] + + set({ + stacks: { + ...state.stacks, + [key]: { + undo: newUndo, + redo: newRedo.slice(-state.capacity), + lastUpdated: Date.now(), + }, + }, + }) + + return entry + }, + redo: (workflowId, blockId, subBlockId) => { + const key = getStackKey(workflowId, blockId, subBlockId) + const state = get() + const stack = state.stacks[key] + if (!stack || stack.redo.length === 0) return null + + const entry = stack.redo[stack.redo.length - 1] + const newRedo = stack.redo.slice(0, -1) + const newUndo = [...stack.undo, entry] + + set({ + stacks: { + ...state.stacks, + [key]: { + undo: newUndo.slice(-state.capacity), + redo: newRedo, + lastUpdated: Date.now(), + }, + }, + }) + + return entry + }, + clear: (workflowId, blockId, subBlockId) => { + const key = getStackKey(workflowId, blockId, subBlockId) + const state = get() + const { [key]: _, ...rest } = state.stacks + set({ stacks: rest }) + }, + clearRedo: (workflowId, blockId, subBlockId) => { + const key = getStackKey(workflowId, blockId, subBlockId) + const state = get() + const stack = state.stacks[key] + if (!stack) return + set({ + stacks: { + ...state.stacks, + [key]: { ...stack, redo: [], lastUpdated: Date.now() }, + }, + }) + }, + getStackSizes: (workflowId, blockId, subBlockId) => { + const key = getStackKey(workflowId, blockId, subBlockId) + const stack = get().stacks[key] + if (!stack) return { undoSize: 0, redoSize: 0 } + return { undoSize: stack.undo.length, redoSize: stack.redo.length } + }, + reset: () => { + set(initialState) + }, + }), + { + name: 'code-undo-redo-store', + storage: createJSONStorage(() => codeUndoRedoStorage), + partialize: (state) => ({ + stacks: state.stacks, + capacity: state.capacity, + }), + } + ), + { name: 'code-undo-redo-store' } + ) +) diff --git a/apps/sim/stores/undo-redo/index.ts b/apps/sim/stores/undo-redo/index.ts index d97adabaf0..5c9d815fc1 100644 --- a/apps/sim/stores/undo-redo/index.ts +++ b/apps/sim/stores/undo-redo/index.ts @@ -1,3 +1,4 @@ +export { useCodeUndoRedoStore } from './code-store' export { runWithUndoRedoRecordingSuspended, useUndoRedoStore } from './store' export * from './types' export * from './utils' From ae5c1bb91e1bc78b3a8281eaafab856654f7aba0 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 26 Jan 2026 18:02:44 -0800 Subject: [PATCH 2/7] address greptile --- apps/sim/hooks/use-code-undo-redo.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/sim/hooks/use-code-undo-redo.ts b/apps/sim/hooks/use-code-undo-redo.ts index e9da1bb577..d318b66add 100644 --- a/apps/sim/hooks/use-code-undo-redo.ts +++ b/apps/sim/hooks/use-code-undo-redo.ts @@ -206,6 +206,12 @@ export function useCodeUndoRedo({ } }, [value]) + useEffect(() => { + return () => { + clearTimer() + } + }, [clearTimer]) + return { recordChange, clearHistory, From bd827d4bafb4248a7d5adddf1af73b15c97cdbe0 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 26 Jan 2026 18:16:55 -0800 Subject: [PATCH 3/7] address bugbot comments --- .../sub-block/components/code/code.tsx | 17 ++++++++--------- apps/sim/hooks/use-code-undo-redo.ts | 19 +++++++++++++++---- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/code/code.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/code/code.tsx index d758a26746..6c43b2e30a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/code/code.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/code/code.tsx @@ -301,15 +301,14 @@ export const Code = memo(function Code({ const updatePromptValue = wandHook?.updatePromptValue || (() => {}) const cancelGeneration = wandHook?.cancelGeneration || (() => {}) - const { recordChange, clearHistory, recordReplace, flushPending, startSession, undo, redo } = - useCodeUndoRedo({ - blockId, - subBlockId, - value: code, - enabled: isFunctionCode, - isReadOnly: readOnly || disabled || isPreview, - isStreaming: isAiStreaming, - }) + const { recordChange, recordReplace, flushPending, startSession, undo, redo } = useCodeUndoRedo({ + blockId, + subBlockId, + value: code, + enabled: isFunctionCode, + isReadOnly: readOnly || disabled || isPreview, + isStreaming: isAiStreaming, + }) const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId, false, { isStreaming: isAiStreaming, diff --git a/apps/sim/hooks/use-code-undo-redo.ts b/apps/sim/hooks/use-code-undo-redo.ts index d318b66add..a40115fdce 100644 --- a/apps/sim/hooks/use-code-undo-redo.ts +++ b/apps/sim/hooks/use-code-undo-redo.ts @@ -154,7 +154,7 @@ export function useCodeUndoRedo({ ) const flushPending = useCallback(() => { - if (!pendingBeforeRef.current) return + if (pendingBeforeRef.current === null) return clearTimer() commitPending() }, [clearTimer, commitPending]) @@ -201,10 +201,21 @@ export function useCodeUndoRedo({ }, [activeWorkflowId, applyValue, blockId, isEnabled, subBlockId]) useEffect(() => { - if (!pendingBeforeRef.current && !isApplyingRef.current) { - lastCommittedValueRef.current = value ?? '' + if (isApplyingRef.current) return + + const nextValue = value ?? '' + + if (pendingBeforeRef.current !== null) { + if (pendingAfterRef.current !== nextValue) { + clearTimer() + resetPending() + lastCommittedValueRef.current = nextValue + } + return } - }, [value]) + + lastCommittedValueRef.current = nextValue + }, [clearTimer, resetPending, value]) useEffect(() => { return () => { From 865a5bb9796ff812f56e7f6f9ca41535464bed0c Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 26 Jan 2026 18:41:11 -0800 Subject: [PATCH 4/7] fix debounce flush --- apps/sim/hooks/use-code-undo-redo.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/sim/hooks/use-code-undo-redo.ts b/apps/sim/hooks/use-code-undo-redo.ts index a40115fdce..3f2c5f5d0f 100644 --- a/apps/sim/hooks/use-code-undo-redo.ts +++ b/apps/sim/hooks/use-code-undo-redo.ts @@ -186,19 +186,25 @@ export function useCodeUndoRedo({ const undo = useCallback(() => { if (!activeWorkflowId || !isEnabled) return + if (pendingBeforeRef.current !== null) { + flushPending() + } const entry = useCodeUndoRedoStore.getState().undo(activeWorkflowId, blockId, subBlockId) if (!entry) return logger.debug('Undo code edit', { blockId, subBlockId }) applyValue(entry.before) - }, [activeWorkflowId, applyValue, blockId, isEnabled, subBlockId]) + }, [activeWorkflowId, applyValue, blockId, flushPending, isEnabled, subBlockId]) const redo = useCallback(() => { if (!activeWorkflowId || !isEnabled) return + if (pendingBeforeRef.current !== null) { + flushPending() + } const entry = useCodeUndoRedoStore.getState().redo(activeWorkflowId, blockId, subBlockId) if (!entry) return logger.debug('Redo code edit', { blockId, subBlockId }) applyValue(entry.after) - }, [activeWorkflowId, applyValue, blockId, isEnabled, subBlockId]) + }, [activeWorkflowId, applyValue, blockId, flushPending, isEnabled, subBlockId]) useEffect(() => { if (isApplyingRef.current) return From b0338d3152d7a16669ecede8e0a45d677f74bafa Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 26 Jan 2026 18:41:23 -0800 Subject: [PATCH 5/7] inc debounce time --- apps/sim/hooks/use-code-undo-redo.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/hooks/use-code-undo-redo.ts b/apps/sim/hooks/use-code-undo-redo.ts index 3f2c5f5d0f..0fdd1de38f 100644 --- a/apps/sim/hooks/use-code-undo-redo.ts +++ b/apps/sim/hooks/use-code-undo-redo.ts @@ -25,7 +25,7 @@ export function useCodeUndoRedo({ enabled = true, isReadOnly = false, isStreaming = false, - debounceMs = 300, + debounceMs = 500, }: UseCodeUndoRedoOptions) { const { collaborativeSetSubblockValue } = useCollaborativeWorkflow() const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId) From be556b970cae190aedc7361a75590fd65596a618 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 26 Jan 2026 19:04:18 -0800 Subject: [PATCH 6/7] fix wand case --- apps/sim/hooks/use-code-undo-redo.ts | 34 +++++++++++++--------------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/apps/sim/hooks/use-code-undo-redo.ts b/apps/sim/hooks/use-code-undo-redo.ts index 0fdd1de38f..10fd6e9fde 100644 --- a/apps/sim/hooks/use-code-undo-redo.ts +++ b/apps/sim/hooks/use-code-undo-redo.ts @@ -41,6 +41,10 @@ export function useCodeUndoRedo({ () => Boolean(enabled && activeWorkflowId && !isReadOnly && !isStreaming && !isBaselineView), [enabled, activeWorkflowId, isReadOnly, isStreaming, isBaselineView] ) + const isReplaceEnabled = useMemo( + () => Boolean(enabled && activeWorkflowId && !isReadOnly && !isBaselineView), + [enabled, activeWorkflowId, isReadOnly, isBaselineView] + ) const lastCommittedValueRef = useRef(value ?? '') const pendingBeforeRef = useRef(null) @@ -110,7 +114,7 @@ export function useCodeUndoRedo({ const recordReplace = useCallback( (nextValue: string) => { - if (!isEnabled || isApplyingRef.current || !activeWorkflowId) return + if (!isReplaceEnabled || isApplyingRef.current || !activeWorkflowId) return if (pendingBeforeRef.current !== null) { commitPending() @@ -137,20 +141,15 @@ export function useCodeUndoRedo({ clearTimer() resetPending() }, - [activeWorkflowId, blockId, clearTimer, commitPending, isEnabled, resetPending, subBlockId] - ) - - const clearHistory = useCallback( - (nextValue?: string) => { - if (!activeWorkflowId) return - clearTimer() - resetPending() - useCodeUndoRedoStore.getState().clear(activeWorkflowId, blockId, subBlockId) - if (nextValue !== undefined) { - lastCommittedValueRef.current = nextValue - } - }, - [activeWorkflowId, blockId, clearTimer, resetPending, subBlockId] + [ + activeWorkflowId, + blockId, + clearTimer, + commitPending, + isReplaceEnabled, + resetPending, + subBlockId, + ] ) const flushPending = useCallback(() => { @@ -207,7 +206,7 @@ export function useCodeUndoRedo({ }, [activeWorkflowId, applyValue, blockId, flushPending, isEnabled, subBlockId]) useEffect(() => { - if (isApplyingRef.current) return + if (isApplyingRef.current || isStreaming) return const nextValue = value ?? '' @@ -221,7 +220,7 @@ export function useCodeUndoRedo({ } lastCommittedValueRef.current = nextValue - }, [clearTimer, resetPending, value]) + }, [clearTimer, isStreaming, resetPending, value]) useEffect(() => { return () => { @@ -231,7 +230,6 @@ export function useCodeUndoRedo({ return { recordChange, - clearHistory, recordReplace, flushPending, startSession, From 7d1a1b8c9eff37fa6ab13c68835ea122bc1ae8a5 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 26 Jan 2026 19:22:21 -0800 Subject: [PATCH 7/7] address comments --- apps/sim/hooks/use-code-undo-redo.ts | 4 ++-- apps/sim/stores/undo-redo/code-store.ts | 28 ------------------------- 2 files changed, 2 insertions(+), 30 deletions(-) diff --git a/apps/sim/hooks/use-code-undo-redo.ts b/apps/sim/hooks/use-code-undo-redo.ts index 10fd6e9fde..d0edbb535e 100644 --- a/apps/sim/hooks/use-code-undo-redo.ts +++ b/apps/sim/hooks/use-code-undo-redo.ts @@ -224,9 +224,9 @@ export function useCodeUndoRedo({ useEffect(() => { return () => { - clearTimer() + flushPending() } - }, [clearTimer]) + }, [flushPending]) return { recordChange, diff --git a/apps/sim/stores/undo-redo/code-store.ts b/apps/sim/stores/undo-redo/code-store.ts index 5b1ebc119d..c421126d59 100644 --- a/apps/sim/stores/undo-redo/code-store.ts +++ b/apps/sim/stores/undo-redo/code-store.ts @@ -25,13 +25,6 @@ interface CodeUndoRedoState { undo: (workflowId: string, blockId: string, subBlockId: string) => CodeUndoRedoEntry | null redo: (workflowId: string, blockId: string, subBlockId: string) => CodeUndoRedoEntry | null clear: (workflowId: string, blockId: string, subBlockId: string) => void - clearRedo: (workflowId: string, blockId: string, subBlockId: string) => void - getStackSizes: ( - workflowId: string, - blockId: string, - subBlockId: string - ) => { undoSize: number; redoSize: number } - reset: () => void } const DEFAULT_CAPACITY = 500 @@ -143,27 +136,6 @@ export const useCodeUndoRedoStore = create()( const { [key]: _, ...rest } = state.stacks set({ stacks: rest }) }, - clearRedo: (workflowId, blockId, subBlockId) => { - const key = getStackKey(workflowId, blockId, subBlockId) - const state = get() - const stack = state.stacks[key] - if (!stack) return - set({ - stacks: { - ...state.stacks, - [key]: { ...stack, redo: [], lastUpdated: Date.now() }, - }, - }) - }, - getStackSizes: (workflowId, blockId, subBlockId) => { - const key = getStackKey(workflowId, blockId, subBlockId) - const stack = get().stacks[key] - if (!stack) return { undoSize: 0, redoSize: 0 } - return { undoSize: stack.undo.length, redoSize: stack.redo.length } - }, - reset: () => { - set(initialState) - }, }), { name: 'code-undo-redo-store',