Skip to content

feat(desktop): download ffmpeg, ffprobe and wtsexporter into the Tools Directory at start (#1053) - #2001

Merged
mbeisser1 merged 23 commits into
mainfrom
feat/1053-tools-download
Oct 8, 2026
Merged

mbeisser1 merged 23 commits into
mainfrom
feat/1053-tools-download

Conversation

@mbeisser1

Copy link
Copy Markdown
Member

Part of #1053 (step 3). It does not close the issue: step 4 (the Import form's message, Try again, and the wait for a download) and step 5 (the user guide) follow.

Feature Description

The desktop app downloads ffmpeg, ffprobe and wtsexporter into the Tools Directory (~/message-crate/tools) by itself. Each time it starts, a background thread checks the Tools Directory and downloads what is missing or older than the pinned release. Login and browsing don't wait. With no internet the app starts and works, the failure is shown in Settings → System → Media, and the download is tried again at the next start.

  • ffmpeg and ffprobe come from eugeneware/ffmpeg-static release b6.1.1. Neither is downloaded when both are on PATH (the step-2 lookup).
  • wtsexporter comes from messagecrate/WhatsApp-Chat-Exporter release 0.13.0-mc.2. It is never looked for on PATH.
  • Each file is checked against a SHA-256 the app carries. A file that doesn't match is deleted, and the failure says the checksum did not match.
  • A file is replaced only after its successor passed. The download goes to a temporary name in the Tools Directory, is checked, unpacked when gzipped, made executable on Unix, and then renamed over the old file.

User Story

As a person who installs the desktop app on a computer without ffmpeg or wtsexporter, I want the app to fetch them by itself, so that a WhatsApp import and Convert work with nothing else installed and nothing clicked.

Implementation Details

The record of what the app wrote. tools/manifest.json holds, per program, the release, asset, SHA-256 and the size of the file written. A file whose size matches its entry is the app's. It is current when the entry names the pinned release, so no program is read again at start-up, and a newer pin replaces it. A file with no entry, or whose size changed, was put there by someone else:

  • ffmpeg or ffprobe that answers -version (media::tool_in_dir) is left in place, such as one linked in from Homebrew. One that doesn't run is downloaded over.
  • wtsexporter is kept, and entered in the record, only when its SHA-256 is the pinned one. Any other wtsexporter is replaced. The issue said "unless it is wtsexporter older than the pinned release"; the release of a hand-placed wtsexporter can't be told from the file (a pipx shim, a renamed release file), and the import reads only the pinned release's JSON, so "not the pinned file" is the rule. A person offline who puts the pinned file there by hand gets it adopted. A different file stays in use until a download succeeds, because replacement waits for a passing download.

The source. Each pinned file's address is {base}/{repo}/releases/download/{tag}/{asset}. base is https://github.com in the app, and the tests point it at an httpmock server, which src-tauri already has as a dev-dependency.

The asset choice. The ffmpeg-static .gz files are downloaded rather than the plain ones, because they are a third of the size (19 to 30 MB against 45 to 83 MB). The checksum is over the .gz as it arrives, before it is unpacked with flate2. Windows on ARM gets the x64 ffmpeg files; whether they run there is unchecked, as the issue says. Linux on ARM gets no wtsexporter, because the fork publishes none for it.

The checksums. ffmpeg-static publishes no checksum file. Each b6.1.1 .gz asset for linux-x64, linux-arm64, darwin-x64, darwin-arm64 and win32-x64 was downloaded with curl -L on 2026-10-08 and hashed with sha256sum, and gzip -t passed on each. The wtsexporter checksums are copied from the release's SHA256SUMS asset, read on 2026-10-08. The app never fetches SHA256SUMS. All are constants in src-tauri/src/tool_downloads.rs, with a comment saying how they were made.

The HTTP client. reqwest 0.12 blocking, with the workspace's rustls features. The connect timeout is 30 seconds, and a 60-second timeout covers each read, so a stalled download fails rather than hanging. The User-Agent is MessageCrate/<build> (desktop app tool download).

The status. tools_status (tagged by state) gains downloading (received, and total or null) and downloadFailed (reason: no connection, the HTTP status with its reason phrase, checksum mismatch, or could not write). A download in progress is shown over whatever was found. A failed one is shown only when the program is not found, because a program found is still the one used. The Settings screen reads the status through TanStack Query (useQuery, keyed ["desktop", "tools-status"], not account-scoped because the programs belong to the computer) with a refetchInterval of one second while any program is downloading.

Checked by hand. A run of the downloader against the real GitHub releases for linux-x64, not committed, downloaded all three files, passed every checksum and wrote the record. The ffmpeg that arrived answered -version: the b6.1.1 linux-x64 file reports ffmpeg version 7.0.2-static (johnvansickle.com), not 6.1.1, which a reviewer may want to know. wtsexporter answered --help.

The GPL rule (ADR 0014) is not touched. The app downloads a file to the person's computer and links nothing.

macOS quarantine is unchecked. The app doesn't try to clear it, and whether a downloaded program runs on macOS without a security prompt is still unchecked, as #1053 says.

Key Files Changed

  • src-tauri/src/tool_downloads.rs (new) - the pinned releases and checksums, the record, the decision per program, the streaming download with its checksum, the replace-after-success, and the background start.
  • src-tauri/src/tool_downloads/tests.rs (new) - the checksum refusal, the replace-after-success, ffmpeg on PATH, hand-placed files, no network, and the platform table.
  • src-tauri/src/commands/tools.rs - downloading and downloadFailed states, tools_status reads the shared ToolDownloads.
  • src-tauri/src/main.rs - manages ToolDownloads and starts the check after the Tools Directory is named.
  • crates/libs/media/src/tools.rs - ffmpeg_on_path() and tool_in_dir(); testutil::locate_tools for tests.
  • web/src/screens/settings/SystemSection.tsx, web/src/lib/tauri.ts - the two new status lines, and useQuery with polling while a download runs.
  • docs/.../settings/system.md, CONTEXT.md, docs/adr/0019 - the download is built; ADR 0019 names the fork as the wtsexporter source, and why.

HTTP API Changes

  • None. The change is in a Tauri command, tools_status, which gains two states.

Database Changes

  • No schema changes
  • schema/sql/*.sql changed (every database rebuilds empty and re-imports; the fingerprint is computed, nothing to bump)

How to Test

  1. cargo test --manifest-path src-tauri/Cargo.toml tool runs the downloader tests against a local httpmock server.
  2. cd web && npx vitest run src/screens/settings/SystemSection.test.tsx covers the in-progress and failed lines and the polling.
  3. In the desktop app on a computer without ffmpeg on PATH, empty ~/message-crate/tools and start the app. Settings → System → Media shows the download's progress, then the three programs found. tools/manifest.json names the releases.
  4. With the network off, start the app with an empty Tools Directory. It starts, and Settings shows download failed with no connection as the reason.

Checklist

  • Code follows the project's style guidelines
  • Self-reviewed the code
  • Added unit tests for new functionality
  • Added integration tests where applicable
  • Existing tests pass locally
  • Updated documentation
  • ./scripts/check-pr.sh passes and formatter rewrites are committed

Also run: cargo test --manifest-path src-tauri/Cargo.toml (182 passed), cargo test -p media, cd web && npx vitest run (2134 passed), cd docs && npx astro check (0 errors).

Related Issues

Part of #1053 (step 3).

Screenshots / Demo

No browser check. The status lines appear only in the desktop app, and the Vitest tests cover the in-progress and failed rows.

Deployment Notes

New src-tauri dependencies: reqwest 0.12 (blocking, rustls; already in the workspace lockfile), sha2 0.11, hex 0.4, flate2 1.1. Not tested: the download on macOS and Windows, and the rename over a running .exe on Windows, which would fail as "could not write" and be retried at the next start.

🤖 Generated with Claude Code
https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1

…s Directory at start (#1053)

At every start the desktop app checks the Tools Directory on a thread of
its own and downloads what is missing or older than the pinned release:
ffmpeg and ffprobe from eugeneware/ffmpeg-static b6.1.1 (the .gz files,
unless both are on PATH), and wtsexporter from
messagecrate/WhatsApp-Chat-Exporter 0.13.0-mc.2. Each file is checked
against a SHA-256 the app carries; one that does not match is deleted.
A new file is written under a temporary name and renamed over the old
one only after it passed, so a failed download leaves the old one in use.

tools/manifest.json records the release, asset, checksum and size of
each file the app wrote, so a start reads no program again. ffmpeg or
ffprobe the app did not write is left alone while it answers -version;
wtsexporter the app did not write is kept only when its SHA-256 is the
pinned one.

tools_status gains the downloading and downloadFailed states, and
Settings -> System -> Media shows them, asking again each second while
a download runs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1

@mbeisser1 mbeisser1 left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Review of 485d442 (up to date with main). Findings whose line is outside the diff follow; the rest are line comments.

  • docs/src/content/docs/docs/user/import-sources/whatsapp.md — Standards · rule — whatsapp.md:11,26-38 is outside the diff and now describes the code wrongly: it says wtsexporter "must be installed separately" and tells the reader to link a pipx install into the Tools Directory. Under this PR's rule (a hand-placed wtsexporter is replaced unless it is the pinned file, system.md:62) a pipx link never matches the pinned checksum, so the app replaces it at every start. troubleshooting.md:99 ("which the desktop app doesn't include") and attachments-and-media.md:76-81 also predate the download. Step 5 rewrites these pages, but a page that now tells the reader to do something the app undoes is a broken instruction: cut the pipx step to a one-line note that the app downloads wtsexporter, or make the pages true in the minimal way.
  • docs/adr/0019-the-desktop-app-downloads-the-programs-it-needs.md:84 — Standards · rule — The ADR says a newer pin "replaces what is there, whoever put it there". The code (need, tool_downloads.rs around line 380) and system.md:59 keep a hand-placed ffmpeg or ffprobe while it runs. The ADR and the code disagree and the PR changes neither to match the other. (Spec reports the same rule as reversed; the fix is in the code, see that thread.)

Comment thread CHANGELOG.md Outdated
Comment thread docs/adr/0019-the-desktop-app-downloads-the-programs-it-needs.md Outdated
Comment thread web/src/screens/settings/SystemSection.tsx Outdated
Comment thread src-tauri/src/tool_downloads.rs Outdated
Comment thread src-tauri/src/tool_downloads.rs
Comment thread src-tauri/src/tool_downloads.rs Outdated
Comment thread src-tauri/src/tool_downloads.rs Outdated
Comment thread src-tauri/src/tool_downloads.rs
Comment thread src-tauri/src/tool_downloads.rs
Comment thread src-tauri/src/tool_downloads.rs Outdated
mbeisser1 and others added 21 commits October 8, 2026 12:29
…endment

The decision text is back as it was on main. The status line says the
download was built in #2001 and that the app records what it wrote beside
the programs. A dated amendment says wtsexporter comes from Message Crate's
fork and why. The glossary names a record beside the files, not its file
name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
…nloads/pins.rs

A newer pinned release is now an edit to one file, which holds the release
names, the per-platform asset tables and the checksums with where each came
from. The comment on the ffmpeg release says what the file is: the b6.1.1
release of ffmpeg-static, whose files report ffmpeg 7.0.2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
…r is the lookup itself

ffmpeg_on_path and resolve_tools both call searched_path instead of each
working out the search path. tool_in_dir no longer forwards to
find_tool_in_dir: the lookup is public under that name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
A file in the Tools Directory is kept only when the record names it with
the pinned release, its stamp (size, modified time and, on Unix, inode)
is the one recorded, and it runs. A file with no entry whose SHA-256 is
the pinned one is adopted. Anything else under a program's name is
replaced after a good download, whoever put it there, as #1053 and ADR
0019 say; a hand-placed ffmpeg that answers -version is no longer kept for
good. The old file stays in use until the new one passed its checksum, so
with no internet it is still the one used.

A recorded file that does not run is downloaded again, and a download that
does not run either fails with "The file was downloaded but does not run"
instead of leaving the row at missing with no reason. The record keeps the
stamp in place of the size alone, so a person's file of the same size is
not taken for the app's. Settings → System says the one rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
… left

An app closed or killed mid-download runs no clean-up, so the temporary
files under the Tools Directory stayed, up to about 110 MB each time. The
check now deletes files with its own temporary prefixes before it starts,
and the prefixes are built in one place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
…ailed line

download_missing fails every wanted program through one closure instead of
two copies of the same loop. The four download failures no longer differ
in whether they say they are retried: none of the reasons says it, and the
failed line in Settings says it once for all of them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
… start

The state was set only once a download's response headers arrived, so
Settings opened during the connect showed missing and never asked again,
and a poll in the gap between ffmpeg and the next program stopped asking
and missed the final found. Each program the check will download is now
downloading, 0 bytes of an unknown total, before the first request, and
Settings asks again while any program is downloading.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
with_download turned Unusable plus a failed download into DownloadFailed,
which hid "found in two places", the cause a person must fix. A failed
download now replaces only a missing program's line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
…ames

It failed only on the intended change, a release bump. It still checks
that each platform pins the programs it should, that each checksum is a
whole SHA-256, and the shape of each release address.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
queryKeys.ts says every cache key is built there and nowhere else. The
desktop tools status key moves there, with a note that it is this
computer's and so carries no account.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
The entry named a file path, PATH, two repository slugs and their release
tags. It now says each program comes from a pinned release of its project,
checked against a checksum the app carries, and that Settings → System
shows progress and failures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
… downloads the programs

The WhatsApp page told the reader to install wtsexporter with pipx and link
it into the Tools Directory, which the app now replaces with the pinned
file at every start. It now says the app downloads wtsexporter into its
Tools Directory and Settings → System shows it. The troubleshooting entry
and the attachments page no longer say the app doesn't include the
programs. Step 5 of #1053 rewrites these pages in full.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
The pins for ffmpeg and ffprobe are of the `.gz`, so a file in the Tools
Directory could never match one, and a lost record or a stamp that
stopped matching downloaded them again at every start. Each pin now
carries the unpacked program's SHA-256 too, the record keeps it, and a
file with no entry or a changed stamp is adopted when its SHA-256 is the
pinned program's. The download checks the unpacked program against it.

The ten unpacked hashes come from `curl -L` of each b6.1.1 asset,
`gunzip -c` and `sha256sum`, on 2026-10-08; the `.gz` hashes matched the
pins already carried.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
A recorded file of the pinned release that did not run, or the pinned
program put there by hand that did not run, was downloaded again at
every start, for good, and each download gave the same file. The check
now keeps it and says it doesn't run without downloading. A newer pin,
or a changed file, still starts a download.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
"The file was downloaded but does not run." named no program and left
nothing to do. ffmpeg and ffprobe now say to install the program with a
package manager, because the app uses the copy on PATH. wtsexporter is
never looked for on PATH, so its line points to Troubleshooting instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
Two copies of the app open at once each ran the check: the second's
sweep of leftover temporary files deleted the first's download in
progress, and both wrote the record. The check now holds an exclusive
lock on `.check.lock` in the Tools Directory while it runs, and a check
that can't take it returns at once, because the first does the work.
The lock is std's `File::try_lock`, so no crate is added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
…ated

Settings → System named `b6.1.1` and `0.13.0-mc.2`, and the doc text in
pins.rs repeated `b6.1.1`, so "a newer pinned release is a change to
this file alone" was untrue. The user page now says "a pinned release",
and the doc text names no tag. The wtsexporter tag is still in the
install hints of the WhatsApp exporter and in the developer install, so
the comment now names those places for a newer wtsexporter release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
System listed which files are replaced, including the app's own file
when it doesn't run, and WhatsApp said a copy put there by hand is
replaced. Both now say the same sentence: a program whose checksum is
the pinned program's is kept, whoever put it there, and anything else is
replaced. System also says a program that doesn't run is not downloaded
again, and that ffmpeg's and ffprobe's reason says to install them with
a package manager.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
The note ended on a link to the Homebrew copies being replaced, which
read as something to do. It now says the app opened from the Dock
doesn't see Homebrew's PATH, downloads its own copies into the Tools
Directory, and that nothing needs doing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
…it there

The page quoted the review telling a person to put ffprobe in the Tools
Directory, which the one Tools Directory rule makes stale: the app
downloads both programs there at start. The page now says so, and that
the package manager commands above install them when the download
fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
"Since 2026-10-02" named no change, unlike the other amendments, which
name the pull request. The wtsexporter the app downloads is the fork's
from #2001, the pull request that adds the download.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
… by how it got there

The reason said "was downloaded", which is untrue for a copy put in the
Tools Directory by hand whose hash matches the pin. Both cases now read
"<program> in the Tools Directory doesn't run on this computer".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gbWRfTT5AEWW1D6hTNDr1
@mbeisser1
mbeisser1 marked this pull request as ready for review October 8, 2026 16:52

@mbeisser1 mbeisser1 left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Re-review of the fix commits (Standards and Correctness, step 4 of the review), pinned to d8f8571. Every finding below is fixed in this push.

Correctness · bug src-tauri/src/tool_downloads.rs:276: a program that downloads but does not run was recorded and downloaded again at every start, for good. Fixed in 16d6ecb: a recorded pinned program that doesn't run shows the failure without downloading; a new pin or a changed stamp starts a download. Test: a second start downloads nothing.
Correctness · bug tool_downloads.rs:621: ffmpeg and ffprobe could never be adopted (their pins are the .gz hashes), so a lost record or a changed stamp re-downloaded them at every start. Fixed in d017884: each pin and the record carry the unpacked program's SHA-256 (computed from the ten b6.1.1 assets with curl -L, gunzip -c, sha256sum); a file with no entry or a changed stamp is adopted when its hash matches; the download also checks the unpacked file. Tests for adoption, re-recording and an unpacked mismatch.
Correctness · bug tool_downloads.rs:87: two app instances deleted each other's temp files and raced on the record. Fixed in fe103ef: the check holds an exclusive lock on .check.lock in the Tools Directory (std File::try_lock); a second check returns at once; a filesystem without locks runs unlocked. Two-thread test.
Correctness · nit tool_downloads.rs:398,404: "was downloaded but doesn't run" was untrue for an adopted hand-placed file. Fixed in d8f8571: " in the Tools Directory doesn't run on this computer".
Standards · judgement system.md:59-60, whatsapp.md:18: the "one rule" overstated adoption and the two pages disagreed. Fixed in 25756e0 (after d017884 the rule is one rule for all three programs).
Standards · rule pins.rs:2-3: "a change to this file alone" was untrue. Fixed in 220fc4a: the user page and the doc text no longer name the tags; the comment names the places a wtsexporter release name still appears (the install hints, AGENTS.md, contributing.md).
Standards · judgement tool_downloads.rs:374: the reason had no contraction and said nothing a person can do. Fixed in 5532870 and d8f8571: ffmpeg and ffprobe point at the package manager (the app uses a copy on PATH), wtsexporter at Troubleshooting.
Standards · judgement system.md:81: the Homebrew sentence. Fixed in 04f63d6.
Standards · judgement attachments-and-media.md:86: stale "Put it in the Tools Directory" advice. Fixed in 36fef7d.
Standards · judgement ADR 0019:92: "Since 2026-10-02". Fixed in 9698bd2: "Since #2001".

@mbeisser1

Copy link
Copy Markdown
Member Author

Review summary for d8f8571.

  • Standards: 11 findings, all Fixed.
  • Spec: 4 findings, all Fixed (the one replacement rule for all three programs; progress shown from the first moment; the ffmpeg version the b6.1.1 release really ships, checked by downloading the Linux asset: its checksum matches the one carried, the build is GPL with libx264 and libx265, and it reports ffmpeg 7.0.2; temp files of an interrupted download swept at start).
  • Correctness: 2 findings, both Fixed.
  • Re-review of the fixes: Standards 7 and Correctness 4 (a re-download loop, ffmpeg and ffprobe never adoptable, two app instances racing, and a wording nit), all Fixed, posted in the second review pinned to d8f8571.
  • Merges of the base: none; the PR was up to date with main throughout.
  • CI: the ci.yml run on d8f8571 ended in success, with no commit needed for it. The separate audit.yml "Audit npm deps" job fails on the docs site for the sharp advisory already filed as npm audit fails on the docs site: sharp below 0.35.5 (GHSA-wq5f-xc86-pv6w) #2000; this PR changes no lockfile.
  • Leftover filed: Flaky desktop test: an_unreachable_server_is_reported_without_the_media_link can lose its free port #2002, a flaky desktop test (an_unreachable_server_is_reported_without_the_media_link) that predates this PR and failed once in four local runs.
  • No user thread is open. Step 4 of The desktop app downloads ffmpeg and wtsexporter into the Tools Directory #1053 (the Import form's missing-program message, Try again, and the wait for a download) is next, as its own PR.

@mbeisser1
mbeisser1 merged commit 726a708 into main Oct 8, 2026
29 of 30 checks passed
@mbeisser1
mbeisser1 deleted the feat/1053-tools-download branch October 8, 2026 17:11
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