Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 85 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,91 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

_Nothing yet._
### Added

- **ACP server** — OmniWork now speaks the [Agent Client Protocol](https://agentclientprotocol.com)
(v1) over stdio, so any ACP harness can drive it as a full coding agent: OpenClaw, acpx, Zed,
Neovim. Where the MCP server makes OmniWork a *tool* your agent calls, this makes it an *agent*
another harness runs.

Register it with `{ "agents": { "omniwork": { "argv": ["npx","-y","omniwork-acp"] } } }` in
`~/.acpx/config.json`, or under `plugins.entries.acpx.config.agents` in `openclaw.json`.

The agent loop's existing events map onto the protocol directly: text and reasoning stream as
message chunks, every tool call reports `pending → in_progress → completed` with its ACP kind,
and `write_file`/`edit_file` arrive as native diffs rather than a byte count. Gated tool calls
become `session/request_permission`, where "allow always" flips the session to auto mode instead
of silently prompting again. The Agent Deck surfaces as *N* live parallel tool calls, installed
skills are published as slash commands, and `session/load` replays a transcript so a crashed
harness resumes where it left off. Approval modes (`ask`, `edits`, `auto`, `plan`) are exposed as
ACP session modes; `ask` is the default, because in ACP the client owns the permission boundary.
- **Add to chat** — highlight any text in the transcript and a small pill offers to quote it
into the composer, or press `⌘L` / `Ctrl+L`. The quote lands in the prompt as visible `> `
lines you can read and edit before sending, dimmed by a highlight layer behind the
textarea.
- **Collapsed pastes** — paste (or quote) more than a few lines and the prompt shows
`[Pasted text #1 +322 lines]` instead of a wall of text, the way Claude Code does. The body
is held aside and spliced back in on the way to the model.

The token is a single unit: one Backspace or Delete touching it removes the whole block
rather than nibbling it into unmatchable debris, so the keypress right after pasting undoes
the paste. Editing a token apart also drops its body — what gets sent is always what you
can see.

Once submitted the block opens under the prompt line, capped and scrollable like a tool
card, with a click to collapse it again. The bodies ride on the transcript event, so they
are still there after a restart.
- **Copy on select** — highlighting text in the transcript copies it to the clipboard
immediately, the way a terminal does. Scoped to agent output: selections inside the
composer, project fields, and modals are left alone, since selecting there means editing.
Toggle with `/copy on|off`; the setting persists in `prefs.json`.
### Changed

- **Delegation is substantially faster.** Three things were making an MCP `delegate` call take
far longer than the work justified:

The agent tried a streaming request first and fell back to a non-streaming one whenever the
stream came back empty — which free `auto` routing does often. Nobody reads the token stream
during delegation, so that fallback was buying nothing and costing a second full request on
every affected step, up to 40 steps per task. Headless callers now make exactly one request per
step, and parallel subagents never stream either, since only their tool labels and final summary
are ever read.

The OmniRoute gateway also booted lazily on the *first* delegate call, so the caller paid the
entire cold start — tens of seconds, or an engine download on lite builds — before any work
started. It now starts in the background at `initialize`, overlapping with the host reading the
tool list.

Finally, delegation reported nothing until it was completely finished, so a slow call and a hung
one looked identical. Delegate calls now emit `notifications/progress` per step and per finished
subagent, and a stalled turn is cut off at `OMNIWORK_DELEGATE_TIMEOUT_MS` (default 10 min) and
returns its partial work instead of hanging until the client gives up.
- Both headless servers (MCP and ACP) share `electron/headless.js` for gateway, skills, memory,
and project wiring, and both honour `OMNIWORK_BASE_URL` / `OMNIWORK_API_KEY` to run against an
OpenAI-compatible endpoint other than the bundled gateway.

### Fixed

- **A wedged gateway no longer takes the app down with it.** If a previous run left an OmniRoute
process bound to port 20128 but no longer answering, every subsequent boot failed — and failed
slowly, then reported only `Gateway exited (code 1)`.

Two things were wrong. The health-check loop called `fetch` with no per-request timeout, so a
process that accepts the connection and never replies parked each poll for undici's 300 s
default; the loop's own 90 s deadline is only evaluated between iterations, so it never fired.
And a child that died instantly on `EADDRINUSE` was indistinguishable from one still starting
up, so the loop kept polling a port owned by somebody else.

Health checks are now bounded per request, and a dead child ends the wait immediately rather
than burning the full budget. The busy port is detected before spawning, by asking the OS
whether we can bind it: since 0.11.1 the child's output goes to `/dev/null` so a detached
gateway can outlive us without taking EPIPE, which means its `EADDRINUSE` is no longer ours to
read. When the port is held, the sidecar re-probes patiently first — the holder is often a
healthy gateway that was merely slower than the 2 s startup probe — and adopts it if it wakes
up. If it really is wedged, the sidecar starts on an OS-assigned free port instead.

It deliberately does not kill the holder. That process may not belong to OmniWork, and taking a
port by force isn't a decision to make on the user's behalf.

## [0.11.1] — 2026-07-29

Expand Down
6 changes: 6 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,17 @@ npm run test:smoke # end-to-end: gateway -> agent -> file written on disk
npm run test:cowork # parallel sessions
npm run test:features # streaming, undo, approval
npm run test:persist # save/restore across restarts
npm run test:selection # copy-on-select, "add to chat", collapsed pastes (real renderer)
```

`test:boot` is the CI gate. The rest need network access and live free-provider
availability, so they are run locally before a release.

`test:selection` is the exception: it launches the app and drives the page through
`OMNIWORK_UI_TEST`, so it needs a display but no network. It also reads the system
clipboard to prove copy-on-select really wrote to it — that assertion is skipped
off macOS/Linux.

## Building installers

```bash
Expand Down
59 changes: 56 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ delegate** *inside* Claude Code or Codex.
| 🌐 **Browsing built in** | `web_search` + `browse_page` render in a real Chromium window — JavaScript pages work, no API key. |
| 🔌 **MCP connections** | Plug in tools (filesystem, GitHub, Postgres, Slack…) via any stdio MCP server. Add from the UI. |
| 🪙 **Delegate tool** | OmniWork is *also* an MCP server — let Claude Code / Codex offload grunt work to its free models. |
| 🔗 **ACP agent** | Speaks Agent Client Protocol, so OpenClaw / acpx / Zed can drive OmniWork as a full coding agent. |
| ✂️ **Select to quote** | Highlighting output copies it instantly; a pill (or `⌘L`) quotes it into the prompt as `> ` lines. |
| 📋 **Collapsed pastes** | Paste 300 lines and the prompt shows `[Pasted text #1 +322 lines]` — the model still gets all of it. |
| 🔒 **100% local** | Everything runs on your machine. The gateway never phones home. |
| 🧩 **MIT, hackable** | Plain CommonJS, no build step for the UI. Fork it, ship it, sell it. |

Expand Down Expand Up @@ -148,6 +151,47 @@ matters — free models drift on instruction details.
Treat the returned summary as a **claim, not evidence**, and verify before calling the work done.
`npm run connect` installs this guidance so your agent applies it automatically.

## Drive OmniWork from OpenClaw, Zed, or any ACP harness 🔌

The MCP server makes OmniWork a *tool* your agent calls. The **ACP server** makes it a full
**coding agent** that another harness drives — OpenClaw, [acpx](https://github.com/openclaw/acpx),
Zed, Neovim, anything that speaks the
[Agent Client Protocol](https://agentclientprotocol.com). The harness owns the UI, the approval
prompts, and the transcript; OmniWork does the work on free models.

```json
// ~/.acpx/config.json or <repo>/.acpxrc.json
{ "agents": { "omniwork": { "argv": ["npx", "-y", "omniwork-acp"] } } }
```

```json
// openclaw.json
{ "plugins": { "entries": { "acpx": { "enabled": true, "config": {
"agents": { "omniwork": { "command": "npx", "args": ["-y", "omniwork-acp"] } }
} } } } }
```

Then `acpx omniwork "fix the flaky test"`, or spawn it as an OpenClaw subagent.

What the harness gets:

| | |
|---|---|
| **Streaming output** | Text, reasoning, and per-tool status as they happen |
| **Native diffs** | `write_file` / `edit_file` arrive as ACP diffs, not "wrote 412 bytes" |
| **Permission prompts** | Every gated tool call becomes `session/request_permission` — "allow always" flips the session to auto |
| **The Agent Deck** | `spawn_subagents` shows up as *N* live parallel tool calls, not one opaque block |
| **Skills as slash commands** | Installed skills are published via `available_commands_update` |
| **Modes** | `ask` (default), `edits`, `auto`, `plan` — switch with `acpx omniwork set-mode plan` |
| **Resumable sessions** | `session/load` replays the transcript, so a crashed harness picks up where it left off |

Modes default to **ask** because ACP's whole point is that the client owns the permission
boundary. Set `OMNIWORK_ACP_MODE=auto` if you'd rather it run unattended.

By default OmniWork runs on its own bundled OmniRoute gateway (free models). To spend a different
provider's budget instead, point it elsewhere — `OMNIWORK_BASE_URL`, `OMNIWORK_API_KEY`, and
`OMNIWORK_MODEL` apply to both the ACP and MCP servers.

## Build from source

Requires **Node.js 22+** (24 recommended).
Expand Down Expand Up @@ -201,6 +245,7 @@ Locally built `.app`s are unsigned — same right-click → Open dance as above.
OmniRoute ──► 278+ providers (free tier default)

mcp-server.js ──► Claude Code / Codex delegate here
acp-server.js ──► OpenClaw / acpx / Zed drive OmniWork here
```

- On boot, `electron/sidecar.js` runs OmniRoute's prebuilt server on a **bundled Node** runtime
Expand All @@ -209,7 +254,9 @@ Locally built `.app`s are unsigned — same right-click → Open dance as above.
- `electron/agent.js` runs an OpenAI-compatible tool-use loop; `spawn_subagents` fans out; MCP
tools merge in namespaced as `mcp__<server>__<tool>`.
- `electron/sessions.js` runs many agents in parallel; `electron/mcp.js` is the MCP client;
`electron/mcp-server.js` exposes OmniWork *as* an MCP server (the delegate tool).
`electron/mcp-server.js` exposes OmniWork *as* an MCP server (the delegate tool), and
`electron/acp-server.js` exposes it as an ACP agent. Both are headless stdio servers sharing
`electron/headless.js` for gateway, skills, and memory wiring.
- Agent file tools are confined to the workspace folder you pick. See [SECURITY.md](SECURITY.md)
for what that does and does not cover.

Expand All @@ -222,7 +269,11 @@ provider keys (stored encrypted, locally), or pick a specific model in the statu
|-----|---------|---------|
| `OMNIWORK_GATEWAY_PORT` | `20128` | Gateway port |
| `OMNIWORK_WORKSPACE` | — | Open a folder on launch |
| `OMNIWORK_MODEL` | `auto` | Pin a model (delegate server) |
| `OMNIWORK_MODEL` | `auto` | Pin a model (MCP + ACP servers) |
| `OMNIWORK_BASE_URL` | — | Run headless servers against another OpenAI-compatible endpoint |
| `OMNIWORK_API_KEY` | `omniwork` | Key for `OMNIWORK_BASE_URL` |
| `OMNIWORK_ACP_MODE` | `ask` | Starting approval mode for ACP sessions |
| `OMNIWORK_DELEGATE_TIMEOUT_MS` | `600000` | Backstop before a stalled delegate returns partial work |
| `OMNIWORK_NODE` | — | Node binary used to run the gateway in dev |
| `OMNIWORK_NODE_VERSION` | build host's | Node version staged into packaged builds |
| `OMNIWORK_DEV` | — | Devtools + verbose logs |
Expand All @@ -240,6 +291,8 @@ electron/
tools.js file/shell/web tools (workspace-confined)
mcp.js MCP client (connect external tool servers)
mcp-server.js MCP server (delegate tool for Claude Code / Codex)
acp-server.js ACP agent (OpenClaw / acpx / Zed drive OmniWork)
headless.js shared bootstrap for both stdio servers
preload.js contextIsolation-safe IPC bridge
renderer/ the terminal UI (index.html, styles.css, app.js)
scripts/
Expand All @@ -248,7 +301,7 @@ scripts/
connect.js registers the MCP server and installs delegate guidance
doctor.js setup verification + repair
gen-icon.js dependency-free app-icon generator
test/ boot · smoke · cowork · features · persist
test/ boot · smoke · cowork · features · persist · mcp · acp · sidecar
```

## Documentation
Expand Down
Loading
Loading