Skip to content

Redesign the convt desktop app UI on one visual system - #68

Open
leoisadev1 wants to merge 52 commits into
mainfrom
cursor/desktop-app-redesign-6808
Open

leoisadev1 wants to merge 52 commits into
mainfrom
cursor/desktop-app-redesign-6808

Conversation

@leoisadev1

@leoisadev1 leoisadev1 commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Redesigns the GPUI desktop app (crates/convt-app) from the theme up. Every window now uses one shared visual system instead of per-window colors and sizes. The redesign itself keeps behavior unchanged: the same ids, labels, flows, license and trial logic, Finder recovery card (CNV-41/42), sign-in and document pack. On top of it, for 0.2.1, this PR adds a macOS menu bar, checks for updates at every launch and every 5 hours, and restyles the Updates card. It also fixes Quit and Close Window from the keyboard: ⌘Q and ⌘W on macOS, Ctrl+Q and Ctrl+W on Linux and Windows. All of this is under "Menus, update checks and the Updates card". The menus, the Finder screens and the popover were then checked on a real macOS desktop in light and dark mode ("macOS: menus, Finder screens and popover"). That check, together with a Linux run of the same screens, found two popover layout bugs, both fixed here. Apart from the privacy page's update-check sentence, the website, /download and the API docs are untouched.

The visual system (src/ui/theme.rs)

  • Scales: space (4 to 32), radius (5, 7, 10, 14) and size type steps (title, display, body, small, caption) in Geist and Geist Mono.
  • Palette: light and dark tokens taken 1:1 from the convt.app theme (apps/web/src/styles.css); see "Brand colors" below.
  • Components:
    • Button: primary, secondary and ghost looks, an optional Lucide icon, small size, disabled, and a loading state with a spinner.
    • badge, icon_tile, callout, card, and group / row for grouped settings.
    • select with a check on the current choice, segmented, switch, checkbox, radio, and framed thumbnails.
  • Brand: mark and lockup are drawn from design-assets/brand (the two overlapping squares, with the overlap shade), in both themes.

Brand colors

Both palettes now come from the web app's tokens (light :root and .dark in apps/web/src/styles.css). Each line in Palette::light() and Palette::dark() names its web token.

