How the ChatGPT Desktop App Handles File Downloads: Inside Tauri's Download API
The ChatGPT desktop application intercepts browser downloads using Tauri’s on_download handler, redirects them to the OS default download directory, and automatically opens completed files using the Tauri shell plugin.
The open-source ChatGPT desktop client by lencx/ChatGPT is built with Tauri, a Rust-based framework that provides native webviews with deep OS integration. When users download files from the embedded ChatGPT web interface, the application handles these events at the native layer rather than delegating to the browser. This article examines the exact implementation in the Rust source code and explains how you can apply the same pattern to your own Tauri applications.
The Download Architecture
All download logic resides in src-tauri/src/core/setup.rs. The application creates the main webview using WebviewBuilder and attaches a custom download handler via the .on_download() method. This handler monitors every DownloadEvent emitted by the webview, allowing the app to override destination paths and execute post-download actions.
According to the source code, the implementation follows a three-stage pipeline: registration, path resolution, and post-processing. The handler uses an Arc<Mutex<PathBuf>> to share the download location between asynchronous event callbacks safely.
Step-by-Step Download Flow
Registering the Download Handler
The webview initialization occurs in setup.rs starting at line 59. The WebviewBuilder::new() constructor chains an on_download callback that receives all download events triggered by the embedded ChatGPT site.
// Conceptual representation based on src-tauri/src/core/setup.rs lines 59-66
WebviewBuilder::new("main", WebviewUrl::External(url))
.on_download(move |webview, event| {
// Event handling logic for Requested and Finished states
true // Keep the download alive
})
Resolving the Destination Path
When the DownloadEvent::Requested variant fires (lines 64-74), the app queries the AppHandle for the system download folder using app_handle.path().download_dir(). It then constructs the full destination path by joining the OS directory with the filename provided by the webview.
The code stores this absolute path in a thread-safe Arc<Mutex<PathBuf>> named download_path, then mutates the event’s destination field to redirect the file to the OS default location. This ensures all downloads land in the user’s standard Downloads folder regardless of the webapp’s suggested path.
Post-Download Automation
Upon receiving DownloadEvent::Finished (lines 75-86), the handler checks the success flag. If the download completed successfully, it retrieves the stored path from the mutex and invokes the Tauri shell plugin to open the file:
// From the Finished event handler (lines 75-86)
if success {
let path = download_path.lock().unwrap().clone();
app_handle.shell().open(path, None).ok();
}
This shell().open() call launches the file with the system’s default application, mimicking the behavior of desktop browsers that auto-open files from the download notification.
Thread-Safe State Management
Because Tauri executes the download callback on a background thread, the implementation wraps the PathBuf in Arc<Mutex<_>>. This atomic reference counting with mutual exclusion allows both the Requested and Finished event handlers to access and modify the download path without data races. The Arc ensures shared ownership across the closure boundary, while the Mutex guarantees exclusive access during writes.
Implementation Example
You can reproduce this pattern in your own Tauri application using the following TypeScript-style pseudo-code, which mirrors the Rust logic found in setup.rs:
import { WebviewBuilder, DownloadEvent } from '@tauri-apps/api/webview';
import { app, shell } from '@tauri-apps/api';
const downloadStore = new Map<string, string>();
WebviewBuilder.new('main', 'https://chat.openai.com')
.on_download(async (_, event) => {
if (event.type === DownloadEvent.Requested) {
// Force download to OS default directory
const downloadDir = await app.path('download');
const finalPath = `${downloadDir}/${event.destination}`;
downloadStore.set(event.id, finalPath);
event.destination = finalPath; // Redirect the download
}
else if (event.type === DownloadEvent.Finished && event.success) {
const filePath = downloadStore.get(event.id);
if (filePath) {
await shell.open(filePath); // Auto-open with default app
}
}
return true;
});
Key Source Files
The download handling system spans three critical files in the src-tauri directory:
-
src-tauri/src/core/setup.rs(lines 59-86): Contains theWebviewBuilderinitialization and the completeon_downloadcallback implementation that manages path resolution and file opening. -
src-tauri/src/main.rs: Bootstraps the Tauri runtime and calls the initialization function fromsetup.rsto register all handlers. -
src-tauri/Cargo.toml: Declares thetauri-plugin-shelldependency required for theshell().open()functionality that launches downloaded files.
Summary
- The ChatGPT desktop app uses Tauri’s
on_downloadcallback insrc-tauri/src/core/setup.rsto intercept all file downloads from the embedded webview. - Downloads are forced into the OS default download directory by mutating the
destinationpath during theDownloadEvent::Requestedphase. - The app stores download locations in an
Arc<Mutex<PathBuf>>to enable safe cross-thread access between the request and completion events. - Upon
DownloadEvent::Finished, the app automatically opens successful downloads usingtauri-plugin-shell, providing a native desktop experience. - This implementation requires the
shellplugin permissions inCargo.tomlto execute system file opening commands.
Frequently Asked Questions
Where does the ChatGPT desktop app save downloaded files?
The application redirects all downloads to the operating system’s default download folder, retrieved via app_handle.path().download_dir(). It ignores the webview’s suggested path and constructs an absolute path combining the OS directory with the original filename, ensuring files always appear in the standard Downloads location.
How does the app automatically open files after downloading?
When the DownloadEvent::Finished event fires with success: true, the handler calls app_handle.shell().open(path, None) using the Tauri shell plugin. This invokes the system’s default application associated with the file’s MIME type, automatically launching documents, images, or archives without manual user navigation.
Why is Arc<Mutex> used for tracking downloads?
Tauri executes download callbacks on background threads separate from the main UI thread. The Arc<Mutex<PathBuf>> wrapper enables thread-safe sharing of the download path between the Requested event (which sets the path) and the Finished event (which retrieves it). Arc provides shared ownership, while Mutex prevents race conditions during concurrent access.
Can I customize the download location in the ChatGPT desktop app?
Currently, the lencx/ChatGPT implementation hardcodes the destination to the OS download directory in setup.rs (lines 64-74). To customize locations, you would need to modify the Rust source to replace download_dir() with a user-configurable path from Tauri’s store or configuration API, then rebuild the application.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →