Skip to content

v0.2: live permission state + Paste to Front App + index diagnostics - #2

Merged
GordonBeeming merged 10 commits into
mainfrom
gb/v0.2-diagnostics
Apr 22, 2026
Merged

GordonBeeming merged 10 commits into
mainfrom
gb/v0.2-diagnostics

Conversation

@GordonBeeming

Copy link
Copy Markdown
Owner

Summary

  • Permissions tab now shows live granted/denied state for Automation (the only permission vista actually needs) and reframes Full Disk Access as optional. Removed the misleading Accessibility row entirely — Carbon's RegisterEventHotKey doesn't need it.
  • Paste to Front App wired up properly: PanelController captures the previously-frontmost app before the panel steals focus, then the action hides the panel, reactivates that app, and sends ⌘V via AppleScript. First invocation triggers the macOS Automation prompt and registers vista in the system list.
  • Folders tab gains a Rescan Now button and a live "Indexed screenshots: N" caption — no need to hunt through the menu bar to retrigger a scan after adding a folder.
  • Indexer diagnostics: probe each watched folder's existence + directory-ness up front, wrap the enumerator with an errorHandler, log seen-vs-matched counts per folder. "0 indexed after Rescan" now has a trail of breadcrumbs you can follow in Console.app.

Test plan

  • Install from the v0.2 cask on a fresh machine
  • Add a folder via Folders → Add Folder…, click Rescan Now, confirm the count goes non-zero (or the logs explain why)
  • Hit ⌘⇧S, pick a result, select Paste to Front App — first time triggers the Automation prompt; subsequent times ⌘V into the previous app
  • Preferences → Permissions shows ✅ Granted after the first Paste

Permissions tab rebuilt:
- Removed the misleading "Accessibility required" row — Carbon's
  RegisterEventHotKey doesn't need Accessibility. Vista's hotkey works
  without it.
- Reframed "Full Disk Access" as optional with accurate copy: folders
  added via NSOpenPanel already work via security-scoped bookmarks; FDA
  is only useful for protected locations the bookmarks can't reach.
- "Paste to Front App" row now shows live state via
  AEDeterminePermissionToAutomateTarget — granted / denied / not yet
  requested, plus a Grant button that triggers the macOS prompt
  (askUserIfNeeded: true). First grant also registers vista in System
  Settings → Privacy → Automation so you can toggle it there.

Paste to Front App wired up for real:
- PanelController captures NSWorkspace.frontmostApplication before it
  steals focus so we know which app to hand the paste back to.
- ActionHandlers.pasteToFrontImpl is a closure the controller sets:
  copies the image, orders the panel out, reactivates the previous
  app on a short delay, then sends Cmd+V via AppleScript. First run
  triggers the Automation prompt.

Folders tab:
- Rescan Now button moved here too (menu bar still has it). Users
  expect to be able to retrigger a scan after adding a folder without
  hunting through the menu bar.
- Live "Indexed screenshots: N" caption under the folder list so the
  scan result is immediately visible without switching to Permissions.

Indexer diagnostics (NSLog; shows up in Console.app under vista):
- Logs each watched folder with existence/isDirectory probe result —
  surfaces permission-denied or stale-path failures instead of
  silently yielding zero candidates.
- Wraps the FileManager enumerator with an errorHandler that logs
  per-entry errors (e.g. iCloud placeholders) but keeps walking.
- Reports seen-vs-matched counts per folder + overall discovered count.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: GitButler <gitbutler@gitbutler.com>
Copilot AI review requested due to automatic review settings April 22, 2026 11:33

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 improves v0.2’s UX and diagnosability by wiring up “Paste to Front App”, showing live Automation permission state in Settings, adding manual rescan + live indexed-count UI, and expanding indexer diagnostics/logging.

Changes:

  • Added preflight checks + errorHandler logging to initial indexing scans, including per-folder enumerated/matched counts.
  • Updated Settings UI: Folders tab gets “Rescan Now” + live indexed count; Permissions tab now probes/requests Automation permission and reframes Full Disk Access as optional.
  • Implemented “Paste to Front App” by capturing the pre-panel frontmost app, reactivating it after dismissal, and issuing a Cmd+V via AppleScript; introduced a PermissionProbe helper.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
