Specifies the ctrl-k command bar for both clients: opening and the
leader escape hatch for a literal Ctrl-K, sigils, empty-query recency,
the key model including the session action page, and touch entry.
The match-and-rank algorithm is pinned exactly — normalization, word
boundaries, the per-character score, embedding choice, tie-break — and
closed with twelve fixtures the TypeScript (#146) and Rust (#147)
matchers are both tested against, so their orders cannot drift.
Moving the sidebar highlight (keyboard or hover) now shows that session in
the content pane without opening it: its own tab comes forward, or a
throwaway tab carries it. The layout, the saved layout and the daemon's
focus report are all left untouched, and the keyboard stays in the list.
Contract in docs/PREVIEW.md.
The browser reached the daemon through maestro-web, a second binary that
served the SPA and reverse-proxied /api/* — two TCP connections and a
WebSocket frame re-encode on every keystroke and every byte of PTY output.
On one machine that hop buys nothing.
The daemon now embeds the bundle and serves it off its own listener, behind
a default-on `web` feature. `--no-default-features` builds without pnpm or
node; MAESTRO_WEB_DIST still takes a prebuilt bundle.
The router's fallback answers an unmatched /api/* with 404 rather than the
SPA shell, so a client expecting JSON gets a status it can read instead of
HTML.
Removes maestro-web and the [web] config section. An existing [web] block
is ignored, not rejected. The installer deletes a leftover
maestro-web.service on upgrade.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The status bar gains the TUI's half of the same contract the web client got:
a coloured dot plus the release the daemon is running, pinned right so a narrow
terminal truncates the key hints rather than the release.
`DaemonLink` mirrors web/src/daemonLink.ts — the events loop reports its
connects and disconnects, a probe task polls `GET /api/info` while the daemon
is unreachable, and past the grace window a modal covers the frame and locks
the keyboard (quit excepted) until it answers. A daemon that comes back on a
new commit cannot be reloaded into a running TUI binary, so the status bar says
`updated: <hash> (restart tui)` instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds POST /api/sessions/:id/input and a CLI command on top of it, so a
script (or another agent) can hand an instruction to a running session
without attaching to it.
The text goes in as one write and the Enter follows 150ms later: agents
that run a full-screen prompt coalesce a burst of input into a paste, and
a newline inside that burst becomes a line break instead of sending the
message. `--no-enter` skips the Enter and leaves the text in the prompt.
The session argument takes an ID, a unique ID prefix, or a session name;
names win over prefixes. A session whose pane has exited is refused (409)
rather than silently accepted — unlike a stray keystroke on an attach
socket, a send that goes nowhere is worth hearing about.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Removes the retained-worktree store end to end: the daemon's state-dir
store, GET/DELETE /api/worktrees, `maestro worktrees list|reclaim`, the
TUI overlay behind `w`, and the web `#/worktrees` view and header link.
The kill guard stays. A forced kill and a dirty natural exit now leave
the worktree on disk untouched instead of parking it in a store, so
removing one is an ordinary `git worktree remove`. A pristine tree is
still removed on exit. DELETE /api/sessions/:id always answers 204.
The header's debug button also goes; Ctrl+Shift+D and `?hud` still
toggle the HUD.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ships `maestro`, `maestro-web` and the SPA bundle as flake packages, plus a
`services.maestro` Home Manager module that runs the daemon and the web UI as
systemd user services.
`maestro-web`'s build.rs now honours MAESTRO_WEB_DIST and hands the resolved
bundle path to `include_dir!` through MAESTRO_WEB_BUNDLE_DIR. Without it the
Nix build would have to run `pnpm install` mid-compile, which the sandbox has
no network for. Unset, the pnpm path is unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nothing in the workspace depended on it — session handles are generated
client-side in web/src/names.ts, which carried its own copy of the
wordlists. Also drops two unused deps: rust-embed (no crate used it;
maestro-web embeds via include_dir) and urlencoding in maestro-cli.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rewrites CLAUDE.md as the current-architecture reference, drops the
registry from INSTALL/PRODUCT/DESIGN and the web README, deletes the
registry Dockerfile, and marks the dated design specs superseded where
they describe the registry, JWT auth, or cross-host attach.
The installer loses its configure step — every config field now has a
default, so a daemon starts with no config file at all.
New shared `maestro-names` crate producing `adjective-instrument` session
handles drawn from the orchestra pit — musical tempo/dynamics/articulation
markings × instruments and forms (e.g. `allegro-cello`, `crescendo-oboe`,
`staccato-viola`), tying handles to the app's conductor metaphor.
- Pure generation takes an injected RNG; no thread_rng/clock in pure paths,
so seeded tests are reproducible (repo invariant).
- `generate_unique` prefers the bare name, appending an incrementing numeric
suffix (`-2`, `-3`, …) only on collision via an injected `is_taken`.
- `generate_thread` is the one impure convenience wrapper for runtime callers.
- Wordlists are public so the web UI can mirror them for instant reroll; the
Rust generator stays the source of truth.
Closes#23. Part of #21 (spawn dialog rework).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- `RunConfig` grows a `config_path: Option<PathBuf>` field, plumbed from the
umbrella binary's global `--config` flag through to `MaestroConfigFile`
loading. Explicit `--config <path>` wins over `$MAESTRO_CONFIG` over the
platform default; a missing file still falls back to shipped defaults.
- `docs/INSTALL.md` gains a "terminal UI" section: that `maestro tui` ships
in the default feature set, how to opt out (`--no-default-features
--features cli,daemon`), the `MAESTRO_DAEMON_URL` override, and a worked
example of `[tui]` / `[tui.keymap.normal]` config pointing at the
full action vocabulary in the design spec.
- Adds a thin-build line for `--features cli` (CLI-only client) and lists
`tui` in the default-feature set.
- CLAUDE.md repo-layout block lists the new `maestro-tui` crate.
Closes the NAP-275 v1 epic.
- Installer: one `cargo install maestro` instead of two; ExecStart becomes
`~/.cargo/bin/maestro daemon`; user unit renamed `maestrod.service` →
`maestro.service`; configure step is `maestro daemon configure`.
- E2E harness: both `Daemon` and `Registry` subprocess wrappers shell out to
the unified `maestro` binary with the appropriate subcommand.
- State dir: `~/.local/state/maestrod` → `~/.local/state/maestro`.
- Docs: CLAUDE.md repo layout, design spec repo layout + "Daemon (`maestro daemon`)"
heading, INSTALL.md (full rewrite of CLI references + new thin-builds section).
Earlier today I picked option 1 from NAP-231's open question (run
`maestro-registry` locally on a different port). That was wrong: the
user-facing framing is "local web UI", and option 2 — a thin front-end
that serves the web bundle and reverse-proxies `/api/*` to whatever
registry the daemon is already registered with — is what actually
delivers that.
What the new crate does:
- `crates/maestro-web` ships a single binary `maestro-web`.
- It embeds the web bundle the same way `maestro-registry` does
(rust-embed against `web/dist/`).
- Mounts the bundle at `/` and a reverse proxy at `/api/*` (catch-all).
- HTTP proxy via `reqwest` — forwards method, headers (minus
hop-by-hop), body, and passes upstream response headers (including
`Set-Cookie`) back to the browser.
- WS proxy upgrades both sides and shuttles frames with
`tokio::select!`. Cookies on the initial `WebSocket` upgrade headers
are forwarded so the registry sees the same browser session.
- Reads `MAESTRO_REGISTRY` (upstream URL) and `MAESTRO_WEB_BIND_ADDR`
(default `127.0.0.1:8888`) from env.
- An integration test (`tests/proxy.rs`) stands up a mock upstream,
proxies an HTTP request with cookies through, and round-trips a text
WebSocket frame to verify the WS path.
Installer changes:
- Wizard now `cargo install`s `maestro-web` (not `maestro-registry`)
when the user opts into the web service.
- Unit template switched: `ExecStart=%h/.cargo/bin/maestro-web` plus
`Environment=MAESTRO_REGISTRY=<upstream>` +
`Environment=MAESTRO_WEB_BIND_ADDR=<bind>`.
- New `--web-registry <URL>` flag. Default in interactive mode: read
`registry_url` straight out of `~/.config/maestrod/config.toml`, so
the proxy lines up with what the daemon is using without further
prompting. Non-interactive runs without either of those error out
rather than guess.
Docs:
- `docs/INSTALL.md` updated unit body + flag table.
- `CLAUDE.md` repo-layout block now lists `maestro-web`.
148 tests pass workspace-wide (9 new in maestro-web, including the
proxy integration test; 15 in installer).
New `maestro-installer` crate exposing the `maestro-install` binary. Bootstrap
flow: `cargo install --git ... maestro-installer && maestro-install` runs
`cargo install` for `maestro-daemon` and `maestro-cli`, optionally drives
`maestrod configure`, and on systemd hosts writes a user unit at
`~/.config/systemd/user/maestrod.service` plus reload/enable/start. Pure
builders (cargo install args, systemctl args, unit body) live in the lib and
are unit-tested; main.rs is the prompt glue.
Refs NAP-225.
Orients future sessions and Linear-driven skills: points at the spec,
the Linear project, the dependency chain (NAP-211 is the starting
point), the target workspace layout, and conventions (TDD, conventional
commits with maestro scope, real tmux+git in integration tests, no
mocking).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>