Skip to content

Ship vista v0.1: screenshot search for macOS - #1

Merged
GordonBeeming merged 16 commits into
mainfrom
gb/phase-1-skeleton-indexer
Apr 22, 2026
Merged

GordonBeeming merged 16 commits into
mainfrom
gb/phase-1-skeleton-indexer

Conversation

@GordonBeeming

Copy link
Copy Markdown
Owner

Summary

  • Full macOS-only OCR screenshot-search app: indexes watched folders with Vision OCR, persists to SQLite + FTS5, served from a nonactivating floating NSPanel summoned by a user-settable global hotkey (default ⌘⇧S).
  • Tabbed preferences (General / Behaviour / Folders / Search / Appearance / Shortcuts / Permissions) covering every Raycast-equivalent knob plus vista's own Panel Size and Preview Size sliders and chord rebinding via a live KeyRecorder.
  • CI pipeline lifted from insomnia, trimmed to GUI-only: push/PR runs build+test on macos-14; published release signs with Developer ID, notarises + staples, builds a DMG, uploads it, and pushes an updated cask to gordonbeeming/homebrew-tap via a dedicated deploy key.

What's in it

  • VistaCore: ScreenshotStore (SQLite + FTS5 with triggers for sync), OCRRecognizer (Vision wrapper, off/fast/accurate), FSEventsWatcher, Indexer (actor-driven incremental scan with mtime+size fingerprint + storage-duration pruner), QueryParser (name: / text: / date: prefixes, natural dates), ThumbnailCache (ImageIO, 256/512/1024 bucketed, disk-backed).
  • Vista app: MenuBarExtra shell, FloatingPanel (borderless HUD NSPanel), SearchViewModel, PanelContentView (adaptive grid, window-scoped NSEvent monitor for true arrow-key grid navigation), ActionHandlers (Open / Copy to Clipboard / Show in Finder / Copy OCR / Pin / Trash), HotKeyManager (Carbon RegisterEventHotKey, zero deps), PreferencesActivation (agent ↔ regular activation flip so prefs window reliably takes focus), AboutView.
  • Preferences: @Observable store, UserDefaults-backed scalars + security-scoped bookmarks for watched folders in Application Support/watched_folders.json, change stream consumed by AppState and dispatched to Indexer / HotKeyManager / FloatingPanel.
  • Bundled AppIcon.icns (from vista.png, full size ladder 16→1024 at 1x/2x).
  • Build badge: VistaGitCommit + VistaReleaseTag baked into Info.plist at build time, rendered as a clickable v0.1 · abc1234 chip in the panel footer + About window linking to the release / commit on GitHub.
  • .claude/skills/create-release for cutting future releases through the same flow insomnia uses.

Test plan

  • swift build --build-tests passes
  • swift test --parallel — 35 tests green (QueryParser 20, ScreenshotStore 12, OCRRecognizer 2, plus the trivial fixtures one)
  • Scripts/build-release.sh 0.1 1 produces a launchable Distribution/Vista.app bundle
  • Running open Distribution/Vista.app: menu bar icon appears, Desktop/iCloud indexing runs, ⌘⇧S summons the panel, typing filters results, arrow keys move through the grid two-dimensionally, Enter runs the configured primary action
  • Preferences window opens in front of other apps, every tab persists its value across relaunch, the folder list add/remove round-trips through security-scoped bookmarks
  • About window opens in front, shows version + commit, badge link opens GitHub
  • Cut v0.1 release: build-and-test + release jobs both green, DMG uploaded as release asset, Casks/vista.rb appears on the tap
  • brew install --cask gordonbeeming/tap/vista on a clean machine installs without Gatekeeper warnings

Notes

  • LSUIElement=YES with activation-policy flipping (.accessory ↔ .regular) on Preferences / About — same pattern Rectangle / 1Password mini-window use. Brief Dock icon during those windows is expected.
  • App sandbox is off — FSEvents on arbitrary user folders is painful inside the sandbox and vista distributes through Homebrew, not the Mac App Store. Hardened runtime is on; notarisation still passes.
  • arm64-only binary for now. Intel support would want a universal build (--arch arm64 --arch x86_64 + lipo) — can add if anyone asks.

GordonBeeming and others added 14 commits April 22, 2026 15:37
Sets up the Swift Package layout (VistaCore library + Vista menu-bar app)
and lands the end-to-end pipeline from FSEvents → Vision OCR → SQLite
FTS5, plus the query parser that feeds the search UI in Phase 2.

VistaCore:
- ScreenshotStore: sqlite3 with external-content FTS5 and triggers that
  keep the fts table in sync with inserts/updates/deletes on the base table.
- OCRRecognizer: thin wrapper over VNRecognizeTextRequest supporting
  off/fast/accurate and optional language hints.
- FSEventsWatcher: CFRunLoop-free async stream of create/modify/rename/
  delete events per watched folder.
- Indexer: actor that does the initial scan, handles FS events, and dedupes
  unchanged files via an (mtime, size) fingerprint.
- QueryParser: name:/text:/date: prefixes plus free-text, with natural
  dates (today, yesterday, last week, march, 2024-12-25).

Vista:
- MenuBarExtra with indexing status, Pause, Rescan Now, Quit. The
  floating panel, preferences, and global hotkey come in Phase 2+.
- Info.plist (LSUIElement = menu bar only, usage descriptions for
  Desktop/Documents/Downloads folder access, NSAppleEventsUsageDescription
  for the future "Paste to front app" action).
- Entitlements: hardened runtime on, app-sandbox off (FSEvents on
  arbitrary folders is painful inside the sandbox; notarisation still
  passes).

Tests: 35 passing. QueryParser, ScreenshotStore (schema + FTS + pinning
+ fingerprint), and OCRRecognizer with a programmatically-rendered fixture.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: GitButler <gitbutler@gitbutler.com>
Lands the interactive surface. ⌘⇧S summons a HUD-style NSPanel that
hosts a SwiftUI grid of thumbnails with the search bar. Typing in the
bar debounces through the query parser into ScreenshotStore.search;
arrows move selection, Enter runs the primary action, Esc dismisses.

Vista target:
- FloatingPanel: NSPanel subclass — nonactivating, HUD level, sized to
  60% of the active screen and re-centered on show. Panel-size
  preference comes in Phase 3.
- HotKeyManager: Carbon RegisterEventHotKey wrapper. Default ⌘⇧S.
  Zero deps — the surface we need is small enough that vendoring is
  cheaper than pulling a package. Rebinding UI lands in Phase 3.
- PanelController: AppKit glue wiring NSHostingView → FloatingPanel →
  VistaCore. Exposes toggle() for the hotkey to bind to.
- SearchViewModel: @observable. Owns queryText, debounces 120ms, runs
  QueryParser → store.search, tracks selectedIndex.
- PanelContentView: SwiftUI. Search field, Images source dropdown stub,
  adaptive LazyVGrid of thumbnails, empty-state copy, footer with
  "Copy to Clipboard ⏎" and "Actions ⌘K" labels. Handles arrow-key
  navigation, Enter, ⌘P pin, ⌘⇧C copy OCR, Esc dismiss.
- ActionHandlers: Open / Copy to Clipboard / Show in Finder / Copy Path
  / Copy OCR Text / Pin / Move to Trash. Paste-to-front-app stubs to
  Copy for now — full AppleScript glue lands with permissions in Phase 3.
- MenuBarContentView: adds "Search Screenshots…" entry and the default
  ⌘⇧S shortcut on the menu item itself as a backup to the global
  hotkey.

VistaCore:
- ThumbnailCache: ImageIO-driven rendering at 256 / 512 / 1024 px, PNG
  on disk under ~/Library/Caches/com.gordonbeeming.vista/thumbs/ with
  a sha1(path)-mtime-size key. In-process NSCache in front of it. Purges
  all sizes for a given source when asked.

Tests: 35 still passing (no existing test broke; new UI surface is
exercised via manual e2e in the Phase 2 verify step).

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: GitButler <gitbutler@gitbutler.com>
Panel was showing a dark strip at the top — the `.titled` style mask
bakes in a titlebar area that the SwiftUI material + rounded-corner
clip can't fully cover. Switched to `.borderless`, dropped
titlebarAppearsTransparent / titleVisibility (no longer applicable),
and made the window non-opaque so the corner clip defines the visible
shape.

