Skip to content

cat COLOPHON.md

How this was built,
down to the file names.

You just used a web operating system with 41 working apps, an agent platform that drives them through 399 registered tools, and a phone shell that reformats every one of them. This page is the view-source: the architecture, the decisions, the bugs that cost the most, and the numbers behind them.

Every figure below was counted from this repository by scripts/collect-stats.mjs, and every claim names the file it came from. Nothing here is estimated.

41
apps

registered in apps/registry.ts

399
agent tools

across 40 tool modules

5
model providers

anthropic · mistral · openai · perplexity

243,322
lines of TypeScript

in 1,184 files

43
zustand stores

one per app, no global blob

52
tools gated on a human

flagged destructive: true

01How it got made

A résumé you can boot,
written by a human directing swarms.

243,322 lines of TypeScript across 1,184 files is not one person typing. It is Jonathan Kumin running fleets of Claude agents in parallel — scoping each one to a directory, giving it a contract to hold, reviewing what came back, and rejecting the parts that were plausible but wrong.

That is the whole point of showing it. Building AI platforms means being good at exactly this: decomposing a system so that many agents can work it at once without colliding, writing contracts precise enough that a model can satisfy them, and keeping enough taste in the loop that the result is a system rather than a pile of working code.

The agents wrote most of the lines. The architecture, the invariants, the refusals, and every judgement call below were the human’s — and the repository shows the seams, because that is more interesting than pretending it doesn’t.

Directed by

Jonathan Kumin

Written with

Claude agents, in parallel

Reviewed by

A human, every merge

What a human still owns

  1. 01

    One reviewable list of what agents can touch

    Apps do not self-register their tools. The registry imports every tool module by hand, so the set of actions an AI can take in this OS is a single file you can read top to bottom in a minute — and a new capability cannot appear without showing up in that diff.

    lib/agentToolRegistry.ts

  2. 02

    A refusal, written down and defended

    Keychain is the only app with no tools.ts, and it never gets one. That decision has a written rationale — prompt injection, transcripts, provider logs — sitting in the app's own README so the next contributor (human or model) reads the argument before reopening the question.

    apps/Keychain/README.md

  3. 03

    Load-bearing behaviour, flagged as such

    The Agents store opens with two invariants marked "must not change" and the exact reason each one matters — an un-awaited startRun that other apps read back synchronously, and a confirmation queue whose head must stay a single slot. That is a reviewer leaving a tripwire, not a generator adding a docstring.

    apps/Agents/store.ts

  4. 04

    Performance rules earned once, then enforced everywhere

    Transform and opacity only; nothing that ticks above a leaf; no backdrop-filter over moving content. Each rule came out of a specific bug (below), and then got written into the file headers of every surface that could reintroduce it.

    components/landing/Landing.tsx · styles/globals.css

02The stack

Six layers, and why each one is the way it is.

Framework

Next.js 14 · Pages Router · React 18

Pages Router for everything a visitor navigates to. The App Router is present but carries only route handlers — app/api/demo-agent, app/api/mcp, app/api/og — because that is the one thing it does better here. TypeScript runs strict with no escape hatch in the compiler config.

pages/ · app/api/ · tsconfig.json

State

zustand · 43 stores

One store per app, colocated with the app, persisted to localStorage through zustand’s persist middleware. No global application blob: Notes cannot reach into Calendar’s state, and an app’s store is the only thing its agent tools are allowed to import — never its UI, so the tool registry stays cheap to load.

apps/*/store.ts · store/windows.ts

Window manager

monotonic z-order · real focus and minimize

Windows carry zIndex, isActive, isMinimized and isMaximized. Raising a window takes the next value off a monotonically increasing counter rather than reordering an array, so focus is O(1) and stable; activateWindow focuses without raising, which is what makes ⌘-Tab cycling feel right instead of shuffling the deck.

store/windows.ts · components/os/WindowManager.tsx

Agent platform

399 tools · 40 modules · 5 providers

A tool-registry pattern: every app exports a { appId, appLabel, getTools() } module of JSON-Schema tool definitions whose execute calls the app’s real store actions. One aggregator imports them by hand. Provider adapters for anthropic, mistral, openai, perplexity sit behind a single runAgentTask(), so the tool layer never learns which model is driving.

lib/agentToolRegistry.ts · lib/agentRuntime.ts · types/agentTools.ts

Execution

Web Worker JS · Pyodide 0.26.4 · esm.sh

The Terminal runs real JavaScript in a dedicated module Worker — no DOM, no OS stores, a fresh global, indirect eval so var and function declarations survive between commands like a REPL should. Python is CPython on WebAssembly, loaded lazily. npm packages resolve through jsDelivr, get written back as exact pins, and load as ES modules from esm.sh, so a second run needs no resolver round-trip.

apps/Terminal/lib/jsRunner.ts · pyRunner.ts · packages.ts

Phone shell

one context · 119 files honour it

The entire mobile contract is a React context holding a boolean. The shell provides true; the desktop provides nothing, so the default is false. Apps call useMobileShell() and render a purpose-built phone layout — single column, push navigation, sheets, 44px targets — instead of a squeezed desktop one. It is UI-layer only: stores and tools are byte-identical in both shells.

components/os/mobile/mobileContext.ts

03The agent platform

Agents don’t simulate the apps.
They call the same functions you do.

Every tool’s execute is a thin wrapper over the store action the app’s own buttons fire. There is no parallel “agent API” to drift out of sync — if a feature works in the UI, it works for an agent, and if it breaks, it breaks in both places at once.

  1. 01

    Registry

    getAllAgentTools() flattens 40 modules into 399 tool definitions; the agent’s config narrows that to the names it is allowed to call.

  2. 02

    Provider

    runAgentTask() dispatches to one of 5 adapters. The request goes from the browser straight to the provider with the user’s own key — there is no server in the path that could hold it.

  3. 03

    Tool call

    The model answers with a tool name and a JSON argument object matching that tool’s inputSchema.

  4. 04

    Gate

    human

    Anything flagged destructive: true52 of the 399 — is pushed onto a FIFO confirmation queue and waits on a human.

  5. 05

    Execute

    execute() calls the app’s real store action. Not a mock: the note is written, the event lands on the calendar, the window opens.

  6. 06

    Result

    A serialisable result goes back into the loop. Expected failures return a descriptive error string rather than throwing, so the model can correct itself.

The whole tool surface

Every module in the registry, largest first — 399 tools, 52 of them gated on a human.

toolsconfirmation-gated
  • Web Browser22 ·5
  • Clock19 ·1
  • Photoshop19 ·3
  • Budget18 ·3
  • Photos17 ·4
  • VSCode17 ·2
  • Music16 ·1
  • Kanban15 ·1
  • Reminders13 ·2
  • Spreadsheet13 ·1
  • Mail12 ·2
  • Transform12
  • Forms10 ·2
  • Read Later10 ·1
  • Contacts9 ·1
  • Database9 ·1
  • Calendar8 ·2
  • Messages8 ·2
  • QR Code8 ·2
  • Settings8
  • Weather8 ·1
  • API Tester7 ·2
  • Automations7 ·1
  • News7 ·1
  • System7
  • Chrome Tabs (real browser)6 ·5
  • Clipboard6 ·1
  • Converter6
  • Finder6 ·1
  • Activity Log5 ·1
  • Agents5 ·1
  • Habits5
  • Maps5
  • Notes5 ·1
  • Terminal5
  • Activity Monitor4
  • Dictionary4
  • Calculator3 ·1
  • Portfolio3
  • Voice Memos3

One name is missing from that list on purpose: Keychain. It is the only app in the OS with no tool module, and it will not get one. Why ↓

counted from lib/agentToolRegistry.ts and each module’s tools.ts on 2026-08-31

04What it cost

Four bugs that rewrote
how everything after them was built.

Each of these ended in a rule, and each rule is written into the file headers of the surfaces that could reintroduce it. That is why 119 files can agree on an animation contract nobody has to remember.

01

The flicker hunt

Symptom

The desktop flickered on mobile GPUs — but only once a window was open.

Root cause

The desktop’s ambient glow was an animated background. Windows sit on top of it with backdrop-filter blurs, and a perpetually-moving backdrop forces the GPU to re-blur every open window’s full area on every frame — forever. Nothing was wrong with either layer alone; the cost was the composition of the two.

The rule it became

No backdrop-filter over anything that moves.

02

Reference-preserving store updates

Symptom

Clicking one window re-rendered every open app.

Root cause

Focus was a map that spread every window into a new object. Fresh object references everywhere meant React.memo could never bail out, so a single click re-rendered the full contents of every other open app — Photoshop’s canvas included.

The rule it became

A reducer that always allocates is a reducer that always re-renders.

03

The animation contract

Symptom

Bottom sheets on the phone shell dropped frames while dragging.

Root cause

Sheets that animate height or width put layout on the critical path of every frame. On a phone, with a keyboard opening and a list reflowing behind the sheet, that is a guaranteed miss.

The rule it became

Animate transform and opacity. Nothing else, anywhere.

04

Focus that behaves like an OS

Symptom

Naive window stacking either reorders an array on every click or raises windows you only meant to focus.

Root cause

Focus and z-order are two different things, and conflating them makes ⌘-Tab feel like shuffling a deck. Reordering an array on every click also invalidates every window’s position in the list — the render problem above, arriving by a second route.

The rule it became

Focus and stacking order are separate concepts. Model them separately.

05Security posture

The interesting decisions
are the ones that said no.

Giving a model 399 ways to act on a user’s data is the easy half. The half worth reviewing is where the capability stops — and whether the stopping is structural or just a dialog box.

Your keys never leave your browser

BYOK, with no server in the path that could hold a key even if it wanted to.

You supply your own API key in Settings. It is written to this browser and read back only to make requests directly from the page to the provider you configured — the Anthropic SDK runs with dangerouslyAllowBrowser, and OpenAI and Perplexity are plain fetch calls to their endpoints. There is no proxy route, because there cannot be one: tool execution calls store actions that live in the browser (Finder’s File System Access handles, Notes’ in-memory state), so a server has nothing to execute against.

Keys are stored locally in this browser and used only for direct browser requests to the provider you configured. They are never sent anywhere else. Anyone with access to this browser profile can read them, so use keys scoped to what you need and remove ones you have stopped using.

apps/Settings/sections/APIKeysSection.tsx · lib/providers/*.ts

The password vault has zero agent tools

Keychain is the one app in the OS with no tool module, and it never gets one.

40 modules are wired into the registry. Keychain is deliberately not among them, and the reason is written down in the app’s own README so the next contributor — human or model — has to read the argument before reopening the question: every credential would become reachable by a model with a prompt-injectable context, the vault’s contents would flow into transcripts and provider logs, and an “are you sure?” dialog would be the only barrier between a well-crafted instruction and the user’s entire credential set.

Keychain deliberately ships no tools.ts, and one will not be added. … the whole security model rests on the decrypted vault existing only inside a session the *user* unlocked, on their machine, in front of them.

apps/Keychain/README.md · lib/agentToolRegistry.ts

Marked private means private, to every tool

Two flags on a clipboard entry, honoured by every read path — and the withholding is disclosed.

A clipboard item carries sensitive (masked in the UI and hidden from agents) and excludeFromAgents (hidden from agents without masking). The list tool filters them out and returns a withheldForPrivacy count plus a note telling the model the board is not fully visible; the read tool refuses by name and says which flag stopped it; the edit tool refuses too. Writing a snippet as sensitive returns a warning that the agent will not be able to read it back. The flags are re-coerced strictly on rehydrate, so a hand-edited localStorage entry cannot arrive unflagged.

Snippet is flagged sensitive by the user, so its content is not readable by tools. Ask the user for it directly.

apps/Clipboard/types.ts · apps/Clipboard/tools.ts · apps/Clipboard/store.ts

Irreversible actions wait for a human

52 of 399 tools are confirmation-gated, in a queue that cannot strand a call.

A tool marked destructive: true suspends the run and pushes onto a FIFO queue; pendingConfirmation is simply the head of that queue, so the modal’s contract never changed when the queue was added behind it. The queue exists because a second destructive call used to silently strand the first. Every entry holds its own resolve, so declining all of them answers each one rather than dropping them — a run can never be left waiting on a promise nothing will settle.

pendingConfirmation is the head of confirmationQueue. It stays a single nullable slot so the modal's contract is unchanged; the queue behind it is what stops a second destructive call from silently stranding the first.

apps/Agents/store.ts · apps/Agents/types.ts

And what it is not

Three limits worth knowing before you decide how much to trust this, stated here rather than left for you to find.

Client-side, not client-side encrypted.
API keys sit in localStorage in plaintext under one key. Anyone with access to the browser profile can read them. The Settings screen says exactly this rather than implying otherwise.
"Sandboxed" means worker isolation.
The Terminal’s runtimes get no DOM, no OS stores and a fresh global — but they run on the page’s own origin and fetch third-party code from esm.sh and jsDelivr. Runaway code is stopped by terminating the worker, which resets the session; the caller is told so it can say so.
Auto-approve is a real off switch.
An agent configured with auto-approval skips the gate, and agents_start_run lets one agent start another — so an auto-approving agent can chain runs unattended. That is the user’s explicit per-agent opt-in, and the registry flags it in a comment right where it is wired in.

06Source

Source available on request.

The repository is private — it holds work-in-progress alongside the parts that ship. If you are evaluating this as engineering work and want to read it, ask and you will get access, or a walkthrough of whichever part you care about. Reviewers usually want the agent runtime or the window manager; both are short enough to read in a sitting.

Framework
Next.js 14, Pages Router
Styling
Tailwind CSS, page-scoped CSS custom properties
Motion
framer-motion — transform and opacity only
Type
Inter, with a system-stack fallback
Figures
scripts/collect-stats.mjs → lib/colophon/stats.ts
Counted
2026-08-31