diff --git a/src/components/NoteList.test.tsx b/src/components/NoteList.test.tsx
index 00a451cd..e61b757c 100644
--- a/src/components/NoteList.test.tsx
+++ b/src/components/NoteList.test.tsx
@@ -1107,6 +1107,46 @@ describe('NoteList — virtual list with large datasets', () => {
expect(screen.getByText('Matteo Cellini')).toBeInTheDocument()
expect(screen.queryByText('Facebook Ads Strategy')).not.toBeInTheDocument()
})
+
+ it('shows deleted notes banner when files are deleted', () => {
+ const filesWithDeleted = [
+ { path: mockEntries[0].path, relativePath: 'project/26q1-laputa-app.md', status: 'modified' as const },
+ { path: '/Users/luca/Laputa/note/gone.md', relativePath: 'note/gone.md', status: 'deleted' as const },
+ { path: '/Users/luca/Laputa/note/also-gone.md', relativePath: 'note/also-gone.md', status: 'deleted' as const },
+ ]
+ render(
+
+ )
+ expect(screen.getByText('Build Laputa App')).toBeInTheDocument()
+ expect(screen.getByText('2 notes deleted')).toBeInTheDocument()
+ })
+
+ it('shows singular form for single deleted note', () => {
+ const filesWithOneDeleted = [
+ { path: '/Users/luca/Laputa/note/gone.md', relativePath: 'note/gone.md', status: 'deleted' as const },
+ ]
+ render(
+
+ )
+ expect(screen.getByText('1 note deleted')).toBeInTheDocument()
+ })
+
+ it('does not show deleted banner when no files are deleted', () => {
+ render(
+
+ )
+ expect(screen.queryByText(/notes? deleted/)).not.toBeInTheDocument()
+ })
+
+ it('does not show deleted banner outside changes view', () => {
+ const filesWithDeleted = [
+ { path: '/Users/luca/Laputa/note/gone.md', relativePath: 'note/gone.md', status: 'deleted' as const },
+ ]
+ render(
+
+ )
+ expect(screen.queryByText(/notes? deleted/)).not.toBeInTheDocument()
+ })
})
})
diff --git a/src/components/NoteList.tsx b/src/components/NoteList.tsx
index 86f1c843..a517f7c8 100644
--- a/src/components/NoteList.tsx
+++ b/src/components/NoteList.tsx
@@ -4,7 +4,7 @@ import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso'
import type { VaultEntry, SidebarSelection, ModifiedFile, NoteStatus } from '../types'
import { Input } from '@/components/ui/input'
import {
- MagnifyingGlass, Plus, CaretDown, CaretRight, Warning, Trash,
+ MagnifyingGlass, Plus, CaretDown, CaretRight, Warning, Trash, TrashSimple,
} from '@phosphor-icons/react'
import { getTypeColor, getTypeLightColor, buildTypeEntryMap } from '../utils/typeColors'
import { NoteItem, getTypeIcon } from './NoteItem'
@@ -110,6 +110,16 @@ function EmptyMessage({ text }: { text: string }) {
return
{text}
}
+function DeletedNotesBanner({ count }: { count: number }) {
+ if (count === 0) return null
+ return (
+
+
+ {count} {count === 1 ? 'note' : 'notes'} deleted
+
+ )
+}
+
function resolveHeaderTitle(selection: SidebarSelection, typeDocument: VaultEntry | null): string {
if (selection.kind === 'entity') return selection.entry.title
if (typeDocument) return typeDocument.title
@@ -158,16 +168,17 @@ function resolveEmptyText(isChangesView: boolean, changesError: string | null |
return query ? 'No matching notes' : 'No notes found'
}
-function ListView({ isTrashView, isChangesView, changesError, expiredTrashCount, searched, query, renderItem, virtuosoRef }: {
+function ListView({ isTrashView, isChangesView, changesError, expiredTrashCount, deletedCount = 0, searched, query, renderItem, virtuosoRef }: {
isTrashView: boolean; isChangesView?: boolean; changesError?: string | null; expiredTrashCount: number
- searched: VaultEntry[]; query: string
+ deletedCount?: number; searched: VaultEntry[]; query: string
renderItem: (entry: VaultEntry) => React.ReactNode
virtuosoRef?: React.RefObject
}) {
const emptyText = resolveEmptyText(!!isChangesView, changesError ?? null, isTrashView, query)
const hasHeader = isTrashView && expiredTrashCount > 0
+ const showDeleted = !!isChangesView && deletedCount > 0
- if (searched.length === 0) {
+ if (searched.length === 0 && !showDeleted) {
return (
{hasHeader &&
}
@@ -176,17 +187,28 @@ function ListView({ isTrashView, isChangesView, changesError, expiredTrashCount,
)
}
+ if (searched.length === 0 && showDeleted) {
+ return (
+
+
+
+ )
+ }
+
return (
-
: undefined,
- }}
- itemContent={(_index, entry) => renderItem(entry)}
- />
+
+ : undefined,
+ }}
+ itemContent={(_index, entry) => renderItem(entry)}
+ />
+ {showDeleted && }
+
)
}
@@ -511,6 +533,10 @@ function NoteListInner({ entries, selection, selectedNote, modifiedFiles, modifi
const typeEntryMap = useTypeEntryMap(entries)
const { isEntityView, isTrashView, searched, searchedGroups, expiredTrashCount } = useNoteListData({ entries, selection, query, listSort, listDirection, modifiedPathSet, modifiedSuffixes })
const isChangesView = selection.kind === 'filter' && selection.filter === 'changes'
+ const deletedCount = useMemo(
+ () => isChangesView ? (modifiedFiles ?? []).filter((f) => f.status === 'deleted').length : 0,
+ [isChangesView, modifiedFiles],
+ )
const entitySelection = isEntityView && selection.kind === 'entity' ? selection : null
const noteListKeyboard = useNoteListKeyboard({ items: searched, selectedNotePath: selectedNote?.path ?? null, onOpen: onReplaceActiveTab, enabled: !isEntityView })
@@ -543,7 +569,7 @@ function NoteListInner({ entries, selection, selectedNote, modifiedFiles, modifi
{entitySelection ? (
) : (
-
+
)}
{multiSelect.isMultiSelecting && (
diff --git a/src/mock-tauri/mock-handlers.ts b/src/mock-tauri/mock-handlers.ts
index 483ab378..045fc102 100644
--- a/src/mock-tauri/mock-handlers.ts
+++ b/src/mock-tauri/mock-handlers.ts
@@ -29,6 +29,7 @@ function mockModifiedFiles(): ModifiedFile[] {
{ path: '/Users/luca/Laputa/26q1-laputa-app.md', relativePath: '26q1-laputa-app.md', status: 'modified' },
{ path: '/Users/luca/Laputa/facebook-ads-strategy.md', relativePath: 'facebook-ads-strategy.md', status: 'modified' },
{ path: '/Users/luca/Laputa/ai-agents-primer.md', relativePath: 'ai-agents-primer.md', status: 'added' },
+ { path: '/Users/luca/Laputa/old-draft.md', relativePath: 'old-draft.md', status: 'deleted' },
]
}
diff --git a/tests/smoke/show-deleted-notes-in-changes-view.spec.ts b/tests/smoke/show-deleted-notes-in-changes-view.spec.ts
new file mode 100644
index 00000000..9bdf060c
--- /dev/null
+++ b/tests/smoke/show-deleted-notes-in-changes-view.spec.ts
@@ -0,0 +1,37 @@
+import { test, expect } from '@playwright/test'
+import { openCommandPalette, executeCommand } from './helpers'
+
+async function navigateToChanges(page: import('@playwright/test').Page) {
+ await openCommandPalette(page)
+ await executeCommand(page, 'Go to Changes')
+ await page.waitForTimeout(500)
+}
+
+test.describe('Show deleted notes in Changes view', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/')
+ await page.waitForLoadState('networkidle')
+ })
+
+ test('changes view shows deleted notes banner', async ({ page }) => {
+ await navigateToChanges(page)
+ const banner = page.locator('[data-testid="deleted-notes-banner"]')
+ await expect(banner).toBeVisible({ timeout: 5000 })
+ await expect(banner).toContainText('1 note deleted')
+ })
+
+ test('deleted banner is visually distinct from note items', async ({ page }) => {
+ await navigateToChanges(page)
+ const banner = page.locator('[data-testid="deleted-notes-banner"]')
+ await expect(banner).toBeVisible({ timeout: 5000 })
+ // Banner should have reduced opacity (visually distinct)
+ const opacity = await banner.evaluate((el) => window.getComputedStyle(el).opacity)
+ expect(parseFloat(opacity)).toBeLessThan(1)
+ })
+
+ test('changes counter in sidebar matches list items plus deleted count', async ({ page }) => {
+ // The sidebar badge should show the total count (modified + added + deleted)
+ const changesBadge = page.locator('text=Changes').locator('..')
+ await expect(changesBadge).toBeVisible({ timeout: 5000 })
+ })
+})