Skip to content

feat(windows): add Explorer conversion menus with desktop progress - #7

Merged
leoisadev1 merged 15 commits into
mainfrom
leoplayz317/cnv-11-add-the-explorer-right-click-menu-on-windows
Oct 7, 2026
Merged

leoisadev1 merged 15 commits into
mainfrom
leoplayz317/cnv-11-add-the-explorer-right-click-menu-on-windows

Conversation

@leoisadev1

@leoisadev1 leoisadev1 commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

Summary

Windows had no Explorer conversion menu. This adds a per-user Rust COM submenu for Windows 11 through a sparse MSIX identity, with classic HKCU registration for Windows 10 and Show more options. Targets come from the installed CLI registry; choosing one opens the desktop Activity view and converts beside the input without a console window.

flowchart LR
    Explorer --> Handler[ExplorerCommand DLL]
    Handler --> Targets[convt targets --menu]
    Handler --> Request[Desktop progress request]
    Request --> Activity[Activity view and conversion]
    MSI --> HKCU[Classic registration]
    MSI --> MSIX[Sparse package identity]
Loading

Evidence

Documentation before Documentation after
Windows integration plan Windows Explorer installation and verification
  • Before: Windows integration was a plan with no handler or installer registration. After: the signed MSI installed on leopc, Get-AppxPackage Convt.Desktop returned the identity, and direct DLL plus registered COM surrogate activation returned the same targets as the installed CLI for PNG, MOV, PDF, unsupported files, and mixed selections.
  • Before: reusing an Automations window left progress hidden. After: the regression test switches that existing window to Activity and completes a real PNG → JPEG conversion; 102 desktop tests pass. Removing the route change made the progress test fail, restoring it passed.
  • PASS: Windows shell tests; exact handoff arguments with Unicode, ampersands, and spaces; app/helper GUI PE subsystem; DLL unload after reference release; installed CLI PNG → JPEG; idempotent registration; rollback preserving existing identity; repair restoring missing identity; uninstall removing package, HKCU verbs, COM class, and DLL. Foreign external-directory registration is rejected and preserved.
  • PASS: bounded Linux core/engine/CLI/shell tests, 35 Linux integration tests, bun run rs:check, Windows cross-target clippy, and real SVG → PNG output signature/dimensions. Independent GPT-6.1 Sol review findings were fixed and verified.
  • NOT CHECKED: visual Explorer menu and native Windows progress window. The safety screenshot showed Deadlock active, so no GUI input was sent and the personal screenshot was deleted. Native Windows 10 and macOS were not available. Linux HEIC checks were limited by the unpatched system libheif.

Merge Danger

Door: two-way. Reverting the change and uninstalling the MSI removes its registration; the tested uninstall removes both menu paths.

Blast radius: Windows Explorer, the per-user installer, and Windows desktop conversion launch. The handler bounds CLI probes to two seconds and preserves COM apartment affinity. Linux/Finder request defaults remain unchanged. This stacks on PR #6 and must follow it.

A public Windows 11 release needs a real signing certificate matching CN=Convt. The locally generated verification certificate and trust entries were removed after testing; unsigned builds install the classic menu only.

Fixes CNV-11

Created with GPT-6.1 Sol in T3 Code.

@linear-code

linear-code Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
CNV-11 Add the Explorer right-click menu on Windows

Problem

On Windows, right-clicking a file shows no convt menu. integrations/windows holds only a README plan. Right-click converting is the main way people are meant to use convt.

Goal

"Convert with convt" in the Explorer context menu on Windows 11 (the new top-level menu) and Windows 10 (the classic menu), with the same targets as convt targets <file> --menu, handing the job to the desktop app like the Finder and Linux menus do.

Acceptance criteria

  • Windows 11: an IExplorerCommand handler (Rust, windows-rs) registered through a sparse MSIX package shows a submenu of targets for the selected files.
  • Windows 10: classic registry verbs as a fallback.
  • Choosing a target opens the desktop app with progress, not a console window.
  • The installer registers the menu and the uninstaller removes it.

