fix: stabilize knowledge note loading and untitled title behavior

This commit is contained in:
tusharmagar 2026-03-05 12:19:30 +05:30
parent c76d08953d
commit e8a666499a
2 changed files with 109 additions and 4 deletions

View file

@ -492,6 +492,7 @@ function App() {
const fileLoadRequestIdRef = useRef(0)
const initialContentByPathRef = useRef<Map<string, string>>(new Map())
const recentLocalMarkdownWritesRef = useRef<Map<string, number>>(new Map())
const untitledRenameReadyPathsRef = useRef<Set<string>>(new Set())
// Global navigation history (back/forward) across views (chat/file/graph/task)
const historyRef = useRef<{ back: ViewState[]; forward: ViewState[] }>({ back: [], forward: [] })
@ -739,6 +740,7 @@ function App() {
const removeEditorCacheForPath = useCallback((path: string) => {
editorContentByPathRef.current.delete(path)
untitledRenameReadyPathsRef.current.delete(path)
setEditorContentByPath((prev) => {
if (!(path in prev)) return prev
const next = { ...prev }
@ -1019,7 +1021,8 @@ function App() {
if (isUntitledPlaceholderName(currentBase)) {
const headingTitle = getHeadingTitle(debouncedContent)
const desiredName = headingTitle ? sanitizeHeadingForFilename(headingTitle) : null
if (desiredName && desiredName !== currentBase) {
const shouldAutoRename = untitledRenameReadyPathsRef.current.has(pathAtStart)
if (shouldAutoRename && desiredName && desiredName !== currentBase) {
const parentDir = pathAtStart.split('/').slice(0, -1).join('/')
let targetPath = `${parentDir}/${desiredName}.md`
if (targetPath !== pathAtStart) {
@ -1041,6 +1044,7 @@ function App() {
renamedFrom = pathAtStart
renamedTo = targetPath
editorPathRef.current = targetPath
untitledRenameReadyPathsRef.current.delete(pathAtStart)
setFileTabs(prev => prev.map(tab => (tab.path === pathAtStart ? { ...tab, path: targetPath } : tab)))
initialContentByPathRef.current.delete(pathAtStart)
const cachedContent = editorContentByPathRef.current.get(pathAtStart)
@ -2157,6 +2161,7 @@ function App() {
if (closingTab && !isGraphTabPath(closingTab.path)) {
removeEditorCacheForPath(closingTab.path)
initialContentByPathRef.current.delete(closingTab.path)
untitledRenameReadyPathsRef.current.delete(closingTab.path)
if (editorPathRef.current === closingTab.path) {
editorPathRef.current = null
}
@ -2756,6 +2761,7 @@ function App() {
parts[parts.length - 1] = finalName
const newPath = parts.join('/')
await window.ipc.invoke('workspace:rename', { from: oldPath, to: newPath })
untitledRenameReadyPathsRef.current.delete(oldPath)
const rewriteForRename = (content: string) =>
isDir ? content : rewriteWikiLinksForRenamedFileInMarkdown(content, oldPath, newPath)
setFileTabs(prev => prev.map(tab => (tab.path === oldPath ? { ...tab, path: newPath } : tab)))
@ -2798,6 +2804,7 @@ function App() {
if (path.endsWith('.md')) {
removeEditorCacheForPath(path)
initialContentByPathRef.current.delete(path)
untitledRenameReadyPathsRef.current.delete(path)
}
// Close any file tab showing the deleted file
const tabForFile = fileTabs.find(t => t.path === path)
@ -3376,6 +3383,10 @@ function App() {
<MarkdownEditor
content={tabContent}
onChange={(markdown) => { if (!isViewingHistory) handleEditorChange(tab.path, markdown) }}
onPrimaryHeadingCommit={() => {
untitledRenameReadyPathsRef.current.add(tab.path)
}}
preserveUntitledTitleHeading={isUntitledPlaceholderName(getBaseName(tab.path))}
placeholder="Start writing..."
wikiLinks={wikiLinkConfig}
onImageUpload={handleImageUpload}

View file

@ -1,6 +1,6 @@
import { useEditor, EditorContent, Extension, Editor } from '@tiptap/react'
import { Plugin, PluginKey } from '@tiptap/pm/state'
import { Decoration, DecorationSet } from '@tiptap/pm/view'
import { Decoration, DecorationSet, EditorView } from '@tiptap/pm/view'
import StarterKit from '@tiptap/starter-kit'
import Link from '@tiptap/extension-link'
import Image from '@tiptap/extension-image'
@ -192,6 +192,8 @@ type WikiLinkConfig = {
interface MarkdownEditorProps {
content: string
onChange: (markdown: string) => void
onPrimaryHeadingCommit?: () => void
preserveUntitledTitleHeading?: boolean
placeholder?: string
wikiLinks?: WikiLinkConfig
onImageUpload?: (file: File) => Promise<string | null>
@ -278,6 +280,8 @@ const TabIndentExtension = Extension.create({
export function MarkdownEditor({
content,
onChange,
onPrimaryHeadingCommit,
preserveUntitledTitleHeading = false,
placeholder = 'Start writing...',
wikiLinks,
onImageUpload,
@ -292,6 +296,7 @@ export function MarkdownEditor({
const [selectionHighlight, setSelectionHighlight] = useState<SelectionHighlightRange>(null)
const selectionHighlightRef = useRef<SelectionHighlightRange>(null)
const [wikiCommandValue, setWikiCommandValue] = useState<string>('')
const onPrimaryHeadingCommitRef = useRef(onPrimaryHeadingCommit)
const wikiKeyStateRef = useRef<{ open: boolean; options: string[]; value: string }>({ open: false, options: [], value: '' })
const handleSelectWikiLinkRef = useRef<(path: string) => void>(() => {})
@ -304,6 +309,68 @@ export function MarkdownEditor({
[]
)
useEffect(() => {
onPrimaryHeadingCommitRef.current = onPrimaryHeadingCommit
}, [onPrimaryHeadingCommit])
const maybeCommitPrimaryHeading = useCallback((view: EditorView) => {
const onCommit = onPrimaryHeadingCommitRef.current
if (!onCommit) return
const { selection, doc } = view.state
if (!selection.empty) return
const { $from } = selection
if ($from.depth < 1 || $from.index(0) !== 0) return
if (!['heading', 'paragraph'].includes($from.parent.type.name)) return
const firstNode = doc.firstChild
if (!firstNode || !['heading', 'paragraph'].includes(firstNode.type.name)) return
onCommit()
}, [])
const preventTitleHeadingDemotion = useCallback((view: EditorView, event: KeyboardEvent) => {
if (!preserveUntitledTitleHeading) return false
if (event.key !== 'Backspace' || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) return false
const { selection } = view.state
if (!selection.empty) return false
const { $from } = selection
if ($from.depth < 1 || $from.index(0) !== 0) return false
if ($from.parent.type.name !== 'heading') return false
const headingLevel = ((
$from.parent.attrs as { level?: number } | null | undefined
)?.level) ?? 0
if (headingLevel !== 1) return false
if ($from.parentOffset !== 0) return false
if ($from.parent.textContent.length > 0) return false
event.preventDefault()
return true
}, [preserveUntitledTitleHeading])
const promoteFirstParagraphToTitleHeading = useCallback((view: EditorView) => {
if (!preserveUntitledTitleHeading) return
const { state, dispatch } = view
const { selection } = state
if (!selection.empty) return
const { $from } = selection
if ($from.depth < 1 || $from.index(0) !== 0) return
if ($from.parent.type.name !== 'paragraph') return
if ($from.parentOffset !== 0) return
if ($from.parent.textContent.length > 0) return
const headingType = state.schema.nodes.heading
if (!headingType) return
const tr = state.tr.setNodeMarkup($from.before(1), headingType, { level: 1 })
dispatch(tr)
}, [preserveUntitledTitleHeading])
const editor = useEditor({
editable,
extensions: [
@ -359,11 +426,14 @@ export function MarkdownEditor({
markdown = postprocessMarkdown(markdown)
onChange(markdown)
},
onBlur: ({ editor }) => {
maybeCommitPrimaryHeading(editor.view)
},
editorProps: {
attributes: {
class: 'prose prose-sm max-w-none focus:outline-none',
},
handleKeyDown: (_view, event) => {
handleKeyDown: (view, event) => {
const state = wikiKeyStateRef.current
if (state.open) {
if (event.key === 'Escape') {
@ -396,6 +466,25 @@ export function MarkdownEditor({
}
}
if (preventTitleHeadingDemotion(view, event)) {
return true
}
const isPrintableKey = event.key.length === 1 && !event.metaKey && !event.ctrlKey && !event.altKey
if (isPrintableKey) {
promoteFirstParagraphToTitleHeading(view)
}
if (
event.key === 'Enter'
&& !event.shiftKey
&& !event.ctrlKey
&& !event.metaKey
&& !event.altKey
) {
maybeCommitPrimaryHeading(view)
}
return false
},
handleClickOn: (_view, _pos, node, _nodePos, event) => {
@ -407,7 +496,12 @@ export function MarkdownEditor({
return false
},
},
}, [editorSessionKey])
}, [
editorSessionKey,
maybeCommitPrimaryHeading,
preventTitleHeadingDemotion,
promoteFirstParagraphToTitleHeading,
])
const orderedFiles = useMemo(() => {
if (!wikiLinks) return []