From b7dc577d23bf653072c086bfaf39a4d270cdab29 Mon Sep 17 00:00:00 2001 From: Kaan Karaca Date: Sun, 5 Apr 2026 13:25:58 +0300 Subject: [PATCH 1/8] feat: surface note emoji through inbox suggestions pipeline Add emoji field to SuggestedNote type and pipe it from note cache through the suggestions API to the renderer. --- apps/desktop/src/main/inbox/suggestions.ts | 14 +++++++++++--- apps/desktop/src/preload/index.d.ts | 1 + apps/desktop/src/renderer/src/types/index.ts | 1 + packages/contracts/src/inbox-api.ts | 1 + 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/main/inbox/suggestions.ts b/apps/desktop/src/main/inbox/suggestions.ts index 01e44af56..b50c1ed6c 100644 --- a/apps/desktop/src/main/inbox/suggestions.ts +++ b/apps/desktop/src/main/inbox/suggestions.ts @@ -39,6 +39,7 @@ interface SimilarNote { noteTitle: string score: number snippet: string + emoji: string | null } interface FilingPattern { @@ -357,7 +358,12 @@ async function findSimilarNotes(content: string, limit: number = 5): Promise Date: Sun, 5 Apr 2026 13:26:03 +0300 Subject: [PATCH 2/8] feat: show folder/note icons and stack filing sections vertically Display folder emoji/icon in dropdown trigger and list items. Show note emoji for AI-suggested notes. Stack File to and Tags line by line instead of side by side. Simplify dropdown by removing confidence percentages and navigation hints. --- .../inbox-detail/filing-section.tsx | 375 ++++++++---------- 1 file changed, 172 insertions(+), 203 deletions(-) diff --git a/apps/desktop/src/renderer/src/components/inbox-detail/filing-section.tsx b/apps/desktop/src/renderer/src/components/inbox-detail/filing-section.tsx index 1b2db5f17..b24f26f6f 100644 --- a/apps/desktop/src/renderer/src/components/inbox-detail/filing-section.tsx +++ b/apps/desktop/src/renderer/src/components/inbox-detail/filing-section.tsx @@ -82,7 +82,8 @@ export const FilingSection = ({ id: fi.path, name: fi.path.split('/').pop() || fi.path, path: fi.path, - parent: fi.path.includes('/') ? fi.path.split('/').slice(0, -1).join('/') : undefined + parent: fi.path.includes('/') ? fi.path.split('/').slice(0, -1).join('/') : undefined, + icon: fi.icon ?? null }) } } @@ -113,19 +114,20 @@ export const FilingSection = ({ if (aiSuggestions.length > 0) { return aiSuggestions .filter((s) => s.destination.type === 'folder' && s.destination.path) - .slice(0, 3) // Show max 3 suggestions + .slice(0, 3) .map((s) => { const path = s.destination.path || '' + const vaultMatch = vaultFolders.find((f) => f.path === path) return { id: path, name: path.split('/').pop() || path || 'Notes', path: path, + icon: vaultMatch?.icon ?? null, aiConfidence: s.confidence, aiReason: s.reason } }) } - // Fallback to first 3 folders if no AI suggestions return vaultFolders.slice(0, 3).map((f) => ({ ...f })) }, [aiSuggestions, vaultFolders]) @@ -166,12 +168,11 @@ export const FilingSection = ({ const displayFolder = selectedFolder ? (suggestedFolders.find((f) => f.id === selectedFolder.id) ?? { ...selectedFolder, + icon: + selectedFolder.icon ?? vaultFolders.find((f) => f.id === selectedFolder.id)?.icon ?? null, aiConfidence: undefined }) : (suggestedFolders[0] ?? null) - const displayConfidence = (displayFolder as SuggestedFolder | null)?.aiConfidence - ? Math.round((displayFolder as SuggestedFolder).aiConfidence! * 100) - : null const displayPath = displayFolder?.path ? displayFolder.path.replace(/\//g, ' / ') : displayFolder?.name || 'Select folder' @@ -226,73 +227,71 @@ export const FilingSection = ({ return (
- {/* File To — Header + Dropdown */} -
-
- - File to - - {isLoadingAISuggestions ? ( -
- -
- ) : hasAISuggestions ? ( -
- - AI -
- ) : null} -
- - {/* Folder Dropdown */} - { - setShowAllFolders(open) - if (!open) setFolderSearch('') - }} - > - - - - + + {/* Folder Dropdown */} + { + setShowAllFolders(open) + if (!open) setFolderSearch('') + }} > - {/* Search */} -
-
+ + + + + {/* Search */} +
setFolderSearch(e.target.value)} onKeyDown={(e) => { @@ -305,147 +304,120 @@ export const FilingSection = ({ autoFocus />
-
- - Use / for subfolders (e.g. Research/AI) - -
-
- - {/* Suggested section */} - {suggestedFolders.length > 0 && !folderSearch.trim() && ( -
-
- + + {/* Suggested */} + {suggestedFolders.length > 0 && !folderSearch.trim() && ( +
+ Suggested -
- {suggestedFolders.map((folder) => { - const confidence = folder.aiConfidence - ? Math.round(folder.aiConfidence * 100) - : null - const isSelected = selectedFolder?.id === folder.id - return ( -
- {confidence && ( - - {confidence}% + + {folder.path ? folder.path.replace(/\//g, ' / ') : 'Notes'} - )} - - ) - })} -
- )} - - {/* All folders section */} -
0 && !folderSearch.trim() && 'border-t border-border/40' - )} - > - {!folderSearch.trim() && ( -
- - All folders - + {isSelected && } + + ) + })}
)} - {canCreateFolder && ( - - )} - {filteredFolders.length === 0 && !canCreateFolder ? ( -

No folders found

- ) : filteredFolders.length === 0 ? null : ( - filteredFolders.map((folder) => { - const isSelected = selectedFolder?.id === folder.id - const parentPath = folder.path?.includes('/') - ? folder.path.split('/').slice(0, -1).join(' / ') - : null - return ( - + )} + {filteredFolders.length === 0 && !canCreateFolder ? ( +

+ No folders found +

+ ) : filteredFolders.length === 0 ? null : ( + filteredFolders.map((folder) => { + const isSelected = selectedFolder?.id === folder.id + return ( + - ) - }) - )} -
-
+ {isSelected && ( + + )} + + ) + }) + )} +
+ +
+
+
- {/* Footer hints */} -
- - ↑↓ navigate · ⏎ select · esc close - -
- - + {/* Tags */} +
- {/* Tags */} - - {/* Link to note */}
@@ -479,11 +451,8 @@ export const FilingSection = ({ : `color-mix(in srgb, var(--tint) ${Math.round(borderOpacity * 100)}%, transparent)` }} > - {linkedNotes.find((ln) => ln.id === suggestion.note.id)?.emoji ? ( - ln.id === suggestion.note.id)!.emoji!} - className="size-3.5 shrink-0" - /> + {suggestion.note.emoji ? ( + ) : ( )} From ccb73d4bd387e25a50c166e741a1e51f1828cbf6 Mon Sep 17 00:00:00 2001 From: Kaan Karaca Date: Sun, 5 Apr 2026 13:26:07 +0300 Subject: [PATCH 3/8] refactor: remove tag usage count from autocomplete suggestions --- .../src/renderer/src/components/filing/tag-autocomplete.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/apps/desktop/src/renderer/src/components/filing/tag-autocomplete.tsx b/apps/desktop/src/renderer/src/components/filing/tag-autocomplete.tsx index 0a80ae383..6534b7063 100644 --- a/apps/desktop/src/renderer/src/components/filing/tag-autocomplete.tsx +++ b/apps/desktop/src/renderer/src/components/filing/tag-autocomplete.tsx @@ -322,11 +322,6 @@ export const TagAutocomplete = ({ > {tag.name} - {tag.count > 0 && ( - - used {tag.count} times - - )} ) })} From 42271006ca29ca8fbe901a24e894a80ad30c9597 Mon Sep 17 00:00:00 2001 From: Kaan Karaca Date: Sun, 5 Apr 2026 13:26:11 +0300 Subject: [PATCH 4/8] refactor: simplify link preview with extracted PreviewImage component Extract image/favicon/fallback rendering into PreviewImage component. Remove unused metadata fields (author, publishedDate, siteName). Flatten conditional rendering for YouTube vs standard previews. --- .../components/inbox-detail/link-preview.tsx | 208 ++++++++---------- 1 file changed, 92 insertions(+), 116 deletions(-) diff --git a/apps/desktop/src/renderer/src/components/inbox-detail/link-preview.tsx b/apps/desktop/src/renderer/src/components/inbox-detail/link-preview.tsx index 573957a56..5a5287abc 100644 --- a/apps/desktop/src/renderer/src/components/inbox-detail/link-preview.tsx +++ b/apps/desktop/src/renderer/src/components/inbox-detail/link-preview.tsx @@ -1,22 +1,43 @@ import { memo, useState } from 'react' -import { ExternalLink, Globe, Play } from '@/lib/icons' -import { extractDomain, formatCompactRelativeTime } from '@/lib/inbox-utils' -import { cn } from '@/lib/utils' +import { Globe, Play } from '@/lib/icons' import { extractYouTubeVideoId } from '@/lib/youtube-utils' import type { InboxItem, InboxItemListItem, LinkMetadata } from '@/types' -const getInitials = (name: string): string => { - const parts = name.replace(/\.[a-z]+$/, '').split(/[\s._-]+/) - if (parts.length >= 2) { - return (parts[0][0] + parts[1][0]).toUpperCase() - } - return name.slice(0, 2).toUpperCase() -} - -const formatPublishedDate = (date: string): string => { - const d = new Date(date) - return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) -} +const PreviewImage = ({ + heroImageUrl, + faviconUrl, + faviconError, + onImgError, + onFaviconError +}: { + heroImageUrl: string | null + faviconUrl: string | null + faviconError: boolean + onImgError: () => void + onFaviconError: () => void +}): React.JSX.Element => + heroImageUrl ? ( + + ) : ( +
+ {faviconUrl && !faviconError ? ( + + ) : ( + + )} +
+ ) interface LinkPreviewProps { item: InboxItem | InboxItemListItem @@ -30,23 +51,33 @@ export const LinkPreview = memo(({ item }: LinkPreviewProps): React.JSX.Element const metadata = 'metadata' in item ? (item.metadata as LinkMetadata | null) : null const raw = metadata as Record | null const heroImageUrl = item.thumbnailUrl || metadata?.heroImage || (raw?.image as string) || null - const domain = item.sourceUrl ? extractDomain(item.sourceUrl) : null - const siteName = metadata?.siteName || (raw?.publisher as string) || domain const faviconUrl = metadata?.favicon || (raw?.logo as string) || null - const author = metadata?.author || null - const publishedDate = - metadata?.publishedDate || (raw?.date as string) - ? formatPublishedDate((metadata?.publishedDate || raw?.date) as string) - : null const excerpt = metadata?.description || metadata?.excerpt || item.content - const capturedAgo = formatCompactRelativeTime(item.createdAt) const youtubeVideoId = item.sourceUrl ? extractYouTubeVideoId(item.sourceUrl) : null const showHeroImage = heroImageUrl && !imgError - return ( -
- {youtubeVideoId && isPlaying ? ( + const previewContent = ( + <> +
+ setImgError(true)} + onFaviconError={() => setFaviconError(true)} + /> +
+

{item.title}

+ {excerpt && ( +

{excerpt}

+ )} + + ) + + if (youtubeVideoId && isPlaying) { + return ( +