Skip to content

Photos become JPEG, screenshots PNG — and automations actually run - #90

Merged
leoisadev1 merged 2 commits into
mainfrom
cursor/desktop-defaults-automations-99e8
Oct 7, 2026
Merged

leoisadev1 merged 2 commits into
mainfrom
cursor/desktop-defaults-automations-99e8

Conversation

@leoisadev1

@leoisadev1 leoisadev1 commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Spikk’s feedback, which Leo agreed with: Add files was sending every image to WebP, and automations did not really cover screenshots vs screen recordings (especially when Cmd+Shift+4 copies to the clipboard but recordings still land on the Desktop).

Image defaults

New installs split stills instead of one “Images → WebP” chip:

  • Photos → JPEG — HEIC/HEIF, AVIF, WebP, JPEG
  • Images → PNG — PNG, SVG, GIF, TIFF and the rest, plus anything named like a screenshot (so a macOS HEIC screenshot becomes PNG, not JPEG)

A file that is already the target (PNG→PNG, JPEG→JPEG) opens Quick convert, same as before.

Existing settings.toml files are left alone. An older file that only has images = "webp" keeps WebP for both kinds. Only a missing file, or a defaults section that never saved images, gets the new split.

First-run’s last step, the Activity chips, and the desktop test skill all say JPEG/PNG now.

Activity defaults: Photos to JPEG, Images to PNG

Automations

Rules are no longer display-only.

  • Fresh installs get Screenshots → PNG (copy the result) and Screen recordings → MP4, plus the existing HEIC→JPEG rule still off.
  • Each enabled rule watches one folder, never recursively. Home and / are refused.
  • On macOS, screenshots use com.apple.screencapture location, and the watch is skipped if target is clipboard (nothing lands on disk). Recordings try a video-location key, then the screenshot folder, then Desktop.
  • On Desktop / Downloads / Pictures, only files that look like screenshots or recordings are converted, so a holiday photo on the Desktop is left alone. A dedicated Screenshots folder can take any still.
  • Growing recordings wait until the size stops changing.
  • Each rule has Copy the converted file.
  • The Automations page says what is watched and why it might be waiting.

Automations: screenshot PNG with copy, recordings MP4, resolved folders

Tests never open the real Desktop; they set Automation.folder (or CONVT_SCREENSHOT_DIR / CONVT_RECORDING_DIR).

Legacy “Exports” placeholders without an explicit watch kind stay idle, so turning this on does not start converting ~/Movies/Exports.

PR #68

Based on main. UI changes stay small (intro copy, resolved path, a checkbox) so they can sit on the redesign. #68 is not merged yet — this should be rebased onto it once it lands.

Out of scope

Homebrew install and the HEIC→WebP orientation report from the same note are not in this PR. Orientation is already applied on HEIC decode; changing the default away from WebP may be enough for the case Spikk hit.

Verification

  • cargo test -p convt-app: 120 passed
  • cargo clippy -p convt-app --all-targets -- -D warnings: clean
  • cargo fmt -p convt-app -- --check: clean
  • Headless Xvfb screenshots of Activity chips and Automations (above). Font spacing under lavapipe is a bit tight; the chips and rules themselves match the new copy.

To show artifacts inline, enable in settings.

Open in Web Open in Cursor 

New installs convert photos to JPEG and other images (including
screenshots) to PNG. Existing settings.toml files keep the image
default they already saved.

Automations now watch one folder per rule — the real macOS screenshot
location, the screen-recording folder, or an explicit path — and can
copy the result to the clipboard.
PNG is now a no-op under the images default, so conversion tests use
BMP. Screenshot watches on catch-all folders only match screenshot
names. HEIC assertions run only when the engine is present.
@cursor
cursor Bot force-pushed the cursor/desktop-defaults-automations-99e8 branch from 0b40856 to cd1a6d5 Compare October 7, 2026 21:57
@leoisadev1
leoisadev1 marked this pull request as ready for review October 7, 2026 21:57

@devin-ai-integration devin-ai-integration 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.

Devin Review found 8 potential issues.

Devin Review

