This guide is the deep reference for the lencx/ChatGPT 2.0 desktop application: its architecture, configuration, build and release pipeline, security model, and internals. It is written for developers, packagers, and administrators who need to build, deploy, audit, or extend the app. End users should start with the User Guide.
1. Executive Summary
ChatGPT is an unofficial, cross-platform desktop client that renders the official ChatGPT website (https://chatgpt.com) inside a native window built with the Tauri 2 framework. The 2.0 line is a ground-up rewrite: a compact Rust backend manages the native window, child web views, configuration persistence, theming, and OS integration, while a small React + TypeScript frontend renders the custom toolbar, the Quick Ask input, and a (placeholder) settings view inside Tauri web views.
| Attribute | Value |
|---|---|
| Project | lencx/ChatGPT |
| Version | 2.0.0 (frontend package.json); crate version 0.0.0 |
| License | AGPL-3.0 |
| Author | lencx <[email removed]> |
| Bundle identifier | com.nofwl.chatgpt |
| Product name | ChatGPT |
| Frameworks | Tauri 2 (beta), React 18, TypeScript 5, Tailwind CSS 3, Vite 5 |
| Rust edition / MSRV | 2021 / 1.77.1 |
| Target platforms | macOS, Windows, Linux |
| Source commit documented | a6de9a8 (2024-08-30) |
Note
The two version numbers are intentional: the user-facing product version comes from the frontend
package.json(2.0.0), whichtauri.conf.jsonreferences via"version": "../package.json". The Rust crate's ownversionfield is0.0.0and is not the release version.
Important
This codebase pins beta releases of Tauri 2 and its plugins (for example
@tauri-apps/api2.0.0-beta.15,tauri = "2.0.0-beta"). Anyone building or maintaining this app should expect beta-era API surfaces and pin dependencies carefully; newer stable Tauri 2 releases may require migration.
2. Architecture
2.1 High-level model
The app is a single native window (label core) that hosts three child web views, plus an optional second native window for settings.
┌─────────────────────────── Native window: "core" ───────────────────────────┐
│ │
│ Webview "titlebar" ── React UI (custom toolbar) │
│ ────────────────────────────────────────────────────────────────────── │
│ │
│ Webview "main" ── https://chatgpt.com (the actual ChatGPT website) │
│ + injected scripts: ask.js, INIT_SCRIPT │
│ │
│ ────────────────────────────────────────────────────────────────────── │
│ Webview "ask" ── React UI (Quick Ask input box, height 120px) │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────── Native window: "settings" (opened on demand) ────────────┐
│ Webview ── React UI (placeholder "Settings" view) │
└──────────────────────────────────────────────────────────────────────┘
The React bundle (index.html → src/main.tsx → src/App.tsx) is loaded into the titlebar, ask, and settings web views. App.tsx inspects getCurrentWebview().label and renders the matching component:
const viewMap = {
titlebar: <Titlebar />,
ask: <Ask />,
settings: <Settings />,
};
export default function App() {
const webview = getCurrentWebview();
return viewMap[webview.label as keyof typeof viewMap] || null;
}
The main web view is not React — it loads chatgpt.com directly. The Rust backend injects two scripts into it at startup (see §2.4).
Note
This "one window, multiple web views" layout is what lets a native toolbar and a native input box float over a remote website without modifying that website. The backend continuously repositions and resizes the three web views as the window changes.
2.2 Layout geometry
The backend computes web view positions from two constants and the current window size and scale factor:
| Constant | Value | Meaning |
|---|---|---|
TITLEBAR_HEIGHT |
28.0 |
Height (logical px) of the custom toolbar web view |
ASK_HEIGHT |
120.0 |
Height (logical px) of the Quick Ask web view when enabled (0 when disabled) |
Defined in src-tauri/src/core/constant.rs. The main web view occupies the remaining space. Platform branches differ:
- macOS: toolbar is pinned to the top (
y = 0); main view starts below it (y = TITLEBAR_HEIGHT); Quick Ask sits at the bottom. - Windows / Linux: main view starts at the top (
y = 0); toolbar and Quick Ask are stacked at the bottom.
Repositioning happens in two places: setup::init (initial build) and the window Resized event handler, both in setup.rs, and again in cmd::set_view_ask when Quick Ask is toggled.
2.3 Process / module map
src-tauri/ Rust backend (the native app)
├── build.rs Build script; sets MACOSX_DEPLOYMENT_TARGET=10.13, runs tauri_build
├── tauri.conf.json Tauri app config (product, bundle, build hooks, security)
├── Info.plist macOS bundle metadata
├── capabilities/desktop.json Capability/permission grants (ACL)
├── scripts/ask.js Userscript injected into chatgpt.com (ChatAsk class)
└── src/
├── main.rs Entry point; registers commands + setup
└── core/
├── mod.rs Module declarations
├── cmd.rs 10 #[command] handlers invoked from the frontend
├── conf.rs AppConf struct + load/save/amend/theme persistence
├── constant.rs Layout constants + INIT_SCRIPT
├── setup.rs Builds the core window, 3 web views, download + resize handlers
├── template.rs Manages the bundled ask.js script (version-aware update)
└── window.rs open_settings command (settings window)
src/ React frontend (loaded into titlebar/ask/settings web views)
├── main.tsx React root render
├── App.tsx Renders component by current web view label
├── types.d.ts AppConf + SVG TypeScript types
├── base.css Tailwind base styles
├── components/WinTitlebar.tsx Min/max/close window buttons (non-macOS)
├── hooks/useInfo.tsx Platform detection (isMac)
├── hooks/useTheme.tsx Reactive window theme
├── icons/*.tsx 16 inline SVG icon components
└── view/
├── Titlebar.tsx The toolbar UI + all invoke() calls
├── Ask.tsx Quick Ask input box
└── Settings.tsx Placeholder ("Settings")
index.html Vite entry; mounts #root and src/main.tsx
vite.config.ts Vite config (port 1420, ignores src-tauri)
tailwind.config.js Tailwind theme (custom app colors, tablet breakpoint)
2.4 Injected scripts (main web view)
Two scripts are injected into the chatgpt.com web view at initialization (setup.rs):
-
ask.js(src-tauri/scripts/ask.js) — defineswindow.ChatAskwith two static methods used by Quick Ask:ChatAsk.sync(message)— finds the page<textarea>and sets its value using the native setter, dispatching aninputevent so the page's React state updates.ChatAsk.submit()— finds the last<button>insidemain formand clicks it to send. The file carries a userscript-style header (@name,@version 0.1.0,@url) that the backend uses for version-based updates (see §6.3).
-
INIT_SCRIPT(string constant inconstant.rs) — runs onDOMContentLoadedand:- Tracks SPA navigation by wrapping
history.pushState/history.replaceStateand listening forpopstate, emitting anavigation:changeevent (with the new URL) to thetitlebarweb view. - Forces in-page links whose
targetis set (and isn't_blank) to open as_blank, so the link-handling logic routes them to the system browser.
- Tracks SPA navigation by wrapping
Important
tauri.conf.jsonsets"withGlobalTauri": true, exposingwindow.__TAURI__inside web views. The injected scripts rely on this (for example__TAURI__.webview.Webview.getByLabel('ask')andwindow.__TAURI__.webviewWindow...emit(...)). Disabling global Tauri would break navigation tracking and Quick Ask focus handling.
3. Frontend ↔ Backend Command Reference
The frontend communicates with Rust exclusively through Tauri's invoke() mechanism. All commands are registered in src-tauri/src/main.rs and defined in cmd.rs (and window.rs for open_settings).
| Command | Params | Returns | Effect |
|---|---|---|---|
view_reload |
— | — | Reloads the main web view (window.location.reload()) |
view_url |
— | tauri::Url |
Returns the current URL of the main web view |
view_go_forward |
— | — | window.history.forward() in the main web view |
view_go_back |
— | — | window.history.back() in the main web view |
window_pin |
pin: bool |
— | Persists stay_on_top and calls set_always_on_top(pin) |
ask_sync |
message: String |
— | Evaluates ChatAsk.sync(<message>) in the main view (live mirror) |
ask_send |
— | — | Evaluates ChatAsk.submit(), then refocuses the ask web view after 500 ms |
set_theme |
theme: String |
— | Persists theme, then restarts the app (app.restart()) |
get_app_conf |
— | AppConf |
Returns the current configuration object |
set_view_ask |
enabled: bool |
— | Persists ask_mode, resizes/repositions all three web views, sets focus |
open_settings |
— | — | Shows the existing settings window or builds it (window.rs) |
Note
ask_syncis debounced on the frontend (Ask.tsx, 300 ms) so keystrokes mirror into the page without flooding the bridge.ask_sendis bound to⌘/Ctrl+Enterviareact-hotkeys-hook.
3.1 Frontend wiring notes
Titlebar.tsxis the hub: on mount it callsget_app_confto hydrate toolbar state (ask_mode,stay_on_top,theme,mac_titlebar_hidden), listens fornavigation:changeto display the current hostname, and listens fortauri://resizeto track full-screen state.Ask.tsxmirrors input viaask_sync(debounced) and submits viaask_send.useInfo.tsxresolves the platform through@tauri-apps/plugin-osto choose macOS vs. non-macOS behavior (toolbar padding, send shortcut).WinTitlebar.tsxprovides minimize / maximize / close for thecorewindow on non-macOS platforms by callingWindow.getByLabel('core')methods directly (no Rust command needed).- Path aliases (
~view,~icons,~hooks,~) are resolved byvite-tsconfig-pathsfromtsconfig.json.
4. Configuration
4.1 Runtime configuration (AppConf)
User preferences persist to a JSON file managed by conf.rs.
Location: <OS config dir>/com.nofwl.chatgpt/config.json
| Platform | Resolved path (typical) |
|---|---|
| macOS | ~/Library/Application Support/com.nofwl.chatgpt/config.json |
| Windows | %APPDATA%\com.nofwl.chatgpt\config.json |
| Linux | ~/.config/com.nofwl.chatgpt/config.json |
Schema (AppConf):
| Field | Type | Default | Meaning |
|---|---|---|---|
theme |
string |
"system" |
One of system, light, dark |
stay_on_top |
bool |
false |
Window pinned always-on-top |
ask_mode |
bool |
false |
Quick Ask box visible |
mac_titlebar_hidden |
bool |
true on macOS, false elsewhere |
Hide custom toolbar until hover (macOS) |
Example config.json:
{
"ask_mode": false,
"mac_titlebar_hidden": true,
"stay_on_top": false,
"theme": "system"
}
Tip
You can edit
config.jsonby hand while the app is closed to preset preferences (for example, force"theme": "dark"or enable"ask_mode": truefor a fleet). Keys are serialized in sorted order becauseamend()round-trips through aBTreeMap.
Loading and self-healing. AppConf::load:
- If the file is missing, writes defaults and returns them.
- If the file exists but fails to deserialize cleanly, it logs the error, starts from defaults, amends them with whatever valid keys were present, saves the repaired file, and returns it.
This means a malformed or partial config is repaired rather than fatal — useful when adding fields across versions. amend() merges a JSON object over the current config via a BTreeMap<String, Value> and re-validates against AppConf.
Theme resolution (get_theme). When theme is "system", the backend uses the dark-light crate to detect the OS appearance (defaulting to Light when undetermined); "dark" and "light" map directly. The resolved Theme is applied to the window at build time.
4.2 Build-time configuration (tauri.conf.json)
{
"productName": "ChatGPT",
"version": "../package.json",
"identifier": "com.nofwl.chatgpt",
"build": {
"beforeDevCommand": "pnpm dev",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "pnpm build",
"frontendDist": "../dist"
},
"app": {
"withGlobalTauri": true,
"windows": [],
"security": { "csp": null }
},
"bundle": {
"active": true,
"targets": "all",
"icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"]
}
}
| Key | Value | Notes |
|---|---|---|
build.devUrl |
http://localhost:1420 |
Must match Vite's fixed port (vite.config.ts, strictPort: true) |
build.frontendDist |
../dist |
Where pnpm build (tsc + vite build) emits the bundle |
app.windows |
[] |
No windows are declared statically — all windows are created at runtime in setup.rs / window.rs |
app.security.csp |
null |
No Content-Security-Policy is enforced by Tauri (see §7) |
bundle.targets |
all |
Produce every installer type the host toolchain supports |
4.3 Other config files
| File | Purpose |
|---|---|
vite.config.ts |
React + tsconfig-paths plugins; dev server on port 1420 (strictPort), ignores src-tauri/** |
tailwind.config.js |
Custom colors (app-gray-1/2/3, app-active: #10a37f), tablet: 480px breakpoint, typography + autoprefixer plugins |
postcss.config.js |
Tailwind + autoprefixer |
tsconfig.json / tsconfig.node.json |
TypeScript + path aliases (bundler resolution) |
rustfmt.toml |
Rust formatting (100 col, 4-space, edition 2021) |
build.rs |
Sets MACOSX_DEPLOYMENT_TARGET=10.13; invokes tauri_build::build() |
Cargo.toml (workspace) |
Release profile: panic = "abort", opt-level = "s", lto = true, codegen-units = 1 — optimized for small binary size |
5. Requirements
5.1 Build toolchain
| Tool | Version / constraint | Why |
|---|---|---|
| Rust | >= 1.77.1 (edition 2021) |
rust-version in src-tauri/Cargo.toml |
| Node.js | >= 18.18 |
Required by @tauri-apps/api engines |
| pnpm | current | Lockfile is pnpm-lock.yaml (lockfileVersion: 9.0); build hooks call pnpm |
| Tauri CLI | 2.0.0-beta.22 |
Dev dependency @tauri-apps/cli |
| Platform build deps | per Tauri prerequisites | e.g. WebView2 (Windows), webkit2gtk (Linux), Xcode CLT (macOS) |
5.2 Runtime
- A working internet connection to reach
https://chatgpt.com. - An OS web view runtime: WebView2 on Windows, WKWebView on macOS, WebKitGTK on Linux (provided/installed per Tauri's platform requirements).
- An OpenAI account for sign-in.
6. Build, Run, and Release
6.1 Development
# Install JS dependencies
pnpm install
# Run the app in dev mode (starts Vite on :1420, then launches the Tauri shell)
pnpm tauri dev
# Tauri compiles the Rust backend, Vite serves the frontend on http://localhost:1420,
# and a native ChatGPT window opens loading chatgpt.com.
The tauri dev command runs beforeDevCommand (pnpm dev) automatically and points the shell at devUrl.
Tip
The frontend alone can be served with
pnpm dev, but the app is only meaningful inside the Tauri shell —getCurrentWebview(),invoke(), and the OS plugins require the Tauri runtime.
6.2 Production build
# Type-check + build the frontend, then bundle native installers for the host platform
pnpm tauri build
# Output installers are written under src-tauri/target/release/bundle/<type>/.
# "targets": "all" produces every bundle type the host toolchain can build.
tauri build runs beforeBuildCommand (pnpm build, i.e. tsc && vite build) to emit ../dist, then compiles the optimized Rust binary using the size-tuned release profile.
Note
Cross-platform installers are produced by building on each target OS (and/or CI), not from a single host. The published Releases cover macOS, Windows, and Linux.
6.3 The bundled ask.js lifecycle
template.rs embeds scripts/ask.js at compile time (include_bytes!) and, at startup (Template::new, called from setup::init), writes/updates a working copy into <config dir>/com.nofwl.chatgpt/scripts/ask.js:
- It parses the
@versionheader from both the bundled and the on-disk copies (via regex). - It overwrites the on-disk file only when the bundled version is newer (semver comparison), when the on-disk version is missing/unparseable, or when the file doesn't exist.
At web view creation, AppConf::load_script(&handle, "ask.js") reads that on-disk copy and injects it as an initialization script.
Important
Because the runtime injects the on-disk
scripts/ask.js, advanced users can patch Quick Ask behavior by editing that file — but on the next launch the bundled version will overwrite it if its@versionis greater. To make a sticky local change, bump the@versionin your edited copy above the bundled value.
7. Security
7.1 Capability / permission model
Tauri 2 gates backend functionality behind an explicit allowlist. capabilities/desktop.json grants a broad set:
{
"identifier": "desktop-capability",
"windows": ["*"],
"remote": { "urls": ["https://chatgpt.com/*"] },
"platforms": ["linux", "macOS", "windows"],
"permissions": [
"window:default", "window:allow-create", "window:allow-start-dragging",
"window:allow-toggle-maximize", "window:allow-minimize", "window:allow-close",
"webview:default", "webview:allow-internal-toggle-devtools",
"webview:allow-set-webview-zoom", "webview:allow-create-webview",
"webview:allow-create-webview-window", "webview:allow-set-webview-focus",
"event:default", "event:allow-emit", "event:allow-emit-to",
"event:allow-listen", "event:allow-unlisten",
"shell:default", "shell:allow-execute", "shell:allow-open",
"os:allow-arch", "os:allow-platform", "os:allow-version",
"os:allow-os-type", "os:default", "shell:default"
]
}
| Permission group | Grants | Used by |
|---|---|---|
window:* |
Create, drag, maximize, minimize, close windows | Toolbar, WinTitlebar, settings window |
webview:* |
Create web views/windows, set zoom, focus, toggle devtools | Multi-web-view layout, Quick Ask focus |
event:* |
Emit/listen to events | navigation:change, resize tracking |
shell:allow-open |
Open URLs/files with the OS default handler | Address-chip "open in browser", auto-open downloads |
shell:allow-execute |
Execute shell commands | Granted via the shell plugin |
os:* |
Query arch/platform/version/type | useInfo platform detection |
Caution
The
remote.urlsACL (https://chatgpt.com/*) is what scopes these capabilities to remote content loaded from ChatGPT. Combined withwithGlobalTauri: true, the remote ChatGPT page haswindow.__TAURI__available within the bounds of the granted permissions. This is required for the injected scripts to function, but it also means the trust boundary includes whateverchatgpt.comserves. Anyone forking this app should review whethershell:allow-executeis actually needed (the code paths observed use onlyshell:allow-open), and consider tighteningremote.urlsand removing unused permissions.
Warning
app.security.cspisnull, so Tauri enforces no Content-Security-Policy. The app relies on loading onlychatgpt.com. Hardening deployments should consider defining a CSP and pruning the permission set to the minimum the features require.
7.2 Download handling
The main web view registers an on_download handler (setup.rs):
- On
DownloadEvent::Requested, the destination is rewritten to the OS Downloads directory (path().download_dir()), so files always land there. - On
DownloadEvent::Finished { success: true, .. }, the file is opened with the OS default application via the shell plugin.
Caution
Auto-opening downloaded files is a convenience that carries risk: any file ChatGPT (or a page served under the allowed origin) hands to the download flow is opened automatically on completion. In higher-security environments, consider disabling auto-open and requiring an explicit user action.
7.3 Data handling
The app stores no conversation data itself. Authentication and chat history live in the embedded web view's session storage and in the user's OpenAI account. The only local persistence is config.json (UI preferences) and the working copy of scripts/ask.js.
7.4 Platform notes
- On Windows release builds,
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]suppresses the extra console window. Do not remove this attribute. build.rssetsMACOSX_DEPLOYMENT_TARGET=10.13for macOS compatibility.- Builds are unsigned by default; distribution requires the operator to handle code signing / notarization if desired.
8. Operations & Troubleshooting
8.1 Resetting an installation
# Close the app first, then remove its config directory to restore defaults.
# macOS
rm -rf ~/Library/Application\ Support/com.nofwl.chatgpt
# Linux
rm -rf ~/.config/com.nofwl.chatgpt
# On next launch, AppConf::load() recreates config.json with defaults
# and template.rs re-creates scripts/ask.js from the bundled copy.
On Windows, delete %APPDATA%\com.nofwl.chatgpt.
8.2 Known issues and developer-facing notes
These are drawn from the community issue tracker. Several reported issues reference other ChatGPT clients (including Electron-based and official apps) and do not apply to this Tauri 2 codebase — triage accordingly.
| Symptom | Likely area | Notes |
|---|---|---|
| Session expired / login fails (macOS) | Embedded web view session | Reload the main view; sessions behave like browser sessions |
Zoom shortcuts (Ctrl +/-/0) inert |
No global zoom shortcut wired | webview:allow-set-webview-zoom is granted but not bound to a shortcut/command in this revision |
| Multiple windows on link click | Link handling | INIT_SCRIPT forces targeted links to _blank; these are routed to the system browser |
| Context-menu crash on previous chats | Upstream / web view | Originates in the embedded page; reproduce at chatgpt.com to confirm scope |
| Inline LaTeX not rendering | Upstream ChatGPT site | Not controlled by this app |
Tip
When triaging a bug report, first determine whether it reproduces in a plain browser at
chatgpt.com. If it does, it belongs to OpenAI's site, not this wrapper. Only behavior tied to the toolbar, Quick Ask, theming, pin, downloads, window management, or config belongs to this codebase.
8.3 Logging
The backend uses the log crate (info! / error!) in conf.rs and template.rs, and eprintln! for view-positioning failures. Run pnpm tauri dev to see backend output in the terminal during development; release builds suppress the console on Windows.
9. Code Quality & Conventions
- Separation of concerns: layout math lives in
setup.rs/cmd.rs, persistence inconf.rs, constants inconstant.rs, script lifecycle intemplate.rs, and window creation inwindow.rs. The frontend keeps all backend calls inview/Titlebar.tsxandview/Ask.tsx. - Configuration robustness:
AppConfself-heals malformed configs rather than crashing (§4.1). - Size-tuned release profile:
panic = "abort",opt-level = "s",lto = true,codegen-units = 1. - Error handling pattern: backend command handlers favor
.unwrap()on Tauri/web view calls. This is concise but means an unexpectedNone/Err(for example a missing web view) will panic the command. View-positioning is the exception — it logs viaeprintln!and continues. template.rsusesanyhowfor ergonomic error context;conf.rsreturnsBox<dyn std::error::Error>.
Note
AST extraction over this repo surfaced 20 functions and 2 types (
AppConf,Template); there are no CLI commands, HTTP endpoints, or server-side config keys — consistent with a thin desktop wrapper whose "API" is the set of Tauri commands documented in §3.
10. Potential Improvements
These are observations for maintainers, derived from the source as documented.
- [ ] Tighten the capability set.
shell:allow-executeis granted but no observed code path executes shell commands; onlyshell:allow-openis used. Removing unused permissions reduces the attack surface. - [ ] Define a CSP.
security.cspisnull; an explicit policy would harden the remote-content trust boundary. - [ ] Reduce
.unwrap()in command handlers. ReturningResultand surfacing errors to the frontend would make failures (missing web view, config I/O) recoverable instead of fatal. - [ ] Finish the Settings window.
view/Settings.tsxis a placeholder; surfacingAppConffields (and future options) there would remove the need to hand-editconfig.json. - [ ] Wire zoom shortcuts. The zoom permission is granted but not bound; binding
Ctrl/⌘+/-/0toset_webview_zoomwould address a recurring request. - [ ] Gate download auto-open. Make the auto-open-on-finish behavior configurable for security-sensitive users.
- [ ] Reconsider the dual version numbers. The crate version is
0.0.0; aligning it with the product version would reduce confusion in build artifacts and logs.
11. Quick Reference
| Item | Value |
|---|---|
| Bundle identifier | com.nofwl.chatgpt |
| Config file | <OS config dir>/com.nofwl.chatgpt/config.json |
| Injected script (working copy) | <OS config dir>/com.nofwl.chatgpt/scripts/ask.js |
| Dev server | http://localhost:1420 (fixed, strictPort) |
| Frontend build output | dist/ (tsc && vite build) |
| Native build output | src-tauri/target/release/bundle/ |
| Embedded site | https://chatgpt.com |
| Backend commands | 11 (10 in cmd.rs, 1 in window.rs) |
| Web views | main, titlebar, ask (in core) + settings window |
| Layout constants | TITLEBAR_HEIGHT = 28, ASK_HEIGHT = 120 |
| License | AGPL-3.0 |
Build & run cheat-sheet:
pnpm install # install JS deps
pnpm tauri dev # develop (Vite :1420 + native shell)
pnpm tauri build # produce native installers
pnpm dev # frontend only (Vite)
pnpm build # tsc + vite build → dist/