- {!query.trim() && (
+ const hasQuery = query.trim().length > 0
+ const hasResults = results.length > 0
+
+ if (!hasQuery) {
+ return (
+
Search across all note contents
-
- Enter to open · Esc to close
-
+
Enter to open · Esc to close
- )}
+
+ )
+ }
- {query.trim() && results.length === 0 && loading && (
-
- Searching...
-
- )}
+ if (!hasResults && loading) {
+ return (
+
+ )
+ }
- {query.trim() && results.length === 0 && !loading && (
+ if (!hasResults) {
+ return (
+
+ )
+ }
- {results.length > 0 && (
- <>
-
-
- {results.length} result{results.length !== 1 ? 's' : ''}{elapsedMs !== null ? ` · ${elapsedMs}ms` : ''}
-
-
-
- {results.map((result, i) => {
- const isA = entryLookup.get(result.path)?.isA ?? result.noteType
- const noteType = isA && isA !== 'Note' ? isA : null
- const typeColor = noteType ? getTypeColor(isA, typeEntryMap[isA ?? '']?.color) : undefined
- const typeLightColor = noteType ? getTypeLightColor(isA, typeEntryMap[isA ?? '']?.color) : undefined
- return (
-
onSelect(result)}
- onMouseEnter={() => onHover(i)}
- >
-
- {result.title}
- {noteType && (
-
- {noteType}
-
- )}
-
- {result.snippet && (
-
- {result.snippet}
-
- )}
-
- )
- })}
-
- >
- )}
+ return (
+
+
)
}
diff --git a/src/mock-tauri/mock-entries.ts b/src/mock-tauri/mock-entries.ts
index 2ebeebf4..6545ed3e 100644
--- a/src/mock-tauri/mock-entries.ts
+++ b/src/mock-tauri/mock-entries.ts
@@ -35,7 +35,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
icon: null,
color: null,
order: null,
- outgoingLinks: [],
+ outgoingLinks: ['quarter/q1-2026', 'topic/software-development', 'person/matteo-cellini', 'person/maria-bianchi', 'person/marco-verdi'],
},
{
path: '/Users/luca/Laputa/responsibility/grow-newsletter.md',
@@ -69,7 +69,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
icon: null,
color: null,
order: null,
- outgoingLinks: [],
+ outgoingLinks: ['essay/on-writing-well', 'essay/engineering-leadership-101', 'essay/ai-agents-primer', 'topic/growth', 'topic/writing'],
},
{
path: '/Users/luca/Laputa/responsibility/manage-sponsorships.md',
@@ -97,7 +97,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
icon: null,
color: null,
order: null,
- outgoingLinks: [],
+ outgoingLinks: ['person/matteo-cellini'],
},
{
path: '/Users/luca/Laputa/procedure/write-weekly-essays.md',
@@ -125,7 +125,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
icon: null,
color: null,
order: null,
- outgoingLinks: [],
+ outgoingLinks: ['responsibility/grow-newsletter'],
},
{
path: '/Users/luca/Laputa/procedure/run-sponsorships.md',
@@ -153,7 +153,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
icon: null,
color: null,
order: null,
- outgoingLinks: [],
+ outgoingLinks: ['responsibility/manage-sponsorships'],
},
{
path: '/Users/luca/Laputa/experiment/stock-screener.md',
@@ -182,7 +182,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
icon: null,
color: null,
order: null,
- outgoingLinks: [],
+ outgoingLinks: ['topic/trading', 'topic/algorithmic-trading', 'data/ema200-backtest-results'],
},
{
path: '/Users/luca/Laputa/note/facebook-ads-strategy.md',
@@ -211,7 +211,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
icon: null,
color: null,
order: null,
- outgoingLinks: [],
+ outgoingLinks: ['project/26q1-laputa-app', 'topic/growth', 'topic/ads'],
},
{
path: '/Users/luca/Laputa/note/budget-allocation.md',
@@ -239,7 +239,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
icon: null,
color: null,
order: null,
- outgoingLinks: [],
+ outgoingLinks: ['project/26q1-laputa-app'],
},
{
path: '/Users/luca/Laputa/person/matteo-cellini.md',
@@ -375,7 +375,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
icon: null,
color: null,
order: null,
- outgoingLinks: [],
+ outgoingLinks: ['project/26q1-laputa-app', 'person/matteo-cellini'],
},
{
path: '/Users/luca/Laputa/topic/software-development.md',
@@ -459,7 +459,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
icon: null,
color: null,
order: null,
- outgoingLinks: [],
+ outgoingLinks: ['responsibility/grow-newsletter'],
},
{
path: '/Users/luca/Laputa/essay/engineering-leadership-101.md',
@@ -488,7 +488,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
icon: null,
color: null,
order: null,
- outgoingLinks: [],
+ outgoingLinks: ['responsibility/grow-newsletter', 'topic/software-development'],
},
{
path: '/Users/luca/Laputa/essay/ai-agents-primer.md',
@@ -516,7 +516,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
icon: null,
color: null,
order: null,
- outgoingLinks: [],
+ outgoingLinks: ['responsibility/grow-newsletter'],
},
// --- Type documents ---
{
@@ -1041,7 +1041,7 @@ function generateBulkEntries(count: number): VaultEntry[] {
icon: null,
color: null,
order: null,
- outgoingLinks: [],
+ outgoingLinks: Array.from({ length: i % 8 }, (_, j) => `note/link-target-${(i + j) % 50}`),
})
}
return entries
diff --git a/src/utils/noteListHelpers.test.ts b/src/utils/noteListHelpers.test.ts
index 0edf7ec4..fc2c5a0f 100644
--- a/src/utils/noteListHelpers.test.ts
+++ b/src/utils/noteListHelpers.test.ts
@@ -1,5 +1,5 @@
import { describe, it, expect, vi, afterEach } from 'vitest'
-import { formatSubtitle, relativeDate } from './noteListHelpers'
+import { formatSubtitle, formatSearchSubtitle, relativeDate } from './noteListHelpers'
import type { VaultEntry } from '../types'
function makeEntry(overrides: Partial
= {}): VaultEntry {
@@ -48,6 +48,80 @@ describe('formatSubtitle', () => {
expect(result).toContain('50 words')
expect(result).toContain('\u00b7')
})
+
+ it('includes link count when outgoingLinks is non-empty', () => {
+ const entry = makeEntry({ modifiedAt: 1700000000, wordCount: 200, outgoingLinks: ['a', 'b', 'c'] })
+ const result = formatSubtitle(entry)
+ expect(result).toContain('3 links')
+ })
+
+ it('uses singular "link" when exactly one', () => {
+ const entry = makeEntry({ wordCount: 100, outgoingLinks: ['one'] })
+ expect(formatSubtitle(entry)).toContain('1 link')
+ expect(formatSubtitle(entry)).not.toContain('1 links')
+ })
+
+ it('omits link count when outgoingLinks is empty', () => {
+ const entry = makeEntry({ modifiedAt: 1700000000, wordCount: 50, outgoingLinks: [] })
+ expect(formatSubtitle(entry)).not.toContain('link')
+ })
+
+ it('formats word count with locale separators for large numbers', () => {
+ const entry = makeEntry({ wordCount: 1240 })
+ const result = formatSubtitle(entry)
+ expect(result).toMatch(/1,?240 words/)
+ })
+})
+
+describe('formatSearchSubtitle', () => {
+ afterEach(() => { vi.restoreAllMocks() })
+
+ it('shows modified date, created date, word count, and links', () => {
+ const now = Math.floor(Date.now() / 1000)
+ const entry = makeEntry({
+ modifiedAt: now - 3600,
+ createdAt: now - 86400 * 30,
+ wordCount: 520,
+ outgoingLinks: ['a', 'b', 'c', 'd', 'e'],
+ })
+ const result = formatSearchSubtitle(entry)
+ expect(result).toContain('1h ago')
+ expect(result).toContain('Created')
+ expect(result).toContain('520 words')
+ expect(result).toContain('5 links')
+ })
+
+ it('omits created date when same as modified', () => {
+ const now = Math.floor(Date.now() / 1000)
+ const entry = makeEntry({ modifiedAt: now, createdAt: now, wordCount: 100 })
+ const result = formatSearchSubtitle(entry)
+ expect(result).not.toContain('Created')
+ })
+
+ it('omits created date when createdAt is null', () => {
+ const now = Math.floor(Date.now() / 1000)
+ const entry = makeEntry({ modifiedAt: now, createdAt: null, wordCount: 100 })
+ const result = formatSearchSubtitle(entry)
+ expect(result).not.toContain('Created')
+ })
+
+ it('shows "Empty" for zero word count', () => {
+ const entry = makeEntry({ modifiedAt: 1700000000, wordCount: 0 })
+ expect(formatSearchSubtitle(entry)).toContain('Empty')
+ })
+
+ it('omits link count when no outgoing links', () => {
+ const entry = makeEntry({ modifiedAt: 1700000000, wordCount: 50, outgoingLinks: [] })
+ expect(formatSearchSubtitle(entry)).not.toContain('link')
+ })
+
+ it('falls back to createdAt when modifiedAt is null', () => {
+ const entry = makeEntry({ createdAt: 1700000000, wordCount: 200, outgoingLinks: ['a'] })
+ const result = formatSearchSubtitle(entry)
+ expect(result).toContain('200 words')
+ expect(result).toContain('1 link')
+ expect(result).not.toContain('Created')
+ })
})
describe('relativeDate', () => {
diff --git a/src/utils/noteListHelpers.ts b/src/utils/noteListHelpers.ts
index ba46ae90..68b559ae 100644
--- a/src/utils/noteListHelpers.ts
+++ b/src/utils/noteListHelpers.ts
@@ -30,10 +30,31 @@ export function formatSubtitle(entry: VaultEntry): string {
const date = getDisplayDate(entry)
if (date) parts.push(relativeDate(date))
if (entry.wordCount > 0) {
- parts.push(`${entry.wordCount} words`)
+ parts.push(`${entry.wordCount.toLocaleString()} words`)
} else {
parts.push('Empty')
}
+ if (entry.outgoingLinks.length > 0) {
+ parts.push(`${entry.outgoingLinks.length} ${entry.outgoingLinks.length === 1 ? 'link' : 'links'}`)
+ }
+ return parts.join(' \u00b7 ')
+}
+
+export function formatSearchSubtitle(entry: VaultEntry): string {
+ const parts: string[] = []
+ const modified = entry.modifiedAt ?? entry.createdAt
+ if (modified) parts.push(relativeDate(modified))
+ if (entry.createdAt && entry.modifiedAt && entry.createdAt !== entry.modifiedAt) {
+ parts.push(`Created ${relativeDate(entry.createdAt)}`)
+ }
+ if (entry.wordCount > 0) {
+ parts.push(`${entry.wordCount.toLocaleString()} words`)
+ } else {
+ parts.push('Empty')
+ }
+ if (entry.outgoingLinks.length > 0) {
+ parts.push(`${entry.outgoingLinks.length} ${entry.outgoingLinks.length === 1 ? 'link' : 'links'}`)
+ }
return parts.join(' \u00b7 ')
}