Sources/VistaCore/Indexer.swift Adds scan diagnostics (folder probing, enumerator error logging, seen/matched counts).
Sources/Vista/SettingsView.swift Wires rescan + live indexed count; replaces static permission rows with live Automation permission UI and optional Full Disk Access row.
Sources/Vista/PermissionProbe.swift Adds Apple Events-based probe/request helpers for Automation (System Events).
Sources/Vista/PanelController.swift Captures previous frontmost app and implements “Paste to Front App” flow (reactivate + scripted Cmd+V).
Sources/Vista/ActionHandlers.swift Adds an injectable implementation hook for Paste-to-Front-App and invokes it after copying.

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

Comment thread Sources/Vista/SettingsView.swift Outdated
Comment thread Sources/Vista/PanelController.swift
Comment thread Sources/Vista/PanelController.swift Outdated
Comment thread Sources/Vista/PermissionProbe.swift
Comment thread Sources/Vista/SettingsView.swift
Comment thread Sources/VistaCore/Indexer.swift Outdated
GordonBeeming and others added 9 commits April 22, 2026 22:07
Gordon reported "Grant does nothing" for Paste to Front App and "my
prod other already has full disk access" but both were showing as
Unknown / Optional regardless of real state. Two fixes:

1. Automation grant path now uses NSAppleScript instead of
   AEDeterminePermissionToAutomateTarget(askUserIfNeeded: true).
   Running a minimal `tell application "System Events"` query
   reliably triggers the macOS Automation prompt on current macOS and
   registers vista in Privacy → Automation. The raw AE API with
   askUserIfNeeded has been flaky for us on macOS 26.

2. FDA now has a live probe: try to open
   /Library/Application Support/com.apple.TCC/TCC.db. That file is
   FDA-gated — readable means granted, EPERM means denied. Probed on
   tab appear; row shows "Granted · Optional" when on, plain
   "Optional" otherwise.

Also added NSLog around the automation read-only probe so unexpected
OSStatus values surface in Console.app — we'll know if Apple
introduces a new code instead of silently reporting Unknown.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: GitButler <gitbutler@gitbutler.com>
`swift run Vista` runs the bare executable, which skips Info.plist so
LSUIElement, bundle id, icon, and usage descriptions all go unread and
dev-build TCC grants end up keyed off the wrong identity.

Scripts/dev-run.sh does a fast debug build, wraps the binary in a
Distribution/Vista Dev.app with a `com.gordonbeeming.vista.dev` bundle
id, and `open`s it. Separate bundle id means dev Automation / Full
Disk Access grants and UserDefaults are isolated from any
brew-installed production copy — you can iterate locally without
stepping on production prefs, and revoke dev-only Automation without
affecting the shipping build.

pkills any running VistaDev first so `open` launches the freshly-built
binary rather than foregrounding the stale instance.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: GitButler <gitbutler@gitbutler.com>
Apps only show up in System Settings → Privacy & Security → Full Disk
Access after they've attempted to read a TCC-gated file at least once.
Vista had no such attempt at launch, so users who wanted to grant FDA
had to "+" browse to Vista.app manually instead of finding it in the
standard list.

New TCCBootstrap.registerWithTCC() fires two cheap read attempts at
launch — /Library/Application Support/com.apple.TCC/TCC.db (canonical
FDA-gated file) and ~/Library/Safari/Bookmarks.plist (second path for
reliability across macOS versions). Both reads are expected to fail
until the user grants FDA; the attempt itself is what TCC records.

Called from AppState.init before the async bootstrap Task starts so
the probe runs even if the ScreenshotStore or Indexer fail to open.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: GitButler <gitbutler@gitbutler.com>
Gordon added his iCloud screenshots folder (4,750 images) via the
Folders tab and the indexer found zero — but the directory is readable
from the shell with no special setup. Root cause: ad-hoc-signed dev
builds get a fresh code signature each rebuild, which invalidates
every security-scoped bookmark the previous build stored. Preferences.
resolveFolders() returned nil for the iCloud bookmark, so the folder
never reached the indexer.

Since vista runs unsandboxed, the security scope is theatre — the app
has full filesystem access from its plain path anyway. Added a
fallback: if `folder.resolve(startAccess: true)` returns nil, use
URL(fileURLWithPath: displayPath) instead. Verified against the iCloud
folder — bookmark fails, fallback kicks in, enumerator finds all 4750.

Also added VistaLog — a tiny tee that calls NSLog AND appends a
timestamped line to ~/Library/Logs/Vista/vista.log, so `tail -f` is
all you need to see indexer diagnostics. macOS's unified-log
filtering has been unreliable for third-party process output, which
was making the original bookmark failure invisible.

Routed the Indexer initialScan logs and the new bookmark-fallback log
through VistaLog so the diagnostic trail actually lands on disk.

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