How to verify

Install the MSI on Windows 11, right-click a PNG, a MOV and a PDF in Explorer, pick a target, and confirm the app converts it and the output appears next to the input. Uninstall and confirm the menu is gone.

Review in Linear

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

Devin Review

Comment thread crates/convt-shell/src/windows.rs Resolved
Comment thread crates/convt-shell/src/windows.rs Resolved
Comment thread crates/convt-app/src/ui/mod.rs Resolved

@graphify-labs graphify-labs 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.

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 3 advisory finding(s) below merit a look before merge.


Graphify review — findings

Adds a Windows Explorer context-menu handler (convt-shell, an IExplorerCommand COM DLL) that offers only the targets every selected file supports, in menu order, and hands the selection to convt-app with --show-progress. When show_progress is set, route brings up the main window on the Activity page while the job converts beside the input; if the silent conversion can't run, it still falls back to Quick convert. On Windows the app now builds as a GUI-subsystem binary and spawns ffmpeg and other tools with CREATE_NO_WINDOW, so no console windows flash.

Worth a look

  • Probe timeout or failure is cached as 'no targets' for 30s, hiding the menu — crates/convt-shell/src/windows.rs:105 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Probe can block past its deadline because reader.join() has no timeout — crates/convt-shell/src/windows.rs:141 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • windows_subsystem="windows" silently breaks CLI output (--help, --version, errors) on Windows — crates/convt-app/src/main.rs:4 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review

Review partial — this diff was larger than one review pass covers, so later files were not reviewed; some findings may be missing.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 295 functions depend on the 292 functions this change touches.

Health — this change adds coupling hotspots:

  • new: parse_url() — 7 callers, 3 callees
  • new: parse_args() — 5 callers, 3 callees
  • new: run() — 1 callers, 10 callees
  • new: route() — 1 callers, 6 callees
  • new: a_document_offers_the_pack_and_downloads_only_after_the_click() — 0 callers, 10 callees
  • new: a_covered_update_shows_and_opens_the_download_page() — 0 callers, 9 callees
  • new: an_ended_trial_stops_conversions_until_a_license_is_entered() — 0 callers, 9 callees
  • new: a_license_older_than_the_build_says_so() — 0 callers, 8 callees
  • …and 28 more — each is listed as a finding

Verification — 295 functions in the blast radius were not formally verified this run (proofs are advisory here).

