Skip to content

Commit e293d63

Browse files
committed
Media index: a delightful "how much to index" slider for image search (M2 FE)
Surfaces the M2 image-ML backend to the user, centered on the importance-threshold slider the feature was asked to "feel nice to use". - **Importance slider** (`MediaIndexImportanceSlider.svelte`) in the "Image search" settings card: five named buckets ("Only my most-used folders" → "Everywhere, even folders I rarely open") over the typed `0.0..=1.0` threshold, so the control reads the SAME importance signal the scheduler enriches by and can't drift. Default is the broadest bucket (threshold `0.0`), deliberately equal to the backend default so the UI and a sparse (unpersisted) store agree — non-regressive vs M1, junk floored out at any level. - **Live, honest preview**: debounced `mediaIndexCoveredCount` over the ENABLED volumes only (local root + opted-in SMB; the backend drops non-opted-in SMB/MTP), rendered "Indexes about 38,900 images across 1,240 folders" with thousands separators + ICU plurals. A `pending` volume is voiced ("still scanning") instead of a confident wrong number, and a drag shows the incremental delta ("Adds about 12,000 images") that folds into the baseline once it settles. - **Persist + live-apply** via the `settings-applier.ts` passthrough → `media_index_set_importance_threshold` (the `mediaIndex.enabled` precedent); a new `mediaIndex.importanceThreshold` sparse setting. - **Honest per-volume progress** from `qualifyingCount`: "12,400 of 38,900 images indexed" (or "Counting images…" while the total is unknown) for the local disk and each network drive. - **Find similar**: a per-tile action in the search grid re-queries via `mediaIndexFindSimilar` from the result's stored path, with a "Similar to <name>" header and a way back. - **A11y + delight**: keyboard-operable buckets, visible focus, AA contrast, `prefers-reduced-motion` respected, and the floor made legible ("junk like node_modules is always skipped"). - Tags need no separate UI: tag labels already fold into the OCR FTS index, so keyword search matches them. Covered by a component test (render, preview branches, progress, slider commit), an extended find-similar test, and the `media-index-slider` E2E (persist + preview). Docs: `media_index/DETAILS.md` § M2 frontend.
1 parent 5c52a47 commit e293d63

17 files changed

Lines changed: 1254 additions & 42 deletions