Preferences was disabled on purpose because the real prefs UI is Phase 3,
but that's the wrong default — users hit the menu item and nothing
happens. Added a Settings scene with a placeholder SettingsView so the
menu item can be a SettingsLink, ⌘, works, and there's a real target
to flesh out when Phase 3 lands.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: GitButler <gitbutler@gitbutler.com>
Replaces the "coming soon" placeholder with the real tabbed settings
window and wires every control through to the running components.

Preferences model:
- Preferences: @observable store. Persists scalars to UserDefaults; the
  watched-folder list is security-scoped bookmarks serialised to
  Application Support/watched_folders.json so hardened-runtime builds
  can re-open user-granted folders across launches.
- Emits Change events on an AsyncStream — AppState listens and
  dispatches each change to the right component instead of recomputing
  everything on every tweak.

Settings window (TabView):
- General: launch at login (SMAppService), pause indexing.
- Behaviour: Primary Action dropdown (Copy / Paste to Front / Open /
  Show in Finder / Copy OCR).
- Folders: system screenshot folder is auto-detected (reads
  com.apple.screencapture location) and shown as a locked row; any
  number of user folders can be added via NSOpenPanel with full
  add / remove / show-in-Finder controls. Include-all-media toggle.
- Search: OCR accuracy (Off / Fast / Accurate), Storage Duration
  (Unlimited / 1w / 1mo / 3mo / 6mo / 1y — prunes unpinned rows only,
  never touches image files).
- Appearance: panel size slider 30-100% with Compact/Comfortable/
  Large/Full presets.
- Shortcuts: KeyRecorderView for the invoke hotkey. Press any
  modifier+key chord, ⎋ cancels, ⌫ clears. Uses UCKeyTranslate so
  non-QWERTY layouts get the right labels.
- Permissions: deep-links into the relevant System Settings panes +
  shows the current indexed count.

VistaCore additions driven by the new prefs:
- Indexer.setOCRRecognizer: swap recogniser at runtime.
- Indexer.prune(olderThan:): storage-duration pruner, called at bootstrap
  and whenever the duration preference changes.
- ScreenshotStore.deleteUnpinned(olderThan:): pinned rows stay regardless.

Crash fix along the way:
- FSEventsWatcher was casting the callback's path payload as CFArray
  without kFSEventStreamCreateFlagUseCFTypes set, so the underlying
  const char*const* got interpreted as objc pointers and crashed the
  moment FSEvents fired. Added the flag.

LSUIElement=NO temporarily so the Settings scene actually appears as a
standard window; kept the menu bar-only experience for the main app by
other means. (Reverting once we verify the Settings flow — low-risk
follow-up.)

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: GitButler <gitbutler@gitbutler.com>
Everything needed for `brew install --cask gordonbeeming/tap/vista` to
work once a GitHub release is cut, lifted from the proven insomnia setup
and trimmed to vista's simpler shape (GUI-only, no CLI archive).

Scripts:
- build-release.sh: swift build -c release, assembles Distribution/
  Vista.app with a dynamically-generated Info.plist (bundle id,
  version, usage descriptions, LSUIElement=YES). Local smoke test:
  bundle launches, menu bar icon appears, indexing runs.
- package-for-homebrew.sh: wraps the .app in a UDZO DMG, computes
  the SHA, and prints the exact cask stanza for local verification.

.github/workflows/build.yml:
- Every push + PR: swift build + swift test on macos-14.
- Published release: import Developer ID cert into a temp keychain,
  codesign --options runtime --entitlements, submit to notarytool
  --wait, staple the ticket, create the DMG, upload as a release
  asset, then SSH into gordonbeeming/homebrew-tap and commit a fresh
  Casks/vista.rb with the new version + SHA256. Uses the same
  HOMEBREW_TAP_DEPLOY_KEY / APPLE_* secret names as insomnia so the
  existing secrets carry over.

App icon punted to a follow-up — the bundle ships fine without one
and we can drop an AppIcon.icns into Resources/ later; build-release.sh
already picks it up conditionally.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: GitButler <gitbutler@gitbutler.com>
Folders tab: the system default screenshot folder was rendered as a
locked System row with no way to opt out. If macOS's screencapture
location defaults to ~/Desktop but the user takes all their screenshots
via Cmd+Shift+5 → Save to iCloud, watching Desktop is pure noise.

