diff --git a/frontend/src/components/shared/code-editor.tsx b/frontend/src/components/shared/code-editor.tsx index 92e26567..bd281d63 100644 --- a/frontend/src/components/shared/code-editor.tsx +++ b/frontend/src/components/shared/code-editor.tsx @@ -17,10 +17,13 @@ export interface CodeEditorProps { value: string; } -// CodeMirror edits the raw document string verbatim — no parse/serialize round-trip — -// so it is byte-faithful for content (Go templates) a markdown editor would normalize. const extensions = [markdown(), EditorView.lineWrapping]; +/** + * Byte-faithful text editor: CodeMirror edits the raw document string verbatim, with no + * parse/serialize round-trip. Don't swap in a markdown/rich editor — it would corrupt + * Go-template tables, ``, and significant whitespace on round-trip. + */ export function CodeEditor({ className, disabled, onBlur, onChange, placeholder, ref, value }: CodeEditorProps) { const { theme } = useTheme(); const isDark = useMemo( diff --git a/frontend/src/features/knowledges/knowledge-form.tsx b/frontend/src/features/knowledges/knowledge-form.tsx index fd454ca0..e6489cbd 100644 --- a/frontend/src/features/knowledges/knowledge-form.tsx +++ b/frontend/src/features/knowledges/knowledge-form.tsx @@ -424,10 +424,7 @@ export function KnowledgeForm({ initialValues, isNew, knowledge, onSubmit }: Kno ); } -// Scoped subscription: the `content` watch lives here, not in KnowledgeForm, so -// only the header re-renders as the user types — the form body, layout, and -// markdown editor stay still. Just enough reactivity to toggle the anonymize -// button's disabled state. +// Don't hoist this useWatch to the parent — it would re-render the whole form per keystroke. function KnowledgeFormHeader({ control, isAnonymizing, isSaving = false, ...rest }: KnowledgeFormHeaderProps) { const content = useWatch({ control, name: 'content' }); const isAnonymizeDisabled = isAnonymizing || isSaving || !content?.trim(); diff --git a/frontend/src/pages/settings/settings-prompt.tsx b/frontend/src/pages/settings/settings-prompt.tsx index 110bcdc9..dfb25724 100644 --- a/frontend/src/pages/settings/settings-prompt.tsx +++ b/frontend/src/pages/settings/settings-prompt.tsx @@ -253,8 +253,7 @@ interface VariablesProps { viewMode: 'code' | 'plain'; } -// Scoped subscription: only this subtree re-renders per keystroke, so the parent -// SettingsPrompt no longer watches the template field (mirrors knowledge-form-controls.tsx). +// Don't hoist this useWatch to the parent — it would re-subscribe the whole page per keystroke. function DiffContent({ control, oldValue, styles }: DiffContentProps) { const newValue = useWatch({ control, name: 'template' }); @@ -297,11 +296,11 @@ function SettingsPrompt() { if (view) { const insert = `{{.${variable}}}`; - const pos = view.state.selection.main.head; + const position = view.state.selection.main.head; view.dispatch({ - changes: { from: pos, insert }, + changes: { from: position, insert }, scrollIntoView: true, - selection: { anchor: pos + insert.length }, + selection: { anchor: position + insert.length }, }); view.focus(); } @@ -317,15 +316,13 @@ function SettingsPrompt() { const matches = [...currentValue.matchAll(new RegExp(variableActionRegex(variable).source, 'g'))]; if (matches.length > 0) { - // Cycle through occurrences: advance from the one the caret is already on - // (wrapping past the last), else jump to the first occurrence at/after the caret. const { selectionEnd, selectionStart } = textarea; - const currentIdx = matches.findIndex( + const currentIndex = matches.findIndex( (match) => match.index === selectionStart && match.index + match[0].length === selectionEnd, ); const target = - currentIdx >= 0 - ? matches[(currentIdx + 1) % matches.length] + currentIndex >= 0 + ? matches[(currentIndex + 1) % matches.length] : (matches.find((match) => match.index >= selectionStart) ?? matches[0]); if (target) { @@ -1214,7 +1211,8 @@ function Variables({ currentTemplate, onVariableClick, variables, viewMode }: Va {variables.map((variable) => { const count = (currentTemplate.match(new RegExp(variableActionRegex(variable).source, 'g')) ?? []) .length; - const isUsed = count > 0; + // Plain-view only: a code-view click just inserts, so don't imply go-to-occurrence. + const isUsed = viewMode === 'plain' && count > 0; const action = viewMode === 'code' ? `Insert {{.${variable}}} into the editor` @@ -1252,8 +1250,7 @@ function Variables({ currentTemplate, onVariableClick, variables, viewMode }: Va ); } -// Scoped subscription: confines the per-keystroke re-render (and the live ×count) to this -// subtree instead of the whole page (mirrors knowledge-form-controls.tsx). +// Don't hoist this useWatch to the parent — it would re-subscribe the whole page per keystroke. function VariablesPanelContainer({ control, onVariableClick, variables, viewMode }: VariablesPanelContainerProps) { const currentTemplate = useWatch({ control, name: 'template' }); diff --git a/frontend/src/pages/settings/settings-provider.tsx b/frontend/src/pages/settings/settings-provider.tsx index 26bda000..afee537a 100644 --- a/frontend/src/pages/settings/settings-provider.tsx +++ b/frontend/src/pages/settings/settings-provider.tsx @@ -1070,8 +1070,7 @@ interface DeleteProviderDialogProps control: Control; } -// Subscribes to the `name` field on its own so a keystroke in the provider-name -// input re-renders only this dialog, not the whole SettingsProvider form tree. +// Don't hoist this useWatch to the parent — a name keystroke would re-render the whole form. function DeleteProviderDialog({ control, handleConfirm, handleOpenChange, isOpen }: DeleteProviderDialogProps) { const providerName = useWatch({ control, name: 'name' });