Lexical Editor Integration in the Prompt Editor#
Dify's prompt editor (web/app/components/base/prompt-editor/) is built on Lexical, Meta's extensible text editor framework. The editor is structured as a LexicalComposer that renders PromptEditorContent β a composition of Lexical plugins that handles rich block types (context, history, variables, workflow variables, agent output, etc.) and two typeahead pickers triggered by / and { .
Entry Points#
| File | Role |
|---|---|
prompt-editor-content.tsx | Registers all plugins and wires OnChangePlugin, HistoryPlugin, UpdateBlock, and DraggableBlockPlugin |
plugins/component-picker-block/index.tsx | ComponentPickerBlock β wraps LexicalTypeaheadMenuPlugin for the slash and brace pickers |
plugins/component-picker-block/hooks.tsx | Option-building hooks: usePromptOptions, useVariableOptions, useExternalToolOptions, useOptions |
hooks.ts | Shared editor hooks: useSelectOrDelete, useTrigger, useLexicalTextEntity, useBasicTypeaheadTriggerMatch |
plugins/update-block.tsx | UpdateBlock β headless plugin for programmatic editor state updates via eventEmitter |
utils.ts | registerLexicalTextEntity, $splitNodeContainingQuery, textToEditorState |
Typeahead Plugin System (ComponentPickerBlock)#
Both the slash picker (/) and brace picker ({) are instances of the same ComponentPickerBlock component, instantiated with different triggerString props . Either picker can be disabled via disableSlashPicker / disableBracePicker props on PromptEditorContent.
LexicalTypeaheadMenuPlugin Wiring#
ComponentPickerBlock wraps Lexical's LexicalTypeaheadMenuPlugin with three key props :
triggerFnβ callscheckForTriggerMatch(see below) on every keystroke to decide whether to open the menu.optionsβ the flat list ofPickerBlockMenuOptionobjects built byuseOptions().menuRenderFnβ renders the floating picker UI via Floating UI. Lexical's internal anchor positioning is intentionally bypassed withanchorClassName="z-50 translate-y-[calc(-100%-3px)]".
The menu uses @floating-ui/react with offset, shift, and flip middleware for positioning . A blurHidden flag (toggled via BLUR_COMMAND / FOCUS_COMMAND) controls whether the picker renders when the editor loses focus .
Option Hooks#
useOptions() aggregates results from three sub-hooks :
usePromptOptionsβ context, query, history, request URL blocks.useVariableOptionsβ per-variablePickerBlockMenuOptionitems; callseditor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, ...)on selection .useExternalToolOptionsβ same pattern for tools backed byexternalToolBlockType.externalTools.
Workflow variables are kept separate in workflowVariableOptions and rendered via VarReferenceVars rather than the flat options list .
Trigger Matcher Customization#
The trigger function is defined in hooks.ts as useBasicTypeaheadTriggerMatch. It constructs a regex at call time from the trigger character and captures trailing text up to maxLength (default 75 characters for the component picker, ).
Key design points:
- Character class escaping β The trigger character is passed through
escapeForCharacterClass(/[[\]\\^-]/g) before embedding it in a character class[${escapedTrigger}]. This handles the{trigger safely β{has no special meaning inside[β¦]. - Valid characters β The match pattern excludes the trigger character and newlines:
[^${escapedTrigger}\n\r]. - Return shape β Returns
{ leadOffset, matchingString, replaceableString }ornullwhen no match .matchingStringis the text typed after the trigger;replaceableStringincludes the trigger.
ComponentPickerBlock wraps this in its own checkForTriggerMatch callback that stores the match in triggerMatchRef so insertion logic can access the last known match state .
Slash Trigger Boundary Behavior#
The slash picker relies on the requireTriggerBoundary option (introduced in PR #39768) to prevent the picker from reopening when the user clicks on existing slash characters in URLs, file paths, or other text. The boundary matcher ensures that slash commands only activate at the beginning of text or after whitespace, avoiding unwanted picker activation on path separators like https://example.com/path.
Special Character Handling & Known Issue#
The { Trigger and Plain-Text Braces#
The brace picker activates on any single { character (without a boundary requirement), which conflicts with common prompt patterns like {variable}, markdown link targets ({source_url}), or Jinja templates. The { trigger is safe at the regex-construction level (see above), but the queryString it captures is passed unsanitized into new RegExp(queryString, 'i') in useVariableOptions and useExternalToolOptions .
Crash: Regex Special Characters in Query String#
If a user types { followed by regex metacharacters (e.g., [, ], }, ), \), the filter call new RegExp(queryString, 'i') throws a SyntaxError, crashing the editor component .
Example trigger text: [Open manual]({source_url}) β after {, the typeahead captures source_url}) as queryString, which is an invalid regex pattern.
Fix pattern (identified in the issue): escape the query string before constructing the regex :
const escapeRegExp = (value: string) =>
value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const regex = new RegExp(escapeRegExp(queryString), 'i')
This fix needs to be applied in two places: useVariableOptions line 180 and useExternalToolOptions line 256 in hooks.tsx.
Update Lifecycle (UpdateBlock)#
UpdateBlock enables programmatic editor replacement via event emitter :
PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTERβ parses a plain-text string viatextToEditorState()and callseditor.setEditorState(), then marks allCustomTextNodeinstances dirty to trigger re-decoration.PROMPT_EDITOR_INSERT_QUICKLYβ focuses the editor and inserts a/node to open the slash picker programmatically.
Reference Block Insertion#
When Agent roster reference blocks are inserted (via the slash picker in the Agent Configure prompt editor), a single trailing separator is added after the reference block and the caret is restored after it. This ensures proper spacing for continued text input.