Unify image search and add visual search workflows - #2
Conversation
7e7f176 to
4f789e7
Compare
…noss/imagyx into agent/spotlight-image-conversion
… scrolling to keep it from lagging
…noss/imagyx into agent/spotlight-image-conversion
… for preview in results
There was a problem hiding this comment.
Pull request overview
This PR unifies the frontend↔Tauri search contract across the main window and Spotlight, adds visual (image-to-image) search workflows, and introduces a Spotlight image conversion flow with exact totals + progressive pagination to keep large libraries responsive.
Changes:
- Introduces a shared
UnifiedSearchInputand visual-search session/services, including upload/drag-drop/paste and “find similar” actions. - Migrates search transport to a paged
SearchPage { items, total }contract and adds Spotlight pagination caching/loading. - Extends the Rust backend with visual-only ranking, exact total counting, embedding fallback, conversion commands, and derivative linking.
Reviewed changes
Copilot reviewed 67 out of 71 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| tsconfig.json | Enables skipLibCheck. |
| src/types.ts | Adds search/conversion types. |
| src/stores/library.ts | Uses paged search + totals. |
| src/spotlight-window.css | Adds Spotlight dock styling. |
| src/services/visual-search.ts | Visual query workflows (file/blob/indexed). |
| src/services/visual-search-session.ts | Visual session state + tokenization. |
| src/services/vision-worker-client.ts | Shares a single vision worker. |
| src/services/unified-search-engine.ts | Unifies embed timing + visual token handling. |
| src/services/thumbnails.ts | Caps concurrent thumbnail generation. |
| src/services/spotlight-search-pagination.ts | Spotlight paging + cache/TTL. |
| src/services/spotlight-converter-keyboard.ts | Guards Spotlight key handling during converter/actions. |
| src/services/semantic-provider.ts | Adds DEV diagnostics around embeddings/replies. |
| src/services/semantic-channel.ts | Adds DEV diagnostics around request/response. |
| src/services/native-dialog-state.ts | Prevents Spotlight hiding during native dialogs. |
| src/main.ts | Installs unified engine + Spotlight visual re-emit. |
| src/i18n/zh/spotlight.ts | Adds conversion/action strings (zh). |
| src/i18n/zh/search.ts | Adds visual-search strings (zh). |
| src/i18n/ru/spotlight.ts | Adds conversion/action strings (ru). |
| src/i18n/ru/search.ts | Adds visual-search strings (ru). |
| src/i18n/pt/spotlight.ts | Adds conversion/action strings (pt). |
| src/i18n/pt/search.ts | Adds visual-search strings (pt). |
| src/i18n/ja/spotlight.ts | Adds conversion/action strings (ja). |
| src/i18n/ja/search.ts | Adds visual-search strings (ja). |
| src/i18n/it/spotlight.ts | Adds conversion/action strings (it). |
| src/i18n/it/search.ts | Adds visual-search strings (it). |
| src/i18n/fr/spotlight.ts | Adds conversion/action strings (fr). |
| src/i18n/fr/search.ts | Adds visual-search strings (fr). |
| src/i18n/es/spotlight.ts | Adds conversion/action strings (es). |
| src/i18n/es/search.ts | Adds visual-search strings (es). |
| src/i18n/en/spotlight.ts | Adds conversion/action strings (en). |
| src/i18n/en/search.ts | Adds visual-search strings (en). |
| src/i18n/en/common.ts | Normalizes key style/duplicates cleanup. |
| src/i18n/de/spotlight.ts | Adds conversion/action strings (de). |
| src/i18n/de/search.ts | Adds visual-search strings (de). |
| src/i18n/ar/spotlight.ts | Adds conversion/action strings (ar). |
| src/i18n/ar/search.ts | Adds visual-search strings (ar). |
| src/config/spotlight-search.ts | Makes lexical/semantic debounce configurable. |
| src/components/UnifiedSearchInput.vue | New shared search input UI. |
| src/components/ui/MovingBorder/MovingBorder.vue | Simplifies border animation layers. |
| src/components/ui/Button/Button.vue | Adds pressed styling for variants. |
| src/components/Spotlight/useSpotlightResultActions.ts | Refactors Spotlight action feedback logic. |
| src/components/Spotlight/SpotlightSearch.vue | Implements progressive timing + diagnostics + preview. |
| src/components/Spotlight/SpotlightInput.vue | Switches to UnifiedSearchInput + pagination load. |
| src/components/Spotlight/SpotlightConverter.vue | Adds Spotlight conversion UI/flow. |
| src/components/SearchHeader.vue | Switches main header to UnifiedSearchInput. |
| src/components/ImagePreviewDialog.vue | Adds optional keyboard shortcut handling. |
| src/components/ImageGrid.vue | Adds context-menu “find similar” action. |
| src/App.vue | Adopts shared timing + exact totals. |
| src/api/tauri.ts | Adds searchPage, visual/session wiring, conversion APIs. |
| src-tauri/src/vector_store/search.rs | Adds vector search diagnostics. |
| src-tauri/src/vector_store/mod.rs | Adds scoped count/contains helpers. |
| src-tauri/src/models.rs | Adds search mode/exclusion + SearchPage. |
| src-tauri/src/lib.rs | Registers new commands. |
| src-tauri/src/indexer/search.rs | Implements paged search + visual mode + totals. |
| src-tauri/src/indexer/mod.rs | Exposes prepare_asset + new search entrypoint. |
| src-tauri/src/indexer/files.rs | Adds AVIF support + exports prepare_asset. |
| src-tauri/src/database/search.rs | Adds counts for browse/lexical/hybrid. |
| src-tauri/src/database/mod.rs | Adds derivatives module. |
| src-tauri/src/database/migrations.rs | Adds image_derivatives table/index. |
| src-tauri/src/database/embeddings.rs | Adds embedding_vector lookup. |
| src-tauri/src/database/derivatives.rs | Adds derivative linking + tests. |
| src-tauri/src/commands/semantic.rs | Adds visual query prep + embedding fetch. |
| src-tauri/src/commands/search.rs | Adds search_image_page + diagnostics. |
| src-tauri/src/commands/files.rs | Adds conversion pipeline + Windows explorer normalization. |
| src-tauri/Cargo.toml | Enables image AVIF feature. |
| src-tauri/Cargo.lock | Updates lock for AVIF deps. |
| README.md | Adds banner + demo video. |
| .github/workflows/ci.yml | Gates CI behind explicit release marker. |
Comments suppressed due to low confidence (2)
src/components/Spotlight/SpotlightConverter.vue:236
- This component introduces new clickable actions as styled native
<button>elements (e.g. the main conversion command). Repo UI rules require using the sharedcomponents/ui/Button/Button.vuefor clickable actions unless a native control is technically required, and recommend extending shared components instead of styling native buttons (AGENTS.md UI rules).
<section class="spotlight-converter" :aria-label="t('spotlight.convert.title')">
<button
class="spotlight-converter__command"
:class="{ 'spotlight-converter__command--busy': status === 'converting' }"
type="button"
:disabled="status === 'converting'"
@click="startConversion()"
>
src/components/ImagePreviewDialog.vue:74
- The keydown listener removal is conditional on
props.keyboardShortcuts. If the prop were to change from true → false while mounted, the listener would be left attached and never removed on unmount.removeEventListeneris safe to call even if the listener wasn't registered.
onBeforeUnmount(() => {
if (props.keyboardShortcuts) window.removeEventListener('keydown', handleKeydown, true)
})
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| function handleKeydown(event: KeyboardEvent) { | ||
| const isSpace = event.code === 'Space' || event.key === ' ' | ||
| if (isSpace && (spacePressed || event.repeat)) { | ||
| event.preventDefault() | ||
| event.stopImmediatePropagation() | ||
| return | ||
| } | ||
| if (isSpace) spacePressed = true |
| <Button | ||
| v-if="view === 'search'" | ||
| class="unified-search-input__upload" | ||
| variant="ghost" | ||
| size="icon" | ||
| :aria-label="t('search.visual.action')" | ||
| :title="t('search.visual.drop_paste_choose')" | ||
| @click="chooseImage" | ||
| > |
| .unified-search-input__visual-thumb img, | ||
| .unified-search-input__visual-thumb :deep(.thumbnail-loader) { width: 100%; height: 100%; object-fit: cover; } |
| <script setup lang="ts"> | ||
| import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue' | ||
| import { listen, type UnlistenFn } from '@tauri-apps/api/event' | ||
| import { | ||
| ArrowLeft, |
| <button type="button" role="menuitem" :disabled="findingSimilar" @click="findSimilarFromMenu"> | ||
| <span class="image-context-menu__icon"> | ||
| <Sparkles v-if="!findingSimilar" :size="17" /> | ||
| <span v-else class="image-context-menu__spinner" /> | ||
| </span> | ||
| <span class="image-context-menu__copy"> | ||
| <strong>{{ t('search.visual.find_similar') }}</strong> | ||
| <small>{{ t('search.visual.find_similar_desc') }}</small> | ||
| </span> | ||
| </button> |
| await imagyxApi.openInFileManager(cleanPath.value, false) | ||
| } | ||
|
|
||
| onMounted(() => window.addEventListener('keydown', handleKeydown, true)) | ||
| onBeforeUnmount(() => window.removeEventListener('keydown', handleKeydown, true)) | ||
| onMounted(() => { | ||
| if (props.keyboardShortcuts) window.addEventListener('keydown', handleKeydown, true) | ||
| }) |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 673c04175a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (isSpace && selectedImage.value) { | ||
| event.preventDefault() | ||
| previewImage.value = selectedImage.value |
There was a problem hiding this comment.
Ignore text fields before opening the preview
When Spotlight already has a selected result, pressing Space while the search input is focused reaches this window-level capture handler, calls preventDefault(), and opens the preview instead of inserting a space. This prevents users from entering multi-word queries after the first results arrive; return early for input, textarea, select, and contenteditable targets before handling preview keys. docs/UI.mdL179-L186
Useful? React with 👍 / 👎.
| pub async fn prepare_visual_query_image(path: String) -> Result<Response, String> { | ||
| tauri::async_runtime::spawn_blocking(move || { | ||
| let image = image::open(Path::new(&path)).map_err(|error| error.to_string())?; |
There was a problem hiding this comment.
Authorize paths before preparing visual queries
Any caller of this registered Tauri command can supply an arbitrary local path and receive decoded RGB pixels, because the path is passed directly to image::open without canonicalization, file-type validation, or authorization. For external visual searches, the native picker/drop flow should grant access to the selected file or copy it into an application-owned temporary location rather than exposing an unrestricted file-reading command. docs/RUST_CODE_QUALITY.mdL72-L84
Useful? React with 👍 / 👎.
| let offset = requested_offset.min(MAX_SEARCH_WINDOW); | ||
| let window_end = offset.saturating_add(limit).min(MAX_SEARCH_WINDOW); |
There was a problem hiding this comment.
Stop pagination at the capped visual-search window
For a visual-search scope containing more than 50,000 embedded images, total remains the full vector count while every requested offset above 50,000 is clamped back to 50,000. At that offset the candidate set is also capped at 50,000 and then skipped entirely, so the frontend continues to consider more results available while every subsequent load returns an empty page; cap the exposed pageable total or explicitly terminate pagination at this window.
Useful? React with 👍 / 👎.
| role="menu" | ||
| :aria-label="t('search.visual.find_similar')" | ||
| > | ||
| <button type="button" role="menuitem" :disabled="findingSimilar" @click="findSimilarFromMenu"> |
There was a problem hiding this comment.
Replace the raw context-menu button
The new find-similar menu item is an ordinary application action but is implemented as a locally styled native button, bypassing the shared Button states, theming, and future design-system updates. Use components/ui/Button/Button.vue with the required menu role and visual variant instead. docs/UI.mdL57-L59
Useful? React with 👍 / 👎.
| } | ||
| .spotlight-action-menu-button + .spotlight-action-menu-button { margin-top: 2px; } | ||
| .spotlight-action-menu-button--featured { min-height: 48px; margin-bottom: 4px; } | ||
| .spotlight-action-menu-button :deep(.ui-button__content) { |
There was a problem hiding this comment.
Expose a public action-button layout contract
This selector reaches into Button's private .ui-button__content class, coupling the Spotlight action menu to an implementation detail of the shared primitive. A Button refactor can silently break the menu layout; add a public prop, variant, slot, or documented root contract for this alignment instead. docs/UI.mdL85-L89
Useful? React with 👍 / 👎.
| @@ -0,0 +1,612 @@ | |||
| <script setup lang="ts"> | |||
There was a problem hiding this comment.
Split the converter into focused components
This newly added component is 612 lines and combines conversion orchestration, keyboard handling, progress/error state, format selection, result actions, template markup, and extensive styling. Split the format picker, command/progress surface, and reusable runtime behavior so the source remains below the repository's 500-line limit and each unit has a focused responsibility. docs/TS_CODE_QUALITY.mdL19-L25
Useful? React with 👍 / 👎.
Why
Imagyx now has several fast image workflows — text search, Spotlight actions and native conversion — but they should feel like one product rather than separate implementations. This change unifies the search interface and search transport, adds image-to-image search, and keeps large libraries responsive through exact totals and progressive pages.
Unified search experience
UnifiedSearchInputcomponent in the main Imagyx window and SpotlightSearch by image
EscapeFind visually similar images
Ctrl/Cmd + KpopoverCtrl/Cmd + Shift + SPure visual ranking
visualsearch modeSpotlight conversion and actions
Enter, format shortcuts and a safeEscapereturn pathCtrl/Cmd + KProgressive results and diagnostics
Localization
All new visual-search labels and interactions are translated in the 10 supported locales: Arabic, Chinese, English, French, German, Italian, Japanese, Portuguese, Russian and Spanish.
Validation
CI safety
GitHub Actions does not run on pushes or pull requests.
ci.ymlcan only be started manually, and all jobs remain skipped unless the actor is exactlyExtraBinossand the required input is exactly[RELEASE].