Desktop field Web token Light Dark
window --page #FFFFFF #0A0B0B
chrome (sidebar, toolbar) --sunken / --raised #F7F8F7 #111312
surface (cards, controls) --raised #FFFFFF #111312
recessed (footers, wells) --sunken #F7F8F7 #161918
border, hairline, selected, track --line #E6E8E7 #232726
control_border --line-strong #D5D9D7 #2E3331
row_divider --divider #EEF0EF #232726
chip / chip_border (neutral badges) --chip / --chip-line #F5F7F6 / #E0E3E1 #161918 / #2E3331
hover --hover #F3F4F3 #1C201E
text / secondary / tertiary --ink / --ink-2 / --ink-3 #0A0A0A / #6B6F6D / #6C716E #EDEFEE / #A1A6A3 / #868B88
green, green_text --green #127A47 #3FCB84
green_tint / green_border --green-tint / --green-line #EEF7F2 / #CFE6D9 #12261B / #3FCB84 at 20%
error / error_border --error / --error-line #B3261E / #F0D4D1 #F2786D / #F2786D at 20%
toggle_off --separator #D0D3D1 #2E3331
overlay (menus) --raised / --hover #FFFFFF #1C201E
shadows --shadow-button, --shadow-float, --shadow-note web values web values
  • Font, radius and mark: the font was already Geist and Geist Mono, as on the web. The mark already used --mark-green-top, --mark-green-bottom and --mark-overlap. Radii are unchanged.
  • Derived, with no web token: error_tint (#FCF3F2 light, #261716 dark, made like --green-tint) and mark_off (the empty checkbox and radio edge, one step stronger than --line-strong).
  • Primary button: it uses the web's .btn-primary ring (#157F4A), inset highlight and shadows. The web fill (#22A867 to #1B9259) gives white text about 3:1, so the desktop fill runs from --green #127A47 to #0F6B3E, which is at least 5.3:1. The old desktop fill's top (#168A51) was 4.39:1, short of AA.
  • Accessibility: a new test (ui::theme::contrast) checks every text color against every background in both palettes for 4.5:1, plus white on the primary fill. It caught one miss before commit (tertiary text on the light error tint), which is fixed.
  • Other off-brand colors: the first-run menu illustration highlighted "Convert with convt" in macOS blue (#2F6FE4). It now uses the brand green. File-type badge colors (audio purple, PDF red and so on) and the macOS window dots in the Finder preview are unchanged, because they stand for file types and the OS.
  • Reference: the convt.app landing is dark only (the route pins theme: "dark"), so its light and dark captures are the same. /download follows the OS, so it is the light-mode reference.

Activity light Activity dark
Quick convert light Quick convert dark Trial card light
Settings General light Settings General dark First run plan light First run plan dark
First run done light First run done dark

convt.app for comparison: convt.app landing (dark only) convt.app/download light convt.app/download dark

The Before/After tables below are from before this palette change.

Menus, update checks and the Updates card

macOS menu bar (src/ui/menus.rs)

  • convt: About convt, Check for Updates…, Settings… (⌘,), Services, Hide convt (⌘H), Hide Others (⌥⌘H), Show All, Quit convt (⌘Q).
  • File: Add Files… (⌘O, the main window's picker; first run comes forward until it's done) and Close Window (⌘W).
  • Edit: Undo, Redo, Cut, Copy, Paste and Select All, as os_action items bound to the text fields' own actions, so ⌘C and the rest work in the license and preset fields.
  • Window: Minimize (⌘M), Zoom, Activity.
  • Help: convt Help (convt.app/docs), Release Notes (convt.app/changelog), Contact Support (convt.app/contact). The repo has no Discord, so support is the contact page.
  • Check for Updates… checks right away, even with automatic checks off, and opens Settings scrolled to the Updates card, which shows up to date, the new version with Download, or the error.
  • cx.set_menus and the Mac-only shortcuts are installed only on macOS. The actions are registered on every platform, and Quit and Close Window are bound everywhere (see below).
  • Handlers run deferred: an item or shortcut is dispatched through the key window, which can't be brought forward or closed mid-dispatch. Without this, Check for Updates… with Settings in front opened a second Settings window; a test caught it.
  • About convt is a small window with the mark, "Version 0.2.0 · built Oct 7, 2026", and links to convt.app, the release notes and the source code (the app is AGPL).

Quit and Close Window shortcuts (bug fix, all platforms)

  • Launch replies reported that ⌘Q and ⌘W do nothing in 0.2.0 on macOS, and Ctrl+Q and Ctrl+W do nothing on Linux (verified) and likely Windows. The cause: gpui-kit deliberately leaves these shortcuts to the app (its own test checks that it binds neither), and convt bound nothing.
  • menus::key_bindings() now binds Quit and Close Window as secondary-q and secondary-w, which GPUI reads as ⌘ on macOS and Ctrl elsewhere. menus::init binds them on every platform. The other Mac shortcuts (mac_key_bindings(): ⌘, ⌘H ⌥⌘H ⌘O ⌘M) and the menu bar stay macOS only.
  • Quit calls cx.quit(). Close Window closes the active window, the one you typed in. Text fields don't bind either key, so both shortcuts work with a field focused. On macOS the menu items show ⌘Q and ⌘W from these bindings, and they stay enabled with no window open (menu bar icon mode), since their handlers are global.
  • Closing the last window keeps the existing behavior: quit unless conversions are running, or keep running in the menu bar on macOS (#83).

Update checks (src/update.rs)

  • Automatic checks (still on by default, same switch) run at every launch and then every 5 hours while the app runs (CHECK_INTERVAL). Before, it was once a UTC day at launch.
  • The schedule compares wall-clock time every 15 minutes (SCHEDULE_TICK) instead of waiting out one long timer, so a check that came due while the computer slept runs soon after it wakes.
  • Check now and Check for Updates… always check, even with automatic checks off. Turning the switch off keeps the last result on screen.
  • update_checked_at (Unix seconds) records the last check that got an answer. It is saved together with the accepted manifest sequence, so the rollback guard is unchanged. The old update_checked day key is ignored.
  • The Settings switch, the "What reaches the network" line, the privacy page, docs/plan.md, docs/update-integration.md and the desktop test skill describe the new schedule.

Updates card (Settings, General)

  • Top row: the mark, "convt 0.2.0", "Built Oct 7, 2026 · Last checked today at 10:12 PM", and Check now. While a check runs, the button shows a spinner and "Checking…" (new Button::loading).
  • Middle row: the result. Up to date gets a green check. A newer build gets a green callout with its version and date, Download and Release notes (changelog#v<version>). A build the license doesn't cover gets the same callout with Renew. A failure is a quiet note.
  • Bottom row: "Check automatically · At launch and every 5 hours".
Light Dark
Update available Updates card, update available, light Updates card, update available, dark
Checking Updates card, checking, light Updates card, checking, dark
Up to date Updates card, up to date, light Updates card, up to date, dark
Check failed Updates card, failed, light Updates card, failed, dark
About convt About convt, light About convt, dark

These are from a real run under Xvfb, against a local server with a manifest signed by a throwaway key (CONVT_UPDATE_URL and CONVT_UPDATE_PUBKEY). The launch check found 0.2.1; I re-signed the manifest for the up-to-date shot, held the server's answer for the checking shot, and removed the manifest for the failure (HTTP 404). The sidebar card from the same launch: Main window with the update card

Linux has no way to open About, so its screenshots come from a scratch copy of the branch patched to open About at launch; that patch isn't committed. The real macOS menu bar is in the next section.

macOS: menus, Finder screens and popover

These come from a real macOS 15 desktop: GitHub's macos-15 runner, on a throwaway branch that isn't part of this PR.

  • The build: it built this branch's code, wrapped the binary in a minimal ad-hoc-signed convt.app using packaging/macos/Info.plist, and drove it with System Events (the same approach as cnv-54-spanish-macos.yml on main). screencapture took the pictures.
  • Scratch-only patch: an environment variable sets the Finder extension state, since the runner has no Finder extension installed. Another opens the popover at launch, since only the tray icon would open it and GPUI can't draw one yet. Neither is committed.

What the run checked, besides the pictures:

  • The menu bar, read back through accessibility: convt, File, Edit, Window and Help, with the items and shortcuts listed above.
  • Additions macOS makes itself: the Window menu's Fill, Center, Full Screen and Move & Resize items, Edit's AutoFill, Dictation and Emoji items, and the Help search field.
  • Settings… and Check for Updates… each open Settings. The update check shows "This build has no key to check updates with", which is right for a build without the release update key.
  • The shortcuts, pressed for real: with Settings in front, ⌘W closed it (two windows down to one), File > Close Window closed the main window, and ⌘Q quit the process.

Bugs this found, now fixed:

  • Traffic lights over the popover header: on macOS the popover's traffic lights covered the lockup. Its header now leaves the same room for them as first run does (b03bc83).
  • Popover switches pushed off the edge: a long rule line (main's folder line plus the rule detail) pushed the switches past the window's right edge. This showed up in the Linux run of the same screens. The text column now shrinks and truncates, and the switch keeps its size (fe15828). windows_fit_their_content_at_their_opening_sizes now opens the popover with a long rule line and checks that the switch fits; without the fix it fails.
Light Dark
convt menu convt menu, light convt menu, dark
File menu File menu, light File menu, dark
Edit menu (dimmed with no text field focused) Edit menu, light Edit menu, dark
Window menu Window menu, light Window menu, dark
Help menu Help menu, light Help menu, dark
Activity, Finder extension off (recovery card) Activity with the Finder recovery card, light Activity with the Finder recovery card, dark
Settings, Finder off Settings Finder row off, light Settings Finder row off, dark
Settings, Finder on Settings Finder row on, light Settings Finder row on, dark
First run, Finder step, off First run Finder step off, light First run Finder step off, dark
First run, Finder step, on First run Finder step on, light First run Finder step on, dark
Popover Popover, light Popover, dark
Check for Updates… (Settings at the Updates card) Check for Updates result, light Check for Updates result, dark

Screens

Activity

  • The sidebar gets the brand lockup, nav icons and an active-jobs count, and ends with "Files stay on this computer".
  • The header shows a live subtitle ("1 converting", "2 recent conversions"), with Clear finished and Add files.
  • Rows show the file name, an arrow and a format badge, with the status as an icon plus text, and ghost Show, Retry and Stop actions.
  • The empty state is a real drop target: the mark, a hint, "Choose files…" and the formats convt handles.
Before After
main-activity-light.png main-activity-light.png
main-empty-light.png main-empty-light.png

main-activity-dark.png main-empty-dark.png

Trial and Pro surfaces

  • The sidebar trial card has a progress bar and a full-width Buy button. The button turns primary, with a red card, once the trial ends.
  • The blocked banner is now an error callout with Buy and Enter license.
  • The License tab has a status card, a "License key" card, and "Pro renewal" as its own section.
Before After
main-trial-light.png main-trial-light.png
settings-license-light.png settings-license-trial-light.png

main-trial-ended-light.png quick-blocked-light.png

Settings

  • The tab bar has icons.
  • General is split into grouped sections:
    • Converting
    • Finder and menu bar (the Finder row shows its status dot and a Manage button)
    • Documents
    • Updates: this version, the last check and Check now, then the result, then the switch (see above)
    • What reaches the network: the same text as before, as one row each with an icon
  • Presets has a list, an empty card and a labelled form.
Before After
settings-general-light.png settings-general-light.png
settings-presets-light.png settings-presets-light.png

settings-general-2-light.png settings-license-dark.png

Quick convert

  • The header shows the file and its folder.
  • The format cards are a four-column grid with a check on the picked one, and presets are pills.
  • Options and Save are grouped rows.
  • The footer is fixed, with "Convert to JPEG" once a target is picked.
  • The job list uses the same rows as Activity.
Before After
quick-light.png quick-light.png

quick-picked-light.png quick-picked-dark.png

First run

  • The window grows from 420×420 to 460×520 (FIRST_RUN_SIZE).
  • Top to bottom: the lockup and step pills, then "Step n of N", then the title and body, then the art, then a footer with Back and the primary action.
  • The plan step uses radio cards and keeps the compact sign-in line.
  • The Finder step's picture is still a non-interactive preview (finder-preview, "Preview", "This picture isn't a switch."), now drawn as a mock System Settings window.
  • The done step shows the context menu with "Convert with convt ›".
Before After
first-run-plan-light.png first-run-plan-light.png
first-run-done-light.png first-run-done-light.png

first-run-plan-dark.png

Also restyled:

  • the Automations page: main's intro line, then the rules as one group, each with its folder line, "Copy the converted file" and the switch
  • the document pack card and its Settings row: removal now asks in an error callout
  • the update card
  • the account section
  • the menu bar popover

Automations, light Automations, dark

Small visible changes besides the look

  • The Convert button reads "Convert to {format}" once a target is picked.
  • The trial-ended card no longer repeats "0 days left" under "Trial ended".
  • The defaults strip reads "Add files converts to" instead of "Add files converts right away to".
  • The Update checks row became the Updates card described above. The ids are the same, plus update-version, update-last-checked and update-notes.
  • The first-run done step highlights "Convert with convt" in brand green instead of macOS blue.

Tests

  • cargo fmt --all --check passes.
  • cargo clippy --all-targets -- -D warnings and cargo clippy -p convt-app --all-targets -- -D warnings pass.
  • cargo test -p convt-app passes (135 tests, including the two contrast tests and main's automation tests), and so does cargo test on the default members. bun run rs:check passes, and so do bun run check, bun run check-types and bun run build for the privacy page change.
  • The latest main is merged in, including #81 (9.x versions in the update tests), #83 (the macOS menu bar app stays alive after its last window closes; the menus and Quit work with it), #90 (automations run) and #80 (TypeScript and database CI jobs). The latest merge (99ac631) adds #91 (Homebrew install), #93 (photo orientation), #88 ($0 Desktop orders on Billing) and #96 (no console window on Windows); the only conflict was ci.yml, below. Main's Automations page and popover content moved into the redesigned layout; the old "Rules don't run yet" callout is gone. CI runs ten checks on the head commit: Rust core and desktop on Linux, macOS and Windows, Web, Release scripts, Database integration tests, and Greptile. All pass.
  • .github/workflows/ci.yml: this branch had fixed ci: run TypeScript and database-backed tests #80's database job cache, which held only sqlx and not cargo-sqlx, so cargo sqlx prepare --check failed on a cache hit. Main has since fixed it the same way, so the merge takes main's file and this PR no longer changes ci.yml.
  • Two flakes from main's tests showed up while CI ran; both passed on rerun. renamed_binary_uses_convt_in_help_and_errors (convt-cli) hit "Text file busy" running a just-copied binary. The database fixture test "pro has its sample data" ran at 00:00 UTC against data seeded before midnight, so its 30-day window lost a day. fb6a3ac is an empty commit that only re-triggered CI.
  • The diff adds no email-shaped literals. The legacy account key in the settings test reads "fixture-account".
  • The Updates tests scroll to the card before clicking (reveal_updates), because the General tab is taller on macOS and Release notes sat below the fold there.
  • New tests for this round:
    • a_running_app_checks_again_every_few_hours: a launch check, then no fetch before CHECK_INTERVAL, a fetch once it has passed, and none with automatic checks off. It drives the timer with advance_clock.
    • update_checks_off_ask_only_when_the_user_does: off makes no launch request, while Check now still checks.
    • check_now_shows_that_it_is_checking and update_dates_read_as_words. The covered-update test also checks the version and last-checked rows, Release notes and Download, and that a second launch checks again.
    • the_menu_bar_has_the_menus_mac_apps_have: cx.set_menus(menus()), read back with cx.get_menus(), item by item.
    • the_menu_shortcuts_are_the_usual_ones: ⌘, opens Settings, and the Close Window shortcut closes the key window.
    • quit_and_close_window_shortcuts_work_on_every_platform: types the platform's literal keys (cmd-q/cmd-w on macOS, ctrl-q/ctrl-w on Linux and Windows). They reach Quit from the main window and from Settings with the license field focused. Close Window closes only the window typed in, then the main window. Quit is enabled and quits from the menu with no window open. Each key has exactly one binding. GPUI's test platform makes quit() a no-op, so a second Quit listener counts the action and passes it on to the app's handler. With the all-platform binding removed, the test fails on Ctrl+Q, so it catches the shipped bug. Linux CI runs it with Ctrl, macOS CI with ⌘, and Windows CI with Ctrl.
    • check_for_updates_checks_now_and_shows_the_result: with automatic checks off, it covers available (Download in view), up to date and offline, all in one Settings window.
    • about_and_help_open_what_they_say: About's version and links, plus the docs, changelog and contact URLs.
    • the_edit_menu_reaches_the_text_fields: the Edit menu's own Select All, Copy, Cut and Undo items, dispatched to a focused license field.
  • Earlier test changes:
    • the_icons_the_windows_draw_are_bundled now lists every icon the windows draw.
    • The first-run tests use FIRST_RUN_SIZE.
    • Activity checks the empty state's empty-add-files button.
    • windows_fit_their_content_at_their_opening_sizes checks that the button fits, and now About too, plus the popover's switch next to a long rule line.
  • The screenshots outside the macOS section are from real runs under Xvfb with a private D-Bus and the fake portal (the test-convt-desktop recipe), in both themes. The licensed shots use a build with CONVT_LICENSE_ENFORCE=1 and a throwaway test key.
  • .agents/skills/test-convt-desktop/SKILL.md now describes the shared theme, the new first-run size and its window position, the update schedule, the Updates card's ids, and how to test the menus and the Quit and Close Window shortcuts.

Release

  • .changeset/desktop-ui-redesign.md bumps @convt/desktop by a patch with one user-facing line, so the redesign ships in 0.2.1. .changeset/desktop-menus-and-update-checks.md adds a second patch line for the menus, the Quit and Close Window shortcuts on every platform, the update schedule and the Updates card.
  • I checked the first one with the Version Packages workflow's own command (bunx --bun @changesets/cli version and sync-version.py) in a scratch worktree, next to the existing Linux changeset. All three versioned packages go to 0.2.1, each gets a 0.2.1 changelog, and the desktop changelog carries the new line.

Not verified here

  • The Finder Sync extension: integrations/macos ("Convert with convt" in Finder) is unchanged and needs Xcode, so it was not built. The macOS run used a bundle without it and set the extension state through the scratch variable, so the Finder screens are real renders of each state, but nobody switched a real extension on.
  • A signed release bundle: the macOS run used an ad-hoc-signed debug build, not packaging/macos/bundle.sh's notarized app.
  • The popover from a tray icon: only the tray icon would open it, and GPUI can't draw one yet, so the popover was opened at launch in the scratch build.
  • Windows desktop: Ctrl+Q and Ctrl+W are covered by the headless test on Windows CI. Nobody pressed them in a real Windows session.
  • Window resizing: I captured each window at its opening size only.

To show artifacts inline, enable in settings.

Open in Web Open in Cursor 

cursoragent and others added 4 commits October 7, 2026 13:02
A shared theme (spacing, radii, type steps, palette, buttons, badges,
callouts, grouped rows, the brand mark) and every window rebuilt on it:
Activity with a branded sidebar, icons and a real empty state; first run
with the step above the art and a larger window; Settings as grouped
sections; Quick convert with a format grid and grouped options; and the
pack, account, update and popover surfaces restyled to match.

Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
@cursor cursor Bot changed the title Redesign the convt desktop app UI Redesign the convt desktop app UI on one visual system Oct 7, 2026
cursoragent and others added 2 commits October 7, 2026 20:14
…design-6808

Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
@leoisadev1
leoisadev1 marked this pull request as ready for review October 7, 2026 20:49

@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 4 potential issues.

Devin Review

Comment on lines +292 to +300
let notifications = theme::switch("notifications", settings.notifications, false, p)
.on_click({
let app = app.clone();
let on = settings.notifications;
move |_, _, cx| {
app.update(cx, |s, cx| s.update_settings(|s| s.notifications = !on, cx))
}
});
let reveal = theme::switch("reveal", settings.reveal_when_done, false, p).on_click({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Notification and reveal switches become indistinguishable

Replacing the checkboxes with theme::switch gives both controls only “On” or “Off” as accessible names. Their setting titles remain separate siblings, so assistive-technology users cannot identify which preference each switch changes.

Learn more

The General tab previously rendered these preferences as checkboxes whose accessible labels named their actions. The shared switch supplies only On or Off as its accessible label. The row places the visible preference title in a separate child without associating it with the switch. Both newly converted preferences therefore expose the same name when they have the same value.

Example: With notifications and Reveal both enabled, an assistive-technology user encounters two controls named “On” and cannot tell which one reveals files.

Recommended fix: Give each switch a descriptive accessible name, including its setting title, either through an optional label parameter on theme::switch or by associating the switch with its theme::row title. Preserve the current toggled state.

Devin Review


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

let items = choices.into_iter().map(|choice| {
let on_pick = on_pick.clone();
let pick = choice.id.clone();
let on = choice.label == current;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Automatic background shows wrong checked option

When Quick convert infers a background, select checks the inferred color instead of “Automatic.” The background remains unset, so the checked option misrepresents what conversion will use for other routes.

Learn more

The shared dropdown currently chooses its checkmark by comparing option labels with the displayed text. Quick convert's background selector displays the inferred default color even when self.background is None, which means Automatic. Since that displayed color matches a concrete option, the dropdown marks the concrete option instead of Automatic. The choice users see as selected is therefore not the stored conversion setting.

Example: For a JPEG target, the control displays White while self.background is None. Opening the dropdown marks White, although Automatic is selected and a different route can infer a different background.

Recommended fix: Pass the actual selected choice id separately from the display value to theme::select, and compare choice ids rather than labels. Pass automatic for an unset background even when displaying the inferred default.

Devin Review


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

Comment on lines +483 to +490
return div()
.id("activity")
.flex()
.flex_col()
.flex_1()
.min_h_0()
.children(notices)
.child(self.empty_state(finder_off, p, 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.

🟡 Empty Activity can hide its file picker

When Activity has no conversions and shows a long error, activity renders its notices without a scroll container. Resizing the window short can push “Choose files…” below the viewport with no way to reach it.

Learn more

The nonempty Activity path uses vertical scrolling, but the empty path does not. notices can contain a multi-line Add files error and, on macOS, the Finder recovery card. They are placed ahead of the flexible empty state in the unscrollable branch, so shrinking the window or displaying enough error lines moves the picker action outside the visible area.

Example: On a small main window, a long unreadable-folder error above the empty state takes the remaining vertical space. The Choose files button falls below the bottom edge and scrolling does nothing.

Recommended fix: Keep the empty Activity branch scrollable as well, with sufficient minimum content height for its empty-state action and notices. Cover a resized window with both notices and the empty-state picker in a layout test.

Devin Review


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

Comment on lines +558 to 567
.h(px(52.))
// The traffic lights sit at the left of a transparent title bar.
.pl(px(if theme::transparent_titlebar() {
84.
} else {
28.
}))
.pr(px(28.))
.child(theme::lockup(13., &p))
.child(steps),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Check first-run title strip on macOS

The first-run strip reserves 84px for macOS traffic lights rather than using the shared title-bar helper. Its native positioning remains unverified; inspect both first-run steps on a Mac for overlap.

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: apex

[High risk] Adds cloud conversion backend and device authentication routes.

The PR needs fixes before merging because Cloud blocks valid conversions, mislabels pages, and can fail or stay stuck while downloading.

Fix All in Claude CodeFindings

  1. P1 Cloud still needs local engines ▶
  2. P1 Downloaded pages get wrong names ▶
  3. P1 Later download links expire ▶
  4. P1 Stop cannot interrupt stalled transfers ▶
  5. P2 Stopped results still get saved ▶
  6. P2 Revoked accounts still look signed-in ▶
  7. P2 Privacy promises do not match ▶
  8. P2 Folder errors cannot scroll ▶
  9. P2 Background checks the wrong choice ▶
  10. P2 Dark-mode checks lack contrast ▶
  11. P2 Unselected choices lack text contrast ▶
  12. P2 Source link names wrong build ▶
  13. P2 Update result overstates the answer ▶
  14. P2 Release notes are missing ▶
  15. P2 Empty controls lack contrast ▶
  16. P2 Hovered links lose contrast ▶

Summary

The PR gives the desktop windows a shared light and dark theme, adds app menus and keyboard shortcuts, and checks for updates at launch and every five hours. It also adds desktop Cloud conversion and changes Add files to open Quick convert.

  • Cloud needs fixes for format choices, page names, expiring download links, and Stop during stalled transfers.
  • Smaller follow-ups cover final cancellation, revoked account state, and privacy text.
  • The removed defaults strip clears its previous clipping finding. The resolved switch-label issue is also fixed.
  • No tests or desktop session were run during this review.

Diagram

sequenceDiagram
    participant User
    participant Desktop as Quick convert
    participant Site as convt.app
    participant Cloud as Cloud API
    participant Storage
    User->>Desktop: Pick Cloud and Convert
    Desktop->>Site: Request credential with device token
    Site->>Site: Check device and paid Pro
    Site-->>Desktop: Five-minute credential
    Desktop->>Cloud: Create job
    Cloud-->>Desktop: Upload link
    Desktop->>Storage: Upload file
    Desktop->>Cloud: Start and poll job
    Cloud-->>Desktop: Output names and short-lived links
    loop Each output
        Desktop->>Storage: Download into staging
    end
    Desktop->>Desktop: Choose page names and publish files
Loading

Reviews (16) · Last reviewed commit: "Build the app tests' sample addresses at..." · Reviewed by Greptile

Comment on lines +483 to +490
return div()
.id("activity")
.flex()
.flex_col()
.flex_1()
.min_h_0()
.children(notices)
.child(self.empty_state(finder_off, p, 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 errors cannot scroll

The empty activity branch no longer scrolls. Dropping several unreadable folders can produce more error lines than the window can show: add() joins every folder error into one message, but users cannot reach the lower lines. Keep this branch scrollable, as the populated branch still is.

Finding confidence: 5/5.

Fix in Claude Code

let items = choices.into_iter().map(|choice| {
let on_pick = on_pick.clone();
let pick = choice.id.clone();
let on = choice.label == current;

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 Background checks the wrong choice

The new menu check matches the displayed label rather than the selected choice. In Background, an unset value shows its default, such as White for JPEG. Selecting Automatic therefore leaves the check beside White, even though those choices behave differently when the target changes. Pass the selected choice ID separately from the displayed value.

Finding confidence: 4/5.

Fix in Claude Code

Comment on lines +593 to +594
.bg(p.green)
.child(icon(IconName::Check, 11., c(0xFFFFFF)))

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 Dark-mode checks lack contrast

Checked controls now use the brighter dark-mode p.green behind a white check. White against #3FCB84 has about 2.1:1 contrast, below the 3:1 target for control marks. The new selected-format check in Quick convert uses the same pair. Use a darker control fill or a dark check in dark mode.

Finding 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

Both palettes now take their colors from apps/web/src/styles.css (light
:root and .dark), with chip tokens for neutral badges, the web's button
ring and shadows, and a darker primary fill so white labels reach AA.
A test checks every text color against every background for 4.5:1.

Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
cursoragent and others added 3 commits October 7, 2026 21:44
Automatic checks (on by default) now run at every launch and every 5
hours while the app runs; Check now runs whether or not they are on.
The last answered check is saved as update_checked_at. Settings, the
network disclosure, the privacy page and the docs say the same.

Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
The card shows this version, its build date and when convt last
checked, with Check now (a spinner and "Checking…" while it runs). A
newer build gets a callout with its version, Release notes and Download
(or Renew), and the switch for automatic checks sits below. Buttons
gain a loading state.

Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
convt, File, Edit, Window and Help menus with the usual shortcuts, set
only on macOS; the actions are registered everywhere. Check for
Updates… checks now and shows Settings at the Updates card. Help links
to the docs, the changelog and the contact page. Handlers run deferred
so an action dispatched through the key window can bring it forward or
close it. Adds a changeset.

Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
chip: c(0xF5F7F6), // --chip
chip_border: c(0xE0E3E1), // --chip-line
text: c(0x0A0A0A), // --ink
secondary: c(0x6B6F6D), // --ink-2

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 Unselected choices lack text contrast

In light mode, unselected Settings tabs and Quick convert quality choices now draw p.secondary (#6B6F6D) on p.track (#E6E8E7). This gives 4.14:1 contrast, below the stated 4.5:1 text target, making these choices harder to read. The new text_meets_aa test misses this because it excludes p.track.

Darken this text or lighten its background, and test this pair. Finding confidence: 5/5.

Fix in Claude Code

"Release notes",
release_notes_url(VERSION),
))
.child(link("about-source", "Source code", SOURCE_URL.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 Source link names wrong build

The new About window’s “Source code” button opens the repository’s current default branch, not the source for the installed release. Someone looking for their release’s source gets a newer, changing code tree instead.

docs/plan.md requires About to link to matching source, and releases already carry a version-specific source archive. Use a source link tied to this build instead of SOURCE_URL. Finding confidence: 5/5.

Fix in Claude Code

.into_any_element(),
Update::UpToDate => note(
lead(IconName::CircleCheck, p.green_text),
format!("You're up to date. convt {VERSION} is the newest version."),

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 Update result overstates the answer

The new Update::UpToDate message says VERSION “is the newest version,” but this state only means no newer build passed the date and package checks. A later release can exist without a package for this install and still produce this message, incorrectly calling the user’s older version the newest.

Say that no newer compatible build is available instead. Finding confidence: 5/5.

Suggested change
format!("You're up to date. convt {VERSION} is the newest version."),
"No newer compatible build is available.",

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

cursoragent and others added 3 commits October 7, 2026 22:29
Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
…design-6808

# Conflicts:
#	crates/convt-app/src/ui/tests.rs

Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
Comment on lines +106 to +107
pub(super) fn release_notes_url(version: &str) -> String {
format!("https://convt.app/changelog#v{version}")

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 Release notes are missing

release_notes_url links About to #v0.2.0, and the upcoming release to #v0.2.1. The website’s changelog only contains 0.1.0, so users reach old notes rather than the version shown by the app. Publish the matching sections or link to the matching published release.

Finding 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

track: c(0x232726), // --line
toggle_off: c(0x2E3331), // --separator
// Derived: a step past --line-strong, so an empty box keeps its edge.
mark_off: c(0x5A605D),

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 Empty controls lack contrast

mark_off is now #5A605D on surface (#111312) in dark mode. That gives about 2.9:1 contrast, below the 3:1 target for control edges. Empty checkboxes and radio buttons both use this pair, making them harder to see. Lighten the outline and test this pair alongside the text colors.

Finding 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

…design-6808

# Conflicts:
#	.agents/skills/test-convt-desktop/SKILL.md
#	crates/convt-app/src/ui/main_window.rs
#	crates/convt-app/src/ui/popover.rs

Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
Comment thread crates/convt-app/src/ui/main_window.rs Outdated
text(size, 16., color)
.font_weight(FontWeight::MEDIUM)
.whitespace_nowrap()
.hover(|s| s.opacity(0.75))

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 Hovered links lose contrast

The new hover effect makes small links harder to read. text_button lowers text opacity to 75%; in light mode, p.secondary on white becomes about #909391, with only about 3.1:1 contrast. This affects Remove license and other shared links. Keep their text opaque and use another hover cue. The contrast tests should cover hover states too.

Finding 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

cursoragent and others added 6 commits October 7, 2026 23:28
The General tab is taller on macOS (Finder and menu bar rows), so Release
notes sat below the fold there.

Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
Ctrl+Q and Ctrl+W did nothing on Linux and Windows, and Cmd+Q and Cmd+W
nothing on macOS before the menu bar: gpui-kit leaves them to the app and
nothing bound them. Bind secondary-q and secondary-w everywhere; the
other Mac shortcuts and the menu bar stay macOS only. A test types them
in each window, including with a text field focused, and quits from the
menu with no window open.

Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
…design-6808

Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
The SQLx CLI cache held only ~/.cargo/bin/sqlx, and the install step
skipped when sqlx was there, so a cache hit left `cargo sqlx` missing.

Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
cursoragent and others added 3 commits October 8, 2026 02:33
…y on macOS

Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
…can't add one

Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
…design-6808

Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
Comment thread crates/convt-app/src/ui/settings_window.rs Outdated
cursoragent and others added 24 commits October 8, 2026 03:08
…f in its toggled state

Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
…design-6808

# Conflicts:
#	crates/convt-app/src/ui/tests.rs
POST /api/device/cloud authenticates the device token like the other
/api/device routes, checks paid Pro with cloudAllowance, and returns the
API address and a five-minute cvt_web_ credential. It answers 403 not_pro,
503 not_configured, 401 signed_out and 429 rate_limited.
Registry::run uses it, and the desktop app's cloud jobs will publish their
downloads through it so they follow the same naming rules.
…hidden defaults

The main window's defaults bar is gone. Add files and files dropped on the
window open Quick convert, so the user picks the format. The popover's drop
bar still uses the defaults in settings.toml. The sidebar's "Files stay on
this computer" line and the empty state's format list go too.
The footer's "Runs on this computer" note becomes a This computer / Cloud
choice. Cloud is dimmed with the reason on hover until the account allows
it. The first time Cloud is picked, Quick convert asks inline that the files
upload to convt's servers and are deleted after; agreeing sets
cloud_consent. Convert with Cloud calls AppState::convert_in_cloud and shows
its error like any other. Settings now says files leave the computer only
through Cloud.
Settings: the two dropdowns share one width and font, and the notification
and reveal switches get their own "When a file is done" group instead of
repeating it under each. The preset form lists targets by kind. Quick
convert says what each image format is for instead of "Image". The
Automations note sits under the rules at a readable width.
A card above the footer looked like it covered the scrolled options, and
its Cancel sat right above the window's. The question now sits on the
footer itself, with Not now and Agree.
cloud_access() decides offline from the stored sign-in and license: Ready
needs a session and a current Pro key (a source build only the session).
convert_in_cloud() queues cloud jobs through the same Runner, Queue and
history as local ones. Each job asks convt.app for a credential with the
device token, then creates, uploads, starts, polls and downloads against
convt-server, and publishes through convt_core::Destination so results land
and are named like local ones. Stop cancels the job on the server; Retry
reruns it in the cloud; refusals read in plain words. Tests script both
APIs; nothing touches the network.
With CONVT_API_URL and CONVT_WEB_TOKEN_SECRET both set, dev-web.sh adds them
to apps/web/.dev.vars, so the dashboard converter and the desktop app's
cloud jobs can reach a local convt-server.
Quick convert's Cloud choice now runs real cloud jobs. The cloud API takes
no conversion options, so with Cloud picked Quick convert says it uses the
standard settings and converts with none.

Refs CNV-61, CNV-62
A job the app gave up on, for example after the server stayed busy while
polling, stayed queued on the server and held storage until it expired.

Refs CNV-62
The heading said Converting above a saved file while the footer said
Done. It now reads Converting, then Converted when every job succeeded,
or Finished when one failed or was cancelled.
A cloud job looked like a local one. Its row now shows a small cloud
icon and "Cloud" after the target format, for running jobs and for
history, which already kept the flag in Setup.

The icon isn't in gpui-kit's default set and the whole Lucide catalog
is megabytes, so the app's asset source serves Lucide's cloud.svg on
top of gpui-kit's. Lucide's ISC notice already ships with
gpui-kit-assets.
At 600x560 the Save card was cut off, even in Cloud mode. The window
now opens at 600x680, plus the 24px title bar macOS draws inside it,
and sections and option rows are a little tighter, so an image's three
options and Save fit without scrolling. Video options still scroll.
A machine with a valid key, or a sign-in that fetched one, was offered
"Start 7-day trial" next to "Pro until". Its plan step now says "You
have Pro" or "You have a license", shows the plan selected, and
Continue moves on without a trial or a key.
GPUI's tooltip follows the pointer and, near the window's bottom edge,
flips up onto the control, so it covered the Cloud label it explained.
A disabled choice now keeps its own hover state and draws the note in
the overlay pass, 8px above its top left corner.
The repo is public and keeps email addresses out of its sources, so the
sample sign-ins are joined with concat! instead of written out.
Comment on lines +399 to +403
pub(super) fn set_cloud(&mut self, cloud: bool, cx: &mut Context<Self>) {
self.cloud = cloud && self.cloud_access(cx).ready();
self.error = None;
cx.notify();
}

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 Cloud still needs local engines

Picking Cloud does not rebuild targets or the files returned by supported(). A Pro user opening a DOCX without LibreOffice still sees the document-pack offer and a disabled Convert button, even though the cloud supports DOCX → PDF. Build the choices and file list from the cloud’s formats when Cloud is selected.

Finding confidence: 5/5.

Fix in Claude Code

Comment on lines +451 to +454
Path::new(name)
.file_stem()
.and_then(|s| s.to_str()?.parse::<usize>().ok())
.map_or(n, |k| k.saturating_sub(1))

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 Downloaded pages get wrong names

pages_of() expects names such as 1.png, but the worker publishes input.png, input-2.png, and so on. It sorts these names before returning them, so a two-page PDF returns input-2.png before input.png. Both names fail the number parse, and the app saves page 2 as report.png and page 1 as report-2.png. Read the page number from the worker’s published names rather than using their list position.

Finding confidence: 5/5.

Fix in Claude Code

Comment on lines +545 to +556
let outputs = self.call(|api, c| api.outputs(c, id)).map_err(err)?;
if outputs.is_empty() {
return Err(fail("cloud_failed", "convt's cloud produced no file."));
}
let mut files = Vec::new();
for (i, output) in outputs.iter().enumerate() {
if cancel.is_cancelled() {
return Err(cancelled());
}
// Staged under an index, never the server's name.
let path = staging.join(format!("{i}.part"));
api.download(&output.url, &path, cancel).map_err(err)?;

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 Later download links expire

follow() gets every output link once, then downloads the files one at a time. The server signs those links for at most five minutes. If the first large output takes longer than that to download, the next request uses an expired link and the whole job fails, discarding the staged results. Refresh the output links before they expire or before each later download.

Finding confidence: 5/5.

Fix in Claude Code

Comment on lines +38 to +44
let transfers = Agent::config_builder()
.max_redirects(0)
.http_status_as_error(false)
.timeout_connect(Some(Duration::from_secs(30)))
.timeout_recv_response(Some(Duration::from_secs(300)))
.build()
.new_agent();

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 Stop cannot interrupt stalled transfers

The transfer client never receives Cancel. Upload checks it between file reads, and download checks it between blocking network reads. If storage stalls inside send(), call(), or body.read(), pressing Stop only sets a flag; it cannot end that wait. The job keeps its cloud slot and does not send the server’s cancel request until the wait returns. Use network waits that can be interrupted or that regularly check cancellation.

Finding confidence: 4/5.

Fix in Claude Code

Comment on lines +418 to +419
Ok(files) => {
let pages = pages_of(&files);

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 Stopped results still get saved

There is no cancellation check between the last download and destination.publish(). If the user presses Stop while the last file is being synced to disk, download() returns success and the app publishes the files as a completed job. Check Cancel again before publishing, as Registry::run() does for local conversions.

Finding confidence: 5/5.

Suggested change
Ok(files) => {
let pages = pages_of(&files);
Ok(files) => {
if cancel.is_cancelled() {
return Err(cancelled());
}
let pages = pages_of(&files);

Fix in Claude Code

Comment on lines +259 to +262
ApiError::SignedOut => fail(
"cloud_signed_out",
"This computer was signed out of convt.app. Sign in again to convert in the cloud.",
),

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 Revoked accounts still look signed-in

When a cloud credential request returns SignedOut, the app records a job error but keeps account.session. Cloud therefore stays enabled, while Settings still shows the account as signed in and offers no Sign in button. After a dashboard revocation, the user is told to sign in again but must first discover that they need to sign out locally. Pass this result back to AppState and clear the matching revoked session, as license renewal already does.

Finding confidence: 5/5.

Fix in Claude Code

Comment on lines +415 to +417
fn consent(&self, p: &Palette, cx: &mut Context<Self>) -> Option<AnyElement> {
if !self.jobs.is_empty() || !self.in_cloud(cx) || self.app.read(cx).settings.cloud_consent {
return None;

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 Privacy promises do not match

Once cloud_consent is saved, later cloud conversions skip the question. The privacy page still says the app “asks you each time” and that the app never uploads a file. Shipping this upload path makes those statements false. Update the desktop and cloud sections to describe uploads and the saved consent, or keep the promised per-conversion question.

Finding 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

This branch has not been deployed

No deployments
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