feat: detect external file renames and offer wikilink update via banner

When the app regains focus, checks git diff for renames (--diff-filter=R).
If renamed .md files are found, shows a non-blocking banner with "Update
wikilinks" and "Ignore" buttons. The update reuses the existing vault-wide
wikilink replacement logic from rename.rs.

New Tauri commands: detect_renames, update_wikilinks_for_renames.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
lucaronin
2026-03-31 11:58:32 +02:00
parent 3ede96a437
commit 93b83007eb
7 changed files with 195 additions and 2 deletions

View File

@@ -0,0 +1,41 @@
import { render, screen, fireEvent } from '@testing-library/react'
import { describe, it, expect, vi } from 'vitest'
import { RenameDetectedBanner } from './RenameDetectedBanner'
describe('RenameDetectedBanner', () => {
const renames = [
{ old_path: 'old-note.md', new_path: 'new-note.md' },
{ old_path: 'folder/draft.md', new_path: 'folder/published.md' },
]
it('renders nothing when renames is empty', () => {
const { container } = render(
<RenameDetectedBanner renames={[]} onUpdate={vi.fn()} onDismiss={vi.fn()} />
)
expect(container.firstChild).toBeNull()
})
it('shows banner with rename count', () => {
render(<RenameDetectedBanner renames={renames} onUpdate={vi.fn()} onDismiss={vi.fn()} />)
expect(screen.getByText(/2 files? renamed/i)).toBeInTheDocument()
})
it('shows Update wikilinks button', () => {
render(<RenameDetectedBanner renames={renames} onUpdate={vi.fn()} onDismiss={vi.fn()} />)
expect(screen.getByText('Update wikilinks')).toBeInTheDocument()
})
it('calls onUpdate when Update button clicked', () => {
const onUpdate = vi.fn()
render(<RenameDetectedBanner renames={renames} onUpdate={onUpdate} onDismiss={vi.fn()} />)
fireEvent.click(screen.getByText('Update wikilinks'))
expect(onUpdate).toHaveBeenCalledOnce()
})
it('calls onDismiss when Ignore button clicked', () => {
const onDismiss = vi.fn()
render(<RenameDetectedBanner renames={renames} onUpdate={vi.fn()} onDismiss={onDismiss} />)
fireEvent.click(screen.getByText('Ignore'))
expect(onDismiss).toHaveBeenCalledOnce()
})
})

View File

@@ -0,0 +1,38 @@
import { ArrowsClockwise } from '@phosphor-icons/react'
export interface DetectedRename {
old_path: string
new_path: string
}
interface RenameDetectedBannerProps {
renames: DetectedRename[]
onUpdate: () => void
onDismiss: () => void
}
export function RenameDetectedBanner({ renames, onUpdate, onDismiss }: RenameDetectedBannerProps) {
if (renames.length === 0) return null
const count = renames.length
return (
<div className="flex items-center gap-3 border-b border-border bg-accent/50 px-4 py-2 text-[13px]">
<ArrowsClockwise size={16} className="shrink-0 text-accent-foreground" />
<span className="flex-1 text-foreground">
{count} file{count !== 1 ? 's' : ''} renamed outside Laputa. Update wikilinks?
</span>
<button
className="shrink-0 cursor-pointer rounded-md bg-primary px-3 py-1 text-[12px] font-medium text-primary-foreground transition-colors hover:bg-primary/90"
onClick={onUpdate}
>
Update wikilinks
</button>
<button
className="shrink-0 cursor-pointer rounded-md border border-border bg-transparent px-3 py-1 text-[12px] font-medium text-muted-foreground transition-colors hover:bg-muted"
onClick={onDismiss}
>
Ignore
</button>
</div>
)
}