Skip to content

Add opt-out for sending screenshots to the context model - #208

Draft
ojhurst wants to merge 1 commit into
zachlatta:mainfrom
ojhurst:feat/optional-screenshot-context
Draft

Add opt-out for sending screenshots to the context model#208
ojhurst wants to merge 1 commit into
zachlatta:mainfrom
ojhurst:feat/optional-screenshot-context

Conversation

@ojhurst

@ojhurst ojhurst commented May 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a "Use screen context for smarter dictation" toggle in Settings → Prompts → Context Prompt, plus a Skip button on the Setup wizard's Screen Recording step. Both default to on, so existing users see zero behavior change.

Why

The context inference is genuinely useful, but it requires the Screen Recording permission — and every dictation captures a JPEG of the active window and ships it to whatever inference endpoint the user has configured. There is currently no way to disable just the screenshot without revoking the OS permission entirely (which also kills the grant, so flipping it back on means re-granting through System Settings).

This PR adds a clean opt-out:

  • Existing user who is happy with how things work: do nothing, defaults are unchanged.
  • Privacy-conscious user who wants the screenshot off: one toggle in Settings.
  • New user who does not want to grant the permission at all: Skip the step in Setup.

How it works

AppContextService.captureActiveWindowScreenshot gates on a new use_screenshot_context UserDefaults flag (default true). When off, it returns the same (nil, nil, error) tuple shape as the existing permission-denied path — so the existing screenshotDataURL == nil fallback throughout the codebase keeps working without modification. No new code paths, just a clean early return with a user-facing error string that points back at the toggle.

The Run Log's "Capture Context" step (Step 1) gets a small no-image caption when contextScreenshotDataURL is nil but contextScreenshotStatus is non-empty — so the user can see at a glance why a given dictation has no screenshot attached.

Test button in the Context Prompt section refuses to run when the toggle is off, with a clear message pointing back at the toggle, instead of silently running a degraded test.

Observability

Adds an os_log subsystem com.zachlatta.freeflow category Context that emits one line per capture outcome:

Outcome Log line
Toggle off screenshot skipped — disabled in settings (Use screen context for smarter dictation = off)
Permission denied screenshot blocked — Screen Recording permission not granted
Captured (active window) screenshot captured via active-window match (N bytes)
Captured (focused title match) screenshot captured via focused-title match (N bytes)
Captured (full-screen fallback) screenshot captured via full-screen fallback (N bytes)
CGWindowListCreateImage nil screenshot failed — CGWindowListCreateImage returned nil
Could not encode screenshot failed — image could not be encoded within size limits

Useful for verifying gate behavior from outside the app:

log show --predicate 'subsystem == "com.zachlatta.freeflow" AND category == "Context"' --info --last 10m --style compact

What changed

  • Sources/AppState.swift — new useScreenshotContext @Published Bool, persisted at UserDefaults key use_screenshot_context, default true.
  • Sources/AppContextService.swiftimport os.log, OSLog declaration, gate in captureActiveWindowScreenshot, and one os_log line on every outcome path.
  • Sources/SettingsView.swift — adds the toggle subcard at the top of contextPromptSection, the test-button refusal, and the no-image caption in the Run Log's Capture Context step.
  • Sources/SetupView.swift — adds a Skip button to the Screen Recording step that sets useScreenshotContext = false and advances without granting the permission.

+79 / -0. Purely additive, no upstream logic modified.

Testing

Verified locally on a downstream build:

  • Default-on behavior is unchanged. log show --predicate 'subsystem == "com.zachlatta.freeflow" AND category == "Context"' shows screenshot captured via active-window match (N bytes) per dictation, same as main today.
  • Toggle off skips the capture. Same log shows screenshot skipped — disabled in settings instead, dictation still completes using the existing fallback context (app + window + selection), and the Run Log's Capture Context step displays the new "Screen context disabled in settings..." caption in place of the missing image.
  • Test button refuses with friendly error when the toggle is off. Clicking Test Context Prompt with the toggle off renders Screen context is disabled. Toggle "Use screen context for smarter dictation" on above to run this test. instead of running a degraded test.

Screenshots

The Settings toggle in Prompts → Context Prompt:

Use screen context for smarter dictation toggle in Settings

The Run Log's Capture Context step when the toggle is off (new no-image caption in this PR):

Run Log shows Screen context disabled in settings caption

The Test Context Prompt button refusal when the toggle is off:

Test Context Prompt refuses when screen context is disabled

Notes

