From 72534052e00da1e7c88490bc92b8fd61646a78e1 Mon Sep 17 00:00:00 2001 From: tusharmagar Date: Thu, 26 Feb 2026 15:29:16 +0530 Subject: [PATCH] fix: cmd+z behaviour on notes --- apps/x/apps/renderer/src/App.tsx | 63 +++++++++++++++++++ .../src/components/markdown-editor.tsx | 26 +++++++- 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index 4f8859ef..d3782925 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -452,6 +452,7 @@ function ContentHeader({ function App() { type ShortcutPane = 'left' | 'right' + type MarkdownHistoryHandlers = { undo: () => boolean; redo: () => boolean } // File browser state (for Knowledge section) const [selectedPath, setSelectedPath] = useState(null) @@ -596,6 +597,8 @@ function App() { // File tab state const [fileTabs, setFileTabs] = useState([]) const [activeFileTabId, setActiveFileTabId] = useState(null) + const [editorSessionByTabId, setEditorSessionByTabId] = useState>({}) + const fileHistoryHandlersRef = useRef>(new Map()) const fileTabIdCounterRef = useRef(0) const newFileTabId = () => `file-tab-${++fileTabIdCounterRef.current}` @@ -2036,6 +2039,13 @@ function App() { } return next }) + setEditorSessionByTabId((prev) => { + if (!(tabId in prev)) return prev + const next = { ...prev } + delete next[tabId] + return next + }) + fileHistoryHandlersRef.current.delete(tabId) }, [activeFileTabId, fileTabs, removeEditorCacheForPath]) const handleNewChatTab = useCallback(() => { @@ -2136,6 +2146,11 @@ function App() { setFileTabs((prev) => prev.map((tab) => ( tab.id === activeFileTabId ? { ...tab, path } : tab ))) + // Rebinds this tab to a different note path: reset editor session to clear undo history. + setEditorSessionByTabId((prev) => ({ + ...prev, + [activeFileTabId]: (prev[activeFileTabId] ?? 0) + 1, + })) return } } @@ -2352,6 +2367,46 @@ function App() { return () => document.removeEventListener('keydown', handleKeyDown) }, []) + // Route undo/redo to the active markdown tab only (prevents cross-tab browser undo behavior). + useEffect(() => { + const handleHistoryKeyDown = (e: KeyboardEvent) => { + const mod = e.metaKey || e.ctrlKey + if (!mod || e.altKey) return + + const key = e.key.toLowerCase() + const wantsUndo = key === 'z' && !e.shiftKey + const wantsRedo = (key === 'z' && e.shiftKey) || (!isMac && key === 'y') + if (!wantsUndo && !wantsRedo) return + + if (!selectedPath || !selectedPath.endsWith('.md') || !activeFileTabId) return + + const target = e.target as EventTarget | null + if (target instanceof HTMLElement) { + const inTipTapEditor = Boolean(target.closest('.tiptap-editor')) + const inOtherTextInput = ( + target instanceof HTMLInputElement + || target instanceof HTMLTextAreaElement + || target.isContentEditable + ) && !inTipTapEditor + if (inOtherTextInput) return + } + + const handlers = fileHistoryHandlersRef.current.get(activeFileTabId) + if (!handlers) return + + e.preventDefault() + e.stopPropagation() + if (wantsUndo) { + handlers.undo() + } else { + handlers.redo() + } + } + + document.addEventListener('keydown', handleHistoryKeyDown, true) + return () => document.removeEventListener('keydown', handleHistoryKeyDown, true) + }, [activeFileTabId, isMac, selectedPath]) + // Keyboard shortcuts for tab management useEffect(() => { const handleTabKeyDown = (e: KeyboardEvent) => { @@ -3136,6 +3191,14 @@ function App() { placeholder="Start writing..." wikiLinks={wikiLinkConfig} onImageUpload={handleImageUpload} + editorSessionKey={editorSessionByTabId[tab.id] ?? 0} + onHistoryHandlersChange={(handlers) => { + if (handlers) { + fileHistoryHandlersRef.current.set(tab.id, handlers) + } else { + fileHistoryHandlersRef.current.delete(tab.id) + } + }} /> ) diff --git a/apps/x/apps/renderer/src/components/markdown-editor.tsx b/apps/x/apps/renderer/src/components/markdown-editor.tsx index 74ad10b8..2db7bcbe 100644 --- a/apps/x/apps/renderer/src/components/markdown-editor.tsx +++ b/apps/x/apps/renderer/src/components/markdown-editor.tsx @@ -195,6 +195,8 @@ interface MarkdownEditorProps { placeholder?: string wikiLinks?: WikiLinkConfig onImageUpload?: (file: File) => Promise + editorSessionKey?: number + onHistoryHandlersChange?: (handlers: { undo: () => boolean; redo: () => boolean } | null) => void } type WikiLinkMatch = { @@ -278,6 +280,8 @@ export function MarkdownEditor({ placeholder = 'Start writing...', wikiLinks, onImageUpload, + editorSessionKey = 0, + onHistoryHandlersChange, }: MarkdownEditorProps) { const isInternalUpdate = useRef(false) const wrapperRef = useRef(null) @@ -400,7 +404,7 @@ export function MarkdownEditor({ return false }, }, - }) + }, [editorSessionKey]) const orderedFiles = useMemo(() => { if (!wikiLinks) return [] @@ -489,12 +493,30 @@ export function MarkdownEditor({ isInternalUpdate.current = true // Pre-process to preserve blank lines const preprocessed = preprocessMarkdown(content) - editor.commands.setContent(preprocessed) + // Treat tab-open content as baseline: do not add hydration to undo history. + editor.chain().setMeta('addToHistory', false).setContent(preprocessed).run() isInternalUpdate.current = false } } }, [editor, content]) + useEffect(() => { + if (!onHistoryHandlersChange) return + if (!editor) { + onHistoryHandlersChange(null) + return + } + + onHistoryHandlersChange({ + undo: () => editor.chain().focus().undo().run(), + redo: () => editor.chain().focus().redo().run(), + }) + + return () => { + onHistoryHandlersChange(null) + } + }, [editor, onHistoryHandlersChange]) + // Force re-render decorations when selection highlight changes useEffect(() => { if (editor) {