CASE STUDY 03 / 05
CuongThai Desktop
Desktop app
An Electron app for macOS, Windows and Linux with auto-update; it includes an AI coding tool that works inside the user’s project folder, and offline AI running locally on llama.cpp.
- Status
- Publicly released, auto-updating
- Timeline
- Since 16 Aug 2026 (first commit) — version 0.5.153
- Platforms
- macOS (Apple Silicon + Intel) · Windows · Linux
- Role
- One lead engineer: design, build, packaging, releases
Context & problem
Some things a browser cannot do: read and edit files in a project folder, run terminal commands, run an AI model with no network. The desktop app brings those to the user’s machine while sharing the web’s account and API.
The risk that comes with it: an AI agent that can read files might read a secret and send it to the model — not a byte modified, yet the secret has left the machine.
Who it serves
People learning to code
Have AI read, edit and run code right in their own project folder.
Learners without reliable internet
Ask AI about a lesson while offline.
Existing web users
The same account, notes and courses in a dedicated app.
Solution & key features
AI Code — an agent on the project folder
Read/edit files, run commands, view diffs, git, MCP, sub-agents, skills and hooks — with five permission modes from “plan only” to “ask nothing”, the last one only behind a confirmation screen.
Offline AI
Scans the machine (RAM, disk, GPU) to offer the right model and llama.cpp runtime (Metal, Vulkan, CUDA or CPU), downloads it and runs it locally.
Auto-update
electron-updater reads releases from GitHub Releases; macOS ships separate Apple Silicon and Intel builds, Windows an NSIS installer, Linux an AppImage and a deb package.
Architecture
Renderer
- ReactApp screens
- PreloadRestricted IPC bridge
IPCMain process
- AgentPath jail + blocklist
- Terminalnode-pty
- Local AIScan, download, run
- Updaterelectron-updater
HTTPSOff the machine
- cuongthai.com APIAccounts, per-user AI wallet
- LLM gatewayThrough the backend, keys hidden
- GitHub ReleasesPublic release repository
Release path
- One release commandmain branch, clean folder, no build in progress
- A never-published version
- Three platforms built in parallelGitHub Actions
- Draft releaseInvisible while files are missing
- Asset list checkMissing macOS update files stops it
Hard technical decisions & trade-offs
01Two independent barriers for the agent
- Problem
- The system prompt tells the model not to read secret files, but a clever sentence planted in a README can persuade a model. “Read-only” does not mean harmless.
- Decision
- Layer 1 — a jail: every path must sit inside the folder the user chose, checked with resolve + realpath (catching symlinks). Layer 2 — a blocklist: .env, private keys, .git, ~/.ssh, ~/.aws… cannot be read even inside the jail. “A prompt is advice; an if statement is a lock.”
- Trade-off
- Sometimes the agent cannot read a file the user really wanted it to; the user then has to grant access explicitly.
02A public release repository separate from the code
- Problem
- The code repository is private, and electron-updater downloads updates WITHOUT a token — releasing from a private repository would break auto-update.
- Decision
- Installers are released to a separate public repository; the source stays private. A release stays a draft until all three platforms have uploaded their files.
- Trade-off
- One more repository to keep in sync, and a release command with gates is needed (see incidents below).
03Offline AI: promise only what has been run
- Problem
- Seeing an NVIDIA card name does not mean llama.cpp can use it: without a Vulkan loader the runtime dies at start-up, looking exactly like a broken app. A CPU-only machine processed prompts at 33.8 tokens/second (measured on real hardware) — a 2,400-word lesson would wait 71 seconds for the first character.
- Decision
- The machine scan returns two confidence levels: “only saw the device name” and “actually ran llama-server --list-devices”. Only the latter may say “your machine is fast”. Machines without a GPU are not offered the 4B model even with spare RAM. Runtimes are tried in priority order and fall back; the llama.cpp build is pinned rather than tracking the latest.
- Trade-off
- Users on weaker machines get a smaller model than their machine “seems” able to run — a responsive experience over maximum quality.
04The agent’s default model chosen by cost INSIDE the loop
- Problem
- By single-call price, a GPT model looks cheaper than the strongest Claude model. But an agent calls tools over many rounds in one task.
- Decision
- Three runs were measured on the agent’s real loop with the same question: the gateway wraps GPT models in ~15 thousand hidden tokens per task, multiplied by the number of rounds, so inside the loop it cost 7.3× the chosen default. The in-app label states this figure.
- Trade-off
- Needs re-measuring whenever the gateway changes how it wraps models.
Real incidents & lessons
One version never shipped, another built twice
19–20/08/2026
- What happened
- 11 version bumps in 4.5 hours from several parallel work sessions: 0.5.39 was bumped but never released; 0.5.40 was built twice and the second run overwrote the files of the already-published release.
- Root cause
- Nothing compared “the number in package.json” with “the number on GitHub Releases”, and the workflow’s concurrency setting only queued runs instead of blocking them.
- Fix & safeguard
- A single release command with gates: on main, clean folder, not behind the remote, no build running, version never published; after building it re-checks the asset list. The workflow also refuses to rebuild a published release.
A release went public with files missing
17/08/2026
- What happened
- Anyone pressing “Check for updates” at that moment got a 404 for the macOS update file — reading like a broken app.
- Root cause
- Three platforms build in parallel; whichever job finished first published the release while the other two were still building.
- Fix & safeguard
- Switched to draft releases, published only when every file is present.
One account exhausting the AI quota blocked every account
11/09/2026
- What happened
- A user reported: account A hit the limit after heavy use, and account B — unused — was limited too.
- Root cause
- The old AI wallet summed every account’s spend for the day against one shared number.
- Fix & safeguard
- A per-person wallet with a sliding 5-hour window (the allowance recovers gradually, no reset cliff), with AI chat and AI Code separated because the agent spends many times more than chat.
Measured numbers
No user, revenue or client numbers here — only what can be counted from source code and the database. Each row says how it was counted so you can check it. Anything that cannot be counted is not shown.
Desktop app codebase
Counted by command on the codebase on 04/10/2026
- Commitsgit rev-list --count HEAD -- desktop
- 480
- Source files (excluding tests).ts/.tsx in desktop/src, excluding *.test.*
- 315
- Lines of code (excluding tests)wc -l over those files
- 80,248
- Test files (Vitest)*.test.* in desktop/src
- 180
- Agent modules*.ts in src/main/agent, excluding *.test.ts
- 42
- Agent permission modesCHE_DO_QUYEN constant in shared/ipc.ts
- 5
Technology
- App
- Electron 33 · React 18 · TypeScript · Vite · Tailwind CSS
- System
- node-pty · electron-updater · safeStorage · ONNX Runtime
- AI
- llama.cpp (Metal · Vulkan · CUDA · CPU) · MCP · Cổng LLM qua backend
- Packaging
- electron-builder · DMG + ZIP (arm64, x64) · NSIS · AppImage + deb · GitHub Actions