Imported from Sider-ai/wandesk-skill (
SKILL.md). Install upstream withnpx skills add Sider-ai/wandesk-skill. Copyright stays with the author.
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 # first run only
npm run dev # desktop UI (Vite) at http://127.0.0.1:5174, backend at 127.0.0.1:9508
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 forwardsserver/app/— the generic app capabilities:agent.ts/db.ts/proxy.ts+/x/*→ app backendsserver/apps/<id>/— an app's OPTIONAL backend (only if it needs server-side power); most apps have noneserver/runtime/— shared essentials:db.ts(the ONE sqlite)·http.ts·registry.ts·settings.tsserver/agent/— the pluggable engine kernel (Claude / Codex);runAgentauto-logs every call totasksserver/schema.sql— system tablesapps/<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.tsxis 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 ownapp_<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 thetaskstable(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 = {
// custom HTTP endpoints, mounted under /apps/<id>/x/* — frontend calls fetch('/apps/<id>/x/...')
async routes(req, res, url, ctx) {
if (ctx.sub === '/ping') { ctx.json(res, 200, { ok: true }); return true; }
return false; // not handled → framework 404s
},
// boot hook for a background loop/timer; return an optional stop()
start(ctx) { const t = setInterval(() => {/* ctx.db(...), ctx.agent(...) */}, 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.tsxfiles +style.css. - Complex — then add folders:
components/,views/,lib/, anddb.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, usingdb(appId, ...)for datacurl -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).