Now the default folder is a checkbox — on by default (matches Raycast)
but clickable off. The state persists to UserDefaults and flows through
Preferences.changes into the Indexer like every other folder change.

KeyRecorder: widened the acceptance criteria.
- Listens to .flagsChanged as well as .keyDown so users holding a Hyper
  mapping see their modifiers echoed live in the button (⌃⌥⇧⌘…) —
  without that feedback, there's no way to tell if the chord is even
  reaching the app.
- Masks modifier flags through deviceIndependentFlagsMask so a stuck
  Caps Lock doesn't pollute the captured chord.
- Rejects pure-modifier keyDowns (some rewriters emit them) and waits
  for the real trigger key.
- Labels: F13-F20 now render correctly.

Added a Shortcuts-tab hint that if a chord doesn't register, another
tool (Karabiner/BTT/Raycast) has probably claimed it at a higher level —
NSEvent monitors can't intercept events that are consumed by a
higher-priority event tap upstream.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: GitButler <gitbutler@gitbutler.com>
Panel size controls how much of the screen the window takes, but at
100% the adaptive grid was still capping column width at 360 pt — so
users got lots of tiny thumbnails on a huge panel instead of the
obvious "bigger window → bigger previews" outcome.

Added a separate Preview Size preference (160-720 pt, default 280,
Small / Medium / Large / XL presets) that drives:
- the grid's per-column minimum (columns still flex up to 1.6× so the
  final row fills the panel cleanly)
