ai-agentmanager 1.5.0 → 1.7.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 +12 -0
- package/README.md +9 -6
- package/package.json +1 -1
- package/public/app.js +76 -12
- package/public/attach.js +100 -0
- package/public/index.html +66 -34
- package/public/menu.js +11 -0
- package/public/review.js +85 -2
- package/public/setup.js +1 -0
- package/public/side.js +191 -0
- package/public/style.css +44 -0
- package/public/walkthrough.js +36 -0
- package/src/backlog.js +4 -1
- package/src/brief.js +75 -0
- package/src/cli.js +32 -6
- package/src/manager.js +110 -5
- package/src/review.js +3 -0
- package/src/server.js +33 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.7.0 (2026-10-31)
|
|
4
|
+
|
|
5
|
+
- **Split view:** **◫ Split** in the terminal bar: the terminal (or a shell, or a browser tab) on the left, and Code, Docs (Diagrams by default, or any other document) or Changes on the right. Resizable and remembered.
|
|
6
|
+
- **Review: changes explained one by one:** **Explain changes** in Review changes puts an explanation above each hunk (what, why, risk, what to check) and **◀ / ▶** or **[ / ]** step through them in reading order, skipping hunks that moved since. A notice and **Explain again** when the code changed after.
|
|
7
|
+
|
|
8
|
+
## 1.6.0 (2026-10-31)
|
|
9
|
+
|
|
10
|
+
- **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.
|
|
11
|
+
- **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).
|
|
12
|
+
- **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`.
|
|
13
|
+
- **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>`.
|
|
14
|
+
|
|
3
15
|
## 1.5.0 (2026-10-31)
|
|
4
16
|
|
|
5
17
|
- **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.
|
package/README.md
CHANGED
|
@@ -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.7.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.7.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.7.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.
|
|
@@ -383,6 +384,7 @@ Each card has at most **three points of about 12 words each**, so they're easy t
|
|
|
383
384
|
|
|
384
385
|
- **Files and diff.** The changed files on the left with lines added and removed, and the diff file by file on the right. Click a file header to fold it; very large files start folded.
|
|
385
386
|
- **Line comments.** Click **+** next to a line (or its line number) to comment on it, and add an overall comment if you like. **Send to agent** types them into the agent as one message, each with its file, line and the code it is about, and asks it to address them. Comments not sent yet are kept in this browser.
|
|
387
|
+
- **Explain changes** explains each change one by one: a title, what it changes, why (from the request and the conversation), the risk and what to check, in the order a reviewer should read them. Each explanation sits above its hunk, and **◀ / ▶** (or **[** and **]**) step through them, opening folded files on the way. When the agent changes the code afterwards, the page says so and **Explain again** brings it up to date. From the CLI: `agentmgr doc <agent> walkthrough`.
|
|
386
388
|
- **Commit all** commits everything in the worktree, new files included, with your git name and email.
|
|
387
389
|
- **Push & open PR** pushes the branch and opens a pull request with `gh`, against the branch the agent started from (title from your text, or from the commits; **Draft** if you like). With a pull request already open, it pushes to it. The pull request is linked to the agent.
|
|
388
390
|
|
|
@@ -908,6 +910,7 @@ Usage comes from the session transcripts and is refreshed every few seconds whil
|
|
|
908
910
|
## Studio: code and terminals
|
|
909
911
|
|
|
910
912
|
- **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.
|
|
913
|
+
- **◫ Split** (in the terminal bar) keeps the terminal, a shell or a browser tab on the left and opens a pane on the right with **Code**, **Docs** (Diagrams by default; Design, Flow, Analysis, Proposal or Explain from the menu, written there if missing) or **Changes** (the change-by-change explanations, each with its diff). Drag the divider to resize; the pane, its tab and its width are remembered in this browser. Desktop only.
|
|
911
914
|
- **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
915
|
- Monaco loads from the jsDelivr CDN the first time you open **Code**. Offline, a plain text editor takes its place.
|
|
913
916
|
- **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"`).
|
|
@@ -920,7 +923,7 @@ Usage comes from the session transcripts and is refreshed every few seconds whil
|
|
|
920
923
|
- **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.
|
|
921
924
|
- **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.
|
|
922
925
|
- If a tab stays blank on a device, open it once with **↗** and accept the certificate there.
|
|
923
|
-
- **Ask
|
|
926
|
+
- **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.
|
|
924
927
|
|
|
925
928
|
## Configuration
|
|
926
929
|
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -4,6 +4,8 @@ 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';
|
|
8
|
+
import { initSide } from '/side.js';
|
|
7
9
|
import { initReview } from '/review.js';
|
|
8
10
|
import { initLand, prChips } from '/land.js';
|
|
9
11
|
import { initTriggers } from '/triggers.js';
|
|
@@ -110,7 +112,8 @@ initSetup({ api, esc, $ });
|
|
|
110
112
|
const updateUi = initUpdate({ api, esc, toast, $ });
|
|
111
113
|
initMenu({ api, esc, toast, $ });
|
|
112
114
|
initMachine({ api, esc, toast, $, Terminal, FitAddon });
|
|
113
|
-
|
|
115
|
+
let sidePane = null; // the split view's right pane (set up once the terminal is)
|
|
116
|
+
const codeUi = initCode({ api, esc, toast, $, agents, isOpen: () => term.kind === 'code' || Boolean(sidePane?.showing('code')) });
|
|
114
117
|
const playbooksUi = initPlaybooks({ api, esc, toast, $, select: (id) => (showTab('agents'), select(id)) });
|
|
115
118
|
$('#new-playbook').onclick = () => {
|
|
116
119
|
const f = $('#new-form');
|
|
@@ -1898,6 +1901,7 @@ const term = {
|
|
|
1898
1901
|
const a = this.agentId && agents.get(this.agentId);
|
|
1899
1902
|
// The agent's own session tab is named after its engine (Codex, Muse Code, …).
|
|
1900
1903
|
$('.term-tab[data-kind="claude"]').textContent = ENGINE_LABEL[a?.engine] ?? 'Claude';
|
|
1904
|
+
sidePane?.follow(this.agentId);
|
|
1901
1905
|
const nums = new Set(a?.runtime.shells ?? []);
|
|
1902
1906
|
if (isShell(this.kind)) nums.add(shellNum(this.kind));
|
|
1903
1907
|
$('#shell-tabs').innerHTML = [...nums]
|
|
@@ -1929,6 +1933,7 @@ const term = {
|
|
|
1929
1933
|
$('#term-wrap').hidden = false;
|
|
1930
1934
|
$('#detail').classList.add('with-term');
|
|
1931
1935
|
$('#term-wrap').classList.remove('browsing');
|
|
1936
|
+
sidePane?.mainShowsCode();
|
|
1932
1937
|
$('#term-wrap').classList.add('coding');
|
|
1933
1938
|
this.ensure();
|
|
1934
1939
|
this.disconnect();
|
|
@@ -2094,6 +2099,20 @@ $('#term-fit').onclick = () => term.refit();
|
|
|
2094
2099
|
|
|
2095
2100
|
// ------------------------------------------------------------------ resizing, expanding, full screen
|
|
2096
2101
|
|
|
2102
|
+
sidePane = initSide({
|
|
2103
|
+
api,
|
|
2104
|
+
esc,
|
|
2105
|
+
toast,
|
|
2106
|
+
$,
|
|
2107
|
+
agents,
|
|
2108
|
+
codeUi,
|
|
2109
|
+
docHtml,
|
|
2110
|
+
renderDiagrams,
|
|
2111
|
+
mainKind: () => term.kind,
|
|
2112
|
+
setMain: (kind) => term.agentId && term.open(term.agentId, kind),
|
|
2113
|
+
refit: () => requestAnimationFrame(() => term.refit()),
|
|
2114
|
+
});
|
|
2115
|
+
|
|
2097
2116
|
const layoutPref = {
|
|
2098
2117
|
get(key) {
|
|
2099
2118
|
try {
|
|
@@ -2113,6 +2132,8 @@ function applyLayout() {
|
|
|
2113
2132
|
const side = layoutPref.get('sideWidth');
|
|
2114
2133
|
const brief = layoutPref.get('briefHeight');
|
|
2115
2134
|
document.documentElement.style.setProperty('--side-w', side ? `${side}px` : '340px');
|
|
2135
|
+
const pane = layoutPref.get('paneWidth');
|
|
2136
|
+
document.documentElement.style.setProperty('--pane-w', pane ? `${pane}%` : '45%');
|
|
2116
2137
|
if (brief) document.documentElement.style.setProperty('--brief-h', `${brief}px`);
|
|
2117
2138
|
else document.documentElement.style.removeProperty('--brief-h');
|
|
2118
2139
|
$('#detail').classList.toggle('brief-sized', Boolean(brief));
|
|
@@ -2155,6 +2176,14 @@ dragHandle($('#split-side'), 'cols', (ev) => {
|
|
|
2155
2176
|
return w;
|
|
2156
2177
|
}, 'sideWidth');
|
|
2157
2178
|
|
|
2179
|
+
// The split view's divider: the right pane's share of the panel, in percent.
|
|
2180
|
+
dragHandle($('#split-pane'), 'cols', (ev) => {
|
|
2181
|
+
const r = $('#term-split').getBoundingClientRect();
|
|
2182
|
+
const pct = Math.min(Math.max(((r.right - ev.clientX) / r.width) * 100, 20), 75);
|
|
2183
|
+
layoutPref.set('paneWidth', Math.round(pct));
|
|
2184
|
+
return pct;
|
|
2185
|
+
}, 'paneWidth');
|
|
2186
|
+
|
|
2158
2187
|
dragHandle($('#split-term'), 'rows', (ev) => {
|
|
2159
2188
|
const top = $('#detail').getBoundingClientRect().top;
|
|
2160
2189
|
const h = Math.min(Math.max(ev.clientY - top, 60), $('#detail').clientHeight - 160);
|
|
@@ -2862,7 +2891,14 @@ $('#term-file').onchange = (e) => {
|
|
|
2862
2891
|
function upsert(a) {
|
|
2863
2892
|
agents.set(a.id, a);
|
|
2864
2893
|
render();
|
|
2894
|
+
agentChanged(a);
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
/** Views that show one agent's details outside the main render: keep them current. */
|
|
2898
|
+
function agentChanged(a) {
|
|
2865
2899
|
if (a.id === term.agentId) term.renderTabs();
|
|
2900
|
+
reviewUi.agentChanged(a);
|
|
2901
|
+
sidePane?.agentChanged(a);
|
|
2866
2902
|
}
|
|
2867
2903
|
|
|
2868
2904
|
$('#counts').addEventListener('click', (e) => {
|
|
@@ -3147,6 +3183,7 @@ $('#recent-bar').addEventListener('click', (e) => {
|
|
|
3147
3183
|
// ------------------------------------------------------------------ new agents dialog
|
|
3148
3184
|
|
|
3149
3185
|
const dialog = $('#new-dialog');
|
|
3186
|
+
const newFiles = attachBox({ slot: $('#new-attach'), form: $('#new-form'), paste: $('#new-form').intents, esc, toast });
|
|
3150
3187
|
function renderProjectOptions() {
|
|
3151
3188
|
$('#project-options').innerHTML = projectsState
|
|
3152
3189
|
.map((p) => `<option value="${esc(p.shortcut)}">${esc(p.name)}${p.repo ? ` · ${esc(p.repo)}` : p.dir ? ` · ${esc(p.dir)}` : ''}</option>`)
|
|
@@ -3224,6 +3261,12 @@ $('#new-form').addEventListener('submit', async (e) => {
|
|
|
3224
3261
|
e.preventDefault();
|
|
3225
3262
|
const f = new FormData(e.target);
|
|
3226
3263
|
const intents = formIntents(e.target);
|
|
3264
|
+
if (newFiles.pending()) {
|
|
3265
|
+
$('#new-error').textContent = 'Still uploading the attached files; try again in a moment';
|
|
3266
|
+
$('#new-error').hidden = false;
|
|
3267
|
+
return;
|
|
3268
|
+
}
|
|
3269
|
+
const attachments = newFiles.paths().length ? newFiles.paths() : undefined;
|
|
3227
3270
|
if (f.get('when') === 'team') {
|
|
3228
3271
|
// "analyst=opus reviewer=opus"
|
|
3229
3272
|
const models = Object.fromEntries(
|
|
@@ -3234,6 +3277,7 @@ $('#new-form').addEventListener('submit', async (e) => {
|
|
|
3234
3277
|
);
|
|
3235
3278
|
try {
|
|
3236
3279
|
const team = await api('POST', '/teams', {
|
|
3280
|
+
attachments,
|
|
3237
3281
|
intent: intents.join('\n'),
|
|
3238
3282
|
critics: Number(f.get('critics')),
|
|
3239
3283
|
mode: f.get('teamMode') || undefined,
|
|
@@ -3253,6 +3297,7 @@ $('#new-form').addEventListener('submit', async (e) => {
|
|
|
3253
3297
|
teams.set(team.id, team);
|
|
3254
3298
|
dialog.close();
|
|
3255
3299
|
e.target.reset();
|
|
3300
|
+
newFiles.clear();
|
|
3256
3301
|
syncWhen();
|
|
3257
3302
|
openTeam(team.id);
|
|
3258
3303
|
toast(team.mode === 'lead' ? 'Team started: one lead session with sub-agents' : `Team started: ${team.members.length} agents`);
|
|
@@ -3274,6 +3319,7 @@ $('#new-form').addEventListener('submit', async (e) => {
|
|
|
3274
3319
|
});
|
|
3275
3320
|
try {
|
|
3276
3321
|
const race = await api('POST', '/races', {
|
|
3322
|
+
attachments,
|
|
3277
3323
|
intent: intents.join('\n'),
|
|
3278
3324
|
variants,
|
|
3279
3325
|
testCommand: f.get('testCommand') || undefined,
|
|
@@ -3288,6 +3334,7 @@ $('#new-form').addEventListener('submit', async (e) => {
|
|
|
3288
3334
|
races.set(race.id, race);
|
|
3289
3335
|
dialog.close();
|
|
3290
3336
|
e.target.reset();
|
|
3337
|
+
newFiles.clear();
|
|
3291
3338
|
syncWhen();
|
|
3292
3339
|
openRace(race.id);
|
|
3293
3340
|
toast(`Race started: ${race.rows.length} attempts`);
|
|
@@ -3300,6 +3347,7 @@ $('#new-form').addEventListener('submit', async (e) => {
|
|
|
3300
3347
|
if (f.get('when') === 'backlog') {
|
|
3301
3348
|
try {
|
|
3302
3349
|
const added = await api('POST', '/backlog', {
|
|
3350
|
+
attachments,
|
|
3303
3351
|
items: intents,
|
|
3304
3352
|
repo: f.get('repo') || undefined,
|
|
3305
3353
|
dir: f.get('dir') || undefined,
|
|
@@ -3315,6 +3363,7 @@ $('#new-form').addEventListener('submit', async (e) => {
|
|
|
3315
3363
|
});
|
|
3316
3364
|
dialog.close();
|
|
3317
3365
|
e.target.reset();
|
|
3366
|
+
newFiles.clear();
|
|
3318
3367
|
showTab('backlog');
|
|
3319
3368
|
toast(`Queued ${added.length} item${added.length > 1 ? 's' : ''}`);
|
|
3320
3369
|
} catch (err) {
|
|
@@ -3325,6 +3374,7 @@ $('#new-form').addEventListener('submit', async (e) => {
|
|
|
3325
3374
|
}
|
|
3326
3375
|
try {
|
|
3327
3376
|
const created = await api('POST', '/agents', {
|
|
3377
|
+
attachments,
|
|
3328
3378
|
priority: Number(f.get('priority')),
|
|
3329
3379
|
intents,
|
|
3330
3380
|
repo: f.get('repo') || undefined,
|
|
@@ -3342,6 +3392,7 @@ $('#new-form').addEventListener('submit', async (e) => {
|
|
|
3342
3392
|
created.forEach((a) => agents.set(a.id, a));
|
|
3343
3393
|
dialog.close();
|
|
3344
3394
|
e.target.reset();
|
|
3395
|
+
newFiles.clear();
|
|
3345
3396
|
select(created[0].id);
|
|
3346
3397
|
toast(`Created ${created.length} agent${created.length > 1 ? 's' : ''}`);
|
|
3347
3398
|
if (created.length === 1 && created[0].runtime.planning) term.open(created[0].id);
|
|
@@ -3418,17 +3469,29 @@ $('#ask-btn').onclick = () => {
|
|
|
3418
3469
|
askDialog.querySelector('textarea').focus();
|
|
3419
3470
|
};
|
|
3420
3471
|
$('#ask-cancel').onclick = () => askDialog.close();
|
|
3472
|
+
const askFiles = attachBox({ slot: $('#ask-attach'), form: $('#ask-form'), paste: $('#ask-form textarea'), esc, toast });
|
|
3421
3473
|
$('#ask-form').addEventListener('submit', async (e) => {
|
|
3422
3474
|
e.preventDefault();
|
|
3423
3475
|
const f = new FormData(e.target);
|
|
3476
|
+
if (askFiles.pending()) {
|
|
3477
|
+
$('#ask-error').textContent = 'Still uploading the attached files; try again in a moment';
|
|
3478
|
+
$('#ask-error').hidden = false;
|
|
3479
|
+
return;
|
|
3480
|
+
}
|
|
3424
3481
|
try {
|
|
3425
3482
|
const a = await api('POST', '/general', {
|
|
3426
3483
|
question: String(f.get('question') || '').trim() || undefined,
|
|
3427
3484
|
name: String(f.get('name') || '').trim() || undefined,
|
|
3485
|
+
engine: f.get('engine') || undefined,
|
|
3486
|
+
attachments: askFiles.paths().length ? askFiles.paths() : undefined,
|
|
3428
3487
|
});
|
|
3429
3488
|
agents.set(a.id, a);
|
|
3430
3489
|
askDialog.close();
|
|
3490
|
+
// Keep the engine for the next question; clear the rest.
|
|
3491
|
+
const engine = e.target.engine.value;
|
|
3431
3492
|
e.target.reset();
|
|
3493
|
+
e.target.engine.value = engine;
|
|
3494
|
+
askFiles.clear();
|
|
3432
3495
|
select(a.id);
|
|
3433
3496
|
// The shared general folder is ready almost immediately; open the terminal once it runs.
|
|
3434
3497
|
const t0 = Date.now();
|
|
@@ -3470,7 +3533,7 @@ function handleEvent(msg) {
|
|
|
3470
3533
|
} else if (msg.t === 'agent') {
|
|
3471
3534
|
agents.set(msg.agent.id, msg.agent);
|
|
3472
3535
|
watchDocs(msg.agent);
|
|
3473
|
-
|
|
3536
|
+
agentChanged(msg.agent);
|
|
3474
3537
|
} else if (msg.t === 'removed') {
|
|
3475
3538
|
agents.delete(msg.id);
|
|
3476
3539
|
if (selectedId === msg.id) select(null);
|
|
@@ -4011,18 +4074,19 @@ const ENGINE_LABEL = { claude: 'Claude', codex: 'Codex', antigravity: 'Antigravi
|
|
|
4011
4074
|
async function loadEngines() {
|
|
4012
4075
|
try {
|
|
4013
4076
|
const list = (await api('GET', '/engines')).filter((e) => e.installed && e.id !== 'claude');
|
|
4014
|
-
const sel = document.querySelector('#engine-field select');
|
|
4015
|
-
for (const e of list) sel.append(Object.assign(document.createElement('option'), { value: e.id, textContent: e.label }));
|
|
4016
|
-
$('#engine-field').hidden = !list.length;
|
|
4017
4077
|
// Something to know before choosing an engine (Muse Code in contributor mode, say).
|
|
4018
4078
|
const notices = Object.fromEntries(list.filter((e) => e.notice).map((e) => [e.id, e.notice]));
|
|
4019
|
-
const
|
|
4020
|
-
const
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4079
|
+
for (const [field, notice] of [['#engine-field', '#engine-notice'], ['#ask-engine-field', '#ask-engine-notice']]) {
|
|
4080
|
+
const sel = document.querySelector(`${field} select`);
|
|
4081
|
+
for (const e of list) sel.append(Object.assign(document.createElement('option'), { value: e.id, textContent: e.label }));
|
|
4082
|
+
$(field).hidden = !list.length;
|
|
4083
|
+
const show = () => {
|
|
4084
|
+
$(notice).textContent = notices[sel.value] ?? '';
|
|
4085
|
+
$(notice).hidden = !notices[sel.value];
|
|
4086
|
+
};
|
|
4087
|
+
sel.addEventListener('change', show);
|
|
4088
|
+
show();
|
|
4089
|
+
}
|
|
4026
4090
|
} catch {}
|
|
4027
4091
|
}
|
|
4028
4092
|
|
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/index.html
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
<button id="assistant-btn" title="An always-on session that finds new work and starts agents for it">Assistant <span id="assistant-dot" class="assistant-dot" hidden></span></button>
|
|
26
26
|
<button id="spend-btn" title="Results across agents (merge rate, time to PR, cost per merged PR), what they cost, and spending limits">Results <span id="spend-today" class="spend-today"></span></button>
|
|
27
27
|
<button id="machine-btn" title="A terminal on this computer (your home folder), from any device you're signed in on">Terminal</button>
|
|
28
|
-
<button id="ask-btn" title="A general
|
|
28
|
+
<button id="ask-btn" title="A general session with no repo or folder, on Claude Code or another engine you have (Codex, Muse Code, …)">Ask Agent</button>
|
|
29
29
|
<button id="new-btn" class="primary">+ New agents</button>
|
|
30
30
|
<div class="menu-wrap">
|
|
31
31
|
<button id="menu-btn" class="menu-btn" type="button" aria-haspopup="menu" aria-expanded="false" title="Settings and preferences">
|
|
@@ -96,41 +96,59 @@
|
|
|
96
96
|
<button id="term-attach" class="ghost small" title="Attach files or images (you can also drop or paste them into the terminal)">📎 Attach</button>
|
|
97
97
|
<input type="file" id="term-file" multiple hidden>
|
|
98
98
|
<button id="term-fit" class="ghost small" title="Refit the terminal to the panel">Fit</button>
|
|
99
|
+
<button id="term-split-btn" class="ghost small desk-only" title="Split: the terminal on the left, and Code, Docs (diagrams) or Changes on the right">◫ Split</button>
|
|
99
100
|
<button id="term-max" class="ghost small desk-only" title="Expand to fill the window (Ctrl+Shift+E)">⤢ Expand</button>
|
|
100
101
|
<button id="term-full" class="ghost small desk-only" title="Full screen (Esc to leave)">⛶ Full screen</button>
|
|
101
102
|
<button id="term-close" class="ghost small" title="Hide the terminal (the agent keeps running)">Hide terminal</button>
|
|
102
103
|
</div>
|
|
103
104
|
<div id="signin-bar" class="signin-bar" hidden></div>
|
|
104
|
-
<div
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
<
|
|
108
|
-
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
<
|
|
121
|
-
<
|
|
122
|
-
|
|
105
|
+
<div class="term-split" id="term-split">
|
|
106
|
+
<div class="term-main" id="term-main">
|
|
107
|
+
<div id="terminal" class="terminal"></div>
|
|
108
|
+
<div id="code" class="code"></div>
|
|
109
|
+
<div id="browser" class="browser">
|
|
110
|
+
<form id="browser-bar" class="browser-bar" autocomplete="off">
|
|
111
|
+
<button type="button" id="browser-back" class="ghost small live-only" title="Back">◀</button>
|
|
112
|
+
<button type="button" id="browser-fwd" class="ghost small live-only" title="Forward">▶</button>
|
|
113
|
+
<button type="button" id="browser-reload" class="ghost small" title="Reload the page">⟳</button>
|
|
114
|
+
<input id="browser-url" placeholder="localhost:5173, a port, or any address" spellcheck="false" autocapitalize="off" autocorrect="off" enterkeyhint="go">
|
|
115
|
+
<button class="small">Go</button>
|
|
116
|
+
<button type="button" id="browser-sound" class="ghost small live-only" title="Hear the page: sound from videos and audio playing on that machine">🔇</button>
|
|
117
|
+
<button type="button" id="browser-keys" class="ghost small live-only touch-only" title="Show the keyboard to type into the page">⌨</button>
|
|
118
|
+
<button type="button" id="browser-mode" class="ghost small" hidden></button>
|
|
119
|
+
<a id="browser-pop" class="ghost small button" target="_blank" rel="noopener" title="Open in a new browser tab on this device" hidden>↗</a>
|
|
120
|
+
</form>
|
|
121
|
+
<div id="browser-hint" class="browser-hint" hidden>Blank? Many sites refuse to show inside another page. Open it with ↗ instead.</div>
|
|
122
|
+
<div id="browser-cert" class="browser-start" hidden>
|
|
123
|
+
<p><b>One more step on this device.</b> Each browser tab goes through its own port, and this device hasn't accepted AI Agent Manager's certificate for this one yet.</p>
|
|
124
|
+
<p>Open it, accept the warning the same way you did for AI Agent Manager (Safari: Show Details → visit this website), then come back here. You only do this once per site; it keeps its port.</p>
|
|
125
|
+
<div><a id="browser-cert-open" class="button primary small" target="_blank" rel="noopener">Open and accept</a> <button type="button" id="browser-cert-retry" class="small">Done, load it</button></div>
|
|
126
|
+
</div>
|
|
127
|
+
<div id="browser-start" class="browser-start" hidden>
|
|
128
|
+
<p>Open a page as this machine sees it: the agent's dev server, a site on its network or VPN, or any address. It works from your phone or laptop too: AI Agent Manager relays it over its own secure connection, for signed-in devices only.</p>
|
|
129
|
+
<div id="browser-suggest"></div>
|
|
130
|
+
</div>
|
|
131
|
+
<div id="browser-frames" class="browser-frames"></div>
|
|
132
|
+
<div id="browser-live" class="browser-live" hidden>
|
|
133
|
+
<img id="browser-live-img" alt="" draggable="false">
|
|
134
|
+
<textarea id="browser-live-keys" class="browser-live-keys" autocapitalize="off" autocomplete="off" autocorrect="off" spellcheck="false" aria-label="Type into the page"></textarea>
|
|
135
|
+
<div id="browser-live-msg" class="browser-live-msg" hidden></div>
|
|
136
|
+
</div>
|
|
123
137
|
</div>
|
|
124
|
-
<div id="browser-start" class="browser-start" hidden>
|
|
125
|
-
<p>Open a page as this machine sees it: the agent's dev server, a site on its network or VPN, or any address. It works from your phone or laptop too: AI Agent Manager relays it over its own secure connection, for signed-in devices only.</p>
|
|
126
|
-
<div id="browser-suggest"></div>
|
|
127
|
-
</div>
|
|
128
|
-
<div id="browser-frames" class="browser-frames"></div>
|
|
129
|
-
<div id="browser-live" class="browser-live" hidden>
|
|
130
|
-
<img id="browser-live-img" alt="" draggable="false">
|
|
131
|
-
<textarea id="browser-live-keys" class="browser-live-keys" autocapitalize="off" autocomplete="off" autocorrect="off" spellcheck="false" aria-label="Type into the page"></textarea>
|
|
132
|
-
<div id="browser-live-msg" class="browser-live-msg" hidden></div>
|
|
133
138
|
</div>
|
|
139
|
+
<div class="splitter split-pane" id="split-pane" role="separator" aria-orientation="vertical" title="Drag to resize; double-click to reset" hidden></div>
|
|
140
|
+
<aside class="side-pane" id="side-pane" hidden>
|
|
141
|
+
<div class="side-bar">
|
|
142
|
+
<div class="term-tabs" role="tablist">
|
|
143
|
+
<button class="term-tab" data-side="code" role="tab" title="The agent's files">Code</button>
|
|
144
|
+
<button class="term-tab" data-side="docs" role="tab" title="Diagrams and the other documents">Docs</button>
|
|
145
|
+
<button class="term-tab" data-side="changes" role="tab" title="Its changes, explained one by one">Changes</button>
|
|
146
|
+
</div>
|
|
147
|
+
<span class="spacer"></span>
|
|
148
|
+
<button type="button" class="ghost small" id="side-close" title="Close the split">✕</button>
|
|
149
|
+
</div>
|
|
150
|
+
<div class="side-body" id="side-body"></div>
|
|
151
|
+
</aside>
|
|
134
152
|
</div>
|
|
135
153
|
<div id="term-drop" class="term-drop" hidden>Drop files to attach</div>
|
|
136
154
|
<div class="keybar" id="keybar" aria-label="Terminal keys">
|
|
@@ -173,6 +191,7 @@
|
|
|
173
191
|
<span class="sr-only">Intents</span>
|
|
174
192
|
<textarea name="intents" rows="6" required placeholder="Add a dark mode toggle that remembers the user's choice Make search results show up as you type Find out why the dashboard takes 5 seconds to load and speed it up Add a one-click CSV export to the reports page"></textarea>
|
|
175
193
|
</label>
|
|
194
|
+
<div id="new-attach"></div>
|
|
176
195
|
<div class="row">
|
|
177
196
|
<label>
|
|
178
197
|
Git repository URL
|
|
@@ -307,16 +326,24 @@
|
|
|
307
326
|
|
|
308
327
|
<dialog id="ask-dialog">
|
|
309
328
|
<form id="ask-form" method="dialog">
|
|
310
|
-
<h2>Ask
|
|
329
|
+
<h2>Ask an agent</h2>
|
|
311
330
|
<p class="hint">A general session with no repo or folder. It is tracked and resumable like any other agent.</p>
|
|
312
331
|
<label>
|
|
313
332
|
Question <span class="hint">optional; you can also just type in the terminal</span>
|
|
314
333
|
<textarea name="question" rows="4" placeholder="How do I tune GC for a JVM with a 32 GB heap?"></textarea>
|
|
315
334
|
</label>
|
|
316
|
-
<
|
|
317
|
-
|
|
318
|
-
<
|
|
319
|
-
|
|
335
|
+
<div id="ask-attach"></div>
|
|
336
|
+
<div class="row">
|
|
337
|
+
<label>
|
|
338
|
+
Name <span class="hint">optional</span>
|
|
339
|
+
<input name="name" placeholder="jvm-gc-questions" />
|
|
340
|
+
</label>
|
|
341
|
+
<label id="ask-engine-field">
|
|
342
|
+
Engine
|
|
343
|
+
<select name="engine"><option value="">Claude Code</option></select>
|
|
344
|
+
<span class="hint warn" id="ask-engine-notice" hidden></span>
|
|
345
|
+
</label>
|
|
346
|
+
</div>
|
|
320
347
|
<p id="ask-error" class="error" hidden></p>
|
|
321
348
|
<div class="dialog-actions">
|
|
322
349
|
<button type="button" id="ask-cancel" class="ghost">Cancel</button>
|
|
@@ -679,6 +706,11 @@
|
|
|
679
706
|
<label class="check">Keep at most <input type="number" name="maxRunning" min="1" max="500" placeholder="no limit" class="num" /> agents open at once</label>
|
|
680
707
|
<p class="hint">Above it, the agents idle longest are closed (they resume when you open them), so the machine stays responsive. Never closed: agents working, waiting for your answer, active in the last 5 minutes, marked Keep, or in a team that is still going. <span id="settings-running"></span></p>
|
|
681
708
|
</fieldset>
|
|
709
|
+
<fieldset class="local-only" id="settings-service">
|
|
710
|
+
<legend>Start at login</legend>
|
|
711
|
+
<label class="check"><input type="checkbox" name="startAtLogin" /> Start AI Agent Manager when I log in</label>
|
|
712
|
+
<p class="hint">After a restart or a crash, the daemon comes back by itself and running agents resume; without it, it starts only when you run <code>agentmgr</code>. <span id="settings-service-state"></span></p>
|
|
713
|
+
</fieldset>
|
|
682
714
|
<fieldset class="local-only">
|
|
683
715
|
<legend>Updates</legend>
|
|
684
716
|
<label class="check"><input type="checkbox" name="updateCheck" /> Check for a new release twice a day</label>
|