Skip to main content
Glama
MiniDraco

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

Maintenance

ActivitySlowing
ResponsivenessNo issues