- the ThumbnailCache size bucket we load from (>380 pt pulls the 1024-px
  cache entry so XL previews aren't upscaled from 512 px)

Appearance tab is now two sections — Panel Size + Preview Size — each
with a slider and four presets.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: GitButler <gitbutler@gitbutler.com>
Preferences window sometimes opened behind whichever app was frontmost
when the menu was clicked, because agent apps (LSUIElement=YES) don't
auto-activate the way regular apps do. Replaced SettingsLink with a
Button that calls NSApp.activate(ignoringOtherApps: true) before
invoking the @Environment(.openSettings) closure, and added a
defence-in-depth onAppear in SettingsView that re-raises the hosting
window via orderFrontRegardless in case activation races with the
initial window creation.

Empty-state hint was hard-coded to "Drop a screenshot onto your
Desktop" which was wrong for anyone who'd disabled the default folder
toggle. Now it reads the current watch list and names a relevant
folder — a user-added folder if any, otherwise the system default if
still enabled, otherwise prompts the user to open Preferences →
Folders to add one.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: GitButler <gitbutler@gitbutler.com>
The Settings scene is tied to SwiftUI's built-in preferences machinery,
which assumes a regular foreground app. For an agent app (LSUIElement
=YES), openSettings() creates the window but macOS refuses to bring
it to front — activate + find-and-raise from onAppear wasn't enough
because the window SwiftUI created didn't always match our search
criteria, and sometimes didn't surface at all.

Switched to a plain Window scene with an explicit identifier
("vista.preferences"). Opening it is: activate the app, openWindow(id:),
and on a 50 ms delay run orderFrontRegardless + makeKeyAndOrderFront on
the window with that identifier — two-pass so we catch both the case
where the window already exists (first pass raises) and the case where
SwiftUI is still creating it (second pass raises after creation).
SettingsView.onAppear does the same lookup by identifier as a final
safety net.

Net effect: the preferences window comes to the front every time, the
⌘, shortcut from the menu bar still works, and we don't need to toggle
activation policy (no Dock icon flicker).

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: GitButler <gitbutler@gitbutler.com>
Two problems from the Settings→Window swap:

1. Window showed but wasn't key.
   setActivationPolicy(.regular) isn't fully synchronous — the Dock
   server acknowledges on a later runloop tick, and activating before
   that acknowledgement arrives is a silent no-op. Result: the window
   appeared, the user clicked it, but keystrokes (including ⌘Q!) still
   flowed to whatever app was frontmost before.
   Fix: stagger willOpen → openWindow → activate+makeKeyAndOrderFront
   across runloop ticks (0 ms, 0 ms async, 50 ms async). Each step's
   async consequences settle before the next runs.

2. Tabs collapsed into a "Navigation Tab Bar" popover in the toolbar.
   SwiftUI's TabView renders the classic macOS preferences-style top
   tabs only inside a Settings scene. Inside a Window scene it falls
   back to a navigation popover on macOS 14+.
   Fix: roll the tab bar by hand. PrefTab enum owns the identity, label,
   and SF Symbol for each tab; SettingsView draws an HStack of
   icon+label buttons with accent-tinted selection state and a switch
   for the selected content. Same visual weight as the built-in
   preferences tabs, entirely under our control.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: GitButler <gitbutler@gitbutler.com>
Arrow keys were behaving like a 1-D list (up/down = prev/next), and
Left/Right did nothing because the focused TextField consumed them
for cursor movement before SwiftUI's .onKeyPress saw anything.

Two changes:

1. Dropped the .onKeyPress handler and installed an NSEvent local
   monitor for .keyDown while the panel is on screen. Local monitors
   run before any responder, so the TextField no longer swallows
   Left/Right. The TextField still types normally because we return
   the event unchanged whenever we don't want to handle it.

2. Made the grid know its column count. A GeometryReader wraps the
   LazyVGrid and recomputes `columnCount` whenever width or the
   preview-size preference changes, using the same formula
   LazyVGrid.adaptive uses internally:
     N = floor((usableWidth + spacing) / (minCol + spacing))
   Left/Right move by ±1; Up/Down move by ±columnCount for true
   same-column row jumps. moveSelection clamps so the edges are safe.

⌘P and ⌘⇧C also routed through the monitor (pin toggle and copy OCR
text), replacing the old .onKeyPress path.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: GitButler <gitbutler@gitbutler.com>
Dropped Resources/AppIcon.icns (generated from vista.png via sips +
iconutil, every standard size from 16 to 1024 at 1x and 2x). Info.plist
now references it via CFBundleIconFile; the build-release.sh conditional
copy already knew how to thread it into the .app bundle, so signing and
notarisation happen on the iconed bundle going forward.

Added an About window (WindowID.about) with:
- Large app-icon preview pulled from the bundle (NSImage named "AppIcon")
- Version string from CFBundleShortVersionString (falls back to "dev"
  for unbundled swift-run builds)
- Clickable rows for source, issues, author, blog — one line of SF
  Symbol + label + muted URL, opens in the default browser
- MIT license + copyright footer

"About Vista" menu bar entry opens it via the same activation-policy
flip + find-and-raise pattern as Preferences, so the window comes to
front reliably in the agent-app activation state.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: GitButler <gitbutler@gitbutler.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: GitButler <gitbutler@gitbutler.com>
New in the running app you can see which source produced it. Clicks
on the badge jump to the release notes for that build.

Info.plist gets two new keys written at bundle time:
- VistaGitCommit — short SHA (falls back to `git rev-parse` for local
  build-release.sh runs; CI passes ${GITHUB_SHA:0:7}).
- VistaReleaseTag — release tag when built from a tag (CI passes
  ${GITHUB_REF_NAME}); empty string for dev builds.

BuildInfo reads both + CFBundleShortVersionString and exposes a footer
badge string ("v0.1 · abc1234") and a link URL that prefers
/releases/tag/<tag> when a tag is present and falls back to
/commit/<sha>, finally to the repo root for unbundled `swift run` builds.

Panel footer + About window each render the badge as a plain button
with a small "open in new window" glyph — opens the chosen URL in the
default browser.

Also copied the create-release skill pattern from insomnia into
.claude/skills/create-release, adapted for vista (repo name, version
scheme, brew cask name). Invokes via "create a release" / "cut a
release" etc.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: GitButler <gitbutler@gitbutler.com>
@GordonBeeming
GordonBeeming marked this pull request as ready for review April 22, 2026 10:52
Copilot AI review requested due to automatic review settings April 22, 2026 10:52
The CI runner (macos-14) ships Swift 5.10; my dev machine is Swift 6.3.
5.10 doesn't infer @mainactor for SwiftUI View structs the way 6.x does,
so every view that touches Preferences / AppState / ActionHandlers (all
@mainactor) failed to build on CI with "main actor-isolated property
cannot be referenced from a non-isolated context" and "call to main
actor-isolated initializer in a synchronous nonisolated context".

Explicit @mainactor on:
- VistaApp (for `@State private var appState = AppState()`)
- SettingsView + all seven private tab structs (GeneralTab,
  BehaviourTab, FoldersTab, SearchTab, AppearanceTab, ShortcutsTab,
  PermissionsTab) — they all bind or read Preferences
- MenuBarContentView (touches AppState + NSApp window management)
- PanelContentView + ResultCell (touch SearchViewModel / ActionHandlers
  / Preferences)
- AboutView (opens windows via NSApp)

Swift 6.x keeps inferring the same actor, so the annotations are no-ops
there; 5.10 now has what it needs.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: GitButler <gitbutler@gitbutler.com>

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 introduces the initial macOS screenshot-search app (“vista”): a SwiftUI menu-bar app backed by a new VistaCore library that watches folders, OCRs images via Vision, indexes metadata + OCR text into SQLite/FTS5, and provides a query parser + thumbnail caching. It also adds CI/release automation for signing/notarization/DMG packaging and Homebrew tap updates.

Changes:

  • Add VistaCore (SQLite/FTS5 ScreenshotStore, Vision OCRRecognizer, Indexer, FSEventsWatcher, QueryParser, ThumbnailCache, shared models/paths).
  • Add Vista app UI + plumbing (floating panel, panel content/grid navigation, preferences + key recorder, global hotkey, action handlers, about window, app state).
  • Add tests, scripts, CI workflow, packaging docs, and licensing/docs updates.

Reviewed changes

Copilot reviewed 36 out of 39 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
Tests/VistaCoreTests/ScreenshotStoreTests.swift New unit tests for store schema/CRUD/search/pinning/fingerprint.
Tests/VistaCoreTests/QueryParserTests.swift New unit tests for query parsing and date parsing edge cases.
Tests/VistaCoreTests/OCRRecognizerTests.swift New OCR smoke tests using runtime-rendered text fixtures.
Tests/VistaCoreTests/Fixtures/.gitkeep Keeps fixtures directory in repo for test resources.
Sources/VistaCore/VistaPaths.swift Centralizes standard filesystem locations and screenshot folder resolution.
Sources/VistaCore/ThumbnailCache.swift Adds disk + in-memory thumbnail caching.
Sources/VistaCore/ScreenshotStore.swift Adds SQLite-backed index with FTS5 search and pinning/pruning APIs.
Sources/VistaCore/QueryParser.swift Parses user search text into structured query terms + date ranges.
Sources/VistaCore/OCRRecognizer.swift Wraps Vision OCR behind a small async API.
Sources/VistaCore/Models.swift Adds shared value types (ScreenshotRecord, Query).
Sources/VistaCore/Indexer.swift Adds actor that scans/watches folders and indexes OCR results into the store.
Sources/VistaCore/FSEventsWatcher.swift Async wrapper around FSEvents for file change streaming.
Sources/Vista/VistaApp.swift App entry point: menubar extra plus Preferences/About window scenes.
Sources/Vista/Vista.entitlements Defines hardened runtime entitlements (sandbox explicitly off, Apple events on).
Sources/Vista/SettingsView.swift Full tabbed preferences UI (folders/search/appearance/hotkeys/permissions, etc.).
Sources/Vista/SearchViewModel.swift Debounced query runner + selection model for the panel UI.
Sources/Vista/PreferencesActivation.swift Activation-policy flip helper for preferences/about windows in LSUIElement app.
Sources/Vista/Preferences.swift Persistent preferences store (UserDefaults + bookmark JSON) + change stream.
Sources/Vista/PanelController.swift Manages the floating panel lifecycle and embeds SwiftUI content.
Sources/Vista/PanelContentView.swift Implements the search UI, thumbnail grid, and keyboard navigation/shortcuts.
Sources/Vista/MenuBarContentView.swift Menubar dropdown UI and window-opening logic.
Sources/Vista/LaunchAtLogin.swift Wrapper over SMAppService for login item registration.
Sources/Vista/KeyRecorderView.swift UI control to capture and persist a key chord.
Sources/Vista/Info.plist App bundle metadata + usage descriptions + displayed license string.
Sources/Vista/HotKeyManager.swift Carbon global hotkey registration and dispatch.
Sources/Vista/FloatingPanel.swift Nonactivating HUD-style panel window implementation.
Sources/Vista/BuildInfo.swift Reads version/commit/tag metadata for UI footer/about window linking.
Sources/Vista/AppState.swift Central coordinator wiring preferences ↔ indexer/store/panel/hotkey.
Sources/Vista/ActionHandlers.swift Implements open/copy/show/copy OCR/pin/trash actions.
Sources/Vista/AboutView.swift About window UI including version badge and project links.
Scripts/package-for-homebrew.sh Local helper to package DMG and print a Homebrew cask snippet.
Scripts/build-release.sh Builds a release .app bundle and generates shipping Info.plist.
README.md Updates docs: description, install/build instructions, license section.
Package.swift Defines two-target SPM layout (VistaCore library + Vista executable) + tests.
LICENSE Adds the repository license text (FSL-1.1-MIT).
.gitignore Adds common build/artifact/cert ignores.
.github/workflows/build.yml Adds CI build/test and release pipeline (sign/notarize/dmg/tap update).
.claude/skills/create-release/SKILL.md Adds release-cutting workflow instructions for automation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread Sources/VistaCore/QueryParser.swift Outdated
Comment thread Sources/VistaCore/QueryParser.swift
Comment thread Sources/VistaCore/OCRRecognizer.swift
Comment thread Sources/VistaCore/ThumbnailCache.swift Outdated
Comment thread README.md Outdated
Comment thread Tests/VistaCoreTests/OCRRecognizerTests.swift
Comment thread Sources/VistaCore/ThumbnailCache.swift Outdated
Comment thread Sources/VistaCore/Indexer.swift Outdated
Comment thread Sources/Vista/SettingsView.swift
Comment thread Sources/VistaCore/FSEventsWatcher.swift
Address 12 review comments:

- QueryParser / Models / ScreenshotStore: switch date ranges from
  ClosedRange<Date> to half-open Range<Date> and compile with
  `>= AND <` in SQL so sub-second captured_at values at day/week/month
  boundaries aren't excluded by an inclusive upper bound that stops a
  whole second early.
- OCRRecognizer: drop unused `import AppKit` (unused-import warning).
- ThumbnailCache: drop unused `import UniformTypeIdentifiers`.
- ThumbnailCache: include source file size in the cache key so files
  that change inside the same whole second still invalidate the cached
  thumbnail. Higher-precision mtimes aren't reliable across APFS copy/
  move, so size is the cheapest stable tiebreaker.
- PanelContentView: remove unused `RelativeDateTimeFormatter` allocation
  in ResultCell.caption — `timeAgoStyle()` handled the formatting.
- OCRRecognizerTests: add `import CoreText` so CTLineCreateWithAttributedString
  resolves on a clean build.
- Indexer: replace O(n) pendingPaths.removeFirst() with an index cursor
  (pendingIndex) so bursty FS events don't quadratic. Compacts the
  array every 256 items to cap memory.
- SettingsView / AppState: replace blind `togglePause()` Toggle setter
  with `setPaused(newValue)` so programmatic state updates and SwiftUI
  replays stay in sync with the indexer.
- FSEventsWatcher: stop finishing the events continuation in stop().
  Indexer.updateWatchedFolders does stop/start to swap paths; finishing
  the continuation on the first stop made all subsequent yields silent
  no-ops, so restart was doing nothing. Moved continuation.finish() to
  deinit (plus an explicit close() for callers that really want to tear
  the stream down early).
- README / Info.plist / build-release.sh / AboutView: align every user-
  visible license string with the actual LICENSE file (FSL-1.1-MIT)
  instead of claiming MIT.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: GitButler <gitbutler@gitbutler.com>
@GordonBeeming
GordonBeeming merged commit b4c1db9 into main Apr 22, 2026
2 checks passed
@GordonBeeming
GordonBeeming deleted the gb/phase-1-skeleton-indexer branch April 22, 2026 11:10
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