2026-02-27 18:17:47 +01:00
import { useCallback , useEffect , useRef } from 'react'
2026-02-17 12:10:21 +01:00
import { invoke } from '@tauri-apps/api/core'
import { isTauri , mockInvoke , addMockEntry , updateMockContent } from '../mock-tauri'
import type { VaultEntry } from '../types'
import type { FrontmatterValue } from '../components/Inspector'
2026-02-22 10:11:52 +01:00
import { useTabManagement } from './useTabManagement'
import { updateMockFrontmatter , deleteMockFrontmatterProperty } from './mockFrontmatterHelpers'
interface NewEntryParams {
path : string
slug : string
title : string
type : string
status : string | null
2026-02-17 12:10:21 +01:00
}
2026-02-21 19:22:44 +01:00
interface RenameResult {
new_path : string
updated_files : number
}
fix: sync in-memory entries after property/rename changes
Two propagation gaps fixed:
1. Property changes (type, status, color, etc.) now immediately sync to
the entries state via frontmatterToEntryPatch(), so sidebar, note list,
and relations panel reflect updates without restart.
2. After renaming a note, all other open tabs reload their content from
disk, picking up updated wikilinks immediately.
Design decisions:
- Used key-mapping approach (frontmatterToEntryPatch) instead of a new
parse_md_file Tauri command — simpler, works in both Tauri and mock
modes, covers all VaultEntry fields.
- Converted useNoteActions to config object to avoid excess arguments.
- Extracted findWikilinkTarget, reloadTabsAfterRename, renameToastMessage
as standalone functions to reduce hook complexity (cc 10→9).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:48:39 +01:00
export interface NoteActionsConfig {
addEntry : ( entry : VaultEntry , content : string ) = > void
feat: add optimistic error recovery for note creation (#69)
Note creation was already optimistic (UI updates before disk write) but
errors were silently swallowed. Now if the disk write fails, the
optimistic entry is reverted (tab closed, entry removed) and the user
sees an error toast. Each note creation is independent, so one failure
in rapid Cmd+N presses doesn't affect the others.
- Add removeEntry to useVaultLoader for reverting optimistic adds
- Refactor persistNewNote to return a Promise for error handling
- Extract navigateWikilink, persistOptimistic, createAndPersist,
and runFrontmatterAndApply helpers to reduce hook complexity
- Add tests for error recovery: single note, type, success path,
and rapid creation with partial failure
- Code health: 8.93→9.01 (CC fixed: 10→7, primitive obsession improved)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:11:59 +01:00
removeEntry : ( path : string ) = > void
fix: sync in-memory entries after property/rename changes
Two propagation gaps fixed:
1. Property changes (type, status, color, etc.) now immediately sync to
the entries state via frontmatterToEntryPatch(), so sidebar, note list,
and relations panel reflect updates without restart.
2. After renaming a note, all other open tabs reload their content from
disk, picking up updated wikilinks immediately.
Design decisions:
- Used key-mapping approach (frontmatterToEntryPatch) instead of a new
parse_md_file Tauri command — simpler, works in both Tauri and mock
modes, covers all VaultEntry fields.
- Converted useNoteActions to config object to avoid excess arguments.
- Extracted findWikilinkTarget, reloadTabsAfterRename, renameToastMessage
as standalone functions to reduce hook complexity (cc 10→9).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:48:39 +01:00
updateContent : ( path : string , content : string ) = > void
entries : VaultEntry [ ]
setToastMessage : ( msg : string | null ) = > void
updateEntry : ( path : string , patch : Partial < VaultEntry > ) = > void
2026-02-27 14:11:31 +01:00
addPendingSave ? : ( path : string ) = > void
removePendingSave ? : ( path : string ) = > void
2026-02-27 18:17:47 +01:00
trackUnsaved ? : ( path : string ) = > void
clearUnsaved ? : ( path : string ) = > void
unsavedPaths? : Set < string >
/** Called when an unsaved note is created so the save system can buffer its initial content. */
markContentPending ? : ( path : string , content : string ) = > void
fix: sync in-memory entries after property/rename changes
Two propagation gaps fixed:
1. Property changes (type, status, color, etc.) now immediately sync to
the entries state via frontmatterToEntryPatch(), so sidebar, note list,
and relations panel reflect updates without restart.
2. After renaming a note, all other open tabs reload their content from
disk, picking up updated wikilinks immediately.
Design decisions:
- Used key-mapping approach (frontmatterToEntryPatch) instead of a new
parse_md_file Tauri command — simpler, works in both Tauri and mock
modes, covers all VaultEntry fields.
- Converted useNoteActions to config object to avoid excess arguments.
- Extracted findWikilinkTarget, reloadTabsAfterRename, renameToastMessage
as standalone functions to reduce hook complexity (cc 10→9).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:48:39 +01:00
}
2026-02-21 19:22:44 +01:00
async function performRename (
path : string ,
newTitle : string ,
vaultPath : string ,
) : Promise < RenameResult > {
if ( isTauri ( ) ) {
return invoke < RenameResult > ( 'rename_note' , { vaultPath , oldPath : path , newTitle } )
}
return mockInvoke < RenameResult > ( 'rename_note' , { vault_path : vaultPath , old_path : path , new_title : newTitle } )
}
function buildRenamedEntry ( entry : VaultEntry , newTitle : string , newPath : string ) : VaultEntry {
const slug = newTitle . toLowerCase ( ) . replace ( /[^a-z0-9]+/g , '-' ) . replace ( /(^-|-$)/g , '' )
return { . . . entry , path : newPath , filename : ` ${ slug } .md ` , title : newTitle }
}
2026-02-22 12:10:17 +01:00
async function loadNoteContent ( path : string ) : Promise < string > {
return isTauri ( )
? invoke < string > ( 'get_note_content' , { path } )
: mockInvoke < string > ( 'get_note_content' , { path } )
2026-02-17 12:10:21 +01:00
}
feat: add optimistic error recovery for note creation (#69)
Note creation was already optimistic (UI updates before disk write) but
errors were silently swallowed. Now if the disk write fails, the
optimistic entry is reverted (tab closed, entry removed) and the user
sees an error toast. Each note creation is independent, so one failure
in rapid Cmd+N presses doesn't affect the others.
- Add removeEntry to useVaultLoader for reverting optimistic adds
- Refactor persistNewNote to return a Promise for error handling
- Extract navigateWikilink, persistOptimistic, createAndPersist,
and runFrontmatterAndApply helpers to reduce hook complexity
- Add tests for error recovery: single note, type, success path,
and rapid creation with partial failure
- Code health: 8.93→9.01 (CC fixed: 10→7, primitive obsession improved)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:11:59 +01:00
/** Persist a newly created note to disk. Returns a Promise for error handling. */
function persistNewNote ( path : string , content : string ) : Promise < void > {
if ( ! isTauri ( ) ) return Promise . resolve ( )
return invoke < void > ( 'save_note_content' , { path , content } ) . then ( ( ) = > { } )
2026-02-24 23:03:01 +01:00
}
refactor: export pure functions from useNoteActions + update coverage config
Export buildNewEntry, slugify, entryMatchesTarget, buildNoteContent,
resolveNewNote, resolveNewType as public functions for direct unit testing.
Add coverage exclusions for untestable files: shadcn/ui primitives,
AI modules (useAIChat, useMcpBridge, ai-chat, AIChatPanel), and types.ts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 12:02:07 +01:00
export function buildNewEntry ( { path , slug , title , type , status } : NewEntryParams ) : VaultEntry {
2026-02-22 10:11:52 +01:00
const now = Math . floor ( Date . now ( ) / 1000 )
return {
path , filename : ` ${ slug } .md ` , title , isA : type ,
aliases : [ ] , belongsTo : [ ] , relatedTo : [ ] ,
status , owner : null , cadence : null , archived : false , trashed : false , trashedAt : null ,
modifiedAt : now , createdAt : now , fileSize : 0 ,
2026-02-26 20:50:29 +01:00
snippet : '' , wordCount : 0 , relationships : { } , icon : null , color : null , order : null , outgoingLinks : [ ] ,
2026-02-17 12:10:21 +01:00
}
}
refactor: export pure functions from useNoteActions + update coverage config
Export buildNewEntry, slugify, entryMatchesTarget, buildNoteContent,
resolveNewNote, resolveNewType as public functions for direct unit testing.
Add coverage exclusions for untestable files: shadcn/ui primitives,
AI modules (useAIChat, useMcpBridge, ai-chat, AIChatPanel), and types.ts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 12:02:07 +01:00
export function slugify ( text : string ) : string {
2026-02-22 10:11:52 +01:00
return text . toLowerCase ( ) . replace ( /[^a-z0-9]+/g , '-' ) . replace ( /(^-|-$)/g , '' )
}
2026-02-17 12:10:21 +01:00
2026-02-24 23:03:01 +01:00
/** Generate a unique "Untitled <type>" name by checking existing entries and pending names. */
export function generateUntitledName ( entries : VaultEntry [ ] , type : string , pending? : Set < string > ) : string {
2026-02-21 19:15:39 +01:00
const baseName = ` Untitled ${ type . toLowerCase ( ) } `
const existingTitles = new Set ( entries . map ( e = > e . title ) )
2026-02-24 23:03:01 +01:00
if ( pending ) pending . forEach ( n = > existingTitles . add ( n ) )
2026-02-21 19:15:39 +01:00
let title = baseName
let counter = 2
while ( existingTitles . has ( title ) ) {
title = ` ${ baseName } ${ counter } `
counter ++
}
return title
}
refactor: export pure functions from useNoteActions + update coverage config
Export buildNewEntry, slugify, entryMatchesTarget, buildNoteContent,
resolveNewNote, resolveNewType as public functions for direct unit testing.
Add coverage exclusions for untestable files: shadcn/ui primitives,
AI modules (useAIChat, useMcpBridge, ai-chat, AIChatPanel), and types.ts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 12:02:07 +01:00
export function entryMatchesTarget ( e : VaultEntry , targetLower : string , targetAsWords : string ) : boolean {
2026-02-22 10:11:52 +01:00
if ( e . title . toLowerCase ( ) === targetLower ) return true
if ( e . aliases . some ( ( a ) = > a . toLowerCase ( ) === targetLower ) ) return true
const pathStem = e . path . replace ( /^.*\/Laputa\// , '' ) . replace ( /\.md$/ , '' )
if ( pathStem . toLowerCase ( ) === targetLower ) return true
const fileStem = e . filename . replace ( /\.md$/ , '' )
if ( fileStem . toLowerCase ( ) === targetLower . split ( '/' ) . pop ( ) ) return true
return e . title . toLowerCase ( ) === targetAsWords
2026-02-17 12:10:21 +01:00
}
2026-02-22 10:11:52 +01:00
async function invokeFrontmatter ( command : string , args : Record < string , unknown > ) : Promise < string > {
return invoke < string > ( command , args )
}
2026-02-21 17:24:01 +01:00
2026-02-22 10:11:52 +01:00
function applyMockFrontmatterUpdate ( path : string , key : string , value : FrontmatterValue ) : string {
const content = updateMockFrontmatter ( path , key , value )
updateMockContent ( path , content )
return content
2026-02-21 17:24:01 +01:00
}
2026-02-22 10:11:52 +01:00
function applyMockFrontmatterDelete ( path : string , key : string ) : string {
const content = deleteMockFrontmatterProperty ( path , key )
updateMockContent ( path , content )
return content
2026-02-21 17:24:01 +01:00
}
2026-02-22 10:11:52 +01:00
const TYPE_FOLDER_MAP : Record < string , string > = {
Note : 'note' , Project : 'project' , Experiment : 'experiment' ,
Responsibility : 'responsibility' , Procedure : 'procedure' ,
Person : 'person' , Event : 'event' , Topic : 'topic' ,
2026-02-21 10:50:50 +01:00
}
2026-02-22 10:11:52 +01:00
const NO_STATUS_TYPES = new Set ( [ 'Topic' , 'Person' ] )
fix: sync in-memory entries after property/rename changes
Two propagation gaps fixed:
1. Property changes (type, status, color, etc.) now immediately sync to
the entries state via frontmatterToEntryPatch(), so sidebar, note list,
and relations panel reflect updates without restart.
2. After renaming a note, all other open tabs reload their content from
disk, picking up updated wikilinks immediately.
Design decisions:
- Used key-mapping approach (frontmatterToEntryPatch) instead of a new
parse_md_file Tauri command — simpler, works in both Tauri and mock
modes, covers all VaultEntry fields.
- Converted useNoteActions to config object to avoid excess arguments.
- Extracted findWikilinkTarget, reloadTabsAfterRename, renameToastMessage
as standalone functions to reduce hook complexity (cc 10→9).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:48:39 +01:00
const ENTRY_DELETE_MAP : Record < string , Partial < VaultEntry > > = {
2026-02-23 15:24:57 +01:00
type : { isA : null } , is_a : { isA : null } , status : { status : null } , color : { color : null } ,
fix: sync in-memory entries after property/rename changes
Two propagation gaps fixed:
1. Property changes (type, status, color, etc.) now immediately sync to
the entries state via frontmatterToEntryPatch(), so sidebar, note list,
and relations panel reflect updates without restart.
2. After renaming a note, all other open tabs reload their content from
disk, picking up updated wikilinks immediately.
Design decisions:
- Used key-mapping approach (frontmatterToEntryPatch) instead of a new
parse_md_file Tauri command — simpler, works in both Tauri and mock
modes, covers all VaultEntry fields.
- Converted useNoteActions to config object to avoid excess arguments.
- Extracted findWikilinkTarget, reloadTabsAfterRename, renameToastMessage
as standalone functions to reduce hook complexity (cc 10→9).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:48:39 +01:00
icon : { icon : null } , owner : { owner : null } , cadence : { cadence : null } ,
aliases : { aliases : [ ] } , belongs_to : { belongsTo : [ ] } , related_to : { relatedTo : [ ] } ,
archived : { archived : false } , trashed : { trashed : false } , order : { order : null } ,
}
/** Map a frontmatter key+value to the corresponding VaultEntry field(s). */
export function frontmatterToEntryPatch (
op : 'update' | 'delete' , key : string , value? : FrontmatterValue ,
) : Partial < VaultEntry > {
const k = key . toLowerCase ( ) . replace ( /\s+/g , '_' )
if ( op === 'delete' ) return ENTRY_DELETE_MAP [ k ] ? ? { }
const str = value != null ? String ( value ) : null
const arr = Array . isArray ( value ) ? value . map ( String ) : [ ]
const updates : Record < string , Partial < VaultEntry > > = {
2026-02-23 15:24:57 +01:00
type : { isA : str } , is_a : { isA : str } , status : { status : str } , color : { color : str } ,
fix: sync in-memory entries after property/rename changes
Two propagation gaps fixed:
1. Property changes (type, status, color, etc.) now immediately sync to
the entries state via frontmatterToEntryPatch(), so sidebar, note list,
and relations panel reflect updates without restart.
2. After renaming a note, all other open tabs reload their content from
disk, picking up updated wikilinks immediately.
Design decisions:
- Used key-mapping approach (frontmatterToEntryPatch) instead of a new
parse_md_file Tauri command — simpler, works in both Tauri and mock
modes, covers all VaultEntry fields.
- Converted useNoteActions to config object to avoid excess arguments.
- Extracted findWikilinkTarget, reloadTabsAfterRename, renameToastMessage
as standalone functions to reduce hook complexity (cc 10→9).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:48:39 +01:00
icon : { icon : str } , owner : { owner : str } , cadence : { cadence : str } ,
aliases : { aliases : arr } , belongs_to : { belongsTo : arr } , related_to : { relatedTo : arr } ,
archived : { archived : Boolean ( value ) } , trashed : { trashed : Boolean ( value ) } ,
order : { order : typeof value === 'number' ? value : null } ,
}
return updates [ k ] ? ? { }
}
2026-02-22 10:11:52 +01:00
function addEntryWithMock ( entry : VaultEntry , content : string , addEntry : ( e : VaultEntry , c : string ) = > void ) {
if ( ! isTauri ( ) ) addMockEntry ( entry , content )
addEntry ( entry , content )
2026-02-21 10:50:50 +01:00
}
refactor: export pure functions from useNoteActions + update coverage config
Export buildNewEntry, slugify, entryMatchesTarget, buildNoteContent,
resolveNewNote, resolveNewType as public functions for direct unit testing.
Add coverage exclusions for untestable files: shadcn/ui primitives,
AI modules (useAIChat, useMcpBridge, ai-chat, AIChatPanel), and types.ts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 12:02:07 +01:00
export function buildNoteContent ( title : string , type : string , status : string | null ) : string {
2026-02-23 15:24:57 +01:00
const lines = [ '---' , ` title: ${ title } ` , ` type: ${ type } ` ]
2026-02-22 10:55:45 +01:00
if ( status ) lines . push ( ` status: ${ status } ` )
lines . push ( '---' )
return ` ${ lines . join ( '\n' ) } \ n \ n# ${ title } \ n \ n `
}
refactor: export pure functions from useNoteActions + update coverage config
Export buildNewEntry, slugify, entryMatchesTarget, buildNoteContent,
resolveNewNote, resolveNewType as public functions for direct unit testing.
Add coverage exclusions for untestable files: shadcn/ui primitives,
AI modules (useAIChat, useMcpBridge, ai-chat, AIChatPanel), and types.ts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 12:02:07 +01:00
export function resolveNewNote ( title : string , type : string ) : { entry : VaultEntry ; content : string } {
2026-02-22 10:55:45 +01:00
const folder = TYPE_FOLDER_MAP [ type ] || slugify ( type )
const slug = slugify ( title )
const status = NO_STATUS_TYPES . has ( type ) ? null : 'Active'
const entry = buildNewEntry ( { path : ` /Users/luca/Laputa/ ${ folder } / ${ slug } .md ` , slug , title , type , status } )
return { entry , content : buildNoteContent ( title , type , status ) }
}
refactor: export pure functions from useNoteActions + update coverage config
Export buildNewEntry, slugify, entryMatchesTarget, buildNoteContent,
resolveNewNote, resolveNewType as public functions for direct unit testing.
Add coverage exclusions for untestable files: shadcn/ui primitives,
AI modules (useAIChat, useMcpBridge, ai-chat, AIChatPanel), and types.ts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 12:02:07 +01:00
export function resolveNewType ( typeName : string ) : { entry : VaultEntry ; content : string } {
2026-02-22 10:55:45 +01:00
const slug = slugify ( typeName )
const entry = buildNewEntry ( { path : ` /Users/luca/Laputa/type/ ${ slug } .md ` , slug , title : typeName , type : 'Type' , status : null } )
2026-02-23 15:24:57 +01:00
return { entry , content : ` --- \ ntype: Type \ n--- \ n \ n# ${ typeName } \ n \ n ` }
2026-02-22 10:55:45 +01:00
}
fix: sync in-memory entries after property/rename changes
Two propagation gaps fixed:
1. Property changes (type, status, color, etc.) now immediately sync to
the entries state via frontmatterToEntryPatch(), so sidebar, note list,
and relations panel reflect updates without restart.
2. After renaming a note, all other open tabs reload their content from
disk, picking up updated wikilinks immediately.
Design decisions:
- Used key-mapping approach (frontmatterToEntryPatch) instead of a new
parse_md_file Tauri command — simpler, works in both Tauri and mock
modes, covers all VaultEntry fields.
- Converted useNoteActions to config object to avoid excess arguments.
- Extracted findWikilinkTarget, reloadTabsAfterRename, renameToastMessage
as standalone functions to reduce hook complexity (cc 10→9).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:48:39 +01:00
function findWikilinkTarget ( entries : VaultEntry [ ] , target : string ) : VaultEntry | undefined {
const targetLower = target . toLowerCase ( )
const targetAsWords = target . split ( '/' ) . pop ( ) ? . replace ( /-/g , ' ' ) . toLowerCase ( ) ? ? targetLower
return entries . find ( ( e ) = > entryMatchesTarget ( e , targetLower , targetAsWords ) )
}
feat: add optimistic error recovery for note creation (#69)
Note creation was already optimistic (UI updates before disk write) but
errors were silently swallowed. Now if the disk write fails, the
optimistic entry is reverted (tab closed, entry removed) and the user
sees an error toast. Each note creation is independent, so one failure
in rapid Cmd+N presses doesn't affect the others.
- Add removeEntry to useVaultLoader for reverting optimistic adds
- Refactor persistNewNote to return a Promise for error handling
- Extract navigateWikilink, persistOptimistic, createAndPersist,
and runFrontmatterAndApply helpers to reduce hook complexity
- Add tests for error recovery: single note, type, success path,
and rapid creation with partial failure
- Code health: 8.93→9.01 (CC fixed: 10→7, primitive obsession improved)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:11:59 +01:00
/** Navigate to a wikilink target, logging a warning if not found. */
function navigateWikilink ( entries : VaultEntry [ ] , target : string , selectNote : ( e : VaultEntry ) = > void ) : void {
const found = findWikilinkTarget ( entries , target )
if ( found ) selectNote ( found )
else console . warn ( ` Navigation target not found: ${ target } ` )
}
2026-02-26 00:03:38 +01:00
/** Dispatch focus-editor event with perf timing marker. */
function signalFocusEditor ( ) : void {
window . dispatchEvent ( new CustomEvent ( 'laputa:focus-editor' , { detail : { t0 : performance.now ( ) } } ) )
}
2026-02-27 14:11:31 +01:00
interface PersistCallbacks {
onFail : ( p : string ) = > void
onStart ? : ( p : string ) = > void
onEnd ? : ( p : string ) = > void
}
/** Persist to disk; track pending state via onStart/onEnd; revert on failure. */
function persistOptimistic ( path : string , content : string , cbs : PersistCallbacks ) : void {
cbs . onStart ? . ( path )
persistNewNote ( path , content )
. then ( ( ) = > cbs . onEnd ? . ( path ) )
. catch ( ( ) = > { cbs . onEnd ? . ( path ) ; cbs . onFail ( path ) } )
feat: add optimistic error recovery for note creation (#69)
Note creation was already optimistic (UI updates before disk write) but
errors were silently swallowed. Now if the disk write fails, the
optimistic entry is reverted (tab closed, entry removed) and the user
sees an error toast. Each note creation is independent, so one failure
in rapid Cmd+N presses doesn't affect the others.
- Add removeEntry to useVaultLoader for reverting optimistic adds
- Refactor persistNewNote to return a Promise for error handling
- Extract navigateWikilink, persistOptimistic, createAndPersist,
and runFrontmatterAndApply helpers to reduce hook complexity
- Add tests for error recovery: single note, type, success path,
and rapid creation with partial failure
- Code health: 8.93→9.01 (CC fixed: 10→7, primitive obsession improved)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:11:59 +01:00
}
2026-02-26 11:41:46 +01:00
/ * * O p t i m i s t i c a l l y o p e n t a b , a d d e n t r y t o v a u l t , a n d p e r s i s t t o d i s k .
* Tab creation ( setTabs / setActiveTabPath ) runs at normal priority so the
* tab appears instantly . addEntry uses startTransition internally so the
* expensive entries update ( NoteList re - filter / sort on 9000 + entries ) is
* deferred and doesn ' t block the tab from rendering . * /
feat: add optimistic error recovery for note creation (#69)
Note creation was already optimistic (UI updates before disk write) but
errors were silently swallowed. Now if the disk write fails, the
optimistic entry is reverted (tab closed, entry removed) and the user
sees an error toast. Each note creation is independent, so one failure
in rapid Cmd+N presses doesn't affect the others.
- Add removeEntry to useVaultLoader for reverting optimistic adds
- Refactor persistNewNote to return a Promise for error handling
- Extract navigateWikilink, persistOptimistic, createAndPersist,
and runFrontmatterAndApply helpers to reduce hook complexity
- Add tests for error recovery: single note, type, success path,
and rapid creation with partial failure
- Code health: 8.93→9.01 (CC fixed: 10→7, primitive obsession improved)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:11:59 +01:00
function createAndPersist (
resolved : { entry : VaultEntry ; content : string } ,
addFn : ( e : VaultEntry , c : string ) = > void ,
openTab : ( e : VaultEntry , c : string ) = > void ,
2026-02-27 14:11:31 +01:00
cbs : PersistCallbacks ,
feat: add optimistic error recovery for note creation (#69)
Note creation was already optimistic (UI updates before disk write) but
errors were silently swallowed. Now if the disk write fails, the
optimistic entry is reverted (tab closed, entry removed) and the user
sees an error toast. Each note creation is independent, so one failure
in rapid Cmd+N presses doesn't affect the others.
- Add removeEntry to useVaultLoader for reverting optimistic adds
- Refactor persistNewNote to return a Promise for error handling
- Extract navigateWikilink, persistOptimistic, createAndPersist,
and runFrontmatterAndApply helpers to reduce hook complexity
- Add tests for error recovery: single note, type, success path,
and rapid creation with partial failure
- Code health: 8.93→9.01 (CC fixed: 10→7, primitive obsession improved)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:11:59 +01:00
) : void {
openTab ( resolved . entry , resolved . content )
2026-02-26 11:41:46 +01:00
addEntryWithMock ( resolved . entry , resolved . content , addFn )
2026-02-27 14:11:31 +01:00
persistOptimistic ( resolved . entry . path , resolved . content , cbs )
feat: add optimistic error recovery for note creation (#69)
Note creation was already optimistic (UI updates before disk write) but
errors were silently swallowed. Now if the disk write fails, the
optimistic entry is reverted (tab closed, entry removed) and the user
sees an error toast. Each note creation is independent, so one failure
in rapid Cmd+N presses doesn't affect the others.
- Add removeEntry to useVaultLoader for reverting optimistic adds
- Refactor persistNewNote to return a Promise for error handling
- Extract navigateWikilink, persistOptimistic, createAndPersist,
and runFrontmatterAndApply helpers to reduce hook complexity
- Add tests for error recovery: single note, type, success path,
and rapid creation with partial failure
- Code health: 8.93→9.01 (CC fixed: 10→7, primitive obsession improved)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:11:59 +01:00
}
2026-02-22 10:55:45 +01:00
async function executeFrontmatterOp ( op : 'update' | 'delete' , path : string , key : string , value? : FrontmatterValue ) : Promise < string > {
if ( op === 'update' ) {
return isTauri ( ) ? invokeFrontmatter ( 'update_frontmatter' , { path , key , value } ) : applyMockFrontmatterUpdate ( path , key , value ! )
}
return isTauri ( ) ? invokeFrontmatter ( 'delete_frontmatter_property' , { path , key } ) : applyMockFrontmatterDelete ( path , key )
}
fix: sync in-memory entries after property/rename changes
Two propagation gaps fixed:
1. Property changes (type, status, color, etc.) now immediately sync to
the entries state via frontmatterToEntryPatch(), so sidebar, note list,
and relations panel reflect updates without restart.
2. After renaming a note, all other open tabs reload their content from
disk, picking up updated wikilinks immediately.
Design decisions:
- Used key-mapping approach (frontmatterToEntryPatch) instead of a new
parse_md_file Tauri command — simpler, works in both Tauri and mock
modes, covers all VaultEntry fields.
- Converted useNoteActions to config object to avoid excess arguments.
- Extracted findWikilinkTarget, reloadTabsAfterRename, renameToastMessage
as standalone functions to reduce hook complexity (cc 10→9).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:48:39 +01:00
function renameToastMessage ( updatedFiles : number ) : string {
if ( updatedFiles === 0 ) return 'Renamed'
return ` Renamed — updated ${ updatedFiles } wiki link ${ updatedFiles > 1 ? 's' : '' } `
}
/** Reload content for open tabs whose wikilinks may have changed after a rename. */
async function reloadTabsAfterRename (
tabPaths : string [ ] ,
updateTabContent : ( path : string , content : string ) = > void ,
) : Promise < void > {
for ( const tabPath of tabPaths ) {
try {
updateTabContent ( tabPath , await loadNoteContent ( tabPath ) )
} catch { /* skip tabs that fail to reload */ }
}
}
feat: add optimistic error recovery for note creation (#69)
Note creation was already optimistic (UI updates before disk write) but
errors were silently swallowed. Now if the disk write fails, the
optimistic entry is reverted (tab closed, entry removed) and the user
sees an error toast. Each note creation is independent, so one failure
in rapid Cmd+N presses doesn't affect the others.
- Add removeEntry to useVaultLoader for reverting optimistic adds
- Refactor persistNewNote to return a Promise for error handling
- Extract navigateWikilink, persistOptimistic, createAndPersist,
and runFrontmatterAndApply helpers to reduce hook complexity
- Add tests for error recovery: single note, type, success path,
and rapid creation with partial failure
- Code health: 8.93→9.01 (CC fixed: 10→7, primitive obsession improved)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:11:59 +01:00
/** Run a frontmatter update/delete and apply the result to state. */
async function runFrontmatterAndApply (
op : 'update' | 'delete' , path : string , key : string , value : FrontmatterValue | undefined ,
callbacks : { updateTab : ( p : string , c : string ) = > void ; updateEntry : ( p : string , patch : Partial < VaultEntry > ) = > void ; toast : ( m : string | null ) = > void } ,
) : Promise < void > {
try {
callbacks . updateTab ( path , await executeFrontmatterOp ( op , path , key , value ) )
const patch = frontmatterToEntryPatch ( op , key , value )
if ( Object . keys ( patch ) . length > 0 ) callbacks . updateEntry ( path , patch )
callbacks . toast ( op === 'update' ? 'Property updated' : 'Property deleted' )
} catch ( err ) {
console . error ( ` Failed to ${ op } frontmatter: ` , err )
callbacks . toast ( ` Failed to ${ op } property ` )
}
}
fix: sync in-memory entries after property/rename changes
Two propagation gaps fixed:
1. Property changes (type, status, color, etc.) now immediately sync to
the entries state via frontmatterToEntryPatch(), so sidebar, note list,
and relations panel reflect updates without restart.
2. After renaming a note, all other open tabs reload their content from
disk, picking up updated wikilinks immediately.
Design decisions:
- Used key-mapping approach (frontmatterToEntryPatch) instead of a new
parse_md_file Tauri command — simpler, works in both Tauri and mock
modes, covers all VaultEntry fields.
- Converted useNoteActions to config object to avoid excess arguments.
- Extracted findWikilinkTarget, reloadTabsAfterRename, renameToastMessage
as standalone functions to reduce hook complexity (cc 10→9).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:48:39 +01:00
export function useNoteActions ( config : NoteActionsConfig ) {
2026-02-27 14:11:31 +01:00
const { addEntry , removeEntry , updateContent , entries , setToastMessage , updateEntry , addPendingSave , removePendingSave } = config
2026-02-22 10:11:52 +01:00
const tabMgmt = useTabManagement ( )
2026-02-27 18:17:47 +01:00
const { setTabs , handleSelectNote , openTabWithContent , handleCloseTab , handleCloseTabRef , activeTabPathRef , handleSwitchTab } = tabMgmt
fix: sync in-memory entries after property/rename changes
Two propagation gaps fixed:
1. Property changes (type, status, color, etc.) now immediately sync to
the entries state via frontmatterToEntryPatch(), so sidebar, note list,
and relations panel reflect updates without restart.
2. After renaming a note, all other open tabs reload their content from
disk, picking up updated wikilinks immediately.
Design decisions:
- Used key-mapping approach (frontmatterToEntryPatch) instead of a new
parse_md_file Tauri command — simpler, works in both Tauri and mock
modes, covers all VaultEntry fields.
- Converted useNoteActions to config object to avoid excess arguments.
- Extracted findWikilinkTarget, reloadTabsAfterRename, renameToastMessage
as standalone functions to reduce hook complexity (cc 10→9).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:48:39 +01:00
const tabsRef = useRef ( tabMgmt . tabs )
2026-02-23 08:53:43 +01:00
// eslint-disable-next-line react-hooks/refs
fix: sync in-memory entries after property/rename changes
Two propagation gaps fixed:
1. Property changes (type, status, color, etc.) now immediately sync to
the entries state via frontmatterToEntryPatch(), so sidebar, note list,
and relations panel reflect updates without restart.
2. After renaming a note, all other open tabs reload their content from
disk, picking up updated wikilinks immediately.
Design decisions:
- Used key-mapping approach (frontmatterToEntryPatch) instead of a new
parse_md_file Tauri command — simpler, works in both Tauri and mock
modes, covers all VaultEntry fields.
- Converted useNoteActions to config object to avoid excess arguments.
- Extracted findWikilinkTarget, reloadTabsAfterRename, renameToastMessage
as standalone functions to reduce hook complexity (cc 10→9).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:48:39 +01:00
tabsRef . current = tabMgmt . tabs
2026-02-27 18:17:47 +01:00
const unsavedPathsRef = useRef ( config . unsavedPaths )
// eslint-disable-next-line react-hooks/refs
unsavedPathsRef . current = config . unsavedPaths
2026-02-17 12:10:21 +01:00
2026-02-22 10:11:52 +01:00
const updateTabContent = useCallback ( ( path : string , newContent : string ) = > {
2026-02-22 10:55:45 +01:00
setTabs ( ( prev ) = > prev . map ( ( t ) = > t . entry . path === path ? { . . . t , content : newContent } : t ) )
2026-02-22 10:11:52 +01:00
updateContent ( path , newContent )
} , [ setTabs , updateContent ] )
2026-02-17 12:10:21 +01:00
feat: add optimistic error recovery for note creation (#69)
Note creation was already optimistic (UI updates before disk write) but
errors were silently swallowed. Now if the disk write fails, the
optimistic entry is reverted (tab closed, entry removed) and the user
sees an error toast. Each note creation is independent, so one failure
in rapid Cmd+N presses doesn't affect the others.
- Add removeEntry to useVaultLoader for reverting optimistic adds
- Refactor persistNewNote to return a Promise for error handling
- Extract navigateWikilink, persistOptimistic, createAndPersist,
and runFrontmatterAndApply helpers to reduce hook complexity
- Add tests for error recovery: single note, type, success path,
and rapid creation with partial failure
- Code health: 8.93→9.01 (CC fixed: 10→7, primitive obsession improved)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:11:59 +01:00
const handleNavigateWikilink = useCallback (
( target : string ) = > navigateWikilink ( entries , target , handleSelectNote ) ,
[ entries , handleSelectNote ] ,
)
const revertOptimisticNote = useCallback ( ( path : string ) = > {
handleCloseTab ( path )
removeEntry ( path )
setToastMessage ( 'Failed to create note — disk write error' )
} , [ handleCloseTab , removeEntry , setToastMessage ] )
2026-02-17 12:10:21 +01:00
2026-02-27 14:11:31 +01:00
const persistCbs : PersistCallbacks = {
onFail : revertOptimisticNote ,
onStart : addPendingSave ,
onEnd : removePendingSave ,
}
2026-02-24 23:03:01 +01:00
const pendingNamesRef = useRef < Set < string > > ( new Set ( ) )
2026-02-22 10:55:45 +01:00
const handleCreateNote = useCallback ( ( title : string , type : string ) = > {
2026-02-27 14:11:31 +01:00
createAndPersist ( resolveNewNote ( title , type ) , addEntry , openTabWithContent , persistCbs )
} , [ openTabWithContent , addEntry , revertOptimisticNote , addPendingSave , removePendingSave ] ) // eslint-disable-line react-hooks/exhaustive-deps -- persistCbs is stable when deps are
2026-02-24 23:03:01 +01:00
const handleCreateNoteImmediate = useCallback ( ( type ? : string ) = > {
const noteType = type || 'Note'
const title = generateUntitledName ( entries , noteType , pendingNamesRef . current )
pendingNamesRef . current . add ( title )
2026-02-27 18:17:47 +01:00
const resolved = resolveNewNote ( title , noteType )
openTabWithContent ( resolved . entry , resolved . content )
addEntryWithMock ( resolved . entry , resolved . content , addEntry )
config . trackUnsaved ? . ( resolved . entry . path )
config . markContentPending ? . ( resolved . entry . path , resolved . content )
2026-02-26 00:03:38 +01:00
signalFocusEditor ( )
2026-02-24 23:03:01 +01:00
setTimeout ( ( ) = > pendingNamesRef . current . delete ( title ) , 500 )
2026-02-27 18:17:47 +01:00
} , [ entries , openTabWithContent , addEntry , config . trackUnsaved , config . markContentPending ] ) // eslint-disable-line react-hooks/exhaustive-deps -- config callbacks are stable
/** Close tab and discard entry+unsaved state if the note was never persisted. */
const handleCloseTabWithCleanup = useCallback ( ( path : string ) = > {
if ( unsavedPathsRef . current ? . has ( path ) ) {
removeEntry ( path )
config . clearUnsaved ? . ( path )
}
handleCloseTab ( path )
} , [ handleCloseTab , removeEntry , config . clearUnsaved ] ) // eslint-disable-line react-hooks/exhaustive-deps -- ref access is stable
// Keep handleCloseTabRef in sync so Cmd+W and menu events also clean up unsaved notes.
useEffect ( ( ) = > { handleCloseTabRef . current = handleCloseTabWithCleanup } )
2026-02-17 12:10:21 +01:00
2026-02-22 10:55:45 +01:00
const handleCreateType = useCallback ( ( typeName : string ) = > {
2026-02-27 14:11:31 +01:00
createAndPersist ( resolveNewType ( typeName ) , addEntry , openTabWithContent , persistCbs )
} , [ openTabWithContent , addEntry , revertOptimisticNote , addPendingSave , removePendingSave ] ) // eslint-disable-line react-hooks/exhaustive-deps -- persistCbs is stable when deps are
2026-02-21 09:41:46 +01:00
feat: add optimistic error recovery for note creation (#69)
Note creation was already optimistic (UI updates before disk write) but
errors were silently swallowed. Now if the disk write fails, the
optimistic entry is reverted (tab closed, entry removed) and the user
sees an error toast. Each note creation is independent, so one failure
in rapid Cmd+N presses doesn't affect the others.
- Add removeEntry to useVaultLoader for reverting optimistic adds
- Refactor persistNewNote to return a Promise for error handling
- Extract navigateWikilink, persistOptimistic, createAndPersist,
and runFrontmatterAndApply helpers to reduce hook complexity
- Add tests for error recovery: single note, type, success path,
and rapid creation with partial failure
- Code health: 8.93→9.01 (CC fixed: 10→7, primitive obsession improved)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:11:59 +01:00
const fmCallbacks = { updateTab : updateTabContent , updateEntry , toast : setToastMessage }
const runFrontmatterOp = useCallback (
( op : 'update' | 'delete' , path : string , key : string , value? : FrontmatterValue ) = >
runFrontmatterAndApply ( op , path , key , value , fmCallbacks ) ,
[ updateTabContent , updateEntry , setToastMessage ] , // eslint-disable-line react-hooks/exhaustive-deps -- fmCallbacks is stable when deps are
)
2026-02-17 12:10:21 +01:00
2026-02-21 19:22:44 +01:00
const handleRenameNote = useCallback ( async (
feat: add optimistic error recovery for note creation (#69)
Note creation was already optimistic (UI updates before disk write) but
errors were silently swallowed. Now if the disk write fails, the
optimistic entry is reverted (tab closed, entry removed) and the user
sees an error toast. Each note creation is independent, so one failure
in rapid Cmd+N presses doesn't affect the others.
- Add removeEntry to useVaultLoader for reverting optimistic adds
- Refactor persistNewNote to return a Promise for error handling
- Extract navigateWikilink, persistOptimistic, createAndPersist,
and runFrontmatterAndApply helpers to reduce hook complexity
- Add tests for error recovery: single note, type, success path,
and rapid creation with partial failure
- Code health: 8.93→9.01 (CC fixed: 10→7, primitive obsession improved)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:11:59 +01:00
path : string , newTitle : string , vaultPath : string ,
2026-02-21 19:22:44 +01:00
onEntryRenamed : ( oldPath : string , newEntry : Partial < VaultEntry > & { path : string } , newContent : string ) = > void ,
) = > {
try {
const result = await performRename ( path , newTitle , vaultPath )
const newContent = await loadNoteContent ( result . new_path )
2026-02-22 12:10:17 +01:00
const entry = entries . find ( ( e ) = > e . path === path )
const newEntry = buildRenamedEntry ( entry ? ? { } as VaultEntry , newTitle , result . new_path )
feat: add optimistic error recovery for note creation (#69)
Note creation was already optimistic (UI updates before disk write) but
errors were silently swallowed. Now if the disk write fails, the
optimistic entry is reverted (tab closed, entry removed) and the user
sees an error toast. Each note creation is independent, so one failure
in rapid Cmd+N presses doesn't affect the others.
- Add removeEntry to useVaultLoader for reverting optimistic adds
- Refactor persistNewNote to return a Promise for error handling
- Extract navigateWikilink, persistOptimistic, createAndPersist,
and runFrontmatterAndApply helpers to reduce hook complexity
- Add tests for error recovery: single note, type, success path,
and rapid creation with partial failure
- Code health: 8.93→9.01 (CC fixed: 10→7, primitive obsession improved)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:11:59 +01:00
const otherTabPaths = tabsRef . current . filter ( t = > t . entry . path !== path ) . map ( t = > t . entry . path )
2026-02-21 19:22:44 +01:00
setTabs ( ( prev ) = > prev . map ( ( t ) = > t . entry . path === path ? { entry : newEntry , content : newContent } : t ) )
2026-02-22 12:10:17 +01:00
if ( activeTabPathRef . current === path ) handleSwitchTab ( result . new_path )
2026-02-21 19:22:44 +01:00
onEntryRenamed ( path , newEntry , newContent )
fix: sync in-memory entries after property/rename changes
Two propagation gaps fixed:
1. Property changes (type, status, color, etc.) now immediately sync to
the entries state via frontmatterToEntryPatch(), so sidebar, note list,
and relations panel reflect updates without restart.
2. After renaming a note, all other open tabs reload their content from
disk, picking up updated wikilinks immediately.
Design decisions:
- Used key-mapping approach (frontmatterToEntryPatch) instead of a new
parse_md_file Tauri command — simpler, works in both Tauri and mock
modes, covers all VaultEntry fields.
- Converted useNoteActions to config object to avoid excess arguments.
- Extracted findWikilinkTarget, reloadTabsAfterRename, renameToastMessage
as standalone functions to reduce hook complexity (cc 10→9).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:48:39 +01:00
await reloadTabsAfterRename ( otherTabPaths , updateTabContent )
setToastMessage ( renameToastMessage ( result . updated_files ) )
2026-02-21 19:22:44 +01:00
} catch ( err ) {
console . error ( 'Failed to rename note:' , err )
setToastMessage ( 'Failed to rename note' )
}
fix: sync in-memory entries after property/rename changes
Two propagation gaps fixed:
1. Property changes (type, status, color, etc.) now immediately sync to
the entries state via frontmatterToEntryPatch(), so sidebar, note list,
and relations panel reflect updates without restart.
2. After renaming a note, all other open tabs reload their content from
disk, picking up updated wikilinks immediately.
Design decisions:
- Used key-mapping approach (frontmatterToEntryPatch) instead of a new
parse_md_file Tauri command — simpler, works in both Tauri and mock
modes, covers all VaultEntry fields.
- Converted useNoteActions to config object to avoid excess arguments.
- Extracted findWikilinkTarget, reloadTabsAfterRename, renameToastMessage
as standalone functions to reduce hook complexity (cc 10→9).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:48:39 +01:00
} , [ entries , setTabs , activeTabPathRef , handleSwitchTab , updateTabContent , setToastMessage ] )
2026-02-21 19:22:44 +01:00
2026-02-17 12:10:21 +01:00
return {
2026-02-22 10:55:45 +01:00
. . . tabMgmt ,
2026-02-27 18:17:47 +01:00
handleCloseTab : handleCloseTabWithCleanup ,
2026-02-17 12:10:21 +01:00
handleNavigateWikilink ,
handleCreateNote ,
2026-02-24 23:03:01 +01:00
handleCreateNoteImmediate ,
2026-02-21 09:41:46 +01:00
handleCreateType ,
2026-02-22 10:55:45 +01:00
handleUpdateFrontmatter : useCallback ( ( path : string , key : string , value : FrontmatterValue ) = > runFrontmatterOp ( 'update' , path , key , value ) , [ runFrontmatterOp ] ) ,
handleDeleteProperty : useCallback ( ( path : string , key : string ) = > runFrontmatterOp ( 'delete' , path , key ) , [ runFrontmatterOp ] ) ,
handleAddProperty : useCallback ( ( path : string , key : string , value : FrontmatterValue ) = > runFrontmatterOp ( 'update' , path , key , value ) , [ runFrontmatterOp ] ) ,
2026-02-21 19:22:44 +01:00
handleRenameNote ,
2026-02-17 12:10:21 +01:00
}
}