apps/desktop/src-tauri/src/media_index/CLAUDE.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ Port rationale, network-fetch, GC safety, FFI, schema, M2 depth: [DETAILS.md](DE
5656

5757
## Not yet
5858

59-
M2 frontend (next agent): the importance slider + covered-count preview, find-similar UI, progress/ETA, exclude UI,
60-
tag-search surfacing — backend commands + shapes are ready ([DETAILS.md](DETAILS.md) § M2), and live-apply of the
61-
threshold + exclude settings needs a `settings-applier.ts` entry. Later: the per-FOLDER override FE trigger, MTP
62-
on-demand, and M3+ (CLIP, faces, durable identity store, captions).
59+
The M2 frontend has landed (slider + covered-count preview, per-volume progress, find-similar; [DETAILS.md](DETAILS.md)
60+
§ M2 frontend). Still open: the per-FOLDER exclude + "always index" FE triggers (a native-menu follow-up; setters +
61+
settings ready), MTP on-demand, and M3+ (CLIP, faces, durable identity store, captions).

apps/desktop/src-tauri/src/media_index/DETAILS.md

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -462,11 +462,44 @@ commands (all `spawn_blocking`, offline-capable, registered in BOTH `ipc.rs` + `
462462
`MediaIndexVolumeState { …, qualifying_count: Option<u64> }`. Live-apply for the threshold + exclude settings needs a
463463
`settings-applier.ts` entry (the one FE handoff the backend can't do).
464464

465+
### M2 frontend (the settings slider, progress, and find-similar)
466+
467+
The user-facing M2 surface lives in the Svelte frontend, not here; this section is the map so the two stay in sync.
468+
469+
- **The importance slider**`src/lib/settings/sections/MediaIndexImportanceSlider.svelte`, rendered in the "Image
470+
search" card in `FileSystemWatchingSection.svelte` when `mediaIndex.enabled` is on. It exposes five NAMED BUCKETS
471+
("Only my most-used folders" → "Everywhere, even folders I rarely open") over the typed threshold; each bucket maps to
472+
a fixed threshold stop `[0.8, 0.6, 0.4, 0.2, 0.0]` (left → right, restrictive → broad). Dragging RIGHT indexes MORE (a
473+
LOWER threshold). The **default is the rightmost bucket, threshold `0.0`** — deliberately equal to the backend
474+
`DEFAULT_IMPORTANCE_THRESHOLD`, so the UI and an unpersisted (sparse) store agree without eagerly writing a default,
475+
and it's non-regressive vs M1 (junk is floored out at any level regardless). The persisted value is the raw threshold;
476+
the slider maps it to the nearest bucket on load.
477+
- **Persist + live-apply** follows the `mediaIndex.enabled` precedent, NOT the per-item delta path: the slider calls
478+
`setSetting('mediaIndex.importanceThreshold', threshold)` and the `settings-applier.ts` passthrough pushes it to
479+
`media_index_set_importance_threshold`. (Threshold is a scalar, so it fits the applier's key→value table — unlike the
480+
network/exclude delta setters, which co-locate persist+IPC in a prefs helper.)
481+
- **Live honest preview** — the slider debounces `media_index_covered_count(threshold, enabledVolumeIds)` over the
482+
enabled volumes (local `root` + opted-in SMB; the backend drops non-opted-in SMB / MTP), rendering "Indexes about N
483+
images across M folders" with thousands separators + ICU plurals. `pending` ⇒ a "still scanning" caveat. A drag also
484+
shows the incremental delta vs the last settled level ("Adds about 12,000 images"), which folds into the baseline once
485+
the value settles (~900 ms). No ETA on the slider: the enriched-rate isn't exposed and a fixed per-image cost would be
486+
dishonest across HEIC/RAW/network, so counts stand alone.
487+
- **Honest per-volume progress** reads `qualifying_count` from `media_index_volume_state`: the local disk line lives in
488+
the slider component, the network lines in `MediaIndexNetworkVolumes.svelte`, both showing "N of M images indexed" (or
489+
"Counting images…" while `qualifying_count` is `null`).
490+
- **Find similar**`ImageSearchResults.svelte` grows a per-tile "Find similar images" action that re-queries the grid
491+
via `media_index_find_similar` from that tile's STORED (index-relative) path (NOT the resolved OS path — the command
492+
keys on the stored path), showing a "Similar to <name>" header with a back button. A new query exits similar mode.
493+
- **Tags need no separate UI**: tag labels fold into `media_ocr` (`source='tag'`), so the existing OCR keyword search
494+
already matches tag words and shows them in the snippet. `OcrHit` carries no `source`, so the grid can't label a hit
495+
as "matched a tag" without a backend field — deferred, not needed for M2.
496+
465497
## What's left for later
466498

467-
- **M2 frontend (next agent):** the importance-threshold slider + live covered-count preview, the find-similar UI, the
468-
progress/ETA surface, the per-folder exclude UI, and tag-search surfacing. The backend commands + shapes above are
469-
ready; live-apply of the threshold + exclude settings needs a `settings-applier.ts` entry.
499+
- **Per-folder photo-search exclude UI (FE trigger):** the backend setter (`media_index_set_excluded_folder`) + the
500+
`mediaIndex.excludedFolders` setting are ready, but the natural trigger is a folder right-click action in the native
501+
(Rust) file context menu, so wiring it is a small backend/menu follow-up (same shape as the deferred per-folder
502+
"always index" override). No FE persist helper exists yet.
470503
- **M3+:** CLIP text→image semantic search, the model-install path, faces (detect/embed/cluster/name), the durable
471504
identity store, and LLM captions.
472505

apps/desktop/src/lib/intl/keys.gen.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1814,14 +1814,18 @@ export type MessageKey =
18141814
| 'search.action.showAll.label'
18151815
| 'search.action.showAll.tooltip'
18161816
| 'search.dialog.title'
1817+
| 'search.imageResults.backToResults'
18171818
| 'search.imageResults.count'
18181819
| 'search.imageResults.countCapped'
18191820
| 'search.imageResults.empty'
1821+
| 'search.imageResults.findSimilar'
18201822
| 'search.imageResults.indexing'
18211823
| 'search.imageResults.networkOff'
18221824
| 'search.imageResults.notIndexed'
18231825
| 'search.imageResults.off'
18241826
| 'search.imageResults.paused'
1827+
| 'search.imageResults.similarEmpty'
1828+
| 'search.imageResults.similarTo'
18251829
| 'search.imageResults.title'
18261830
| 'search.notAFolderToast'
18271831
| 'search.recent.runAria'
@@ -2139,19 +2143,36 @@ export type MessageKey =
21392143
| 'settings.mediaIndex.card'
21402144
| 'settings.mediaIndex.enabled.description'
21412145
| 'settings.mediaIndex.enabled.label'
2146+
| 'settings.mediaIndex.importanceThreshold.bucket.everywhere'
2147+
| 'settings.mediaIndex.importanceThreshold.bucket.most'
2148+
| 'settings.mediaIndex.importanceThreshold.bucket.mostUsed'
2149+
| 'settings.mediaIndex.importanceThreshold.bucket.often'
2150+
| 'settings.mediaIndex.importanceThreshold.bucket.sometimes'
2151+
| 'settings.mediaIndex.importanceThreshold.deltaAdd'
2152+
| 'settings.mediaIndex.importanceThreshold.deltaRemove'
2153+
| 'settings.mediaIndex.importanceThreshold.description'
2154+
| 'settings.mediaIndex.importanceThreshold.floor'
2155+
| 'settings.mediaIndex.importanceThreshold.label'
2156+
| 'settings.mediaIndex.importanceThreshold.pending'
2157+
| 'settings.mediaIndex.importanceThreshold.preview'
2158+
| 'settings.mediaIndex.importanceThreshold.previewCounting'
2159+
| 'settings.mediaIndex.importanceThreshold.previewNone'
21422160
| 'settings.mediaIndex.networkVolumes.alwaysAria'
21432161
| 'settings.mediaIndex.networkVolumes.alwaysHelp'
21442162
| 'settings.mediaIndex.networkVolumes.alwaysLabel'
21452163
| 'settings.mediaIndex.networkVolumes.description'
21462164
| 'settings.mediaIndex.networkVolumes.indexed'
2147-
| 'settings.mediaIndex.networkVolumes.indexing'
21482165
| 'settings.mediaIndex.networkVolumes.intro'
21492166
| 'settings.mediaIndex.networkVolumes.label'
21502167
| 'settings.mediaIndex.networkVolumes.none'
21512168
| 'settings.mediaIndex.networkVolumes.notIndexedYet'
21522169
| 'settings.mediaIndex.networkVolumes.optInHelp'
21532170
| 'settings.mediaIndex.networkVolumes.optInLabel'
21542171
| 'settings.mediaIndex.networkVolumes.paused'
2172+
| 'settings.mediaIndex.progress.counting'
2173+
| 'settings.mediaIndex.progress.done'
2174+
| 'settings.mediaIndex.progress.local'
2175+
| 'settings.mediaIndex.progress.ofTotal'
21552176
| 'settings.navigationAndFileOps.card.fileOperations'
21562177
| 'settings.navigationAndFileOps.card.navigation'
21572178
| 'settings.network.card.connection'

apps/desktop/src/lib/intl/messages/en/search.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,25 @@
107107
"@search.imageResults.paused": {
108108
"description": "Shown when a network drive disconnected mid-indexing: what was already indexed stays searchable, and indexing resumes when the drive reconnects."
109109
},
110+
"search.imageResults.findSimilar": "Find similar images",
111+
"@search.imageResults.findSimilar": {
112+
"description": "Label for the action (a button on each image tile) that re-runs the grid to show images that look like this one, using on-device image similarity."
113+
},
114+
"search.imageResults.similarTo": "Similar to {name}",
115+
"@search.imageResults.similarTo": {
116+
"description": "Heading shown above the grid when it's showing images similar to a chosen one, instead of text-match results.",
117+
"placeholders": {
118+
"name": "the file name of the image the results are similar to, shown verbatim (for example beach.jpg)"
119+
}
120+
},
121+
"search.imageResults.backToResults": "Back to search",
122+
"@search.imageResults.backToResults": {
123+
"description": "Button that leaves the \"similar images\" view and returns to the text-match results for the current query."
124+
},
125+
"search.imageResults.similarEmpty": "No similar images found.",
126+
"@search.imageResults.similarEmpty": {
127+
"description": "Shown in the \"similar images\" view when this drive has no images close enough to the chosen one (for example the chosen image isn't indexed yet)."
128+
},
110129
"search.snapshot.defaultLabel": "Search",
111130
"@search.snapshot.defaultLabel": {
112131
"description": "Fallback breadcrumb and tab title for a saved search-results view when there's no query text to name it. Same word as the dialog title."

apps/desktop/src/lib/intl/messages/en/settings.json

Lines changed: 95 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -625,10 +625,6 @@
625625
"@settings.mediaIndex.networkVolumes.paused": {
626626
"description": "Status line for a network drive whose indexing paused because the drive disconnected mid-pass. What''s already indexed stays searchable."
627627
},
628-
"settings.mediaIndex.networkVolumes.indexing": "Indexing photos now",
629-
"@settings.mediaIndex.networkVolumes.indexing": {
630-
"description": "Status line shown while a network drive''s photos are being indexed right now."
631-
},
632628
"settings.mediaIndex.networkVolumes.indexed": "{count, plural, one {{countText} photo indexed} other {{countText} photos indexed}}",
633629
"@settings.mediaIndex.networkVolumes.indexed": {
634630
"description": "Status line showing how many of a drive''s photos are already indexed and searchable.",
@@ -641,6 +637,101 @@
641637
"@settings.mediaIndex.networkVolumes.notIndexedYet": {
642638
"description": "Status line for a drive that''s opted in but hasn''t indexed any photos yet (the pass hasn''t run or found images)."
643639
},
640+
"settings.mediaIndex.importanceThreshold.label": "How much to index",
641+
"@settings.mediaIndex.importanceThreshold.label": {
642+
"description": "Label above the slider that sets how deep image indexing goes — from only the folders you use most, to everywhere. In the \"Image search\" card, Settings > Behavior > File system watching."
643+
},
644+
"settings.mediaIndex.importanceThreshold.description": "Cmdr indexes the folders you use most first. Choose how deep to go.",
645+
"@settings.mediaIndex.importanceThreshold.description": {
646+
"description": "Help text under the \"How much to index\" slider label. Explains that indexing is prioritized by how often you use a folder, and this slider sets the cut-off."
647+
},
648+
"settings.mediaIndex.importanceThreshold.bucket.mostUsed": "Only my most-used folders",
649+
"@settings.mediaIndex.importanceThreshold.bucket.mostUsed": {
650+
"description": "Slider position (most restrictive, far left): index only the folders the user opens most often."
651+
},
652+
"settings.mediaIndex.importanceThreshold.bucket.often": "Folders I use often",
653+
"@settings.mediaIndex.importanceThreshold.bucket.often": {
654+
"description": "Slider position: index folders the user opens often, not only the very most-used ones."
655+
},
656+
"settings.mediaIndex.importanceThreshold.bucket.sometimes": "Folders I open now and then",
657+
"@settings.mediaIndex.importanceThreshold.bucket.sometimes": {
658+
"description": "Slider position (middle): index folders the user opens occasionally."
659+
},
660+
"settings.mediaIndex.importanceThreshold.bucket.most": "Most folders",
661+
"@settings.mediaIndex.importanceThreshold.bucket.most": {
662+
"description": "Slider position: index most folders, leaving out only rarely-touched ones."
663+
},
664+
"settings.mediaIndex.importanceThreshold.bucket.everywhere": "Everywhere, even folders I rarely open",
665+
"@settings.mediaIndex.importanceThreshold.bucket.everywhere": {
666+
"description": "Slider position (broadest, far right, the default): index images everywhere, including folders the user rarely opens."
667+
},
668+
"settings.mediaIndex.importanceThreshold.preview": "{images, plural, one {Indexes about {imagesText} image} other {Indexes about {imagesText} images}} {folders, plural, one {in {foldersText} folder} other {across {foldersText} folders}}",
669+
"@settings.mediaIndex.importanceThreshold.preview": {
670+
"description": "Live count under the slider, showing how much the current level covers across the drives being indexed. Both numbers are approximate and update as the user drags.",
671+
"placeholders": {
672+
"images": { "content": "The number of images that would be indexed, used to choose singular or plural wording." },
673+
"imagesText": { "content": "The same image count, preformatted with thousands separators." },
674+
"folders": { "content": "The number of folders that would be indexed, used to choose singular or plural wording." },
675+
"foldersText": { "content": "The same folder count, preformatted with thousands separators." }
676+
}
677+
},
678+
"settings.mediaIndex.importanceThreshold.previewNone": "Nothing matches at this level yet. Drag right to index more.",
679+
"@settings.mediaIndex.importanceThreshold.previewNone": {
680+
"description": "Shown in place of the count when the current slider level covers no folders (for example, set to the most restrictive level on a Mac with no strongly-used folders yet)."
681+
},
682+
"settings.mediaIndex.importanceThreshold.previewCounting": "Working out how much this covers…",
683+
"@settings.mediaIndex.importanceThreshold.previewCounting": {
684+
"description": "Placeholder shown under the slider while Cmdr is still computing the covered counts (before the first result comes back)."
685+
},
686+
"settings.mediaIndex.importanceThreshold.pending": "Some drives are still scanning, so these numbers will grow.",
687+
"@settings.mediaIndex.importanceThreshold.pending": {
688+
"description": "Caveat appended under the covered-count preview when at least one drive being indexed hasn''t finished scanning, so the shown totals are a lower bound."
689+
},
690+
"settings.mediaIndex.importanceThreshold.deltaAdd": "{images, plural, one {Adds about {imagesText} image} other {Adds about {imagesText} images}}",
691+
"@settings.mediaIndex.importanceThreshold.deltaAdd": {
692+
"description": "Shown while dragging the slider toward more coverage: how many more images this new level would index compared with the saved level.",
693+
"placeholders": {
694+
"images": { "content": "The number of extra images, used to choose singular or plural wording." },
695+
"imagesText": { "content": "The same number, preformatted with thousands separators." }
696+
}
697+
},
698+
"settings.mediaIndex.importanceThreshold.deltaRemove": "{images, plural, one {Leaves out about {imagesText} image} other {Leaves out about {imagesText} images}}",
699+
"@settings.mediaIndex.importanceThreshold.deltaRemove": {
700+
"description": "Shown while dragging the slider toward less coverage: how many images this new level would leave out compared with the saved level.",
701+
"placeholders": {
702+
"images": { "content": "The number of images left out, used to choose singular or plural wording." },
703+
"imagesText": { "content": "The same number, preformatted with thousands separators." }
704+
}
705+
},
706+
"settings.mediaIndex.importanceThreshold.floor": "Junk like node_modules and system caches is always skipped.",
707+
"@settings.mediaIndex.importanceThreshold.floor": {
708+
"description": "Quiet reassurance under the slider that build/cache folders are never indexed at any level. Do not translate \"node_modules\" (a fixed folder name developers recognize)."
709+
},
710+
"settings.mediaIndex.progress.local": "This Mac",
711+
"@settings.mediaIndex.progress.local": {
712+
"description": "Name shown for the built-in local disk in the per-drive image-indexing progress list (as opposed to a named network drive)."
713+
},
714+
"settings.mediaIndex.progress.ofTotal": "{total, plural, one {{enrichedText} of {totalText} image indexed} other {{enrichedText} of {totalText} images indexed}}",
715+
"@settings.mediaIndex.progress.ofTotal": {
716+
"description": "Honest progress line for a drive: how many of its qualifying images are indexed so far.",
717+
"placeholders": {
718+
"total": { "content": "The total number of qualifying images, used to choose singular or plural wording." },
719+
"enrichedText": { "content": "The number indexed so far, preformatted with thousands separators." },
720+
"totalText": { "content": "The total number of qualifying images, preformatted with thousands separators." }
721+
}
722+
},
723+
"settings.mediaIndex.progress.counting": "Counting images…",
724+
"@settings.mediaIndex.progress.counting": {
725+
"description": "Progress line shown for a drive while Cmdr is still counting how many images qualify (the honest total isn''t known yet)."
726+
},
727+
"settings.mediaIndex.progress.done": "{total, plural, one {{totalText} image indexed} other {{totalText} images indexed}}",
728+
"@settings.mediaIndex.progress.done": {
729+
"description": "Progress line for a drive whose images are all indexed (indexed count equals the total).",
730+
"placeholders": {
731+
"total": { "content": "The total number of indexed images, used to choose singular or plural wording." },
732+
"totalText": { "content": "The same number, preformatted with thousands separators." }
733+
}
734+
},
644735
"settings.indexing.askForEachDrive.label": "Ask for each drive",
645736
"@settings.indexing.askForEachDrive.label": {
646737
"description": "Label for the toggle that controls whether Cmdr asks, the first time you open a network share or USB device, if you''d like to index it. In Settings > Behavior > File system watching.",

0 commit comments

Comments
 (0)