| name | wandesk |
| description | Open and operate Wandesk โ a local desktop where you build the user small apps. Use when the user wants a persistent app, a dashboard, a tracker, a tool with a UI and saved data, or asks to open/manage their desk. |
Wandesk โ a desk for your AI agent
Wandesk is a local desktop the user runs in their browser. You (the agent โ Claude Code, or
Codex) build and operate the apps on it. There is no AI inside Wandesk โ you are the
intelligence; Wandesk is the workspace, the storage, and the UI.
Boot the desk
npm install
npm run dev
Then open it for the user โ don't just print the URL; put the desk on their screen
(macOS open http://127.0.0.1:5174, Linux xdg-open โฆ, Windows start โฆ; if you can't
open a browser, tell them the address as a fallback).
(Production: npm run build && npm start โ everything on http://127.0.0.1:9508.)
Architecture in one breath
One backend process, one database (database/wandesk.db), apps are declaration + frontend:
server/index.ts โ boot + prefix routing(/api โ api/,/apps โ app/,else static)
server/api/ โ system API, split per route, every layer has an index that only forwards
server/app/ โ the generic app capabilities: agent.ts / db.ts / proxy.ts + /x/* โ app backends
server/apps/<id>/ โ an app's OPTIONAL backend (only if it needs server-side power); most apps have none
server/runtime/ โ shared essentials: db.ts(the ONE sqlite)ยท http.ts ยท registry.ts ยท settings.ts
server/agent/ โ the pluggable engine kernel (Claude / Codex); runAgent auto-logs every call to tasks
server/schema.sql โ system tables
apps/<id>/ โ an app's DECLARATION: app.json + schema.sql + APP.md(no code)
ui/src/apps/<id>/ โ the app's React frontend; ui/src/system/ โ the desktop shell
What an app is
An app is one id, a declaration and a frontend:
apps/<id>/ โ app.json(manifest)+ schema.sql(its tables)+ APP.md(notes)
ui/src/apps/<id>/ โ React; index.tsx is the entry, mounted by the desktop:
export default function ({ appId }) { โฆ }.
The app id is passed to the component by the desktop. Never hardcode it in generic db/agent/proxy calls.
One database, prefixed tables
ALL data lives in database/wandesk.db. Naming rule:
- app tables:
app_<id>_*(e.g. app_notes_pages;kebab-case ids use underscores in table names)
- system tables(no prefix, owned by the system):
tasks(every AI call)ยท chats + messages(Assistant sidebar)ยท settings(kv, GETยทPOST /api/settings)
An app only touches its own app_<id>_* tables. Schemas are executed idempotently at boot
and on POST /api/reload.
Capabilities an app can call
From the frontend, via thin clients in ui/src/system/lib/:
db(appId, sql, params) โ run SQL on wandesk.db(convention: your own app_<id>_* tables only)
agent(appId, prompt, { data, system, conversationId, model, schema }) โ one agent turn on the
configured engine (Claude or Codex), the user's own login, no key; use for any AI/generation.
Every call is recorded in the tasks table(watch it in ไปปๅก็ฎก็ๅจ, taskbar right-click)
proxy(appId, url, opts) โ CORS-free outbound fetch
import { db } from '../../system/lib/db';
await db(appId, `SELECT * FROM app_${appId}_items ORDER BY id DESC`);
await db(appId, `INSERT INTO app_${appId}_items (text) VALUES (?)`, [text]);
Most apps stop here โ frontend + these three capabilities, no backend. Reach for a backend
ONLY when the frontend genuinely can't do it: a background loop/timer, holding a secret to call an
external API, or custom server-side endpoints. See "App backend" below.
App backend (optional โ most apps don't have one)
When an app needs server-side power, add server/apps/<id>/index.ts exporting an AppBackend
(server/apps/types.ts). It is asymmetric with the frontend: create it only when needed.
import type { AppBackend } from '../types.js';
const backend: AppBackend = {
async routes(req, res, url, ctx) {
if (ctx.sub === '/ping') { ctx.json(res, 200, { ok: true }); return true; }
return false;
},
start(ctx) { const t = setInterval(() => {}, 60_000); return () => clearInterval(t); },
};
export default backend;
ctx is the same base as the frontend: { appId, sub, db(sql,params), agent(prompt,opts), json, readBody }
โ same ONE sqlite (your app_<id>_* tables), same engine. Backends load at boot and on POST /api/reload
(newly-added ones); editing an existing backend's code needs a server restart (dev: tsx watch auto-restarts).
Note: a backend runs with full server privileges โ keep it to what truly needs the server.
Frontend layout โ a recommended convention (not enforced)
- Simple โ
index.tsx + style.css. Most apps live here.
- Moderate โ
index.tsx + a few flat sibling .tsx files + style.css.
- Complex โ then add folders:
components/, views/, lib/, and db.ts.
Rough graduation: 1โ2 around ~250โ300 lines in index.tsx; 2โ3 when flat files pile up (~5+).
Class names prefixed by the app id; lean against routing/state libraries and premature folders.
Full version: ui/src/apps/README.md.
Create an app
apps/<id>/app.json โ { "id": "<id>", "name": "...", "icon": "<emoji>" }
apps/<id>/schema.sql โ CREATE TABLE IF NOT EXISTS app_<id>_...(idempotent, prefixed!)
ui/src/apps/<id>/index.tsx (+ style.css) โ the React UI, using db(appId, ...) for data
curl -X POST http://127.0.0.1:9508/api/reload โ re-scan + provision the new tables
See apps/notes/ + ui/src/apps/notes/ for the reference unit.
Language
The product is Chinese-first: write ALL user-facing UI text directly in Chinese. There is no i18n
layer โ no tokens, no language packs. A new app's display name is just the Chinese name in its
app.json.
Engine (Claude or Codex)
The agent capability picks its engine per call: settings table engine key
(switching takes effect immediately)โ wandesk.config.json โ default claude. The settings table can also
set a default model(used only when the caller didn't pass one). system stays fully per-app โ
there is no global system prompt. Both engines ride the user's local CLI login, no API key.
Safety
Do not delete the user's database/ without asking. Make the smallest change that satisfies the
request; if something breaks, read the error and fix the app's files. Never touch tables that are
not yours (app_<yourid>_* only).