Skip to content

Unify image search and add visual search workflows - #2

Merged
ExtraBinoss merged 156 commits into
mainfrom
agent/spotlight-image-conversion
Jul 28, 2026
Merged

ExtraBinoss merged 156 commits into
mainfrom
agent/spotlight-image-conversion

Conversation

@ExtraBinoss

@ExtraBinoss ExtraBinoss commented Jul 27, 2026 •

Copy link
Copy Markdown
Owner

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

  • uses the same UnifiedSearchInput component in the main Imagyx window and Spotlight
  • shares the same search request contract, Tauri API, result totals, pagination behavior and MobileCLIP timing configuration
  • keeps the fast progressive text pipeline: lexical search after 25 ms by default and semantic search after 80 ms of quiet time
  • supports the same folder autocomplete, clear action, visual mode, result badge and keyboard behavior on both surfaces
  • displays the exact number of available results independently from the number currently loaded
  • loads only the first page initially, then appends more results near the bottom of the virtualized view

Search by image

  • adds an image/upload action directly inside the shared search input
  • accepts an image selected from the native file picker
  • accepts native window drag-and-drop and browser/DOM drag-and-drop
  • accepts image paste from the clipboard
  • shows a local reference thumbnail, filename, MobileCLIP preparation state and exact result count in the input
  • keeps external reference images temporary and never adds them to the library
  • restores the previous text/folder search with the back button or Escape
  • protects Spotlight from closing while the native file picker temporarily owns focus
  • ignores stale image inference when a newer image is selected before the previous one finishes
  • re-emits an active visual session after Spotlight reopens, so the retained reference always reloads its results instead of appearing alone

Find visually similar images

  • adds Find visually similar images as the first action in Spotlight’s Ctrl/Cmd + K popover
  • adds the direct shortcut Ctrl/Cmd + Shift + S
  • uses the currently selected image thumbnail as the action icon
  • removes the unnecessary AI badge and decorative AI wording from the action
  • adds the same action to the main image grid’s right-click menu
  • reuses the selected image’s existing embedding instead of running MobileCLIP again
  • reads from the in-memory vector store and falls back to the persisted SQLite embedding during startup
  • excludes the source image from its own result set

Pure visual ranking

  • adds a dedicated native visual search mode
  • ranks results only by cosine similarity; filename and FTS scores are not mixed into image-to-image search
  • returns exact totals for embedded images, including folder scope and source exclusion
  • supports offset/limit pagination with semantic scores attached to every result
  • reuses one module-level MobileCLIP vision worker for indexing and visual queries inside each webview, avoiding duplicate model instances within a surface

Spotlight conversion and actions

  • converts locally between AVIF, WEBP, PNG, JPG and ICO
  • writes converted files beside the source, indexes them immediately and persists the derivative relationship
  • clones the source embedding when available, avoiding a second AI pass
  • keeps conversion keyboard-first with arrows, Enter, format shortcuts and a safe Escape return path
  • replaces repeated row buttons with a single full-width blurred action surface inspired by Raycast
  • applies the dock shadow across the complete results width instead of wrapping the controls in another card
  • keeps Copy and More actions adjacent, transparent and blended into the same backdrop blur
  • keeps Copy visible and exposes similar search, conversion, reveal and open actions through Ctrl/Cmd + K
  • guards converter, visual-search and action-popover keyboard events before Spotlight’s global handlers

Progressive results and diagnostics

  • returns exact folder, lexical, hybrid and visual totals
  • keeps the first result page at 60 items for text/visual search
  • requests deeper pages only when the user scrolls near the end
  • preserves virtualized rendering in both the Spotlight list and main image grid
  • records correlated development diagnostics across Vue, Tauri, SQLite and the vector store
  • logs debounce time, IPC time, count time, candidate windows, offsets, vector scans and appended pages
  • disables diagnostic work outside development builds

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

  • reviewed the shared Vue input, visual session lifecycle, pagination state and Tauri request mapping statically
  • reviewed the pure visual ranking, exact total calculation, source exclusion and persisted-embedding fallback in Rust
  • verified that all 10 locale files expose the same visual-search keys
  • passed a targeted TypeScript syntax check for the updated Spotlight action component
  • verified that the PR remains open, draft and mergeable
  • a local repository clone was unavailable because outbound network resolution is disabled in the execution environment
  • Rust and the complete TypeScript project were therefore not compiled in this final pass
  • no GitHub Actions workflow was started

CI safety

GitHub Actions does not run on pushes or pull requests. ci.yml can only be started manually, and all jobs remain skipped unless the actor is exactly ExtraBinoss and the required input is exactly [RELEASE].

@ExtraBinoss
ExtraBinoss force-pushed the agent/spotlight-image-conversion branch 2 times, most recently from 7e7f176 to 4f789e7 Compare July 27, 2026 23:19
@ExtraBinoss ExtraBinoss changed the title Add native Spotlight image conversion Unify image search and add visual search workflows Jul 28, 2026
@ExtraBinoss
ExtraBinoss marked this pull request as ready for review July 28, 2026 14:42
Copilot AI review requested due to automatic review settings July 28, 2026 14:42
@ExtraBinoss
ExtraBinoss merged commit 51eb5d2 into main Jul 28, 2026
@ExtraBinoss
ExtraBinoss deleted the agent/spotlight-image-conversion branch July 28, 2026 14:42

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 UnifiedSearchInput and 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 shared components/ui/Button/Button.vue for 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. removeEventListener is 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.

Comment on lines 712 to +719
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
Comment on lines +308 to +316
<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"
>
Comment on lines +434 to +435
.unified-search-input__visual-thumb img,
.unified-search-input__visual-thumb :deep(.thumbnail-loader) { width: 100%; height: 100%; object-fit: cover; }
Comment on lines +1 to +5
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { listen, type UnlistenFn } from '@tauri-apps/api/event'
import {
ArrowLeft,
Comment on lines +396 to +405
<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>
Comment on lines 66 to +71
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)
})

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +756 to +758
if (isSpace && selectedImage.value) {
event.preventDefault()
previewImage.value = selectedImage.value

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +174 to +176
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())?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +39 to +40
let offset = requested_offset.min(MAX_SEARCH_WINDOW);
let window_end = offset.saturating_add(limit).min(MAX_SEARCH_WINDOW);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants