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