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 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` starts the daemon at login.
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 Claude** starts a session with no repo, folder or intent, for questions. It is tracked and resumable like any other agent.
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.5.0 # or a specific one
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.5.0 --depth 1 https://github.com/jdmunta/ai-agentmanager-core.git
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.5.0 && npm install` (the tag you want) |
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 Claude** (or `agentmgr ask`) creates a `general` agent. All general sessions share `~/.agentmanager/general`, so Claude's folder-trust prompt only appears the first time.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-agentmanager",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "description": "Run background coding agents (Claude Code, Codex) per intent, and keep track of them from a web UI, your phone or the CLI",
5
5
  "keywords": [
6
6
  "claude",
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
- const codeUi = initCode({ api, esc, toast, $, agents, isOpen: () => term.kind === 'code' });
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
- if (msg.agent.id === term.agentId) term.renderTabs();
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 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();
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
 
@@ -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 Claude session with no repo or folder">Ask Claude</button>
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 id="terminal" class="terminal"></div>
105
- <div id="code" class="code"></div>
106
- <div id="browser" class="browser">
107
- <form id="browser-bar" class="browser-bar" autocomplete="off">
108
- <button type="button" id="browser-back" class="ghost small live-only" title="Back">◀</button>
109
- <button type="button" id="browser-fwd" class="ghost small live-only" title="Forward">▶</button>
110
- <button type="button" id="browser-reload" class="ghost small" title="Reload the page">⟳</button>
111
- <input id="browser-url" placeholder="localhost:5173, a port, or any address" spellcheck="false" autocapitalize="off" autocorrect="off" enterkeyhint="go">
112
- <button class="small">Go</button>
113
- <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>
114
- <button type="button" id="browser-keys" class="ghost small live-only touch-only" title="Show the keyboard to type into the page">⌨</button>
115
- <button type="button" id="browser-mode" class="ghost small" hidden></button>
116
- <a id="browser-pop" class="ghost small button" target="_blank" rel="noopener" title="Open in a new browser tab on this device" hidden>↗</a>
117
- </form>
118
- <div id="browser-hint" class="browser-hint" hidden>Blank? Many sites refuse to show inside another page. Open it with ↗ instead.</div>
119
- <div id="browser-cert" class="browser-start" hidden>
120
- <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>
121
- <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>
122
- <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>
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&#10;Make search results show up as you type&#10;Find out why the dashboard takes 5 seconds to load and speed it up&#10;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 Claude</h2>
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
- <label>
317
- Name <span class="hint">optional</span>
318
- <input name="name" placeholder="jvm-gc-questions" />
319
- </label>
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>