Default-on means zero regression for existing users. Defaults can flip later in a separate PR if that becomes desired.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added toggle in settings to enable or disable screen context functionality
    • Added Skip option in setup flow for screen recording permissions
    • Added fallback status indicator when context capture is unavailable
  • Improvements

    • Settings now persist across app sessions
    • Enhanced explanatory text for context capture behavior in settings

Review Change Stack

Adds a "Use screen context for smarter dictation" toggle in Settings →
Prompts → Context Prompt, plus a Skip button on the Setup wizard's Screen
Recording step. Both default to on, so existing users see zero behavior
change.

Motivation: the context inference is genuinely useful but requires
Screen Recording permission, which feels invasive to some users. Every
dictation captures a JPEG of the active window and ships it to whatever
inference endpoint they have configured. Today there is no way to
disable just the screenshot without revoking the OS permission (which
also kills the grant, so flipping it back on means re-granting).

How it works:

AppContextService.captureActiveWindowScreenshot gates on the new
use_screenshot_context UserDefaults flag (default true). When off it
returns the same (nil, nil, error) tuple shape as the permission-denied
path — so the existing screenshotDataURL == nil fallback handles
everything downstream. The error string is user-facing copy that points
the user back at the toggle, and it surfaces in the Run Log's Capture
Context step via a new no-image caption added in this PR.

Test button in Settings refuses to run when the toggle is off, with a
clear message pointing back at the toggle.

Observability: adds an os_log subsystem com.zachlatta.freeflow category
Context that emits one line per capture outcome (skipped / blocked /
captured-active-window / captured-focused-title / captured-fullscreen /
failed). Useful for verifying gate behavior from outside the app via
log show.

79 insertions, 0 deletions. Purely additive — no upstream logic
modified.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 24, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1c119362-d661-47af-afa2-fad699a1f294

📥 Commits

Reviewing files that changed from the base of the PR and between 2ea3a5e and 6044ba0.

📒 Files selected for processing (4)
  • Sources/AppContextService.swift
  • Sources/AppState.swift
  • Sources/SettingsView.swift
  • Sources/SetupView.swift

📝 Walkthrough

Walkthrough

A new user-configurable useScreenshotContext setting controls whether the app captures screenshots for AI context inference. The setting is persisted in AppState, gated in the screenshot service with conditional logging, and exposed through the settings interface and onboarding flow with appropriate error messaging and UI controls.

Changes

Screenshot Context Opt-In

Layer / File(s) Summary
State persistence and data model
Sources/AppState.swift
AppState introduces useScreenshotContext as a @Published boolean property with didSet persistence to UserDefaults, defaulting to true on first load via a new storage key constant.
Screenshot service logging and feature gating
Sources/AppContextService.swift
Adds os.log import and a file-scoped OSLog instance, implements a feature gate that reads useScreenshotContext from UserDefaults and skips screenshot capture with a settings-specific error message when disabled, and logs informational and error outcomes at key checkpoints: active-window match, focused-title match, full-screen fallback, and failure cases (permission denied, nil image, encoding failures).
Settings and setup UI for screenshot context control
Sources/SettingsView.swift, Sources/SetupView.swift
SettingsView adds a toggle for useScreenshotContext with explanatory text, updates context prompt test logic to guard against running when disabled and set an error early, and renders contextScreenshotStatus when no screenshot image is available. SetupView adds specialized Skip/Continue buttons for the .screenRecording setup step, with Skip disabling useScreenshotContext and both buttons advancing with animation.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • zachlatta/freeflow#100: Both PRs modify the context screenshot capture pipeline in AppContextService and context prompt settings flow in SettingsView; this PR gates capture with useScreenshotContext, while the retrieved PR wires dimension limits.

Poem

🐰 A screenshot toggle, neat and clean,
Users choose what they want seen.
Skip or continue, the choice is yours—
Privacy-first opens all the doors!
hops with joy

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding a user-facing opt-out mechanism for screenshot capture in context inference, which is implemented across all four modified files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@marcbodea

Copy link
Copy Markdown
Collaborator

does not build

Sources/SettingsView.swift:1696:13: error: cannot find 'SettingsSubcard' in scope
1694 |
1695 | return VStack(alignment: .leading, spacing: 10) {
1696 | SettingsSubcard {
| `- error: cannot find 'SettingsSubcard' in scope
1697 | VStack(alignment: .leading, spacing: 6) {
1698 | Toggle("Use screen context for smarter dictation", isOn: $appState.useScreenshotContext)
make: *** [build/FreeFlow Dev.app/Contents/MacOS/FreeFlow Dev] Error 1

@marcbodea
marcbodea marked this pull request as draft June 3, 2026 17:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants