dream-os
by MiniDraco
README.md
# Dream OS
> A transparent, nestable desktop element layer for Windows — **that Claude can build on.**
Rainmeter-style widgets rendered in web tech, sitting on your desktop under your real windows. The difference: Dream OS ships an **MCP server**, so Claude can read the desktop, add gadgets, update them live, and capture your ideas — through a proper tool interface instead of you hand-editing config files.
Styled after the *Freelancer* (2003) HUD, because that game's UI is nested translucent frames — which is exactly what this is.
```
┌─ Claude ──┐ stdio ┌─ MCP server ─┐ HTTP ┌─ Dream OS ─┐
│ tools │ ────────► │ 8 tools │ ────────► │ desktop │
└───────────┘ └──────────────┘ :8787 └────────────┘
```
## What it does
- **Transparent full-desktop overlay** with dynamic click-through — clicks pass to whatever is underneath, and the layer only becomes solid while your cursor is over a gadget.
- **Sits under your windows.** It's a desktop layer, not an always-on-top nag.
- **Nestable elements.** Drop a gadget into a group and it becomes a DOM child, so dragging the parent moves everything inside it with zero extra code.
- **Drag, resize, persist.** Layout survives restarts.
- **Claude-drivable.** Everything above is reachable over MCP.
## The toolbag
Every gadget is one self-registering file in `renderer/elementals/`:
```js
defineElemental('clock', {
label: 'Clock',
size: { w: 210, h: 120 },
render(node, ctx) { /* return an HTMLElement */ },
});
```
Shipped elementals:
| type | what it is |
|---|---|
| `capture` | zero-friction idea box; Enter files a timestamped line |
| `note` | free text that persists |
| `clock` | time + date readout |
| `group` | pure container — drag it, children follow |
| `text` | static label / paragraph |
| `markdown` | rendered markdown (safe subset, everything escaped) |
| `list` | bulleted or checkable list |
| `meter` | labelled progress bars with tone colouring |
| `status` | named lamps: `ok` / `warn` / `bad` / `idle` |
| `table` | simple data table with sticky headers |
| `log` | scrolling line feed |
| `link` | clickable shortcuts (URL or file path) |
A gadget reads everything from `node.props`, which is exactly what Claude writes over the API — so anything you can build by hand, Claude can build too.
## MCP tools
| tool | purpose |
|---|---|
| `dreamos_state` | list what's on the desktop |
| `dreamos_elementals` | list the toolbag and each type's props |
| `dreamos_add` | add an element (optionally nested) |
| `dreamos_update` | move / resize / retitle / patch props |
| `dreamos_remove` | delete an element and its children |
| `dreamos_move` | re-parent an element |
| `dreamos_capture` | append to the capture log |
| `dreamos_read_captures` | read the capture log back |
The MCP server has **zero dependencies** — plain Node, newline-delimited JSON-RPC.
## Install
```bash
npm install
npm start
```
### Quick launch
`Dream OS.bat` brings up the overlay **and** both local voice services in one go, and `Stop Dream OS.bat` shuts them all down. Running it twice is safe — anything already listening is skipped, so it doubles as a "make sure everything is up" command.
```powershell
.\launch.ps1 # start whatever isn't already running
.\launch.ps1 -Stop # shut everything down
.\launch.ps1 -NoVoice # overlay only, skip the speech services
```
Cold start is about 20 seconds, most of it Kokoro loading its 325 MB model.
Register the MCP server with Claude Code (`.mcp.json` in your project, or your user config):
```json
{
"mcpServers": {
"dream-os": {
"command": "node",
"args": ["C:/path/to/dream-os/mcp/server.js"]
}
}
}
```
Dream OS must be running — the MCP server bridges to its control API and will say so plainly if it can't reach it.
## Shortcuts
There's a **tray icon** too — left-click toggles the layer, right-click for the full menu (show/hide, edit, float on top, capture box, data folder, quit).
| key | action |
|---|---|
| `Ctrl+Alt+H` | **hide / show the layer** |
| `Ctrl+Alt+D` | toggle edit mode (headers, drag handles, toolbar) |
| `Ctrl+Alt+N` | jump to the capture box |
| `Ctrl+Alt+T` | float above everything (toggle) |
| `Ctrl+Alt+Q` | quit — the escape hatch |
## Talking to Claude from the desktop
The `claude` elemental is a message box wired to the Claude desktop app. Type, press Enter: the text goes on the clipboard, Claude is focused, pasted and sent. The reply comes back by tailing Claude's own session transcript at `~/.claude/projects/<project>/<session-id>.jsonl`.
**It costs no extra tokens** — it's the same conversation, just a different surface. Records in that transcript are typed, so filtering `thinking` / `tool_use` / `tool_result` from the visible `text` is the entire "cleaner". No OCR, no screen scraping.
### Tag routing
A line beginning `#dream:<id>` in Claude's *prose* updates that element — no tool call at all:
```
#dream:console
All systems nominal.
```
A JSON object payload is merged into the element's props; anything else becomes `props.text`. Guards: the element must already exist, the tag must start a line (so inline mentions are inert), and the element flashes gold when touched so the magic is at least visible.
## Icon
`build/gen-icon.js` generates the icon as a real PNG with zero image dependencies — hand-rolled CRC32 and zlib chunks — so it can be re-themed from the same colour values as the CSS rather than living as a binary blob.
```bash
node build/gen-icon.js
```
## Where it sits (Wallpaper Engine, Fences, Rainmeter)
Windows has no arbitrary "layers". There is **one z-order** for normal windows, plus a wallpaper layer (`WorkerW`) beneath it where Wallpaper Engine and the Fences *background* live. Fences popups — and every application — are ordinary top-level windows.
So "above the wallpaper, below everything else" is exactly one call:
```c
SetWindowPos(hwnd, HWND_BOTTOM, 0,0,0,0, SWP_NOSIZE|SWP_NOMOVE|SWP_NOACTIVATE)
```
Electron doesn't expose it, so `zorder.js` reaches it through [koffi](https://github.com/Koromix/koffi) (prebuilt, no node-gyp). Dream OS parks at the bottom on startup and re-sinks on `blur`, so interacting with a gadget raises it briefly and it drops back afterwards. `Ctrl+Alt+T` pins it above everything when you want the HUD in front.
If koffi is unavailable the module degrades to no-ops and the app still runs — it just won't stay underneath.
## Theming
One block at the top of `renderer/style.css` drives everything:
```css
--h: 258; /* base hue — 210 blue · 288 purple · 160 green · 20 amber */
--lum: 27%; /* master darkness. lower = darker */
```
Panels, insets, borders, text and glow all derive from those via `hsl()`.
## Control API
Loopback only, port 8787.
```
GET /health GET /state[?flat=1] POST /state
GET /elementals POST /elements GET /elements/:id
PATCH /elements/:id DELETE /elements/:id POST /elements/:id/move
GET /captures POST /captures
```
`seed-desktop.js` is a worked example that builds a whole desktop through it.
## Development
`renderer/app.js` includes a shim: with no Electron preload it stubs the API and, if Dream OS is running, mirrors the **live** desktop over HTTP. So you can serve `renderer/` as a static site and iterate on styling without relaunching the overlay.
```bash
python -m http.server 8757 --directory renderer
```
## Notes from the build
A few things worth knowing, found the hard way:
- **Elements are DOM children, not separate OS windows.** One window per widget is the obvious Rainmeter-style approach, but then nesting means manually syncing window positions on every drag. As DOM nodes, nesting is free.
- **`.dp-body` needs `min-height: 0`.** Flex items default to `min-height: auto`, which lets content push the inset box out past the panel border.
- **Don't use `setPointerCapture` for dragging.** It routes events to the element and silently kills the whole drag if it throws. Window-level capture-phase listeners are robust, and survive a fast drag outrunning the cursor.
- **Atomic save can fail on EFS-encrypted directories.** Where `%APPDATA%` carries the EFS attribute, Windows treats the encryption boundary as a device boundary: Node's `fs.renameSync` fails with `EXDEV` *within a single folder*, and .NET's `File.Replace` throws access-denied. The store attempts the atomic path and falls back to a direct write plus a `.bak`.
## License
MIT © minidraco711
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues