ai-agentmanager 1.3.1 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +28 -7
- package/docs/distributed.md +2 -1
- package/docs/engines.md +2 -1
- package/docs/plugins.md +2 -1
- package/package.json +1 -1
- package/public/app.js +185 -17
- package/public/code.js +449 -0
- package/public/hub/hub.js +34 -6
- package/public/index.html +70 -7
- package/public/machine.js +120 -0
- package/public/menu.js +92 -0
- package/public/setup.js +40 -14
- package/public/style.css +102 -2
- package/public/transport.js +19 -3
- package/public/update.js +88 -0
- package/src/brief.js +69 -0
- package/src/cleanup.js +71 -2
- package/src/cli.js +57 -2
- package/src/engines/index.js +10 -2
- package/src/engines/muse.js +303 -0
- package/src/fleet.js +1 -0
- package/src/git.js +2 -2
- package/src/handoff.js +240 -0
- package/src/hub.js +50 -2
- package/src/hubactions.js +14 -1
- package/src/identity.js +41 -0
- package/src/manager.js +73 -17
- package/src/server.js +48 -8
- package/src/studio.js +255 -0
- package/src/update.js +160 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.0 (2026-10-31)
|
|
4
|
+
|
|
5
|
+
- **Muse Code engine:** Meta's Muse Code CLI (`muse`) as a fourth engine: `--engine muse` or **Muse Code** in the Engine picker. Session adoption, resume (messages typed once idle), conversation and usage, trust and approval questions in the inbox, a clean stop, and Explain. The picker warns when Muse is in contributor mode. Stuck detection from commands and edits isn't there yet for Muse.
|
|
6
|
+
- **Terminals:** reloading the page no longer types the terminal's answers to old color and cursor queries into the program (seen with Muse Code), in agents' terminals and the Terminal. The agent's terminal tab is named after its engine (Codex, Antigravity, Muse Code).
|
|
7
|
+
- **One agent or one per line:** New agents asks whether each line starts its own agent (a list of tickets) or the whole text goes to one agent (a long description, kept as written). The button shows how many agents it will create, and the choice is remembered.
|
|
8
|
+
- **Diagrams:** a **Diagrams** button beside the documents draws the problem and its solution for a quick grasp, diagrams only: the problem, architecture (changes highlighted), design, data flow with animated arrows, the key interaction and the plan. Drawn from the analysis, design, flow, proposal and plan already written, plus the agent's work and the repo layout. Also `agentmgr doc <agent> diagrams`.
|
|
9
|
+
- **Home page:** shows what you can do (agents, teams and races, Inbox, Code, shells and the browser, Terminal, review and Land, Backlog and the assistant, Results, your phone, several machines), each with a button to it, and stays after Getting started is done or hidden. It scrolls when it doesn't fit.
|
|
10
|
+
- **Restart from the app's own terminal:** `agentmgr restart` or `agentmgr update` typed in an agent's shell or the Terminal no longer dies halfway (daemon stopped, never started) when the restart closes that terminal; it finishes, and the terminal can be reopened.
|
|
11
|
+
- **Remote typing:** over remote access, typing or a paste whose request fails (a network blip) is retried in order instead of dropped, and you're told if it still can't get through.
|
|
12
|
+
|
|
13
|
+
## 1.4.0 (2026-10-31)
|
|
14
|
+
|
|
15
|
+
- **Move an agent to another machine:** **Move to…** on the hub (or `agentmgr fleet move <machine>/<agent> --to <machine>`) continues an agent elsewhere with its conversation, branch and uncommitted work. The work goes through the repo's remote on a temporary handoff branch; the conversation goes through the hub in pieces (not stored). Both machines must allow the new `move` permission. A failed move puts everything back; the copy left behind is marked moved and can't be resumed.
|
|
16
|
+
- **Code:** a **Code** tab on every agent: the agent's files in a tree, tabs and Monaco (the editor inside VS Code). Save (refused if the agent changed the file meanwhile, unless you overwrite), search the folder, compare with where the agent started, create, rename and delete. Open files pick up the agent's edits. Paths can't leave the agent's folder. Plugins can check changes with the `agent.files` policy.
|
|
17
|
+
- **Open in VS Code or Cursor:** opens the agent's folder and file in the editor installed on this computer.
|
|
18
|
+
- **Terminal:** a shell on this computer in your home folder, from the top bar, from any signed-in device (phone included). Several at once; they keep running when hidden.
|
|
19
|
+
- **Update from the app:** when a new release is out, **Update to …** appears in the top bar (checked twice a day; can be turned off), and one click installs it and restarts; running agents resume. **About → Update and restart** does the same, and so does `agentmgr update`. A clone at a release tag moves to the newest tag (dependencies installed when they changed); an npm install runs `npm install -g`. A clone on a branch or with uncommitted changes is left alone.
|
|
20
|
+
- **☰ menu:** the top bar's menu shows who this runs as (your Claude account and organization, and the computer's user) and holds **Settings**, **Clean up**, **Alerts**, **Remote access** and **About**.
|
|
21
|
+
- **A limit on open agents:** keep at most N agents open (**Settings**, or `agentmgr cleanup --max-open N`); above it, the ones idle longest are closed and resume when opened. Agents working, waiting for an answer, active in the last 5 minutes, marked Keep or in a running team are never closed for it. Off by default.
|
|
22
|
+
- **Drag to move:** drag an agent onto another project in the list to move it there.
|
|
23
|
+
- **Home:** clicking **AI Agent Manager** in the top bar goes back to the agent list with nothing open.
|
|
24
|
+
|
|
3
25
|
## 1.3.1 (2026-10-31)
|
|
4
26
|
|
|
5
27
|
- **Restart comes back:** `agentmgr restart` waits for the old daemon to exit (it closes every agent first, which takes a while with many), instead of giving up after 20 seconds and reporting success with no daemon left. `serve` started while one is shutting down waits for it instead of exiting, and `shutdown` returns once the daemon has stopped. `/api/health` says `closing` while it shuts down.
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Run one background [Claude Code](https://claude.com/claude-code) agent per inten
|
|
|
6
6
|
|
|
7
7
|

|
|
8
8
|
|
|
9
|
-
- **Intents in, agents out.** Give
|
|
9
|
+
- **Intents in, agents out.** Give a list, one per line (say, a ticket each), and each becomes its own `claude` process. Or pick **One agent for all of it** and a long description goes to a single agent as written.
|
|
10
10
|
- **Repo or folder.** Give a git URL (cloned once, then each agent gets its own worktree and branch) or a local folder.
|
|
11
11
|
- **Fast clones.** URLs are cloned as partial clones (`--filter=blob:none`): history now, file contents only for what's checked out.
|
|
12
12
|
- **Progress.** Clone and checkout progress shows in the UI and `agentmgr ls`.
|
|
@@ -83,13 +83,13 @@ Runs on macOS and Linux; on Windows, use WSL2. Requires Node 20+, `git`, and the
|
|
|
83
83
|
|
|
84
84
|
```sh
|
|
85
85
|
npm install -g ai-agentmanager # the latest release
|
|
86
|
-
npm install -g ai-agentmanager@1.
|
|
86
|
+
npm install -g ai-agentmanager@1.5.0 # or a specific one
|
|
87
87
|
```
|
|
88
88
|
|
|
89
89
|
Or from the repository, at a release tag:
|
|
90
90
|
|
|
91
91
|
```sh
|
|
92
|
-
git clone --branch v1.
|
|
92
|
+
git clone --branch v1.5.0 --depth 1 https://github.com/jdmunta/ai-agentmanager-core.git
|
|
93
93
|
cd ai-agentmanager-core
|
|
94
94
|
npm install
|
|
95
95
|
npm link # optional: puts `agentmgr` on your PATH
|
|
@@ -104,12 +104,12 @@ npm install
|
|
|
104
104
|
npm link
|
|
105
105
|
```
|
|
106
106
|
|
|
107
|
-
**Updating.**
|
|
107
|
+
**Updating.** `agentmgr update` installs the newest release and restarts onto it; running agents pause briefly and resume. In the app, **Update to …** appears in the top bar when a release is out (checked twice a day; turn it off in **☰ → Settings** or with `agentmgr update --auto-check off`), and **About → Update and restart** does the same. A clone at a release tag (or one you `npm link`ed) moves to the newest tag; a clone on a branch or with uncommitted changes is left alone. By hand, use the command for how you installed, then `agentmgr restart`:
|
|
108
108
|
|
|
109
109
|
| Installed with | Update |
|
|
110
110
|
| --- | --- |
|
|
111
111
|
| npm | `npm install -g ai-agentmanager@latest` |
|
|
112
|
-
| A clone at a release tag | `git fetch --tags && git checkout v1.
|
|
112
|
+
| A clone at a release tag | `git fetch --tags && git checkout v1.5.0 && npm install` (the tag you want) |
|
|
113
113
|
| A clone of `main` | `git pull && npm install` |
|
|
114
114
|
|
|
115
115
|
**Check your setup.** `agentmgr doctor` checks Node.js, Claude Code and its sign-in, git, `gh`, the data folder, the daemon's port and Chrome, and says how to fix anything missing.
|
|
@@ -327,6 +327,7 @@ The Brief panel's **Share for review** buttons, or `agentmgr doc <agent> <kind>`
|
|
|
327
327
|
| **Flow** | Summary, end-to-end flowchart with a step-by-step walk-through, key interactions as a sequence diagram, failure and edge cases |
|
|
328
328
|
| **Proposal** | Summary, problem, recommendation, scope, plan, effort and dependencies, risks, decisions needed |
|
|
329
329
|
| **Human vs AI** | Your top 5 decisions, each with why it needs you, the stakes, options and what the work suggests, plus a box for your answer. Up to 5 questions the AI answers, each with its confidence, evidence and how to check it. A second model's verdict on each. |
|
|
330
|
+
| **Diagrams** | Diagrams only, for a quick grasp: the problem, the architecture (with what changes highlighted), the design, the data flow (animated) and the key interaction, plus the plan. Drawn from the documents already written and the agent's work. Optional: **Write all** leaves it out. |
|
|
330
331
|
|
|
331
332
|
**Human vs AI.** This review takes two separate read-only calls.
|
|
332
333
|
1. **The split.** The agent's engine splits the open questions. *For you to decide* covers judgment calls such as intent, priorities, risk appetite and trade-offs. *Answered by AI* covers reasoning and evaluation: correctness, edge cases, test coverage and what's left.
|
|
@@ -480,7 +481,7 @@ The sidebar groups agents into projects, shown as a tree:
|
|
|
480
481
|
|
|
481
482
|
**Edit** on an agent changes its name, its request, priority, permission mode and model. Changing the request updates what AI Agent Manager records (and tracks any new Jira links); tick **Also tell the agent** to send it to the session as well. Permission mode and model apply from its next launch.
|
|
482
483
|
|
|
483
|
-
To list an agent under another project, use **✎ Move** next to its project (or the Edit dialog) and pick a known project, a new project name, or **Its own repo or folder** to move it back. Only the grouping changes: its code stays in its own worktree and branch.
|
|
484
|
+
To list an agent under another project, drag it onto that project in the list, or use **✎ Move** next to its project (or the Edit dialog) and pick a known project, a new project name, or **Its own repo or folder** to move it back. Only the grouping changes: its code stays in its own worktree and branch.
|
|
484
485
|
|
|
485
486
|
```bash
|
|
486
487
|
agentmgr edit 3fa2b1 --name login-timeout --priority P1 --intent "Fix the login timeout on slow networks" --tell
|
|
@@ -723,12 +724,14 @@ Or use **Remote access** in the header of the UI on this computer.
|
|
|
723
724
|
- **Firewall.** If the macOS firewall is off, the port is open to your whole network while remote access is on. Turn it off when you don't need it.
|
|
724
725
|
- **Streaming.** The phone UI streams with Server-Sent Events plus plain HTTPS requests. iOS Safari doesn't apply an accepted self-signed certificate to WebSockets.
|
|
725
726
|
|
|
726
|
-
## Engines: Claude Code, Codex and
|
|
727
|
+
## Engines: Claude Code, Codex, Antigravity and Muse Code
|
|
727
728
|
|
|
728
729
|
Agents run Claude Code by default. With OpenAI's Codex CLI installed (`npm i -g @openai/codex`, signed in), an agent can run Codex instead: `agentmgr add --engine codex …`, or the **Engine** picker in New agent (shown once a second engine is installed). Everything else works the same way: worktrees, the brief and questions, the inbox (Codex's approval menus included), token usage, stuck detection, PR tracking, Explain, the backlog and the hub. Codex records no cost, and planning while a repo clones and forks are Claude-only for now. Set `"engine": "codex"` in config.json to make it the default; `engines.codex.bin` points at another binary. On the hub, each machine reports the engines it has (`engine:codex`), so shared backlog work can ask for one. See [docs/engines.md](docs/engines.md).
|
|
729
730
|
|
|
730
731
|
Google's **Antigravity CLI** (`agy`, signed in with your Google account) works the same way: `--engine antigravity`. It replaced Gemini CLI for personal Google accounts in June 2026. AI Agent Manager adopts the conversation agy starts, resumes it with `--conversation`, reads token usage, commands and file writes from its transcript, shows its permission menus (commands, file changes, access outside the folder) in the inbox, pre-trusts agent folders in agy's settings, and runs Explain in agy's print mode. agy asks before writing the brief too, unless the agent runs in `acceptEdits` mode. `engines.antigravity.bin` points at another binary.
|
|
731
732
|
|
|
733
|
+
Meta's **Muse Code** (`muse`, installed with `curl -fsSL https://dev.meta.ai/install.sh | bash` and signed in with `muse login`) works the same way: `--engine muse`. AI Agent Manager adopts the session Muse starts, resumes it with `muse resume` (a message for a resumed session is typed once Muse is idle), trusts the agent's folder for each run with `--trust-workspace` instead of saving it, reads the conversation and token usage from Muse's session log, shows its trust and approval questions in the inbox, stops it cleanly with SIGTERM, and runs Explain with `muse exec` in the read-only profile. Commands and edits aren't read from its log yet, so stuck detection has less to go on for Muse agents. When Muse runs in contributor mode (an account without a plan), the engine picker says so: Meta may use your code and conversations to improve its products. `engines.muse.bin` points at another binary, and `engines.muse.args` adds arguments to every run.
|
|
734
|
+
|
|
732
735
|
## Several machines: the hub
|
|
733
736
|
|
|
734
737
|
With agents on more than one Mac, a **hub** shows all of them in one place: every machine's agents and one inbox of what needs you. Where a machine allows it, you can also act from the hub: answer, message, start and stop agents, start new ones, and open live terminals. Each Mac keeps running its own daemon. It connects out to the hub, so no ports need opening on it, and the hub's requests travel back over that same connection.
|
|
@@ -770,6 +773,7 @@ agentmgr leave # the hub forgets this Mac
|
|
|
770
773
|
| `terminal` | Open the live Claude terminal |
|
|
771
774
|
| `shell` | Open shells in an agent's folder. These run any command, so grant with care. |
|
|
772
775
|
| `signin` | Open this machine's own page already signed in: **Open on <machine>** asks for a one-time link (single use, valid for a minute) instead of the access token. That page has full control, beyond the rest of this policy, so grant it only on a hub that only you can sign in to. |
|
|
776
|
+
| `move` | Move agents to and from other machines that also allow it (**Move to…** on the hub, or `agentmgr fleet move`). See **Moving an agent** below. |
|
|
773
777
|
|
|
774
778
|
`projects` (names or shortcuts) limits all of it to those projects: other agents aren't shown and can't be touched. `"allow": []` stops sharing but stays joined. The machine checks every request itself, even if the hub thinks it's allowed, matches agents by exact ID, and only runs a fixed list of actions (deleting agents isn't one). Each request, and every refusal, is written to `~/.agentmanager/hub-audit.log`; the hub keeps its own `audit.log` too.
|
|
775
779
|
|
|
@@ -782,6 +786,14 @@ agentmgr leave # the hub forgets this Mac
|
|
|
782
786
|
- **When a machine drops mid-item.** Its item shows as **stalled** and isn't restarted elsewhere on its own, because the work in that machine's worktree would be lost or duplicated. Wait for the machine, or **Requeue** to start over on a different one.
|
|
783
787
|
- **Pausing.** `agentmgr fleet backlog pause` (or **Auto-start** off) stops new starts; **Start now** runs one item past the limits.
|
|
784
788
|
|
|
789
|
+
**Moving an agent to another machine.** **Move to…** on an agent in the hub (or `agentmgr fleet move mini/3fa2b1 --to laptop`) continues it on another machine with the same conversation, the same branch and its uncommitted work. Use it to pick up on the laptop what the mini started, or to free a busy machine. Both machines must allow `move`, and the other machine needs the same project (matched by repo).
|
|
790
|
+
|
|
791
|
+
1. On the machine it leaves, its session closes. A snapshot of the folder (commits, edits and new files) is pushed to the repo's own remote as `agentmgr-handoff/<id>`.
|
|
792
|
+
2. The other machine creates the agent in its matching project with the same session id. It checks out the branch, restores the uncommitted work, places the conversation where Claude looks for it (with folder paths rewritten), and resumes it if it was running.
|
|
793
|
+
3. The first machine marks its copy **Moved to <machine>**, which can't be resumed (two copies of one conversation would diverge), and the handoff branch is deleted.
|
|
794
|
+
|
|
795
|
+
The conversation passes through the hub in pieces, in memory only for the move, so the hub keeps neither code nor transcripts. If any step fails, the copy made on the other machine is removed, and the agent opens again where it was. It needs a repository with a remote that both machines can reach; Claude Code agents only, for now.
|
|
796
|
+
|
|
785
797
|
**How machines prove who they are.** Joining makes an Ed25519 key pair on the Mac. The private key stays in `~/.agentmanager/hub-link.json` (mode 600), and the hub keeps only the public key. Each connection signs a fresh challenge from the hub. Invite codes work once. **Remove** on the hub (or `agentmgr hub remove <machine>`) disconnects a machine, and it stops retrying until it joins again.
|
|
786
798
|
|
|
787
799
|
**Signing in with Google.** Set `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` (a Web application OAuth client whose redirect URI is `<hub address>/api/auth/google/callback`) and `AGENTMGR_HUB_GOOGLE_EMAILS` (comma-separated accounts allowed in). The sign-in page then offers **Sign in with Google**, with the access token folded away below it. It uses the authorization code flow with PKCE, state and nonce, and accepts only verified emails on that list. Without the list, Google sign-in stays off.
|
|
@@ -870,6 +882,8 @@ agentmgr cleanup --strategy close-delete --close-after 14 --delete-after 14
|
|
|
870
882
|
|
|
871
883
|
Agents closed before clean up was turned on count as closed from the first check, so nothing is deleted the day you turn it on.
|
|
872
884
|
|
|
885
|
+
**A limit on open agents.** Many open sessions slow the machine down. In **☰ → Settings** (or `agentmgr cleanup --max-open 12`), set how many agents may be open at once: above it, the agents idle longest are closed, and they resume when you open them. Never closed for the limit: agents working, waiting for your answer, active in the last 5 minutes, marked **Keep**, or in a team that is still going. Off by default (`--max-open off`).
|
|
886
|
+
|
|
873
887
|
## Saving state: flush and restart
|
|
874
888
|
|
|
875
889
|
- **What's saved immediately.** Agent records (session IDs, settings, links) are written to `state.json` on every change, fsynced, then atomically replaced. Claude writes each conversation to its own transcript as it goes.
|
|
@@ -891,6 +905,13 @@ Usage comes from the session transcripts and is refreshed every few seconds whil
|
|
|
891
905
|
- **Where it shows.** Per session in **Session history** and `agentmgr sessions`, and summed per agent in the list, the session card, `agentmgr ls` (TOKENS) and `agentmgr show`.
|
|
892
906
|
- **Cost.** The figure Claude Code records itself (`cost-state`), shown once it has written one.
|
|
893
907
|
|
|
908
|
+
## Studio: code and terminals
|
|
909
|
+
|
|
910
|
+
- **Code** (a tab next to **Claude** in the terminal pane) opens the agent's folder: a file tree, tabs, and Monaco, the editor inside VS Code, with syntax highlighting and search within a file. **Save** (Ctrl/Cmd+S) writes into the agent's folder. If the agent changed the file since you opened it, you're asked before anything is overwritten. Files you haven't edited pick up the agent's changes as it makes them. **Search** finds text across the folder (git grep, so ignored files are skipped). **Diff** compares the file with where the agent started, side by side, and you can edit on the right. **+ / ▣ / ✎ / 🗑** create, rename and delete. Every path stays inside the agent's folder: `..`, and links that point outside, are refused.
|
|
911
|
+
- **VS Code / Cursor** opens the agent's folder (and the open file) in the editor installed on this computer. The buttons show when its `code` or `cursor` command is found.
|
|
912
|
+
- Monaco loads from the jsDelivr CDN the first time you open **Code**. Offline, a plain text editor takes its place.
|
|
913
|
+
- **Terminal** in the top bar opens a shell on this computer in your home folder, not tied to any agent. It works from any device you're signed in on, so you can run any command from your phone. **+ Terminal** opens more. They keep running when hidden; **×** ends one. Plugins can refuse it with the `terminal.open` policy (`kind: "machine"`).
|
|
914
|
+
|
|
894
915
|
## Shell and general sessions
|
|
895
916
|
|
|
896
917
|
- **Open shell** (or `agentmgr shell <agent>`) starts `$SHELL -l` in the agent's working folder. It shows as a **Shell 1** tab next to the **Claude** tab in the terminal pane; **+ Shell** (or `agentmgr shell <agent> -n 2`) opens more, and **×** on a tab closes that shell. Shells live as long as the daemon and don't start Claude.
|
package/docs/distributed.md
CHANGED
|
@@ -16,7 +16,7 @@ AI Agent Manager runs every agent on the machine its daemon runs on. Once you ha
|
|
|
16
16
|
|
|
17
17
|
## Non-goals (for now)
|
|
18
18
|
|
|
19
|
-
-
|
|
19
|
+
- Live migration (moving a process mid-turn). Moving an agent between turns is supported: see **Moving agents** below.
|
|
20
20
|
- Several people sharing one hub. The design leaves room for it, but 0.3 is single-user.
|
|
21
21
|
- A multi-user hosted service. The hub will move to Railway, but as a single-user hub with one access token.
|
|
22
22
|
|
|
@@ -100,6 +100,7 @@ The hub sends `rpc create` to the chosen runner, which runs the agent through it
|
|
|
100
100
|
- **Inbox:** the union of every runner's inbox items, each tagged with its machine. Answers go back as `rpc`.
|
|
101
101
|
- **Conflict radar across machines:** runners send their change sets (files and line ranges, not file contents). The hub runs the same `findConflicts` across runners, keyed by normalized repo URL instead of the local clone path.
|
|
102
102
|
- **Races across machines:** a race may place each attempt on a different runner, for example the same intent on macOS and Linux. Tests run on the attempt's own runner.
|
|
103
|
+
- **Moving agents:** the hub runs a move as fixed steps on both machines (`handoff-export`, `-chunk`, `-done` on the source; `handoff-import`, `-put`, `-finish`, `-cancel` on the target). Steps can't be called one by one from the actions API. Both machines must allow `move`. Code goes through the repo's own remote, as a snapshot commit on `agentmgr-handoff/<id>` that is deleted afterwards. The transcript goes through the hub in 512 KB pieces (base64, under the runner link's 1 MB frames), held in memory only. The target rewrites `cwd` in the transcript to its own folder. The source copy is marked `movedTo` and refuses to start.
|
|
103
104
|
- **Forks across machines (later):** packing the checkpoint as a `git bundle` with the transcript and unpacking it on the target runner. This is the only feature that moves code between machines, so it waits for the trust model to settle.
|
|
104
105
|
|
|
105
106
|
## Security
|
package/docs/engines.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Design: several agent engines (Claude Code, Codex CLI, Gemini CLI)
|
|
2
2
|
|
|
3
|
-
Status: phase 1, Codex and
|
|
3
|
+
Status: phase 1, Codex, Antigravity and Muse Code built. Gemini CLI dropped: since June 18, 2026 it no longer serves personal Google accounts, and Antigravity CLI (`agy`, `src/engines/antigravity.js`, checked against 1.4.3, fixtures in `test/fixtures/antigravity/`) replaced it. 0.4 (branch `v0.4-engines`) · 2026-10-18. Targets the current releases of each CLI, checked in phase 1.
|
|
4
4
|
|
|
5
5
|
AI Agent Manager runs every agent with Claude Code. If Anthropic has an outage, a rate limit hits, or prices change, every agent stops at once. Running agents on other engines as well makes the fleet resilient across providers, and lets each task use the engine that suits it.
|
|
6
6
|
|
|
@@ -167,6 +167,7 @@ The e2e suite uses a fake Claude (`test/fake-claude.js`). Each engine gets its o
|
|
|
167
167
|
| 1. Adapter seam ✅ | `src/engines/claude.js` with today's behaviour behind the interface; `engine` on agents; real CLIs checked and recorded as fixtures |
|
|
168
168
|
| 2. Antigravity CLI ✅ | Replaces Gemini CLI. `src/engines/antigravity.js`: launch with instructions in the first message, adoption of the conversation agy picks (`cache/last_conversations.json`), resume with `--conversation`, usage, commands and writes from `brain/<id>/.system_generated/logs/transcript_full.jsonl`, command / file / access menus, trust in `settings.json` `trustedWorkspaces`, Explain in print mode (`--mode plan`, prompt attached to `--print=`). Non-Claude engines get a shorter conventions note: a smaller model turned the Claude-specific one into a search of the whole disk. |
|
|
169
169
|
| 3. Codex CLI ✅ | `src/engines/codex.js`: launch and resume, session adoption, brief folder (`--add-dir`), usage, stuck detection, prompts, trust, Explain through `codex exec`; engine pickers in the CLI, the page and the hub; machines report `engine:*` labels for the shared backlog. Not yet: planning while cloning and forks. Built before Gemini, which waits on a sign-in. |
|
|
170
|
+
| 3b. Muse Code ✅ (partly) | `src/engines/muse.js`, checked against Muse Code 1.4.14 (fixtures in `test/fixtures/muse/`). Launch with instructions in the first message; adoption of the session Muse picks (`$XDG_DATA_HOME/muse/sessions/YYYY/MM/DD/<id>/session.jsonl`, folder from the `runtime.session.metadata` record's `workspace_root`); `muse resume <id>`, which takes no message, so a message for a resumed session is typed once it is idle (`supports.resumePrompt: false`); `--trust-workspace` on each launch instead of saving trust; Plan → `:read-only` profile, Auto → `:auto-review`, Bypass → `--yolo`; prompts, replies and usage from the run events; stopped with SIGTERM (`stopSignal`), since a hangup leaves no clean end in its log; Explain through `muse exec` in the read-only profile (options after `exec`: `oneShotExtra`). A notice in the engine picker when Muse runs in contributor mode (Meta may use the content). Not yet, because real model calls weren't available while building it: commands and edits from the log (no stuck detection from them), and the approval menus are matched from the wording in the binary rather than a recorded screen. |
|
|
170
171
|
| 4. Fail over | Engine health, preference lists in both backlogs, engines reported to the hub, **Continue on…** handoff |
|
|
171
172
|
|
|
172
173
|
## Open questions
|
package/docs/plugins.md
CHANGED
|
@@ -56,7 +56,8 @@ Policies are synchronous so every code path can call them. Keep them fast; do sl
|
|
|
56
56
|
| `agent.commit` | Before Commit in the review screen commits an agent's worktree | `agent`, `message` |
|
|
57
57
|
| `agent.pr` | Before the review screen pushes an agent's branch and opens a pull request | `agent`, `title`, `draft` |
|
|
58
58
|
| `agent.merge` | Before Merge (or auto-merge) in the Land tab merges an agent's pull request | `agent`, `url`, `method`, `auto` |
|
|
59
|
-
| `terminal.open` | Before a live terminal is attached (browser, CLI `attach`, hub) | `agent`, `kind` (`claude` or `shell:N`) |
|
|
59
|
+
| `terminal.open` | Before a live terminal is attached (browser, CLI `attach`, hub) | `agent`, `kind` (`claude` or `shell:N`); for **Open Terminal** (a shell on the machine, not tied to an agent) `agent` is null and `kind` is `machine` |
|
|
60
|
+
| `agent.files` | Before the studio changes a file in an agent's folder | `agent`, `op` (`write`, `create`, `rename` or `delete`), `path`, and `to` for a rename |
|
|
60
61
|
| `hub.action` | Before an action the hub asked for, after this machine's own `hub.allow` check | `action`, `agent` |
|
|
61
62
|
|
|
62
63
|
`agent` is a summary: id, name, kind, engine, intent, repo, workdir, branch, permissionMode, model, project, status.
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -10,6 +10,10 @@ import { initTriggers } from '/triggers.js';
|
|
|
10
10
|
import { initSpend } from '/spend.js';
|
|
11
11
|
import { initPlaybooks } from '/playbooks.js';
|
|
12
12
|
import { initSetup } from '/setup.js';
|
|
13
|
+
import { initUpdate } from '/update.js';
|
|
14
|
+
import { initMenu } from '/menu.js';
|
|
15
|
+
import { initCode } from '/code.js';
|
|
16
|
+
import { initMachine } from '/machine.js';
|
|
13
17
|
|
|
14
18
|
const $ = (sel) => document.querySelector(sel);
|
|
15
19
|
const agents = new Map();
|
|
@@ -30,7 +34,7 @@ const ticketHash = location.hash.match(/(?:^#|&)ticket=([\w-]+)/)?.[1] ?? null;
|
|
|
30
34
|
const goHash = ticketHash ? decodeURIComponent(location.hash.match(/(?:^#|&)go=([^&]*)/)?.[1] ?? '') : '';
|
|
31
35
|
const [hashId, hashView] = loginHash ? [null, null] : (ticketHash ? goHash : location.hash.slice(1)).split('/');
|
|
32
36
|
let selectedId = hashId || null;
|
|
33
|
-
let openTermOnLoad = hashView === 'term' || isShell(hashView) || isBrowser(hashView);
|
|
37
|
+
let openTermOnLoad = hashView === 'term' || hashView === 'code' || isShell(hashView) || isBrowser(hashView);
|
|
34
38
|
let filterText = '';
|
|
35
39
|
// "Only agents created in the last N days": remembered in this browser.
|
|
36
40
|
const recent = (() => {
|
|
@@ -103,6 +107,10 @@ const reviewUi = initReview({ api, esc, toast, agents, $ });
|
|
|
103
107
|
const triggersUi = initTriggers({ api, esc, toast, $ });
|
|
104
108
|
initSpend({ api, esc, toast, $ });
|
|
105
109
|
initSetup({ api, esc, $ });
|
|
110
|
+
const updateUi = initUpdate({ api, esc, toast, $ });
|
|
111
|
+
initMenu({ api, esc, toast, $ });
|
|
112
|
+
initMachine({ api, esc, toast, $, Terminal, FitAddon });
|
|
113
|
+
const codeUi = initCode({ api, esc, toast, $, agents, isOpen: () => term.kind === 'code' });
|
|
106
114
|
const playbooksUi = initPlaybooks({ api, esc, toast, $, select: (id) => (showTab('agents'), select(id)) });
|
|
107
115
|
$('#new-playbook').onclick = () => {
|
|
108
116
|
const f = $('#new-form');
|
|
@@ -237,7 +245,9 @@ const INSIGHT_ICONS = { why: '🎯', what: '📦', how: '🛠', now: '📍' };
|
|
|
237
245
|
|
|
238
246
|
// ------------------------------------------------------------------ stakeholder documents
|
|
239
247
|
|
|
240
|
-
const DOCS = { analysis: 'Analysis', design: 'Design', flow: 'Flow', proposal: 'Proposal', humanai: 'Human vs AI' };
|
|
248
|
+
const DOCS = { analysis: 'Analysis', design: 'Design', flow: 'Flow', proposal: 'Proposal', humanai: 'Human vs AI', diagrams: 'Diagrams' };
|
|
249
|
+
// Optional extras that "Write all" leaves out.
|
|
250
|
+
const DOC_EXTRAS = new Set(['diagrams']);
|
|
241
251
|
const DOC_TITLES = { ...DOCS, plan: 'Implementation plan' };
|
|
242
252
|
/** What the document dialog shows: a stakeholder document, or the agent's plan under review. */
|
|
243
253
|
const docOf = (a, kind) => (kind === 'plan' ? a?.planReview && { markdown: a.planReview.markdown, at: a.planReview.updatedAt } : a?.docs?.[kind]);
|
|
@@ -300,7 +310,7 @@ function docHtml(markdown) {
|
|
|
300
310
|
}
|
|
301
311
|
|
|
302
312
|
function docButtons(a) {
|
|
303
|
-
const missing = Object.keys(DOCS).filter((k) => !a.docs?.[k]?.markdown && !a.docs?.[k]?.pending);
|
|
313
|
+
const missing = Object.keys(DOCS).filter((k) => !DOC_EXTRAS.has(k) && !a.docs?.[k]?.markdown && !a.docs?.[k]?.pending);
|
|
304
314
|
const all = missing.length
|
|
305
315
|
? `<button class="small ghost" data-doc-all title="Write ${missing.map((k) => DOCS[k]).join(', ')} in the background; you'll be told when each is ready">Write all in background</button>`
|
|
306
316
|
: '';
|
|
@@ -309,7 +319,7 @@ function docButtons(a) {
|
|
|
309
319
|
const d = a.docs?.[k];
|
|
310
320
|
const stale = d?.markdown && a.brief?.updatedAt && d.briefUpdatedAt !== a.brief.updatedAt;
|
|
311
321
|
return `<button class="small doc-btn ${d?.markdown ? 'has' : ''}" data-doc="${k}" ${d?.pending ? 'disabled' : ''}
|
|
312
|
-
title="${d?.markdown ? `Open the ${label.toLowerCase()} document${stale ? ' (the brief changed since)' : ''}` : `Write a ${label.toLowerCase()} document for stakeholders`}">${d?.pending ? `${label}…` : label}${d?.markdown ? (stale ? ' •' : ' ✓') : ''}</button>`;
|
|
322
|
+
title="${k === 'diagrams' ? (d?.markdown ? `Open the diagrams${stale ? ' (the brief changed since)' : ''}` : 'Draw the problem and its solution for a quick grasp: architecture, design, data flow (animated) and the key interaction, diagrams only. Built from the documents already written and the agent\'s work') : d?.markdown ? `Open the ${label.toLowerCase()} document${stale ? ' (the brief changed since)' : ''}` : `Write a ${label.toLowerCase()} document for stakeholders`}">${d?.pending ? `${label}…` : label}${d?.markdown ? (stale ? ' •' : ' ✓') : ''}</button>`;
|
|
313
323
|
})
|
|
314
324
|
.join('')}</span>`;
|
|
315
325
|
}
|
|
@@ -324,8 +334,9 @@ function renderDocDialog() {
|
|
|
324
334
|
const plan = docView.kind === 'plan';
|
|
325
335
|
$('#doc-title').textContent = `${label} · ${a?.name ?? ''}`;
|
|
326
336
|
const body = $('#doc-body');
|
|
337
|
+
body.classList.toggle('diagrams-view', docView.kind === 'diagrams');
|
|
327
338
|
if (d?.pending) {
|
|
328
|
-
body.innerHTML = `<p class="hint">${docView.kind === 'humanai' ? 'Splitting the open questions and answering the AI ones, then asking a second model to check…' : `Writing the ${label.toLowerCase()} document from the agent's work so far…`}</p>`;
|
|
339
|
+
body.innerHTML = `<p class="hint">${docView.kind === 'humanai' ? 'Splitting the open questions and answering the AI ones, then asking a second model to check…' : docView.kind === 'diagrams' ? "Drawing the diagrams from the documents and the agent's work so far…" : `Writing the ${label.toLowerCase()} document from the agent's work so far…`}</p>`;
|
|
329
340
|
body.dataset.rendered = '';
|
|
330
341
|
} else if (d?.error) body.innerHTML = `<p class="error">Couldn't write it: ${esc(d.error)}</p>`;
|
|
331
342
|
else if (plan && !d?.markdown) body.innerHTML = '<p class="hint">The agent is investigating and hasn\'t written its plan yet. You\'ll see it here (and in the inbox) when it has.</p>';
|
|
@@ -337,8 +348,8 @@ function renderDocDialog() {
|
|
|
337
348
|
}
|
|
338
349
|
}
|
|
339
350
|
const stale = !plan && d?.markdown && a.brief?.updatedAt && d.briefUpdatedAt !== a.brief.updatedAt;
|
|
340
|
-
const by = d?.writer ? ` by ${d.writer}${d.critique ? `, checked by ${d.critique.label}` : ''}` : '';
|
|
341
|
-
$('#doc-meta').textContent = d?.markdown ? `${plan ? `Plan · ${PLAN_LABEL[a.planReview.status]} · updated` : 'Written'} ${ago(d.at)}${by}${stale ? ' · the brief has changed since; Regenerate for the latest' : ''}` : '';
|
|
351
|
+
const by = d?.writer ? ` by ${d.writer}${d.critique ? `, checked by ${d.critique.label}` : ''}${d.from ? ` from ${d.from.length ? d.from.join(', ') : "the agent's work"}` : ''}` : '';
|
|
352
|
+
$('#doc-meta').textContent = d?.markdown ? `${plan ? `Plan · ${PLAN_LABEL[a.planReview.status]} · updated` : docView.kind === 'diagrams' ? 'Drawn' : 'Written'} ${ago(d.at)}${by}${stale ? ' · the brief has changed since; Regenerate for the latest' : ''}` : '';
|
|
342
353
|
for (const id of ['doc-copy', 'doc-download', 'doc-print']) $(`#${id}`).disabled = !d?.markdown;
|
|
343
354
|
$('#doc-print').hidden = plan;
|
|
344
355
|
$('#doc-regen').hidden = plan;
|
|
@@ -622,7 +633,7 @@ const noRepo = (a) => a.kind === 'general' || a.kind === 'assistant';
|
|
|
622
633
|
function agentItem(a) {
|
|
623
634
|
const st = statusOf(a);
|
|
624
635
|
const idle = !isActive(a);
|
|
625
|
-
return `<li data-id="${a.id}" class="agent pr${a.priority ?? 2} ${a.id === selectedId ? 'selected' : ''} ${idle ? 'idle' : ''}">
|
|
636
|
+
return `<li data-id="${a.id}" ${a.kind === 'general' || a.kind === 'assistant' ? '' : 'draggable="true" '}class="agent pr${a.priority ?? 2} ${a.id === selectedId ? 'selected' : ''} ${idle ? 'idle' : ''}">
|
|
626
637
|
<div class="line1">${prioChip(a.priority ?? 2)}${a.results?.outcome === 'blocked' ? '<span class="outcome-mark" title="Blocked">⛔</span>' : a.results?.outcome === 'done' ? '<span class="outcome-mark" title="Done">✅</span>' : ''}<span class="name">${esc(a.name)}</span>${flags(a)}<span class="pill ${st}">${pillText(st)}</span></div>
|
|
627
638
|
<div class="intent">${a.kind === 'general' ? '<span class="tag">general</span> ' : a.kind === 'assistant' ? '<span class="tag assistant">assistant</span> ' : a.startedBy ? '<span class="tag assistant" title="Started by the assistant">🤖 by assistant</span> ' : a.trigger ? `<span class="tag assistant" title="Started from ${a.trigger.source === 'github' ? 'a labeled GitHub issue' : 'the webhook'}${a.trigger.url ? `: ${esc(a.trigger.url)}` : ''}">⚡ ${a.trigger.source === 'github' ? 'GitHub' : 'webhook'}</span> ` : a.playbook ? `<span class="tag assistant" title="Started by the playbook ${esc(a.playbook.name)}">▶ ${esc(a.playbook.name)}</span> ` : ''}${a.engine && a.engine !== 'claude' ? `<span class="tag engine">${esc(ENGINE_LABEL[a.engine] ?? a.engine)}</span> ` : ''}${esc(a.intent || (a.kind === 'assistant' ? 'Finds new work and starts agents for it' : 'General session'))}</div>
|
|
628
639
|
${a.status === 'preparing' && a.progress ? `<div class="progress-line">${esc(progressText(a.progress))}</div>` : ''}
|
|
@@ -643,6 +654,13 @@ function agentItem(a) {
|
|
|
643
654
|
}
|
|
644
655
|
|
|
645
656
|
/** Agents grouped into projects, busiest and most urgent first. */
|
|
657
|
+
// Dragging an agent onto another project moves it there (as Edit → Project does). The list
|
|
658
|
+
// doesn't redraw mid-drag (updates arrive every few seconds), so the drag isn't lost.
|
|
659
|
+
let dragging = null;
|
|
660
|
+
let listStale = false;
|
|
661
|
+
/** What a drop on this project sends as the agent's project: its shortcut, else its name. */
|
|
662
|
+
const dropRef = (g) => (['assistant', 'general', 'other'].includes(g.key) ? '' : g.shortcut || g.name || '');
|
|
663
|
+
|
|
646
664
|
function projectGroups(list) {
|
|
647
665
|
const groups = new Map();
|
|
648
666
|
for (const a of list) {
|
|
@@ -686,6 +704,7 @@ function projectGroups(list) {
|
|
|
686
704
|
}
|
|
687
705
|
|
|
688
706
|
function renderList() {
|
|
707
|
+
if (dragging) return void (listStale = true);
|
|
689
708
|
const waiting = [...agents.values()].filter((a) => a.runtime.attention).length;
|
|
690
709
|
document.title = waiting ? `(${waiting}) AI Agent Manager` : 'AI Agent Manager';
|
|
691
710
|
// "@cc" shows one project; any other text also matches shortcuts.
|
|
@@ -727,7 +746,7 @@ function renderList() {
|
|
|
727
746
|
const n = g.agents.length;
|
|
728
747
|
// While filtering, show every match; otherwise respect collapsed projects.
|
|
729
748
|
const open = filterText || !collapsed.has(g.key) || g.agents.some((a) => a.id === selectedId);
|
|
730
|
-
return `<li class="project ${open ? 'open' : ''}">
|
|
749
|
+
return `<li class="project ${open ? 'open' : ''}" data-key="${esc(g.key)}" data-move="${esc(dropRef(g))}">
|
|
731
750
|
<button class="project-head" data-toggle="${esc(g.key)}" aria-expanded="${open}" title="${esc(g.full || g.name)}">
|
|
732
751
|
<span class="caret">${open ? '▾' : '▸'}</span>
|
|
733
752
|
${g.shortcut ? `<span class="shortcut" data-rename="${esc(g.shortcut)}" title="Shortcut: type @${esc(g.shortcut)} to filter, or use it as the project when adding agents (click to rename)">${esc(g.shortcut)}</span>` : ''}
|
|
@@ -1603,6 +1622,9 @@ function renderDetail() {
|
|
|
1603
1622
|
${it.status === 'review' ? `<span class="spacer"></span><button class="small primary" data-backlog-accept="${it.id}">Accept</button> <button class="small" data-backlog-reopen="${it.id}">Reopen</button>` : ''}</div>`;
|
|
1604
1623
|
})()}
|
|
1605
1624
|
${a.error ? `<div class="error-box">${esc(a.error)}</div>` : ''}
|
|
1625
|
+
${a.movedTo ? `<p class="hint">Moved to <b>${esc(a.movedTo.machine)}</b> ${ago(a.movedTo.at)}: it continues there with the same conversation and work. This copy stays closed; delete it when you no longer need its folder.</p>` : ''}
|
|
1626
|
+
${a.movedFrom ? `<p class="hint">Moved here from <b>${esc(a.movedFrom.machine ?? 'another machine')}</b> ${ago(a.movedFrom.at)}, with its conversation and uncommitted work.</p>` : ''}
|
|
1627
|
+
${!running && a.closedBy === 'cap' ? `<p class="hint">Closed ${ago(a.closedAt)} to stay under the limit on open agents (Settings), as the one idle longest. Resume it any time${a.keep ? '' : ', or mark it Keep'}.</p>` : ''}
|
|
1606
1628
|
${!running && a.closedBy === 'cleanup' ? `<p class="hint">Closed by clean up ${ago(a.closedAt)}, after a while with no activity. Resume it any time${a.keep ? '' : ', or mark it Keep'}.</p>` : ''}
|
|
1607
1629
|
${a.status === 'preparing'
|
|
1608
1630
|
? `<div class="progress-box">
|
|
@@ -1765,6 +1787,9 @@ const term = {
|
|
|
1765
1787
|
this.xterm.loadAddon(this.fit);
|
|
1766
1788
|
this.xterm.open($('#terminal'));
|
|
1767
1789
|
this.xterm.onData((d) => {
|
|
1790
|
+
// Replaying the backlog makes xterm answer the color and cursor queries in it again; those
|
|
1791
|
+
// answers are stale, and the program would get them as typing.
|
|
1792
|
+
if (this.replaying) return;
|
|
1768
1793
|
lastTyped = Date.now();
|
|
1769
1794
|
this.send({ t: 'i', d });
|
|
1770
1795
|
});
|
|
@@ -1788,6 +1813,8 @@ const term = {
|
|
|
1788
1813
|
shellExited: false,
|
|
1789
1814
|
|
|
1790
1815
|
open(id, kind = 'claude') {
|
|
1816
|
+
if (kind === 'code') return this.openCode(id);
|
|
1817
|
+
$('#term-wrap').classList.remove('coding');
|
|
1791
1818
|
if (isBrowser(kind)) return this.openBrowser(id, kind);
|
|
1792
1819
|
liveView.close();
|
|
1793
1820
|
$('#term-wrap').classList.remove('browsing');
|
|
@@ -1821,6 +1848,7 @@ const term = {
|
|
|
1821
1848
|
this.conn = null;
|
|
1822
1849
|
if (ev.code === 1008) toast(`Terminal unavailable: ${ev.reason}`, 5000);
|
|
1823
1850
|
},
|
|
1851
|
+
onInputLost: () => this.conn === conn && toast('Some typing did not reach the computer (connection lost); check the terminal before going on', 8000),
|
|
1824
1852
|
},
|
|
1825
1853
|
);
|
|
1826
1854
|
this.conn = conn;
|
|
@@ -1831,7 +1859,8 @@ const term = {
|
|
|
1831
1859
|
if (msg.t === 'hello') {
|
|
1832
1860
|
// Also sent again after a reconnect: start from a clean screen and replay.
|
|
1833
1861
|
this.xterm.reset();
|
|
1834
|
-
this.
|
|
1862
|
+
this.replaying = true;
|
|
1863
|
+
this.xterm.write(msg.backlog, () => (this.replaying = false));
|
|
1835
1864
|
if (!matchMedia('(pointer: coarse)').matches) this.xterm.focus();
|
|
1836
1865
|
} else if (msg.t === 'error') {
|
|
1837
1866
|
toast(`Terminal unavailable: ${msg.error}`, 5000);
|
|
@@ -1847,6 +1876,10 @@ const term = {
|
|
|
1847
1876
|
|
|
1848
1877
|
syncOverlay() {
|
|
1849
1878
|
if (isBrowser(this.kind)) return this.renderTabs();
|
|
1879
|
+
if (this.kind === 'code') {
|
|
1880
|
+
$('#term-overlay').hidden = true;
|
|
1881
|
+
return this.renderTabs();
|
|
1882
|
+
}
|
|
1850
1883
|
const a = this.agentId && agents.get(this.agentId);
|
|
1851
1884
|
const shell = isShell(this.kind);
|
|
1852
1885
|
const closed = shell ? this.shellExited : a && !a.runtime.running && a.status !== 'preparing';
|
|
@@ -1863,6 +1896,8 @@ const term = {
|
|
|
1863
1896
|
renderTabs() {
|
|
1864
1897
|
renderSignIn();
|
|
1865
1898
|
const a = this.agentId && agents.get(this.agentId);
|
|
1899
|
+
// The agent's own session tab is named after its engine (Codex, Muse Code, …).
|
|
1900
|
+
$('.term-tab[data-kind="claude"]').textContent = ENGINE_LABEL[a?.engine] ?? 'Claude';
|
|
1866
1901
|
const nums = new Set(a?.runtime.shells ?? []);
|
|
1867
1902
|
if (isShell(this.kind)) nums.add(shellNum(this.kind));
|
|
1868
1903
|
$('#shell-tabs').innerHTML = [...nums]
|
|
@@ -1887,6 +1922,24 @@ const term = {
|
|
|
1887
1922
|
// Browser tabs: one iframe per tab, kept while the panel is open so switching back doesn't reload it.
|
|
1888
1923
|
frames: new Map(), // "<agent>:<n>" -> iframe
|
|
1889
1924
|
|
|
1925
|
+
// Code: the agent's files in an editor, in place of the terminal (like a browser tab).
|
|
1926
|
+
openCode(id) {
|
|
1927
|
+
history.replaceState(null, '', `#${id}/code`);
|
|
1928
|
+
liveView.close();
|
|
1929
|
+
$('#term-wrap').hidden = false;
|
|
1930
|
+
$('#detail').classList.add('with-term');
|
|
1931
|
+
$('#term-wrap').classList.remove('browsing');
|
|
1932
|
+
$('#term-wrap').classList.add('coding');
|
|
1933
|
+
this.ensure();
|
|
1934
|
+
this.disconnect();
|
|
1935
|
+
this.agentId = id;
|
|
1936
|
+
this.kind = 'code';
|
|
1937
|
+
$('#term-overlay').hidden = true;
|
|
1938
|
+
this.renderTabs();
|
|
1939
|
+
$('#term-title').textContent = agents.get(id)?.workdir ?? '';
|
|
1940
|
+
codeUi.open(id);
|
|
1941
|
+
},
|
|
1942
|
+
|
|
1890
1943
|
openBrowser(id, kind) {
|
|
1891
1944
|
history.replaceState(null, '', `#${id}/${kind}`);
|
|
1892
1945
|
$('#term-wrap').hidden = false;
|
|
@@ -2842,6 +2895,63 @@ $('#agent-list').addEventListener('click', (e) => {
|
|
|
2842
2895
|
if (li) select(li.dataset.id);
|
|
2843
2896
|
});
|
|
2844
2897
|
|
|
2898
|
+
const dropTarget = (e) => {
|
|
2899
|
+
const li = e.target.closest?.('li.project[data-move]');
|
|
2900
|
+
const a = dragging && agents.get(dragging);
|
|
2901
|
+
return li && li.dataset.move && a && (a.projectInfo?.key ?? 'other') !== li.dataset.key ? li : null;
|
|
2902
|
+
};
|
|
2903
|
+
const clearDrop = () => document.querySelectorAll('li.project.drop-target').forEach((x) => x.classList.remove('drop-target'));
|
|
2904
|
+
$('#agent-list').addEventListener('dragstart', (e) => {
|
|
2905
|
+
const li = e.target.closest?.('li[data-id][draggable="true"]');
|
|
2906
|
+
if (!li) return;
|
|
2907
|
+
dragging = li.dataset.id;
|
|
2908
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
2909
|
+
e.dataTransfer.setData('text/plain', agents.get(dragging)?.name ?? dragging);
|
|
2910
|
+
li.classList.add('dragging');
|
|
2911
|
+
document.body.classList.add('dragging-agent');
|
|
2912
|
+
});
|
|
2913
|
+
$('#agent-list').addEventListener('dragover', (e) => {
|
|
2914
|
+
const li = dropTarget(e);
|
|
2915
|
+
if (!li) return;
|
|
2916
|
+
e.preventDefault();
|
|
2917
|
+
e.dataTransfer.dropEffect = 'move';
|
|
2918
|
+
if (!li.classList.contains('drop-target')) {
|
|
2919
|
+
clearDrop();
|
|
2920
|
+
li.classList.add('drop-target');
|
|
2921
|
+
}
|
|
2922
|
+
});
|
|
2923
|
+
$('#agent-list').addEventListener('dragleave', (e) => {
|
|
2924
|
+
const li = e.target.closest?.('li.project');
|
|
2925
|
+
if (li && !li.contains(e.relatedTarget)) li.classList.remove('drop-target');
|
|
2926
|
+
});
|
|
2927
|
+
$('#agent-list').addEventListener('drop', (e) => {
|
|
2928
|
+
const li = dropTarget(e);
|
|
2929
|
+
if (!li) return;
|
|
2930
|
+
e.preventDefault();
|
|
2931
|
+
const a = agents.get(dragging);
|
|
2932
|
+
const to = li.dataset.move;
|
|
2933
|
+
const label = li.querySelector('.pname')?.textContent ?? to;
|
|
2934
|
+
act(async () => upsert(await api('PATCH', `/agents/${a.id}`, { project: to })), `Moved ${a.name} to ${label}`);
|
|
2935
|
+
});
|
|
2936
|
+
document.addEventListener('dragend', () => {
|
|
2937
|
+
if (!dragging) return;
|
|
2938
|
+
dragging = null;
|
|
2939
|
+
clearDrop();
|
|
2940
|
+
document.body.classList.remove('dragging-agent');
|
|
2941
|
+
document.querySelector('li.agent.dragging')?.classList.remove('dragging');
|
|
2942
|
+
if (listStale) {
|
|
2943
|
+
listStale = false;
|
|
2944
|
+
renderList();
|
|
2945
|
+
}
|
|
2946
|
+
});
|
|
2947
|
+
|
|
2948
|
+
// The title goes home: the Agents tab with no agent open.
|
|
2949
|
+
$('#home-link').onclick = (e) => {
|
|
2950
|
+
e.preventDefault();
|
|
2951
|
+
showTab('agents');
|
|
2952
|
+
select(null);
|
|
2953
|
+
};
|
|
2954
|
+
|
|
2845
2955
|
$('#detail-body').addEventListener('click', async (e) => {
|
|
2846
2956
|
const a = agents.get(selectedId);
|
|
2847
2957
|
if (!a) return;
|
|
@@ -3047,6 +3157,7 @@ function openNewDialog(mode = 'now') {
|
|
|
3047
3157
|
renderProjectOptions();
|
|
3048
3158
|
$('#new-error').hidden = true;
|
|
3049
3159
|
dialog.querySelector(`input[name="when"][value="${mode}"]`).checked = true;
|
|
3160
|
+
restoreIntentSplit();
|
|
3050
3161
|
syncWhen();
|
|
3051
3162
|
dialog.showModal();
|
|
3052
3163
|
dialog.querySelector('textarea').focus();
|
|
@@ -3056,7 +3167,55 @@ function syncWhen() {
|
|
|
3056
3167
|
dialog.querySelector('.now-only').hidden = when !== 'now';
|
|
3057
3168
|
dialog.querySelector('.race-only').hidden = when !== 'race';
|
|
3058
3169
|
dialog.querySelector('.team-only').hidden = when !== 'team';
|
|
3059
|
-
|
|
3170
|
+
// A race or a team always works on the whole text.
|
|
3171
|
+
dialog.querySelector('.intent-split').hidden = when === 'race' || when === 'team';
|
|
3172
|
+
syncIntentPlaceholder();
|
|
3173
|
+
syncIntentCount();
|
|
3174
|
+
}
|
|
3175
|
+
|
|
3176
|
+
/** The form's intents: one per line, or the whole text as one (kept as written, blank lines and all). */
|
|
3177
|
+
function formIntents(form) {
|
|
3178
|
+
const text = form.intents.value;
|
|
3179
|
+
const whole = form.split.value === 'one' || ['race', 'team'].includes(form.when.value);
|
|
3180
|
+
if (whole) return text.trim() ? [text.trim()] : [];
|
|
3181
|
+
return text
|
|
3182
|
+
.split('\n')
|
|
3183
|
+
.map((s) => s.trim())
|
|
3184
|
+
.filter(Boolean);
|
|
3185
|
+
}
|
|
3186
|
+
|
|
3187
|
+
function syncIntentCount() {
|
|
3188
|
+
const form = $('#new-form');
|
|
3189
|
+
const when = form.when.value;
|
|
3190
|
+
const n = formIntents(form).length;
|
|
3191
|
+
const split = !['race', 'team'].includes(when);
|
|
3192
|
+
$('#intent-count').textContent = split && n ? (when === 'backlog' ? `${n} item${n > 1 ? 's' : ''}` : `${n} agent${n > 1 ? 's' : ''}`) : '';
|
|
3193
|
+
const submit = dialog.querySelector('button[type=submit]');
|
|
3194
|
+
submit.textContent = { backlog: n > 1 ? `Add ${n} to backlog` : 'Add to backlog', race: 'Start race', team: 'Start team' }[when] ?? (n > 1 ? `Create ${n} agents` : 'Create agent');
|
|
3195
|
+
}
|
|
3196
|
+
$('#new-form').intents.addEventListener('input', syncIntentCount);
|
|
3197
|
+
for (const r of dialog.querySelectorAll('input[name="split"]')) {
|
|
3198
|
+
r.addEventListener('change', () => {
|
|
3199
|
+
try {
|
|
3200
|
+
localStorage.setItem('agentmgr.intentSplit', r.value);
|
|
3201
|
+
} catch {}
|
|
3202
|
+
syncIntentPlaceholder();
|
|
3203
|
+
syncIntentCount();
|
|
3204
|
+
});
|
|
3205
|
+
}
|
|
3206
|
+
const LINES_PLACEHOLDER = $('#new-form').intents.placeholder;
|
|
3207
|
+
const WHOLE_PLACEHOLDER = 'Describe the work in as much detail as you like: the problem, what done looks like, constraints, links. It all goes to one agent.';
|
|
3208
|
+
/** The remembered choice (a form reset forgets it), and a placeholder to match. */
|
|
3209
|
+
function restoreIntentSplit() {
|
|
3210
|
+
const form = $('#new-form');
|
|
3211
|
+
try {
|
|
3212
|
+
form.split.value = localStorage.getItem('agentmgr.intentSplit') === 'one' ? 'one' : 'lines';
|
|
3213
|
+
} catch {}
|
|
3214
|
+
syncIntentPlaceholder();
|
|
3215
|
+
}
|
|
3216
|
+
function syncIntentPlaceholder() {
|
|
3217
|
+
const form = $('#new-form');
|
|
3218
|
+
form.intents.placeholder = form.split.value === 'one' || ['race', 'team'].includes(form.when.value) ? WHOLE_PLACEHOLDER : LINES_PLACEHOLDER;
|
|
3060
3219
|
}
|
|
3061
3220
|
for (const r of dialog.querySelectorAll('input[name="when"]')) r.addEventListener('change', syncWhen);
|
|
3062
3221
|
$('#new-btn').onclick = () => openNewDialog('now');
|
|
@@ -3064,10 +3223,7 @@ $('#new-cancel').onclick = () => dialog.close();
|
|
|
3064
3223
|
$('#new-form').addEventListener('submit', async (e) => {
|
|
3065
3224
|
e.preventDefault();
|
|
3066
3225
|
const f = new FormData(e.target);
|
|
3067
|
-
const intents =
|
|
3068
|
-
.split('\n')
|
|
3069
|
-
.map((s) => s.trim())
|
|
3070
|
-
.filter(Boolean);
|
|
3226
|
+
const intents = formIntents(e.target);
|
|
3071
3227
|
if (f.get('when') === 'team') {
|
|
3072
3228
|
// "analyst=opus reviewer=opus"
|
|
3073
3229
|
const models = Object.fromEntries(
|
|
@@ -3243,6 +3399,9 @@ $('#about-check').onclick = async () => {
|
|
|
3243
3399
|
out.hidden = false;
|
|
3244
3400
|
out.textContent = 'Checking…';
|
|
3245
3401
|
try {
|
|
3402
|
+
// On this computer the daemon can install it; from a phone, just say what is out.
|
|
3403
|
+
const local = await updateUi.check().catch(() => null);
|
|
3404
|
+
if (local) return void (out.innerHTML = local);
|
|
3246
3405
|
const r = await api('GET', '/about/latest');
|
|
3247
3406
|
out.innerHTML = r.newer
|
|
3248
3407
|
? `Version ${esc(r.latest)} is available. Update with <code>npm install -g ${esc(aboutInfo?.package ?? 'ai-agentmanager')}@latest</code>, then <code>agentmgr restart</code>.`
|
|
@@ -3304,7 +3463,7 @@ function handleEvent(msg) {
|
|
|
3304
3463
|
msg.agents.forEach(watchDocs);
|
|
3305
3464
|
if (selectedId && !agents.has(selectedId)) selectedId = null;
|
|
3306
3465
|
if (openTermOnLoad && selectedId) {
|
|
3307
|
-
if (isShell(hashView) || isBrowser(hashView)) term.open(selectedId, hashView);
|
|
3466
|
+
if (isShell(hashView) || isBrowser(hashView) || hashView === 'code') term.open(selectedId, hashView);
|
|
3308
3467
|
else if (agents.get(selectedId).runtime.running) term.open(selectedId);
|
|
3309
3468
|
}
|
|
3310
3469
|
openTermOnLoad = false;
|
|
@@ -3848,13 +4007,22 @@ $('#login-form').addEventListener('submit', async (e) => {
|
|
|
3848
4007
|
});
|
|
3849
4008
|
|
|
3850
4009
|
// Engines this machine can run; the New agent form offers a choice once there's more than Claude.
|
|
3851
|
-
const ENGINE_LABEL = { claude: 'Claude', codex: 'Codex', antigravity: 'Antigravity' };
|
|
4010
|
+
const ENGINE_LABEL = { claude: 'Claude', codex: 'Codex', antigravity: 'Antigravity', muse: 'Muse Code' };
|
|
3852
4011
|
async function loadEngines() {
|
|
3853
4012
|
try {
|
|
3854
4013
|
const list = (await api('GET', '/engines')).filter((e) => e.installed && e.id !== 'claude');
|
|
3855
4014
|
const sel = document.querySelector('#engine-field select');
|
|
3856
4015
|
for (const e of list) sel.append(Object.assign(document.createElement('option'), { value: e.id, textContent: e.label }));
|
|
3857
4016
|
$('#engine-field').hidden = !list.length;
|
|
4017
|
+
// Something to know before choosing an engine (Muse Code in contributor mode, say).
|
|
4018
|
+
const notices = Object.fromEntries(list.filter((e) => e.notice).map((e) => [e.id, e.notice]));
|
|
4019
|
+
const show = () => {
|
|
4020
|
+
const n = $('#engine-notice');
|
|
4021
|
+
n.textContent = notices[sel.value] ?? '';
|
|
4022
|
+
n.hidden = !notices[sel.value];
|
|
4023
|
+
};
|
|
4024
|
+
sel.addEventListener('change', show);
|
|
4025
|
+
show();
|
|
3858
4026
|
} catch {}
|
|
3859
4027
|
}
|
|
3860
4028
|
|