Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
TagDropdown,
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
import { getActiveWorkflowSearchHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight'
import { useEditorUndoRedo } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-editor-undo-redo'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import type { WandControlHandlers } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block'
import { restoreCursorAfterInsertion } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/utils'
Expand All @@ -46,9 +47,7 @@ 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 type { ActiveSearchTarget } from '@/stores/panel/editor/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'

const logger = createLogger('Code')

Expand Down Expand Up @@ -258,12 +257,8 @@ export const Code = memo(function Code({
const emitTagSelection = useTagSelection(blockId, subBlockId)
const [languageValue] = useSubBlockValue<string>(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 =
Expand Down Expand Up @@ -344,14 +339,7 @@ export const Code = memo(function Code({
const updatePromptValue = wandHook?.updatePromptValue || (() => {})
const cancelGeneration = wandHook?.cancelGeneration || (() => {})

const { recordChange, recordReplace, flushPending, startSession, undo, redo } = useCodeUndoRedo({
blockId,
subBlockId,
value: code,
enabled: isFunctionCode,
isReadOnly: readOnly || disabled || isPreview,
isStreaming: isAiStreaming,
})
const handleEditorUndoRedo = useEditorUndoRedo()

const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId, false, {
isStreaming: isAiStreaming,
Expand Down Expand Up @@ -404,10 +392,9 @@ export const Code = memo(function Code({
setCode(generatedCode)
if (!isPreview && !disabled) {
setStoreValue(generatedCode)
recordReplace(generatedCode)
}
}
}, [disabled, isPreview, recordReplace, setStoreValue])
}, [disabled, isPreview, setStoreValue])

useEffect(() => {
if (!editorRef.current) return
Expand Down Expand Up @@ -550,7 +537,6 @@ export const Code = memo(function Code({

setCode(newValue)
setStoreValue(newValue)
recordChange(newValue)
const newCursorPosition = dropPosition + 1
setCursorPosition(newCursorPosition)

Expand Down Expand Up @@ -582,7 +568,6 @@ export const Code = memo(function Code({
if (!isPreview && !readOnly) {
setCode(newValue)
emitTagSelection(newValue)
recordChange(newValue)
restoreCursorAfterInsertion(textarea, newCursorPosition)
} else {
setTimeout(() => textarea?.focus(), 0)
Expand All @@ -602,7 +587,6 @@ export const Code = memo(function Code({
if (!isPreview && !readOnly) {
setCode(newValue)
emitTagSelection(newValue)
recordChange(newValue)
restoreCursorAfterInsertion(textarea, newCursorPosition)
} else {
setTimeout(() => textarea?.focus(), 0)
Expand Down Expand Up @@ -699,7 +683,6 @@ export const Code = memo(function Code({
if (!isAiStreaming && !isPreview && !disabled && !readOnly) {
setCode(newCode)
setStoreValue(newCode)
recordChange(newCode)

const textarea = editorRef.current?.querySelector('textarea')
if (textarea) {
Expand All @@ -718,7 +701,7 @@ export const Code = memo(function Code({
}
}
},
[isAiStreaming, isPreview, disabled, readOnly, recordChange, setStoreValue]
[isAiStreaming, isPreview, disabled, readOnly, setStoreValue]
)

const handleKeyDown = useCallback(
Expand All @@ -731,37 +714,17 @@ export const Code = memo(function Code({
e.preventDefault()
return
}
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()
}
handleEditorUndoRedo(e)
},
[isAiStreaming, isFunctionCode, redo, undo]
[isAiStreaming, handleEditorUndoRedo]
)

const handleEditorFocus = useCallback(() => {
startSession(codeRef.current)
if (!isPreview && !disabled && !readOnly && codeRef.current.trim() === '') {
setShowTags(true)
setCursorPosition(0)
}
}, [disabled, isPreview, readOnly, startSession])

const handleEditorBlur = useCallback(() => {
flushPending()
}, [flushPending])
}, [disabled, isPreview, readOnly])

/**
* Renders the line numbers, aligned with wrapped visual lines and highlighting the active line.
Expand Down Expand Up @@ -881,7 +844,6 @@ export const Code = memo(function Code({
onValueChange={handleValueChange}
onKeyDown={handleKeyDown}
onFocus={handleEditorFocus}
onBlur={handleEditorBlur}
highlight={highlightCode}
{...getCodeEditorProps({ isStreaming: isAiStreaming, isPreview, disabled })}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
TagDropdown,
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
import { getActiveWorkflowSearchHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight'
import { useEditorUndoRedo } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-editor-undo-redo'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import { restoreCursorAfterInsertion } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/utils'
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
Expand Down Expand Up @@ -142,6 +143,7 @@ export function ConditionInput({
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId)

const emitTagSelection = useTagSelection(blockId, subBlockId)
const handleEditorUndoRedo = useEditorUndoRedo()
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
const availableEnvVars = useAvailableEnvVarKeys(workspaceId)
const shouldHighlightEnvVar = useMemo(
Expand Down Expand Up @@ -1268,6 +1270,7 @@ export function ConditionInput({
}
}}
onKeyDown={(e) => {
if (handleEditorUndoRedo(e)) return
if (e.key === 'Escape') {
setConditionalBlocks((blocks) =>
blocks.map((b) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export function SelectorInput({
allowSearch={allowSearch}
onOptionChange={(value) => {
if (!isPreview) {
collaborativeSetSubblockValue(blockId, subBlock.id, value)
collaborativeSetSubblockValue(blockId, subBlock.id, value, { recordUndo: true })
}
}}
activeSearchTarget={activeSearchTarget}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { handleKeyboardActivation } from '@/lib/core/utils/keyboard'
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
import { getActiveWorkflowSearchHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight'
import { useEditorUndoRedo } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-editor-undo-redo'
import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
Expand Down Expand Up @@ -127,6 +128,7 @@ export function FieldFormat({
isPreview,
disabled,
})
const handleEditorUndoRedo = useEditorUndoRedo()

const value = isPreview ? previewValue : storeValue
const fields: Field[] = Array.isArray(value) && value.length > 0 ? value : [createDefaultField()]
Expand Down Expand Up @@ -443,6 +445,7 @@ export function FieldFormat({
<Editor
value={fieldValue}
onValueChange={getEditorValueChangeHandler(field.id)}
onKeyDown={handleEditorUndoRedo}
highlight={jsonHighlight}
disabled={isReadOnly}
{...getCodeEditorProps({ disabled: isReadOnly })}
Expand Down Expand Up @@ -478,6 +481,7 @@ export function FieldFormat({
<Editor
value={fieldValue}
onValueChange={getEditorValueChangeHandler(field.id)}
onKeyDown={handleEditorUndoRedo}
highlight={jsonHighlight}
disabled={isReadOnly}
{...getCodeEditorProps({ disabled: isReadOnly })}
Expand Down Expand Up @@ -515,6 +519,7 @@ export function FieldFormat({
<Editor
value={fieldValue}
onValueChange={getEditorValueChangeHandler(field.id)}
onKeyDown={handleEditorUndoRedo}
highlight={jsonHighlight}
disabled={isReadOnly}
{...getCodeEditorProps({ disabled: isReadOnly })}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from '@/components/emcn'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/core/utils/cn'
import { useEditorUndoRedo } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-editor-undo-redo'
import {
createEnvVarPattern,
createWorkflowVariablePattern,
Expand Down Expand Up @@ -54,6 +55,7 @@ export function CodeEditor({
wandButtonDisabled = false,
}: CodeEditorProps) {
const [visualLineHeights, setVisualLineHeights] = useState<number[]>([])
const handleEditorUndoRedo = useEditorUndoRedo()

const editorRef = useRef<HTMLDivElement>(null)

Expand Down Expand Up @@ -209,7 +211,10 @@ export function CodeEditor({
<Editor
value={value}
onValueChange={onChange}
onKeyDown={onKeyDown}
onKeyDown={(e) => {
if (handleEditorUndoRedo(e)) return
onKeyDown?.(e)
}}
highlight={(code) => customHighlight(code)}
disabled={disabled}
{...getCodeEditorProps({ disabled })}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type React from 'react'
import { useCallback, useRef } from 'react'
import { useUndoRedo } from '@/hooks/use-undo-redo'

/**
* Routes undo/redo keyboard shortcuts to the workflow undo stack while a text
* editor is focused, suppressing the browser/editor-native undo so the workflow
* stack stays the single source of truth.
*
* The returned handler is stable for the lifetime of the component and always
* calls the latest undo/redo (via refs), so it is safe to use inside callbacks
* with empty dependency arrays.
*
* @returns A keydown handler that returns `true` when it handled an undo/redo
* shortcut, letting callers stop further processing of the event.
*/
export function useEditorUndoRedo() {
const { undo, redo } = useUndoRedo()
const undoRef = useRef(undo)
const redoRef = useRef(redo)
undoRef.current = undo
redoRef.current = redo

return useCallback((event: React.KeyboardEvent): boolean => {
if (!(event.metaKey || event.ctrlKey)) return false

const key = event.key.toLowerCase()
const isUndo = key === 'z' && !event.shiftKey
const isRedo = (key === 'z' && event.shiftKey) || key === 'y'
if (!isUndo && !isRedo) return false

event.preventDefault()
event.stopPropagation()
if (isUndo) {
undoRef.current()
} else {
redoRef.current()
}
return true
}, [])
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createLogger } from '@sim/logger'
import { useParams } from 'next/navigation'
import { checkEnvVarTrigger } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/env-var-dropdown'
import { checkTagTrigger } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
import { useEditorUndoRedo } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-editor-undo-redo'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import type { SubBlockConfig } from '@/blocks/types'
import { useTagSelection } from '@/hooks/kb/use-tag-selection'
Expand Down Expand Up @@ -176,6 +177,7 @@ export function useSubBlockInput(options: UseSubBlockInputOptions): UseSubBlockI
})

const emitTagSelection = useTagSelection(blockId, subBlockId)
const handleEditorUndoRedo = useEditorUndoRedo()

// Local content enables immediate UI updates and streaming text display
const [localContent, setLocalContent] = useState<string>('')
Expand Down Expand Up @@ -265,6 +267,7 @@ export function useSubBlockInput(options: UseSubBlockInputOptions): UseSubBlockI

const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLTextAreaElement | HTMLInputElement>) => {
if (handleEditorUndoRedo(e)) return
if (e.key === 'Escape') {
setShowEnvVars(false)
setShowTags(false)
Expand Down Expand Up @@ -458,6 +461,7 @@ export function useSubBlockInput(options: UseSubBlockInputOptions): UseSubBlockI
})
},
onKeyDown: (e: React.KeyboardEvent<HTMLTextAreaElement | HTMLInputElement>) => {
if (handleEditorUndoRedo(e)) return
if (e.key === 'Escape') {
updateFieldState(fieldId, {
showEnvVars: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ export function useSubBlockValue<T = any>(

// Emit the value to socket/DB and update local store
const emitValue = useCallback(
(value: T) => {
collaborativeSetSubblockValue(blockId, subBlockId, value)
(value: T, linkedUpdates?: Array<{ subblockId: string; value: unknown }>) => {
collaborativeSetSubblockValue(blockId, subBlockId, value, { recordUndo: true, linkedUpdates })
lastEmittedValueRef.current = value
},
[blockId, subBlockId, collaborativeSetSubblockValue]
Expand Down Expand Up @@ -161,7 +161,9 @@ export function useSubBlockValue<T = any>(
return
}

// Handle model changes for provider-based blocks - clear API key when provider changes (non-streaming)
// Handle model changes for provider-based blocks - clear API key when provider changes
// (non-streaming). The clear is grouped into the model edit's single undo step.
let linkedUpdates: Array<{ subblockId: string; value: unknown }> | undefined
if (
subBlockId === 'model' &&
isProviderBasedBlock &&
Expand All @@ -174,13 +176,13 @@ export function useSubBlockValue<T = any>(
const oldProvider = oldModelValue ? getProviderFromModel(oldModelValue) : null
const newProvider = getProviderFromModel(newValue)
if (oldProvider !== newProvider) {
collaborativeSetSubblockValue(blockId, 'apiKey', '')
linkedUpdates = [{ subblockId: 'apiKey', value: '' }]
}
}
}

// Emit immediately; the client queue coalesces same-key ops and the server debounces
emitValue(valueCopy as T)
emitValue(valueCopy as T, linkedUpdates)

if (triggerWorkflowUpdate) {
useWorkflowStore.getState().triggerUpdate()
Expand All @@ -198,7 +200,6 @@ export function useSubBlockValue<T = any>(
isStreaming,
emitValue,
isBaselineView,
collaborativeSetSubblockValue,
isProviderBasedBlock,
]
)
Expand Down
Loading
Loading