Comment on lines +87 to +89
} else {
self.submitted.insert(path);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Screen recordings skipped on shared Desktop

When both rules watch Desktop, drain_ready marks new recordings submitted under the screenshot rule. The recording rule then skips them, so recordings never convert.

Learn more

The watcher shares submitted and seen across all rules. The screenshot rule comes first in example_automations. When both rules resolve to Desktop, it marks a new .mov submitted after deciding that it does not match. The recording rule then skips that path before checking whether it matches. This also suppresses other rules that share a directory.

Example: On a Mac using Desktop for both captures, Screen Recording 1.mov appears. The screenshot rule ignores it but inserts its path into submitted; the recording rule never queues MP4 conversion.

Recommended fix: Track per-rule processing independently, or evaluate all matching rules for each new stable path before marking it processed. Test two enabled rules watching one folder with different file types.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +359 to +361
_automations: if cfg!(not(test)) {
Some(crate::automation::watch(cx))
} else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Automations stop when the window closes

With no active jobs, closing the last window ends the AppState watcher. last_window_closed quits the app without considering enabled automations, so later captures go unconverted.

Learn more

Automations poll only while the AppState task exists. last_window_closed calls cx.quit() when there are no windows and no active conversions. The tray icon cannot currently keep a usable popover open, so closing the main window ends the only process watching folders.

Example: An idle user closes convt, then saves Screenshot 1.heic on Desktop. No process remains to notice or convert it to PNG.

Recommended fix: Decide and implement an app lifetime for enabled automations, such as continuing to run after the last window closes with an explicit Quit path. Cover closing the last window while watching and then creating a new capture.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +81 to +86
if matches_rule(rule, &path, &dir) {
self.submitted.insert(path.clone());
if let Some(to) = format_by_id(&rule.to) {
self.submitted.insert(path.with_extension(to.extension()));
}
ready.push((index, path));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Failed automation files never retry

When poll_automations cannot queue a file, drain_ready has already marked its path submitted. Activating a license or restoring conversion support cannot retry that file.

Learn more

The watcher records a path as submitted before poll_automations verifies that its target is reachable or that convert accepts it. On subsequent polls, the submitted check skips the path without inspecting why it did not convert. This makes a temporary license or engine failure permanent for that file.

Example: A screenshot arrives while the trial has expired. Its PNG conversion is rejected. After the user activates a license, the screenshot remains unconverted despite continuing polls.

Recommended fix: Separate detection from successful submission. Acknowledge the file only after a job is accepted, and provide a controlled retry path for temporary failures without repeating the error every second.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +308 to +314
if let Some(dir) = video_dir()
.map(|p| p.join("Captures"))
.filter(|p| p.is_dir())
{
return Some(dir);
}
desktop_dir()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Custom macOS recording folder ignored

When macOS uses a custom capture location without a recording-specific key, recording_dir watches Desktop instead. Recordings saved in the configured capture folder never convert.

Learn more

The screenshot rule resolves the macOS com.apple.screencapture location setting in screenshot_location. The recording rule probes separate recording keys, then falls back to a Captures directory or Desktop without checking the screenshot location. A Mac that stores both captures under its configured location consequently watches different folders for the two rules.

Example: com.apple.screencapture location is ~/Work/Captures and none of the recording-specific keys is present. A new screen recording lands in ~/Work/Captures; the recording rule polls Desktop.

Recommended fix: On macOS, fall back to the configured capture location before generic Captures/Desktop paths, while preserving any explicit recording directory override.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +51 to +53
for (index, rule) in rules.iter().enumerate() {
if !rule.is_watched() {
continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Re-enabled rules convert older files

If a file arrives while a rule is off, drain_ready retains its old directory snapshot. Re-enabling the rule treats that existing file as new and converts it.

Learn more

The watcher primes a directory only once in drain_ready. Disabling a rule leaves that snapshot in WatchState. Re-enabling it resumes from the older snapshot, not from the folder contents at enable time.

Example: Disable the screenshot rule, save Screenshot 2.heic, then re-enable it. The next two polls convert the old file even though it was already in the folder at activation.

Recommended fix: Reprime the affected rule when it becomes enabled again. Keep priming per rule so other rules watching the same directory remain unaffected.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +68 to +72
for (path, len) in entries {
if self.submitted.contains(&path) {
self.seen.insert(path, len);
continue;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Reused capture names remain ignored

submitted retains deleted paths. A later capture using the same filename is skipped, while tracked paths keep accumulating.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +226 to +227
fn is_too_broad(dir: &Path) -> bool {
dir.parent().is_none() || home_dir().is_some_and(|home| same_path(dir, &home))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Root aliases pass the folder guard

/tmp/.. resolves to root but has a lexical parent. The watcher accepts and polls it every second.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +759 to +760
let copy = rule.copies_to_clipboard();
match self.convert(std::slice::from_ref(&path), to, &Options::default(), cx) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Automation destination follows global setting

convert uses the global output directory. The rule text says “save beside original” even when that directory redirects results.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

@greptile-apps

greptile-apps Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 1/5 Tier: plus

[High risk] Adds file-watching automation that runs on every startup.

The PR needs fixes before merging because the default rules interfere with each other and several paths permanently miss conversions.

Fix All in Claude CodeFindings

  1. P1 Rules interfere with each other ▶
  2. P1 Custom-folder recordings are missed ▶
  3. P1 Reused filenames stop converting ▶
  4. P1 Blocked files are forgotten ▶
  5. P2 Folder scans pause the window ▶
  6. P2 Re-enabling converts old files ▶
  7. P2 Cancelled copy IDs accumulate ▶
  8. P2 Folder links bypass safeguards ▶
  9. P2 Output location is misstated ▶

Summary

This PR splits new-install image defaults into Photos → JPEG and Images → PNG, preserves saved image defaults, and adds folder-driven automations with optional clipboard copying.

  • Shared watcher state breaks the default two-rule setup.
  • Custom recording folders and reused filenames can miss conversions.
  • Rejected jobs are forgotten before they enter Activity.
  • Smaller fixes cover UI stalls, toggle snapshots, folder links, cancelled copy IDs, and output-location text.

Acknowledged by leoisadev1: legacy Exports placeholders without an explicit watch kind intentionally remain idle.

Verification was static only. Tests and macOS behavior were not run. Desktop launch permission was not supplied, so no UI capture was made. Existing PR discussion could not be read without GitHub authentication.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Enabled automation rule] --> B[Resolve one folder]
  B --> C[List files and compare sizes]
  C --> D{New matching file?}
  D -->|Yes| E[Mark path submitted]
  E --> F{Target and license allow work?}
  F -->|Yes| G[Queue conversion]
  F -->|No| H[Path stays skipped]
  G --> I[Save result and history]
  I --> J{Copy enabled?}
  J -->|Yes| K[Copy output]
  L[Add files or drop] --> M{Photo or other image?}
  M --> N[JPEG or PNG default]
  N --> O{Usable target?}
  O -->|Yes| G
  O -->|No| P[Quick convert]
Loading

Reviews (1) · Last reviewed commit: "Fix desktop default and automation tests..." · Reviewed by Greptile

Comment on lines +40 to +43
pub struct WatchState {
primed: HashSet<PathBuf>,
seen: HashMap<PathBuf, u64>,
submitted: HashSet<PathBuf>,

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 Rules interfere with each other

When both default rules watch Desktop, they share seen and submitted. For a new screenshot, the screenshot rule records its size. The recording rule then treats that same observation as stable and marks the nonmatching image submitted, so the screenshot never converts. A new recording can instead be submitted during its first poll, before its size settles.

Keep file observations separate from rule checks, and test both rules together. Confidence: 5/5.

Fix in Claude Code

Comment on lines +308 to +314
if let Some(dir) = video_dir()
.map(|p| p.join("Captures"))
.filter(|p| p.is_dir())
{
return Some(dir);
}
desktop_dir()

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 Custom-folder recordings are missed

If recordings land in a custom screenshot folder and no separate video-location key is set, recording_dir() watches Captures or Desktop instead. It skips the promised screenshot-folder fallback, so those recordings never convert automatically.

Read the screenshot location before falling back to Desktop, while still allowing recordings when screenshots target the clipboard. Confidence: 4/5.

Fix in Claude Code

Comment on lines +104 to +105
if this
.update(cx, |state, cx| state.poll_automations(cx))

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 Folder scans pause the window

watch() runs folder discovery and the full directory scan inside the UI update every second. On macOS, discovery also waits for defaults processes, and drawing rules calls the same lookup through source_line(). A large or slow watched folder can repeatedly stall the window.

Move these checks to background work and let drawing read the latest saved status. Confidence: 5/5.

Knowledge Base Used: Desktop application workflows

Fix in Claude Code

Comment on lines +61 to +66
if self.primed.insert(dir.clone()) {
for (path, len) in &entries {
self.seen.insert(path.clone(), *len);
self.submitted.insert(path.clone());
}
continue;

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 Re-enabling converts old files

A folder is snapshotted only once for the app's lifetime. Turning a rule off and back on does not reset primed, so files created while it was off are converted after re-enabling it. That breaks the stated behavior of leaving existing files alone when a rule is turned on.

Take a fresh snapshot when a rule becomes enabled. Confidence: 5/5.

Fix in Claude Code

Comment on lines +69 to +71
if self.submitted.contains(&path) {
self.seen.insert(path, len);
continue;

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 Reused filenames stop converting

submitted remembers each pathname forever, and the skip branch ignores later changes to that file. After a watched file is deleted and recreated—or replaced with a new capture using the same name—it never converts again until the app restarts.

Remove vanished entries and track file changes so a new file at an old path gets a fresh stability check. This also keeps seen and submitted from growing forever. Confidence: 5/5.

Fix in Claude Code

Comment on lines +81 to +86
if matches_rule(rule, &path, &dir) {
self.submitted.insert(path.clone());
if let Some(to) = format_by_id(&rule.to) {
self.submitted.insert(path.with_extension(to.extension()));
}
ready.push((index, path));

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 Blocked files are forgotten

drain_ready() marks a file submitted before poll_automations() checks whether it can queue the conversion. If the license blocks work, a document pack is temporarily locked, or the target is unavailable, the file stays skipped forever. No job is created, so Activity offers no retry for that file.

Keep it pending until a job is accepted, with a delay between repeated attempts. Confidence: 5/5.

Knowledge Base Used: Desktop application workflows

Fix in Claude Code

Comment on lines +824 to +827
let outcome = match &entry.status {
Status::Done(outputs) => {
batch.done += 1;
if self.automation_copies.remove(&entry.id) {

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 Cancelled copy IDs accumulate

Cancelled jobs never leave automation_copies. The new cleanup runs for success and failure only, so each cancelled copy-enabled automation keeps an unused job ID for the rest of the session.

Remove the ID for every finished job, then use the saved result of that removal to decide whether to copy successful outputs. Confidence: 5/5.

Fix in Claude Code

Comment on lines +226 to +227
fn is_too_broad(dir: &Path) -> bool {
dir.parent().is_none() || home_dir().is_some_and(|home| same_path(dir, &home))

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 Folder links bypass safeguards

The root-folder check uses the supplied path rather than its resolved destination. An explicit folder such as ~/watch-root that links to / passes this check and gets watched. A link to Desktop with a different name also bypasses is_catch_all_folder(), allowing ordinary photos to convert and copy.

Resolve folder links before applying these checks, and compare shared folders by their actual paths. Confidence: 5/5.

Fix in Claude Code

Comment on lines +729 to +732
rule.detail = if copy {
"copy to clipboard".into()
} else {
"save beside original".into()

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 Output location is misstated

Turning off copying sets detail to “save beside original,” but automated conversions use convert(), which honors the global output_dir. With a custom output folder, the popover tells users the wrong place to find their result.

Either make this rule explicitly save beside the input or describe the configured output location instead. Confidence: 5/5.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

@leoisadev1
leoisadev1 merged commit 807a94f into main Oct 7, 2026
10 checks passed
@leoisadev1
leoisadev1 deleted the cursor/desktop-defaults-automations-99e8 branch October 7, 2026 22:44
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