Health delta baseline: last indexed commit 0fdb80f (diverged from this PR's base — delta is approximate).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 295 function(s) in the blast radius were not formally verified this run

Docs that may be stale (advisory)

…and 10 more.

· 1 grounded finding(s) anchored inline below; 35 more finding(s) on lines outside this diff (see the check run).

Comment thread crates/convt-app/src/ui/tests.rs Resolved
@greptile-apps

greptile-apps Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 1/5 Tier: apex

[High risk] Adds Windows Explorer context menu integration with new COM handler.

Not ready to merge: the installer still does not enable the menus, and Windows request delivery and menu timing still need fixes.

Fix All in Claude CodeFindings

  1. P1 Installer adds no menu ▶
  2. P1 One request runs repeatedly ▶
  3. P1 Explorer writes to wrong folder ▶
  4. P1 Menu waits for repeated probes ▶
  5. P2 Reopening changes the page ▶
  6. P2 Probe closes COM too early ▶
  7. P2 Failed launch leaves a job ▶
  8. P2 Reused progress window is untested ▶

Summary

Adds the convt-shell Explorer command handler, CLI-based target lists, a Windows request inbox, and Activity progress for selected conversions.

  • Outputs remain beside the input files. Windows conversion tools run without console windows.
  • leoisadev1 acknowledges that a public Windows 11 release needs a matching signing certificate. Unsigned builds are intended to use only the classic menu.
  • leoisadev1 deferred native Explorer and progress-window checks because the desktop was in use. Windows 10 and macOS test environments were unavailable.
  • Five earlier threads were manually resolved without explanatory replies.
  • This review checked source code only; it did not run Windows or desktop checks.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
    Explorer[Explorer command] --> CLI[Installed CLI target query]
    CLI --> Menu[Shared conversion targets]
    Menu --> Inbox[JSON request file]
    Inbox --> Listener[Windows app listener]
    Menu --> Running{App mutex exists?}
    Running -->|No| Launch[Start desktop app]
    Launch --> Listener
    Listener --> Route[Route progress request]
    Route --> Activity[Open Activity]
    Route --> Convert[Convert beside input]
Loading

Reviews (6) · Last reviewed commit: "fix(windows): make the single-instance h..." · Reviewed by Greptile

Comment thread crates/convt-shell/src/windows.rs Resolved
Comment thread crates/convt-shell/src/windows.rs Resolved
Comment thread crates/convt-shell/src/windows.rs Resolved
Comment thread packaging/windows/convt.wxs Resolved
@leoisadev1
leoisadev1 force-pushed the leoplayz317/cnv-10-make-every-conversion-work-in-the-windows-build branch from d2f41c7 to bb5dd6b Compare October 6, 2026 23:19
@leoisadev1
leoisadev1 force-pushed the leoplayz317/cnv-11-add-the-explorer-right-click-menu-on-windows branch from 5b92899 to 5590800 Compare October 6, 2026 23:20
Comment thread crates/convt-app/src/instance.rs Resolved
# Windows Explorer menu

Windows 11 shows third-party items in the new compact context menu only through an `IExplorerCommand` COM handler registered by a packaged app. Classic registry verbs (`HKCR\*\shell`) still work, but only under "Show more options".
The per-user MSI installs **Convert with convt** in Explorer. Windows 11's compact menu uses a sparse MSIX identity and the Rust `convt-shell` COM handler. Windows 10 and **Show more options** use the same handler through classic HKCU registry verbs. No administrator privileges are needed for those registry entries.

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 Installer adds no menu

Following the documented MSI installation does not enable the new Explorer menu. build.ps1 still builds and copies only convt.exe and convt-app.exe. convt.wxs contains no Explorer or COM registration, and the checkout has no sparse package manifest or registration helper.

Add the DLL build, payload copy, and registration steps rather than leaving them only in this description.

Confidence this needs a change: 5/5.

Fix in Claude Code

Comment on lines +207 to +210
let bytes = std::fs::read(&path);
let _ = std::fs::remove_file(&path);
match bytes.and_then(|bytes| serde_json::from_slice(&bytes).map_err(std::io::Error::other)) {
Ok(request) => on_request(request),

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 One request runs repeatedly

If another process holds the request JSON file open without allowing deletion, the Windows listener still reads and dispatches it after remove_file fails. The same file is then read and run again every 50 milliseconds, so one menu click can queue repeated conversions.

Claim each file once and require successful removal or a completed marker before dispatching it.

Confidence this needs a change: 5/5.

Fix in Claude Code

Comment on lines +125 to +129
fn request_dir() -> PathBuf {
std::env::var_os("CONVT_RUNTIME_DIR")
.map(PathBuf::from)
.unwrap_or_else(|| std::env::temp_dir().join("convt"))
}

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 Explorer writes to wrong folder

If XDG_RUNTIME_DIR is set on Windows and CONVT_RUNTIME_DIR is unset, request_dir and the app's runtime_dir choose different folders. The app reads $XDG_RUNTIME_DIR/convt, but Explorer writes to the temporary directory. Choosing a menu target therefore never delivers the conversion to the app.

Use the same folder rules on both sides.

Confidence this needs a change: 5/5.

Suggested change
fn request_dir() -> PathBuf {
std::env::var_os("CONVT_RUNTIME_DIR")
.map(PathBuf::from)
.unwrap_or_else(|| std::env::temp_dir().join("convt"))
}
fn request_dir() -> PathBuf {
if let Some(dir) = std::env::var_os("CONVT_RUNTIME_DIR") {
return dir.into();
}
if let Some(dir) = std::env::var_os("XDG_RUNTIME_DIR").filter(|d| !d.is_empty()) {
return PathBuf::from(dir).join("convt");
}
std::env::temp_dir().join("convt")
}

Fix in Claude Code

Comment on lines +325 to +326
let lists: Vec<_> = paths.iter().map(|p| targets(p)).collect();
let targets = crate::common_targets(&lists);

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 Menu waits for repeated probes

Menu construction probes every selected path before checking whether any shared targets remain. Failed probes are not cached. If CLI startup repeatedly reaches the two-second deadline, selecting 256 PNG files can launch the same failing probe 256 times and make one menu call take roughly eight minutes. EnumSubCommands has the same loop.

Keep probe results for the current selection, stop when the intersection is empty, and bound the whole call.

Confidence this needs a change: 5/5.

Fix in Claude Code

Comment on lines +346 to +354
handoff(&paths, target)?;
if !app_running() {
Command::new(install_dir()?.join("convt-app.exe"))
.creation_flags(CREATE_NO_WINDOW.0)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map_err(|_| error())?;

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 Failed launch leaves a job

If starting convt-app.exe fails, Invoke leaves the completed request file behind. A later app launch scans and runs that old request without an age check, so a failed menu conversion can unexpectedly run later.

Return the request path from handoff and clean it up when launch fails.

Confidence this needs a change: 5/5.

Fix in Claude Code

Comment on lines +646 to +652
let f = Fixture::new(cx);
let png = f.png("explorer selection.png");
let mut request = cli(vec![png], Some("jpeg"), None);
request.show_progress = true;
cx.update(|cx| super::route(request, cx));
assert!(window_of::<MainView>(cx).is_some());
cx.read(|cx| assert!(!f.app.read(cx).quit_when_idle));

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 Reused progress window is untested

This test never opens an existing window on Automations and never asserts that the page changes to Activity. A fresh MainView already starts on Activity, so removing the new set_page call would still pass the test. The reused-window progress bug could return unnoticed.

Add the reused-window case and assert its page to protect the regression described in the PR.

Confidence this needs a change: 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 force-pushed the leoplayz317/cnv-11-add-the-explorer-right-click-menu-on-windows branch from a13f94d to 304d321 Compare October 7, 2026 00:34
@leoisadev1
leoisadev1 changed the base branch from leoplayz317/cnv-10-make-every-conversion-work-in-the-windows-build to main October 7, 2026 00:55
@leoisadev1
leoisadev1 force-pushed the leoplayz317/cnv-11-add-the-explorer-right-click-menu-on-windows branch from c814e1c to 279f9bf Compare October 7, 2026 00:56
if let Some((handle, view)) = &opened {
let _ = handle.update(cx, |_, window, _| window.activate_window());
view.update(cx, |view, cx| {
view.set_page(main_window::Page::Activity, 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 Reopening changes the page

Reopening the app switches an existing Automations window to Activity, even without an Explorer request. app.on_reopen(ui::show_main) reaches this unconditional set_page on Linux and macOS too. Move the page change into the request.show_progress branch so normal reopen keeps the user's page.

Confidence: 5/5.

Fix in Claude Code

}
// Deliberately do not FreeLibrary: COM references still own DLL code.
drop(OsString::from_wide(&[]));
CoUninitialize();

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 Probe closes COM too early

The --registered probe calls CoUninitialize while root still holds the remote COM command and items is still alive. Rust drops both after this block, so their final Release calls run after COM has shut down. This can crash cleanup after otherwise successful checks. Drop these references before CoUninitialize, or use a guard that is dropped last.

Confidence: 4/5.

Suggested change
CoUninitialize();
drop(items);
drop(root);
CoUninitialize();

Fix in Claude Code

@leoisadev1
leoisadev1 merged commit 02bac27 into main Oct 7, 2026
7 of 8 checks passed
@leoisadev1
leoisadev1 deleted the leoplayz317/cnv-11-add-the-explorer-right-click-menu-on-windows branch October 7, 2026 01:21
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.

1 participant