Two complaints to address:
1. "Each dev install needs to re-do permissions" — prior dev-run.sh
   used a separate `.dev` bundle id on purpose, which meant dev and
   brew-installed were distinct TCC principals: Automation, Full Disk
   Access, and folder bookmarks all had to be granted separately.
2. "Should auto-tail the log after launch" — previously had to run
   `tail -f` in a second terminal every time.

Changes:
- Bundle id now matches prod (com.gordonbeeming.vista). TCC grants and
  UserDefaults preferences are shared with any brew-installed copy;
  grant once in either build and it carries across.
- Script pkills any running `Vista` process before launching so dev
  and prod never fight over the same bundle id at runtime. `open -n`
  bypasses Launch Services caching of the /Applications path.
- Ad-hoc sign with `codesign -s - --identifier com.gordonbeeming.vista`
  so the signing identifier is stable across rebuilds, which keeps
  TCC grants from being invalidated on every `swift build`.
- Automatic `tail -f ~/Library/Logs/Vista/vista.log` after launch.
  `--no-tail` skips the tail for scripted use.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: GitButler <gitbutler@gitbutler.com>
The prior dev-run.sh produced Distribution/Vista Dev.app with an
executable named `VistaDev` and bundle id com.gordonbeeming.vista.dev.
The current script uses `Vista` + com.gordonbeeming.vista — but the
rename left users with both a running VistaDev process and a stale
Vista Dev.app on disk.

Kill both `Vista` and `VistaDev` at startup (exact-name pkill, silent
if absent) and rm the old Vista Dev.app bundle so Launch Services
stops advertising a version that will never run again.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: GitButler <gitbutler@gitbutler.com>
- SettingsView: capitalise "Vista" in the Full Disk Access hint so the
  product name reads consistently with the rest of the UI.
- PanelController.pasteToPreviousFrontmost: guard on a non-nil
  previousFrontmostApp. Without the guard, reactivating target + sending
  Cmd+V would fire even when we never captured a target, landing the
  keystroke in whichever app happened to be frontmost after the panel
  hid — often vista itself or Finder. The clipboard copy from
  ActionHandlers is still useful on its own; the keystroke is what we
  skip.
- PermissionProbe: add explicit `import CoreServices` for the AE-family
  types and constants. Not strictly required on current macOS (AppKit
  re-exports them transitively) but makes the dependency explicit and
  keeps the file resolving on older SDKs.
- Indexer: fix the log message mismatch — the guard checks existence
  + isDir, not readability, so "skipping — path does not exist or is
  not a directory" is the honest description.
- SettingsView.PermissionsTab: re-probe Automation + Full Disk Access
  state on NSApplication.didBecomeActiveNotification, not just on tab
  appear. Covers the common flow of clicking "Open Settings", toggling
  the permission in System Settings, and returning — previously the
  chip stayed stale until the user switched tabs.

Reverted Copilot's suggestion to call `activate(options:)`: the no-arg
`activate()` is macOS 14+ which matches our deployment target, and the
`activateIgnoringOtherApps` option is deprecated on 14+ (it's a no-op
there anyway). Kept the no-arg form to avoid the deprecation warning.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: GitButler <gitbutler@gitbutler.com>
`swift build -c release` alone produces a bare executable with no
Info.plist, which is exactly the "dev feels different from prod"
friction we fixed with Scripts/dev-run.sh. The README still pointed
people at `swift build -c release` as the primary local-dev path,
which is misleading.

Reorganised so dev-run.sh is the headline instruction with a short
explanation of what it does (shared bundle id, stable ad-hoc
identifier, auto-tail). `build-release.sh` mentioned for the
signed + notarised release flow CI uses.

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

1. The badge button was the only focusable control on the About
   window, so SwiftUI drew its default accent-coloured focus ring
   around it whenever the window was key — making it look like a
   text field. `.focusEffectDisabled()` on the button drops the
   ring without affecting tap behaviour.

2. BuildInfo.footerBadge prepended "v" to the version string
   unconditionally, which produced "vdev · 41f7640" on dev builds
   where CFBundleShortVersionString is the literal "dev". Now we
   only prepend the "v" when the version actually starts with a
   digit (i.e. looks like semver), so dev builds show "dev · 41f7640"
   and tagged builds still show "v0.2 · abc1234".

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: GitButler <gitbutler@gitbutler.com>
@GordonBeeming
GordonBeeming merged commit 541f062 into main Apr 22, 2026
2 checks passed
@GordonBeeming
GordonBeeming deleted the gb/v0.2-diagnostics branch April 22, 2026 12:49
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