ai-agentmanager 1.4.0 → 1.6.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 +17 -0
- package/README.md +12 -8
- package/docs/engines.md +2 -1
- package/package.json +1 -1
- package/public/app.js +110 -16
- package/public/attach.js +100 -0
- package/public/hub/hub.js +1 -1
- package/public/index.html +28 -7
- package/public/machine.js +8 -2
- package/public/menu.js +11 -0
- package/public/setup.js +41 -14
- package/public/style.css +30 -2
- package/public/transport.js +19 -3
- package/src/backlog.js +4 -1
- package/src/brief.js +144 -0
- package/src/cli.js +44 -6
- package/src/engines/index.js +10 -2
- package/src/engines/muse.js +303 -0
- package/src/manager.js +171 -17
- package/src/review.js +3 -0
- package/src/server.js +33 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.6.0 (2026-10-31)
|
|
4
|
+
|
|
5
|
+
- **Changes explained one by one:** `agentmgr doc <agent> walkthrough` explains each hunk of an agent's changes (what, why, risk, what to check) in the order a reviewer should read them, from the same diff the review screen shows. The review screen and a split view for it come next.
|
|
6
|
+
- **Start at login:** a switch in ☰ → Settings installs (or removes) the login service, so the daemon and running agents come back by themselves after a restart, without a terminal command. Getting started lists it until it's on, and `agentmgr ui` mentions it when it's off. Same service as `agentmgr service install` (launchd on macOS, systemd on Linux).
|
|
7
|
+
- **Attachments:** **📎 Attach files** in New agents and Ask Agent: pick, drop or paste screenshots, logs and documents. Each agent gets its own copy, listed in its request, in a folder Claude Code and Codex can read without a prompt (a team or race shares one copy). Backlog items keep theirs until they start. CLI: `--file-attach <path>` on `agentmgr add` and `agentmgr ask`.
|
|
8
|
+
- **Ask Agent:** **Ask Claude** is now **Ask Agent**, with an Engine choice (Claude Code, Codex, Antigravity, Muse Code) and the engine's notice, if any. `agentmgr ask --engine <engine>`.
|
|
9
|
+
|
|
10
|
+
## 1.5.0 (2026-10-31)
|
|
11
|
+
|
|
12
|
+
- **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.
|
|
13
|
+
- **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).
|
|
14
|
+
- **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.
|
|
15
|
+
- **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`.
|
|
16
|
+
- **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.
|
|
17
|
+
- **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.
|
|
18
|
+
- **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.
|
|
19
|
+
|
|
3
20
|
## 1.4.0 (2026-10-31)
|
|
4
21
|
|
|
5
22
|
- **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.
|
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`.
|
|
@@ -42,7 +42,7 @@ Run one background [Claude Code](https://claude.com/claude-code) agent per inten
|
|
|
42
42
|
- **Follows your conventions.** Before changing anything, agents review `CLAUDE.md` / `AGENTS.md`, skills, hooks and settings (repo and `~/.claude`), and follow them.
|
|
43
43
|
- **Tokens and cost.** Input, output and cache tokens are tracked per session, subagents included, alongside the cost Claude Code reports.
|
|
44
44
|
- **No trust prompt.** Folders agents run in are pre-trusted, so Claude's "trust this folder" prompt doesn't block them.
|
|
45
|
-
- **Survives restarts and reboots.** Agents that were running when the daemon stopped (restart, crash or reboot) are resumed with the same session IDs when it starts again. `agentmgr service install`
|
|
45
|
+
- **Survives restarts and reboots.** Agents that were running when the daemon stopped (restart, crash or reboot) are resumed with the same session IDs when it starts again. For that after a reboot, turn on **Start at login** in ☰ → Settings (or run `agentmgr service install`); without it, the daemon starts only when you run `agentmgr`.
|
|
46
46
|
- **Shells next to each agent.** **Open shell** gives a terminal in the agent's folder for running commands by hand. **+ Shell** opens as many more as you need, each in its own tab.
|
|
47
47
|
- **Browser tabs.** **+ Browser** opens a page as this machine sees it, in a tab next to the agent's terminals. That can be the agent's dev server on `localhost:5173`, a site on its network or VPN, or any address. Local addresses the agent prints are offered as suggestions. It works from your phone or laptop too (see below).
|
|
48
48
|
- **Live browser.** Browser tabs run a real Chrome on this machine and stream it into the tab: you see its screen and your clicks, scrolling, typing and paste go back to it, from the laptop or the phone. Because the page runs here, `localhost` is this machine, sites that refuse to be framed load normally, and popups (a sign-in window) open as tabs of their own and come back when they close. Chrome (or Chromium, Brave, Edge; `browserBin` in config.json picks one) runs headless with its own profile in `~/.agentmanager/browser-profile`, so sign-ins are remembered; it starts on the first view and stops after 15 minutes with nobody watching. Downloads go to `~/Downloads/agentmanager`. A site that still refuses it as a bot may accept `"browserHeadless": false`, which runs Chrome with a real window on this machine's screen. On Linux where Chrome's sandbox can't start (containers, Ubuntu 23.10+ user-namespace restrictions) the tab says so; `"browserNoSandbox": true` runs it without the sandbox, at your own risk. **Lite** on the bar switches a tab to the frame mode (the page loads in your own browser; quicker for a dev server).
|
|
@@ -51,7 +51,8 @@ Run one background [Claude Code](https://claude.com/claude-code) agent per inten
|
|
|
51
51
|
- **Your layout.** Drag the edge of the agent list, or the line between the brief and the terminal, to resize them; double-click a line to reset it. **Expand** (Ctrl+Shift+E) fills the window with the terminal, shell or browser tab; **Full screen** fills the screen. Agents active in the last day sort to the top; idle ones are dimmed.
|
|
52
52
|
- **As an app.** In Chrome or Edge, click the install icon in the address bar (or ⋮ → Cast, save and share → Install page as app). AI Agent Manager then opens in its own window with its own Dock or taskbar icon, and the daemon keeps running as before.
|
|
53
53
|
- **About.** The version next to the title opens the About box: release date, author, license, links, and how this daemon runs (data folder, port, Node.js, which `claude`). **Check for updates** asks npm for the newest version. In a terminal: `agentmgr about` (`--check` for updates).
|
|
54
|
-
- **General sessions.** **Ask
|
|
54
|
+
- **General sessions.** **Ask Agent** starts a session with no repo, folder or intent, for questions, on Claude Code or any other engine you have (Codex, Antigravity, Muse Code). It is tracked and resumable like any other agent.
|
|
55
|
+
- **Attachments.** **📎 Attach files** in New agents and Ask Agent (or drop files on the dialog, or paste a screenshot into the text box) adds a screenshot, a log or a document to the request. Each agent gets its own copy in `~/.agentmanager/uploads/<agent>/`, listed in its request, and Claude Code and Codex can read that folder without asking. From the CLI: `agentmgr add … --file-attach error.log` and `agentmgr ask … --file-attach shot.png`.
|
|
55
56
|
|
|
56
57
|
New in 0.2, for running many agents at once:
|
|
57
58
|
|
|
@@ -83,13 +84,13 @@ Runs on macOS and Linux; on Windows, use WSL2. Requires Node 20+, `git`, and the
|
|
|
83
84
|
|
|
84
85
|
```sh
|
|
85
86
|
npm install -g ai-agentmanager # the latest release
|
|
86
|
-
npm install -g ai-agentmanager@1.
|
|
87
|
+
npm install -g ai-agentmanager@1.6.0 # or a specific one
|
|
87
88
|
```
|
|
88
89
|
|
|
89
90
|
Or from the repository, at a release tag:
|
|
90
91
|
|
|
91
92
|
```sh
|
|
92
|
-
git clone --branch v1.
|
|
93
|
+
git clone --branch v1.6.0 --depth 1 https://github.com/jdmunta/ai-agentmanager-core.git
|
|
93
94
|
cd ai-agentmanager-core
|
|
94
95
|
npm install
|
|
95
96
|
npm link # optional: puts `agentmgr` on your PATH
|
|
@@ -109,7 +110,7 @@ npm link
|
|
|
109
110
|
| Installed with | Update |
|
|
110
111
|
| --- | --- |
|
|
111
112
|
| npm | `npm install -g ai-agentmanager@latest` |
|
|
112
|
-
| A clone at a release tag | `git fetch --tags && git checkout v1.
|
|
113
|
+
| A clone at a release tag | `git fetch --tags && git checkout v1.6.0 && npm install` (the tag you want) |
|
|
113
114
|
| A clone of `main` | `git pull && npm install` |
|
|
114
115
|
|
|
115
116
|
**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 +328,7 @@ The Brief panel's **Share for review** buttons, or `agentmgr doc <agent> <kind>`
|
|
|
327
328
|
| **Flow** | Summary, end-to-end flowchart with a step-by-step walk-through, key interactions as a sequence diagram, failure and edge cases |
|
|
328
329
|
| **Proposal** | Summary, problem, recommendation, scope, plan, effort and dependencies, risks, decisions needed |
|
|
329
330
|
| **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. |
|
|
331
|
+
| **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
332
|
|
|
331
333
|
**Human vs AI.** This review takes two separate read-only calls.
|
|
332
334
|
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.
|
|
@@ -723,12 +725,14 @@ Or use **Remote access** in the header of the UI on this computer.
|
|
|
723
725
|
- **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
726
|
- **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
727
|
|
|
726
|
-
## Engines: Claude Code, Codex and
|
|
728
|
+
## Engines: Claude Code, Codex, Antigravity and Muse Code
|
|
727
729
|
|
|
728
730
|
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
731
|
|
|
730
732
|
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
733
|
|
|
734
|
+
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.
|
|
735
|
+
|
|
732
736
|
## Several machines: the hub
|
|
733
737
|
|
|
734
738
|
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.
|
|
@@ -917,7 +921,7 @@ Usage comes from the session transcripts and is refreshed every few seconds whil
|
|
|
917
921
|
- **From another device** (remote access), AI Agent Manager gives each site its own HTTPS port. That port relays to the site as the machine sees it, so you can check from your phone what the agent built or what the machine can reach. Absolute links, redirects and hot-reload WebSockets on the same site keep working, and the site's "don't show me in a frame" headers are dropped. A link or redirect to a different site leaves the relay.
|
|
918
922
|
- **Safety:** the relay serves only signed-in devices. It never relays to AI Agent Manager's own ports on any of the machine's addresses, and it removes AI Agent Manager's sign-in cookie before forwarding. Sites other than localhost must present a valid certificate. The relays close when remote access is turned off.
|
|
919
923
|
- If a tab stays blank on a device, open it once with **↗** and accept the certificate there.
|
|
920
|
-
- **Ask
|
|
924
|
+
- **Ask Agent** (or `agentmgr ask`, with `--engine` for another engine) creates a `general` agent. All general sessions share `~/.agentmanager/general`, so Claude's folder-trust prompt only appears the first time.
|
|
921
925
|
|
|
922
926
|
## Configuration
|
|
923
927
|
|
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/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -4,6 +4,7 @@ import { marked } from '/vendor/marked/marked.esm.js';
|
|
|
4
4
|
import DOMPurify from '/vendor/dompurify/purify.es.mjs';
|
|
5
5
|
import { touchScroll } from '/touchscroll.js';
|
|
6
6
|
import { openStream, post, REMOTE } from '/transport.js';
|
|
7
|
+
import { attachBox } from '/attach.js';
|
|
7
8
|
import { initReview } from '/review.js';
|
|
8
9
|
import { initLand, prChips } from '/land.js';
|
|
9
10
|
import { initTriggers } from '/triggers.js';
|
|
@@ -245,7 +246,9 @@ const INSIGHT_ICONS = { why: '🎯', what: '📦', how: '🛠', now: '📍' };
|
|
|
245
246
|
|
|
246
247
|
// ------------------------------------------------------------------ stakeholder documents
|
|
247
248
|
|
|
248
|
-
const DOCS = { analysis: 'Analysis', design: 'Design', flow: 'Flow', proposal: 'Proposal', humanai: 'Human vs AI' };
|
|
249
|
+
const DOCS = { analysis: 'Analysis', design: 'Design', flow: 'Flow', proposal: 'Proposal', humanai: 'Human vs AI', diagrams: 'Diagrams' };
|
|
250
|
+
// Optional extras that "Write all" leaves out.
|
|
251
|
+
const DOC_EXTRAS = new Set(['diagrams']);
|
|
249
252
|
const DOC_TITLES = { ...DOCS, plan: 'Implementation plan' };
|
|
250
253
|
/** What the document dialog shows: a stakeholder document, or the agent's plan under review. */
|
|
251
254
|
const docOf = (a, kind) => (kind === 'plan' ? a?.planReview && { markdown: a.planReview.markdown, at: a.planReview.updatedAt } : a?.docs?.[kind]);
|
|
@@ -308,7 +311,7 @@ function docHtml(markdown) {
|
|
|
308
311
|
}
|
|
309
312
|
|
|
310
313
|
function docButtons(a) {
|
|
311
|
-
const missing = Object.keys(DOCS).filter((k) => !a.docs?.[k]?.markdown && !a.docs?.[k]?.pending);
|
|
314
|
+
const missing = Object.keys(DOCS).filter((k) => !DOC_EXTRAS.has(k) && !a.docs?.[k]?.markdown && !a.docs?.[k]?.pending);
|
|
312
315
|
const all = missing.length
|
|
313
316
|
? `<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>`
|
|
314
317
|
: '';
|
|
@@ -317,7 +320,7 @@ function docButtons(a) {
|
|
|
317
320
|
const d = a.docs?.[k];
|
|
318
321
|
const stale = d?.markdown && a.brief?.updatedAt && d.briefUpdatedAt !== a.brief.updatedAt;
|
|
319
322
|
return `<button class="small doc-btn ${d?.markdown ? 'has' : ''}" data-doc="${k}" ${d?.pending ? 'disabled' : ''}
|
|
320
|
-
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>`;
|
|
323
|
+
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>`;
|
|
321
324
|
})
|
|
322
325
|
.join('')}</span>`;
|
|
323
326
|
}
|
|
@@ -332,8 +335,9 @@ function renderDocDialog() {
|
|
|
332
335
|
const plan = docView.kind === 'plan';
|
|
333
336
|
$('#doc-title').textContent = `${label} · ${a?.name ?? ''}`;
|
|
334
337
|
const body = $('#doc-body');
|
|
338
|
+
body.classList.toggle('diagrams-view', docView.kind === 'diagrams');
|
|
335
339
|
if (d?.pending) {
|
|
336
|
-
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>`;
|
|
340
|
+
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>`;
|
|
337
341
|
body.dataset.rendered = '';
|
|
338
342
|
} else if (d?.error) body.innerHTML = `<p class="error">Couldn't write it: ${esc(d.error)}</p>`;
|
|
339
343
|
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>';
|
|
@@ -345,8 +349,8 @@ function renderDocDialog() {
|
|
|
345
349
|
}
|
|
346
350
|
}
|
|
347
351
|
const stale = !plan && d?.markdown && a.brief?.updatedAt && d.briefUpdatedAt !== a.brief.updatedAt;
|
|
348
|
-
const by = d?.writer ? ` by ${d.writer}${d.critique ? `, checked by ${d.critique.label}` : ''}` : '';
|
|
349
|
-
$('#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' : ''}` : '';
|
|
352
|
+
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"}` : ''}` : '';
|
|
353
|
+
$('#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' : ''}` : '';
|
|
350
354
|
for (const id of ['doc-copy', 'doc-download', 'doc-print']) $(`#${id}`).disabled = !d?.markdown;
|
|
351
355
|
$('#doc-print').hidden = plan;
|
|
352
356
|
$('#doc-regen').hidden = plan;
|
|
@@ -1784,6 +1788,9 @@ const term = {
|
|
|
1784
1788
|
this.xterm.loadAddon(this.fit);
|
|
1785
1789
|
this.xterm.open($('#terminal'));
|
|
1786
1790
|
this.xterm.onData((d) => {
|
|
1791
|
+
// Replaying the backlog makes xterm answer the color and cursor queries in it again; those
|
|
1792
|
+
// answers are stale, and the program would get them as typing.
|
|
1793
|
+
if (this.replaying) return;
|
|
1787
1794
|
lastTyped = Date.now();
|
|
1788
1795
|
this.send({ t: 'i', d });
|
|
1789
1796
|
});
|
|
@@ -1842,6 +1849,7 @@ const term = {
|
|
|
1842
1849
|
this.conn = null;
|
|
1843
1850
|
if (ev.code === 1008) toast(`Terminal unavailable: ${ev.reason}`, 5000);
|
|
1844
1851
|
},
|
|
1852
|
+
onInputLost: () => this.conn === conn && toast('Some typing did not reach the computer (connection lost); check the terminal before going on', 8000),
|
|
1845
1853
|
},
|
|
1846
1854
|
);
|
|
1847
1855
|
this.conn = conn;
|
|
@@ -1852,7 +1860,8 @@ const term = {
|
|
|
1852
1860
|
if (msg.t === 'hello') {
|
|
1853
1861
|
// Also sent again after a reconnect: start from a clean screen and replay.
|
|
1854
1862
|
this.xterm.reset();
|
|
1855
|
-
this.
|
|
1863
|
+
this.replaying = true;
|
|
1864
|
+
this.xterm.write(msg.backlog, () => (this.replaying = false));
|
|
1856
1865
|
if (!matchMedia('(pointer: coarse)').matches) this.xterm.focus();
|
|
1857
1866
|
} else if (msg.t === 'error') {
|
|
1858
1867
|
toast(`Terminal unavailable: ${msg.error}`, 5000);
|
|
@@ -1888,6 +1897,8 @@ const term = {
|
|
|
1888
1897
|
renderTabs() {
|
|
1889
1898
|
renderSignIn();
|
|
1890
1899
|
const a = this.agentId && agents.get(this.agentId);
|
|
1900
|
+
// The agent's own session tab is named after its engine (Codex, Muse Code, …).
|
|
1901
|
+
$('.term-tab[data-kind="claude"]').textContent = ENGINE_LABEL[a?.engine] ?? 'Claude';
|
|
1891
1902
|
const nums = new Set(a?.runtime.shells ?? []);
|
|
1892
1903
|
if (isShell(this.kind)) nums.add(shellNum(this.kind));
|
|
1893
1904
|
$('#shell-tabs').innerHTML = [...nums]
|
|
@@ -3137,6 +3148,7 @@ $('#recent-bar').addEventListener('click', (e) => {
|
|
|
3137
3148
|
// ------------------------------------------------------------------ new agents dialog
|
|
3138
3149
|
|
|
3139
3150
|
const dialog = $('#new-dialog');
|
|
3151
|
+
const newFiles = attachBox({ slot: $('#new-attach'), form: $('#new-form'), paste: $('#new-form').intents, esc, toast });
|
|
3140
3152
|
function renderProjectOptions() {
|
|
3141
3153
|
$('#project-options').innerHTML = projectsState
|
|
3142
3154
|
.map((p) => `<option value="${esc(p.shortcut)}">${esc(p.name)}${p.repo ? ` · ${esc(p.repo)}` : p.dir ? ` · ${esc(p.dir)}` : ''}</option>`)
|
|
@@ -3147,6 +3159,7 @@ function openNewDialog(mode = 'now') {
|
|
|
3147
3159
|
renderProjectOptions();
|
|
3148
3160
|
$('#new-error').hidden = true;
|
|
3149
3161
|
dialog.querySelector(`input[name="when"][value="${mode}"]`).checked = true;
|
|
3162
|
+
restoreIntentSplit();
|
|
3150
3163
|
syncWhen();
|
|
3151
3164
|
dialog.showModal();
|
|
3152
3165
|
dialog.querySelector('textarea').focus();
|
|
@@ -3156,7 +3169,55 @@ function syncWhen() {
|
|
|
3156
3169
|
dialog.querySelector('.now-only').hidden = when !== 'now';
|
|
3157
3170
|
dialog.querySelector('.race-only').hidden = when !== 'race';
|
|
3158
3171
|
dialog.querySelector('.team-only').hidden = when !== 'team';
|
|
3159
|
-
|
|
3172
|
+
// A race or a team always works on the whole text.
|
|
3173
|
+
dialog.querySelector('.intent-split').hidden = when === 'race' || when === 'team';
|
|
3174
|
+
syncIntentPlaceholder();
|
|
3175
|
+
syncIntentCount();
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
/** The form's intents: one per line, or the whole text as one (kept as written, blank lines and all). */
|
|
3179
|
+
function formIntents(form) {
|
|
3180
|
+
const text = form.intents.value;
|
|
3181
|
+
const whole = form.split.value === 'one' || ['race', 'team'].includes(form.when.value);
|
|
3182
|
+
if (whole) return text.trim() ? [text.trim()] : [];
|
|
3183
|
+
return text
|
|
3184
|
+
.split('\n')
|
|
3185
|
+
.map((s) => s.trim())
|
|
3186
|
+
.filter(Boolean);
|
|
3187
|
+
}
|
|
3188
|
+
|
|
3189
|
+
function syncIntentCount() {
|
|
3190
|
+
const form = $('#new-form');
|
|
3191
|
+
const when = form.when.value;
|
|
3192
|
+
const n = formIntents(form).length;
|
|
3193
|
+
const split = !['race', 'team'].includes(when);
|
|
3194
|
+
$('#intent-count').textContent = split && n ? (when === 'backlog' ? `${n} item${n > 1 ? 's' : ''}` : `${n} agent${n > 1 ? 's' : ''}`) : '';
|
|
3195
|
+
const submit = dialog.querySelector('button[type=submit]');
|
|
3196
|
+
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');
|
|
3197
|
+
}
|
|
3198
|
+
$('#new-form').intents.addEventListener('input', syncIntentCount);
|
|
3199
|
+
for (const r of dialog.querySelectorAll('input[name="split"]')) {
|
|
3200
|
+
r.addEventListener('change', () => {
|
|
3201
|
+
try {
|
|
3202
|
+
localStorage.setItem('agentmgr.intentSplit', r.value);
|
|
3203
|
+
} catch {}
|
|
3204
|
+
syncIntentPlaceholder();
|
|
3205
|
+
syncIntentCount();
|
|
3206
|
+
});
|
|
3207
|
+
}
|
|
3208
|
+
const LINES_PLACEHOLDER = $('#new-form').intents.placeholder;
|
|
3209
|
+
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.';
|
|
3210
|
+
/** The remembered choice (a form reset forgets it), and a placeholder to match. */
|
|
3211
|
+
function restoreIntentSplit() {
|
|
3212
|
+
const form = $('#new-form');
|
|
3213
|
+
try {
|
|
3214
|
+
form.split.value = localStorage.getItem('agentmgr.intentSplit') === 'one' ? 'one' : 'lines';
|
|
3215
|
+
} catch {}
|
|
3216
|
+
syncIntentPlaceholder();
|
|
3217
|
+
}
|
|
3218
|
+
function syncIntentPlaceholder() {
|
|
3219
|
+
const form = $('#new-form');
|
|
3220
|
+
form.intents.placeholder = form.split.value === 'one' || ['race', 'team'].includes(form.when.value) ? WHOLE_PLACEHOLDER : LINES_PLACEHOLDER;
|
|
3160
3221
|
}
|
|
3161
3222
|
for (const r of dialog.querySelectorAll('input[name="when"]')) r.addEventListener('change', syncWhen);
|
|
3162
3223
|
$('#new-btn').onclick = () => openNewDialog('now');
|
|
@@ -3164,10 +3225,13 @@ $('#new-cancel').onclick = () => dialog.close();
|
|
|
3164
3225
|
$('#new-form').addEventListener('submit', async (e) => {
|
|
3165
3226
|
e.preventDefault();
|
|
3166
3227
|
const f = new FormData(e.target);
|
|
3167
|
-
const intents =
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3228
|
+
const intents = formIntents(e.target);
|
|
3229
|
+
if (newFiles.pending()) {
|
|
3230
|
+
$('#new-error').textContent = 'Still uploading the attached files; try again in a moment';
|
|
3231
|
+
$('#new-error').hidden = false;
|
|
3232
|
+
return;
|
|
3233
|
+
}
|
|
3234
|
+
const attachments = newFiles.paths().length ? newFiles.paths() : undefined;
|
|
3171
3235
|
if (f.get('when') === 'team') {
|
|
3172
3236
|
// "analyst=opus reviewer=opus"
|
|
3173
3237
|
const models = Object.fromEntries(
|
|
@@ -3178,6 +3242,7 @@ $('#new-form').addEventListener('submit', async (e) => {
|
|
|
3178
3242
|
);
|
|
3179
3243
|
try {
|
|
3180
3244
|
const team = await api('POST', '/teams', {
|
|
3245
|
+
attachments,
|
|
3181
3246
|
intent: intents.join('\n'),
|
|
3182
3247
|
critics: Number(f.get('critics')),
|
|
3183
3248
|
mode: f.get('teamMode') || undefined,
|
|
@@ -3197,6 +3262,7 @@ $('#new-form').addEventListener('submit', async (e) => {
|
|
|
3197
3262
|
teams.set(team.id, team);
|
|
3198
3263
|
dialog.close();
|
|
3199
3264
|
e.target.reset();
|
|
3265
|
+
newFiles.clear();
|
|
3200
3266
|
syncWhen();
|
|
3201
3267
|
openTeam(team.id);
|
|
3202
3268
|
toast(team.mode === 'lead' ? 'Team started: one lead session with sub-agents' : `Team started: ${team.members.length} agents`);
|
|
@@ -3218,6 +3284,7 @@ $('#new-form').addEventListener('submit', async (e) => {
|
|
|
3218
3284
|
});
|
|
3219
3285
|
try {
|
|
3220
3286
|
const race = await api('POST', '/races', {
|
|
3287
|
+
attachments,
|
|
3221
3288
|
intent: intents.join('\n'),
|
|
3222
3289
|
variants,
|
|
3223
3290
|
testCommand: f.get('testCommand') || undefined,
|
|
@@ -3232,6 +3299,7 @@ $('#new-form').addEventListener('submit', async (e) => {
|
|
|
3232
3299
|
races.set(race.id, race);
|
|
3233
3300
|
dialog.close();
|
|
3234
3301
|
e.target.reset();
|
|
3302
|
+
newFiles.clear();
|
|
3235
3303
|
syncWhen();
|
|
3236
3304
|
openRace(race.id);
|
|
3237
3305
|
toast(`Race started: ${race.rows.length} attempts`);
|
|
@@ -3244,6 +3312,7 @@ $('#new-form').addEventListener('submit', async (e) => {
|
|
|
3244
3312
|
if (f.get('when') === 'backlog') {
|
|
3245
3313
|
try {
|
|
3246
3314
|
const added = await api('POST', '/backlog', {
|
|
3315
|
+
attachments,
|
|
3247
3316
|
items: intents,
|
|
3248
3317
|
repo: f.get('repo') || undefined,
|
|
3249
3318
|
dir: f.get('dir') || undefined,
|
|
@@ -3259,6 +3328,7 @@ $('#new-form').addEventListener('submit', async (e) => {
|
|
|
3259
3328
|
});
|
|
3260
3329
|
dialog.close();
|
|
3261
3330
|
e.target.reset();
|
|
3331
|
+
newFiles.clear();
|
|
3262
3332
|
showTab('backlog');
|
|
3263
3333
|
toast(`Queued ${added.length} item${added.length > 1 ? 's' : ''}`);
|
|
3264
3334
|
} catch (err) {
|
|
@@ -3269,6 +3339,7 @@ $('#new-form').addEventListener('submit', async (e) => {
|
|
|
3269
3339
|
}
|
|
3270
3340
|
try {
|
|
3271
3341
|
const created = await api('POST', '/agents', {
|
|
3342
|
+
attachments,
|
|
3272
3343
|
priority: Number(f.get('priority')),
|
|
3273
3344
|
intents,
|
|
3274
3345
|
repo: f.get('repo') || undefined,
|
|
@@ -3286,6 +3357,7 @@ $('#new-form').addEventListener('submit', async (e) => {
|
|
|
3286
3357
|
created.forEach((a) => agents.set(a.id, a));
|
|
3287
3358
|
dialog.close();
|
|
3288
3359
|
e.target.reset();
|
|
3360
|
+
newFiles.clear();
|
|
3289
3361
|
select(created[0].id);
|
|
3290
3362
|
toast(`Created ${created.length} agent${created.length > 1 ? 's' : ''}`);
|
|
3291
3363
|
if (created.length === 1 && created[0].runtime.planning) term.open(created[0].id);
|
|
@@ -3362,17 +3434,29 @@ $('#ask-btn').onclick = () => {
|
|
|
3362
3434
|
askDialog.querySelector('textarea').focus();
|
|
3363
3435
|
};
|
|
3364
3436
|
$('#ask-cancel').onclick = () => askDialog.close();
|
|
3437
|
+
const askFiles = attachBox({ slot: $('#ask-attach'), form: $('#ask-form'), paste: $('#ask-form textarea'), esc, toast });
|
|
3365
3438
|
$('#ask-form').addEventListener('submit', async (e) => {
|
|
3366
3439
|
e.preventDefault();
|
|
3367
3440
|
const f = new FormData(e.target);
|
|
3441
|
+
if (askFiles.pending()) {
|
|
3442
|
+
$('#ask-error').textContent = 'Still uploading the attached files; try again in a moment';
|
|
3443
|
+
$('#ask-error').hidden = false;
|
|
3444
|
+
return;
|
|
3445
|
+
}
|
|
3368
3446
|
try {
|
|
3369
3447
|
const a = await api('POST', '/general', {
|
|
3370
3448
|
question: String(f.get('question') || '').trim() || undefined,
|
|
3371
3449
|
name: String(f.get('name') || '').trim() || undefined,
|
|
3450
|
+
engine: f.get('engine') || undefined,
|
|
3451
|
+
attachments: askFiles.paths().length ? askFiles.paths() : undefined,
|
|
3372
3452
|
});
|
|
3373
3453
|
agents.set(a.id, a);
|
|
3374
3454
|
askDialog.close();
|
|
3455
|
+
// Keep the engine for the next question; clear the rest.
|
|
3456
|
+
const engine = e.target.engine.value;
|
|
3375
3457
|
e.target.reset();
|
|
3458
|
+
e.target.engine.value = engine;
|
|
3459
|
+
askFiles.clear();
|
|
3376
3460
|
select(a.id);
|
|
3377
3461
|
// The shared general folder is ready almost immediately; open the terminal once it runs.
|
|
3378
3462
|
const t0 = Date.now();
|
|
@@ -3951,13 +4035,23 @@ $('#login-form').addEventListener('submit', async (e) => {
|
|
|
3951
4035
|
});
|
|
3952
4036
|
|
|
3953
4037
|
// Engines this machine can run; the New agent form offers a choice once there's more than Claude.
|
|
3954
|
-
const ENGINE_LABEL = { claude: 'Claude', codex: 'Codex', antigravity: 'Antigravity' };
|
|
4038
|
+
const ENGINE_LABEL = { claude: 'Claude', codex: 'Codex', antigravity: 'Antigravity', muse: 'Muse Code' };
|
|
3955
4039
|
async function loadEngines() {
|
|
3956
4040
|
try {
|
|
3957
4041
|
const list = (await api('GET', '/engines')).filter((e) => e.installed && e.id !== 'claude');
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
4042
|
+
// Something to know before choosing an engine (Muse Code in contributor mode, say).
|
|
4043
|
+
const notices = Object.fromEntries(list.filter((e) => e.notice).map((e) => [e.id, e.notice]));
|
|
4044
|
+
for (const [field, notice] of [['#engine-field', '#engine-notice'], ['#ask-engine-field', '#ask-engine-notice']]) {
|
|
4045
|
+
const sel = document.querySelector(`${field} select`);
|
|
4046
|
+
for (const e of list) sel.append(Object.assign(document.createElement('option'), { value: e.id, textContent: e.label }));
|
|
4047
|
+
$(field).hidden = !list.length;
|
|
4048
|
+
const show = () => {
|
|
4049
|
+
$(notice).textContent = notices[sel.value] ?? '';
|
|
4050
|
+
$(notice).hidden = !notices[sel.value];
|
|
4051
|
+
};
|
|
4052
|
+
sel.addEventListener('change', show);
|
|
4053
|
+
show();
|
|
4054
|
+
}
|
|
3961
4055
|
} catch {}
|
|
3962
4056
|
}
|
|
3963
4057
|
|
package/public/attach.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Attachments for a request that has no agent yet (New agents, Ask): pick, drop or paste files
|
|
2
|
+
// (a screenshot, a log). Each uploads to the daemon right away and shows as a chip; the form sends
|
|
3
|
+
// the uploaded paths, and the daemon copies them to each agent it creates.
|
|
4
|
+
|
|
5
|
+
const fmtSize = (n) => (n < 1024 ? `${n} B` : n < 1048576 ? `${(n / 1024).toFixed(0)} KB` : `${(n / 1048576).toFixed(1)} MB`);
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `slot` is an element to fill with the button and chips; `form` takes drops; `paste` (a textarea)
|
|
9
|
+
* takes pasted files. Returns { paths(), pending(), clear() }.
|
|
10
|
+
*/
|
|
11
|
+
export function attachBox({ slot, form, paste, esc, toast }) {
|
|
12
|
+
const items = []; // { id, name, size, path?, error?, uploading }
|
|
13
|
+
let seq = 0;
|
|
14
|
+
slot.classList.add('attach-box');
|
|
15
|
+
slot.innerHTML = `<button type="button" class="small ghost attach-pick" title="Attach files: a screenshot, a log, a document. You can also drop them here or paste into the text box">📎 Attach files</button>
|
|
16
|
+
<input type="file" multiple hidden />
|
|
17
|
+
<span class="attach-chips"></span>`;
|
|
18
|
+
const input = slot.querySelector('input[type=file]');
|
|
19
|
+
const chips = slot.querySelector('.attach-chips');
|
|
20
|
+
|
|
21
|
+
function render() {
|
|
22
|
+
chips.innerHTML = items
|
|
23
|
+
.map(
|
|
24
|
+
(it) => `<span class="attach-chip ${it.error ? 'fail' : ''}" title="${esc(it.error ?? it.path ?? 'Uploading…')}">${esc(it.name)} <span class="hint">${it.uploading ? 'uploading…' : it.error ? 'failed' : fmtSize(it.size)}</span><button type="button" class="link" data-drop="${it.id}" title="Remove">×</button></span>`,
|
|
25
|
+
)
|
|
26
|
+
.join('');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function add(files) {
|
|
30
|
+
for (const file of files) {
|
|
31
|
+
const name = file.name || `pasted-${Date.now()}.${(file.type.split('/')[1] || 'bin').replace(/\W/g, '')}`;
|
|
32
|
+
const it = { id: ++seq, name, size: file.size, uploading: true };
|
|
33
|
+
items.push(it);
|
|
34
|
+
render();
|
|
35
|
+
try {
|
|
36
|
+
const res = await fetch(`/api/uploads?name=${encodeURIComponent(name)}`, {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
credentials: 'same-origin',
|
|
39
|
+
headers: { 'content-type': 'application/octet-stream', 'x-agentmgr': '1' },
|
|
40
|
+
body: file,
|
|
41
|
+
});
|
|
42
|
+
const data = await res.json().catch(() => ({}));
|
|
43
|
+
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
|
|
44
|
+
Object.assign(it, { path: data.path, size: data.size });
|
|
45
|
+
} catch (err) {
|
|
46
|
+
it.error = err.message;
|
|
47
|
+
toast(`Could not attach ${name}: ${err.message}`, 5000);
|
|
48
|
+
}
|
|
49
|
+
it.uploading = false;
|
|
50
|
+
render();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
slot.querySelector('.attach-pick').onclick = () => input.click();
|
|
55
|
+
input.onchange = () => {
|
|
56
|
+
add([...input.files]);
|
|
57
|
+
input.value = '';
|
|
58
|
+
};
|
|
59
|
+
chips.addEventListener('click', (e) => {
|
|
60
|
+
const id = Number(e.target.closest('[data-drop]')?.dataset.drop);
|
|
61
|
+
if (!id) return;
|
|
62
|
+
items.splice(
|
|
63
|
+
items.findIndex((it) => it.id === id),
|
|
64
|
+
1,
|
|
65
|
+
);
|
|
66
|
+
render();
|
|
67
|
+
});
|
|
68
|
+
form.addEventListener('dragover', (e) => {
|
|
69
|
+
if ([...(e.dataTransfer?.types ?? [])].includes('Files')) {
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
form.classList.add('dropping');
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
form.addEventListener('dragleave', (e) => {
|
|
75
|
+
if (!form.contains(e.relatedTarget)) form.classList.remove('dropping');
|
|
76
|
+
});
|
|
77
|
+
form.addEventListener('drop', (e) => {
|
|
78
|
+
form.classList.remove('dropping');
|
|
79
|
+
const files = [...(e.dataTransfer?.files ?? [])];
|
|
80
|
+
if (!files.length) return;
|
|
81
|
+
e.preventDefault();
|
|
82
|
+
add(files);
|
|
83
|
+
});
|
|
84
|
+
// A pasted screenshot (or copied files) attaches; pasted text stays text.
|
|
85
|
+
paste?.addEventListener('paste', (e) => {
|
|
86
|
+
const files = [...(e.clipboardData?.files ?? [])];
|
|
87
|
+
if (!files.length) return;
|
|
88
|
+
e.preventDefault();
|
|
89
|
+
add(files);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
paths: () => items.filter((it) => it.path).map((it) => it.path),
|
|
94
|
+
pending: () => items.some((it) => it.uploading),
|
|
95
|
+
clear() {
|
|
96
|
+
items.length = 0;
|
|
97
|
+
render();
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
package/public/hub/hub.js
CHANGED
|
@@ -42,7 +42,7 @@ function toast(msg, bad = false) {
|
|
|
42
42
|
toast.timer = setTimeout(() => (t.hidden = true), bad ? 6000 : 2500);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
const ENGINE_LABEL = { claude: 'Claude Code', codex: 'Codex', antigravity: 'Antigravity' };
|
|
45
|
+
const ENGINE_LABEL = { claude: 'Claude Code', codex: 'Codex', antigravity: 'Antigravity', muse: 'Muse Code' };
|
|
46
46
|
/** Engines a machine reports installed (its engine:* labels). */
|
|
47
47
|
const enginesOf = (runner) => (runner?.labels ?? []).filter((l) => l.startsWith('engine:')).map((l) => l.slice(7));
|
|
48
48
|
/** Fill an engine picker; hidden while Claude Code is the only choice. */
|