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
01

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.

02

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.

03

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.

04

Architecture

  1. Renderer

    • ReactApp screens
    • PreloadRestricted IPC bridge
    IPC
  2. Main process

    • AgentPath jail + blocklist
    • Terminalnode-pty
    • Local AIScan, download, run
    • Updaterelectron-updater
    HTTPS
  3. Off the machine

    • cuongthai.com APIAccounts, per-user AI wallet
    • LLM gatewayThrough the backend, keys hidden
    • GitHub ReleasesPublic release repository
The agent runs in the main process behind two independent barriers; model API keys live on the backend, never in the app.

Release path

  1. One release commandmain branch, clean folder, no build in progress
  2. A never-published version
  3. Three platforms built in parallelGitHub Actions
  4. Draft releaseInvisible while files are missing
  5. Asset list checkMissing macOS update files stops it
05

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.
06

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.
07

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
08

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