ai-agentmanager 1.3.0 → 1.4.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,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.4.0 (2026-10-31)
4
+
5
+ - **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.
6
+ - **Code:** a **Code** tab on every agent: the agent's files in a tree, tabs and Monaco (the editor inside VS Code). Save (refused if the agent changed the file meanwhile, unless you overwrite), search the folder, compare with where the agent started, create, rename and delete. Open files pick up the agent's edits. Paths can't leave the agent's folder. Plugins can check changes with the `agent.files` policy.
7
+ - **Open in VS Code or Cursor:** opens the agent's folder and file in the editor installed on this computer.
8
+ - **Terminal:** a shell on this computer in your home folder, from the top bar, from any signed-in device (phone included). Several at once; they keep running when hidden.
9
+ - **Update from the app:** when a new release is out, **Update to …** appears in the top bar (checked twice a day; can be turned off), and one click installs it and restarts; running agents resume. **About → Update and restart** does the same, and so does `agentmgr update`. A clone at a release tag moves to the newest tag (dependencies installed when they changed); an npm install runs `npm install -g`. A clone on a branch or with uncommitted changes is left alone.
10
+ - **☰ menu:** the top bar's menu shows who this runs as (your Claude account and organization, and the computer's user) and holds **Settings**, **Clean up**, **Alerts**, **Remote access** and **About**.
11
+ - **A limit on open agents:** keep at most N agents open (**Settings**, or `agentmgr cleanup --max-open N`); above it, the ones idle longest are closed and resume when opened. Agents working, waiting for an answer, active in the last 5 minutes, marked Keep or in a running team are never closed for it. Off by default.
12
+ - **Drag to move:** drag an agent onto another project in the list to move it there.
13
+ - **Home:** clicking **AI Agent Manager** in the top bar goes back to the agent list with nothing open.
14
+
15
+ ## 1.3.1 (2026-10-31)
16
+
17
+ - **Restart comes back:** `agentmgr restart` waits for the old daemon to exit (it closes every agent first, which takes a while with many), instead of giving up after 20 seconds and reporting success with no daemon left. `serve` started while one is shutting down waits for it instead of exiting, and `shutdown` returns once the daemon has stopped. `/api/health` says `closing` while it shuts down.
18
+ - **Pull request status is current:** checking an agent's pull requests while a check is already running (after an update, a stop or a push) waits for it and checks again, instead of returning what that earlier check read, so a pull request ready to merge could still show as needing fixes.
19
+
3
20
  ## 1.3.0 (2026-10-31)
4
21
 
5
22
  - **Review changes:** each agent's changes in one screen: changed files, the diff file by file (new files included), line comments sent to the agent as one message, **Commit all**, and **Push & open PR** with `gh`. `agentmgr changes`, `comment`, `commit`, `pr`.
package/README.md CHANGED
@@ -83,13 +83,13 @@ Runs on macOS and Linux; on Windows, use WSL2. Requires Node 20+, `git`, and the
83
83
 
84
84
  ```sh
85
85
  npm install -g ai-agentmanager # the latest release
86
- npm install -g ai-agentmanager@1.3.0 # or a specific one
86
+ npm install -g ai-agentmanager@1.4.0 # or a specific one
87
87
  ```
88
88
 
89
89
  Or from the repository, at a release tag:
90
90
 
91
91
  ```sh
92
- git clone --branch v1.3.0 --depth 1 https://github.com/jdmunta/ai-agentmanager-core.git
92
+ git clone --branch v1.4.0 --depth 1 https://github.com/jdmunta/ai-agentmanager-core.git
93
93
  cd ai-agentmanager-core
94
94
  npm install
95
95
  npm link # optional: puts `agentmgr` on your PATH
@@ -104,12 +104,12 @@ npm install
104
104
  npm link
105
105
  ```
106
106
 
107
- **Updating.** Use the command for how you installed, then run `agentmgr restart`. Running agents pause briefly and resume.
107
+ **Updating.** `agentmgr update` installs the newest release and restarts onto it; running agents pause briefly and resume. In the app, **Update to …** appears in the top bar when a release is out (checked twice a day; turn it off in **☰ → Settings** or with `agentmgr update --auto-check off`), and **About → Update and restart** does the same. A clone at a release tag (or one you `npm link`ed) moves to the newest tag; a clone on a branch or with uncommitted changes is left alone. By hand, use the command for how you installed, then `agentmgr restart`:
108
108
 
109
109
  | Installed with | Update |
110
110
  | --- | --- |
111
111
  | npm | `npm install -g ai-agentmanager@latest` |
112
- | A clone at a release tag | `git fetch --tags && git checkout v1.3.0 && npm install` (the tag you want) |
112
+ | A clone at a release tag | `git fetch --tags && git checkout v1.4.0 && npm install` (the tag you want) |
113
113
  | A clone of `main` | `git pull && npm install` |
114
114
 
115
115
  **Check your setup.** `agentmgr doctor` checks Node.js, Claude Code and its sign-in, git, `gh`, the data folder, the daemon's port and Chrome, and says how to fix anything missing.
@@ -480,7 +480,7 @@ The sidebar groups agents into projects, shown as a tree:
480
480
 
481
481
  **Edit** on an agent changes its name, its request, priority, permission mode and model. Changing the request updates what AI Agent Manager records (and tracks any new Jira links); tick **Also tell the agent** to send it to the session as well. Permission mode and model apply from its next launch.
482
482
 
483
- To list an agent under another project, use **✎ Move** next to its project (or the Edit dialog) and pick a known project, a new project name, or **Its own repo or folder** to move it back. Only the grouping changes: its code stays in its own worktree and branch.
483
+ To list an agent under another project, drag it onto that project in the list, or use **✎ Move** next to its project (or the Edit dialog) and pick a known project, a new project name, or **Its own repo or folder** to move it back. Only the grouping changes: its code stays in its own worktree and branch.
484
484
 
485
485
  ```bash
486
486
  agentmgr edit 3fa2b1 --name login-timeout --priority P1 --intent "Fix the login timeout on slow networks" --tell
@@ -770,6 +770,7 @@ agentmgr leave # the hub forgets this Mac
770
770
  | `terminal` | Open the live Claude terminal |
771
771
  | `shell` | Open shells in an agent's folder. These run any command, so grant with care. |
772
772
  | `signin` | Open this machine's own page already signed in: **Open on <machine>** asks for a one-time link (single use, valid for a minute) instead of the access token. That page has full control, beyond the rest of this policy, so grant it only on a hub that only you can sign in to. |
773
+ | `move` | Move agents to and from other machines that also allow it (**Move to…** on the hub, or `agentmgr fleet move`). See **Moving an agent** below. |
773
774
 
774
775
  `projects` (names or shortcuts) limits all of it to those projects: other agents aren't shown and can't be touched. `"allow": []` stops sharing but stays joined. The machine checks every request itself, even if the hub thinks it's allowed, matches agents by exact ID, and only runs a fixed list of actions (deleting agents isn't one). Each request, and every refusal, is written to `~/.agentmanager/hub-audit.log`; the hub keeps its own `audit.log` too.
775
776
 
@@ -782,6 +783,14 @@ agentmgr leave # the hub forgets this Mac
782
783
  - **When a machine drops mid-item.** Its item shows as **stalled** and isn't restarted elsewhere on its own, because the work in that machine's worktree would be lost or duplicated. Wait for the machine, or **Requeue** to start over on a different one.
783
784
  - **Pausing.** `agentmgr fleet backlog pause` (or **Auto-start** off) stops new starts; **Start now** runs one item past the limits.
784
785
 
786
+ **Moving an agent to another machine.** **Move to…** on an agent in the hub (or `agentmgr fleet move mini/3fa2b1 --to laptop`) continues it on another machine with the same conversation, the same branch and its uncommitted work. Use it to pick up on the laptop what the mini started, or to free a busy machine. Both machines must allow `move`, and the other machine needs the same project (matched by repo).
787
+
788
+ 1. On the machine it leaves, its session closes. A snapshot of the folder (commits, edits and new files) is pushed to the repo's own remote as `agentmgr-handoff/<id>`.
789
+ 2. The other machine creates the agent in its matching project with the same session id. It checks out the branch, restores the uncommitted work, places the conversation where Claude looks for it (with folder paths rewritten), and resumes it if it was running.
790
+ 3. The first machine marks its copy **Moved to <machine>**, which can't be resumed (two copies of one conversation would diverge), and the handoff branch is deleted.
791
+
792
+ The conversation passes through the hub in pieces, in memory only for the move, so the hub keeps neither code nor transcripts. If any step fails, the copy made on the other machine is removed, and the agent opens again where it was. It needs a repository with a remote that both machines can reach; Claude Code agents only, for now.
793
+
785
794
  **How machines prove who they are.** Joining makes an Ed25519 key pair on the Mac. The private key stays in `~/.agentmanager/hub-link.json` (mode 600), and the hub keeps only the public key. Each connection signs a fresh challenge from the hub. Invite codes work once. **Remove** on the hub (or `agentmgr hub remove <machine>`) disconnects a machine, and it stops retrying until it joins again.
786
795
 
787
796
  **Signing in with Google.** Set `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` (a Web application OAuth client whose redirect URI is `<hub address>/api/auth/google/callback`) and `AGENTMGR_HUB_GOOGLE_EMAILS` (comma-separated accounts allowed in). The sign-in page then offers **Sign in with Google**, with the access token folded away below it. It uses the authorization code flow with PKCE, state and nonce, and accepts only verified emails on that list. Without the list, Google sign-in stays off.
@@ -870,6 +879,8 @@ agentmgr cleanup --strategy close-delete --close-after 14 --delete-after 14
870
879
 
871
880
  Agents closed before clean up was turned on count as closed from the first check, so nothing is deleted the day you turn it on.
872
881
 
882
+ **A limit on open agents.** Many open sessions slow the machine down. In **☰ → Settings** (or `agentmgr cleanup --max-open 12`), set how many agents may be open at once: above it, the agents idle longest are closed, and they resume when you open them. Never closed for the limit: agents working, waiting for your answer, active in the last 5 minutes, marked **Keep**, or in a team that is still going. Off by default (`--max-open off`).
883
+
873
884
  ## Saving state: flush and restart
874
885
 
875
886
  - **What's saved immediately.** Agent records (session IDs, settings, links) are written to `state.json` on every change, fsynced, then atomically replaced. Claude writes each conversation to its own transcript as it goes.
@@ -891,6 +902,13 @@ Usage comes from the session transcripts and is refreshed every few seconds whil
891
902
  - **Where it shows.** Per session in **Session history** and `agentmgr sessions`, and summed per agent in the list, the session card, `agentmgr ls` (TOKENS) and `agentmgr show`.
892
903
  - **Cost.** The figure Claude Code records itself (`cost-state`), shown once it has written one.
893
904
 
905
+ ## Studio: code and terminals
906
+
907
+ - **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.
908
+ - **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.
909
+ - Monaco loads from the jsDelivr CDN the first time you open **Code**. Offline, a plain text editor takes its place.
910
+ - **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"`).
911
+
894
912
  ## Shell and general sessions
895
913
 
896
914
  - **Open shell** (or `agentmgr shell <agent>`) starts `$SHELL -l` in the agent's working folder. It shows as a **Shell 1** tab next to the **Claude** tab in the terminal pane; **+ Shell** (or `agentmgr shell <agent> -n 2`) opens more, and **×** on a tab closes that shell. Shells live as long as the daemon and don't start Claude.
@@ -16,7 +16,7 @@ AI Agent Manager runs every agent on the machine its daemon runs on. Once you ha
16
16
 
17
17
  ## Non-goals (for now)
18
18
 
19
- - Moving a running agent between machines. A fork across machines is in scope for a later phase; a live migration isn't.
19
+ - Live migration (moving a process mid-turn). Moving an agent between turns is supported: see **Moving agents** below.
20
20
  - Several people sharing one hub. The design leaves room for it, but 0.3 is single-user.
21
21
  - A multi-user hosted service. The hub will move to Railway, but as a single-user hub with one access token.
22
22
 
@@ -100,6 +100,7 @@ The hub sends `rpc create` to the chosen runner, which runs the agent through it
100
100
  - **Inbox:** the union of every runner's inbox items, each tagged with its machine. Answers go back as `rpc`.
101
101
  - **Conflict radar across machines:** runners send their change sets (files and line ranges, not file contents). The hub runs the same `findConflicts` across runners, keyed by normalized repo URL instead of the local clone path.
102
102
  - **Races across machines:** a race may place each attempt on a different runner, for example the same intent on macOS and Linux. Tests run on the attempt's own runner.
103
+ - **Moving agents:** the hub runs a move as fixed steps on both machines (`handoff-export`, `-chunk`, `-done` on the source; `handoff-import`, `-put`, `-finish`, `-cancel` on the target). Steps can't be called one by one from the actions API. Both machines must allow `move`. Code goes through the repo's own remote, as a snapshot commit on `agentmgr-handoff/<id>` that is deleted afterwards. The transcript goes through the hub in 512 KB pieces (base64, under the runner link's 1 MB frames), held in memory only. The target rewrites `cwd` in the transcript to its own folder. The source copy is marked `movedTo` and refuses to start.
103
104
  - **Forks across machines (later):** packing the checkpoint as a `git bundle` with the transcript and unpacking it on the target runner. This is the only feature that moves code between machines, so it waits for the trust model to settle.
104
105
 
105
106
  ## Security
package/docs/plugins.md CHANGED
@@ -56,7 +56,8 @@ Policies are synchronous so every code path can call them. Keep them fast; do sl
56
56
  | `agent.commit` | Before Commit in the review screen commits an agent's worktree | `agent`, `message` |
57
57
  | `agent.pr` | Before the review screen pushes an agent's branch and opens a pull request | `agent`, `title`, `draft` |
58
58
  | `agent.merge` | Before Merge (or auto-merge) in the Land tab merges an agent's pull request | `agent`, `url`, `method`, `auto` |
59
- | `terminal.open` | Before a live terminal is attached (browser, CLI `attach`, hub) | `agent`, `kind` (`claude` or `shell:N`) |
59
+ | `terminal.open` | Before a live terminal is attached (browser, CLI `attach`, hub) | `agent`, `kind` (`claude` or `shell:N`); for **Open Terminal** (a shell on the machine, not tied to an agent) `agent` is null and `kind` is `machine` |
60
+ | `agent.files` | Before the studio changes a file in an agent's folder | `agent`, `op` (`write`, `create`, `rename` or `delete`), `path`, and `to` for a rename |
60
61
  | `hub.action` | Before an action the hub asked for, after this machine's own `hub.allow` check | `action`, `agent` |
61
62
 
62
63
  `agent` is a summary: id, name, kind, engine, intent, repo, workdir, branch, permissionMode, model, project, status.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-agentmanager",
3
- "version": "1.3.0",
3
+ "version": "1.4.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
@@ -10,6 +10,10 @@ import { initTriggers } from '/triggers.js';
10
10
  import { initSpend } from '/spend.js';
11
11
  import { initPlaybooks } from '/playbooks.js';
12
12
  import { initSetup } from '/setup.js';
13
+ import { initUpdate } from '/update.js';
14
+ import { initMenu } from '/menu.js';
15
+ import { initCode } from '/code.js';
16
+ import { initMachine } from '/machine.js';
13
17
 
14
18
  const $ = (sel) => document.querySelector(sel);
15
19
  const agents = new Map();
@@ -30,7 +34,7 @@ const ticketHash = location.hash.match(/(?:^#|&)ticket=([\w-]+)/)?.[1] ?? null;
30
34
  const goHash = ticketHash ? decodeURIComponent(location.hash.match(/(?:^#|&)go=([^&]*)/)?.[1] ?? '') : '';
31
35
  const [hashId, hashView] = loginHash ? [null, null] : (ticketHash ? goHash : location.hash.slice(1)).split('/');
32
36
  let selectedId = hashId || null;
33
- let openTermOnLoad = hashView === 'term' || isShell(hashView) || isBrowser(hashView);
37
+ let openTermOnLoad = hashView === 'term' || hashView === 'code' || isShell(hashView) || isBrowser(hashView);
34
38
  let filterText = '';
35
39
  // "Only agents created in the last N days": remembered in this browser.
36
40
  const recent = (() => {
@@ -103,6 +107,10 @@ const reviewUi = initReview({ api, esc, toast, agents, $ });
103
107
  const triggersUi = initTriggers({ api, esc, toast, $ });
104
108
  initSpend({ api, esc, toast, $ });
105
109
  initSetup({ api, esc, $ });
110
+ const updateUi = initUpdate({ api, esc, toast, $ });
111
+ initMenu({ api, esc, toast, $ });
112
+ initMachine({ api, esc, toast, $, Terminal, FitAddon });
113
+ const codeUi = initCode({ api, esc, toast, $, agents, isOpen: () => term.kind === 'code' });
106
114
  const playbooksUi = initPlaybooks({ api, esc, toast, $, select: (id) => (showTab('agents'), select(id)) });
107
115
  $('#new-playbook').onclick = () => {
108
116
  const f = $('#new-form');
@@ -622,7 +630,7 @@ const noRepo = (a) => a.kind === 'general' || a.kind === 'assistant';
622
630
  function agentItem(a) {
623
631
  const st = statusOf(a);
624
632
  const idle = !isActive(a);
625
- return `<li data-id="${a.id}" class="agent pr${a.priority ?? 2} ${a.id === selectedId ? 'selected' : ''} ${idle ? 'idle' : ''}">
633
+ return `<li data-id="${a.id}" ${a.kind === 'general' || a.kind === 'assistant' ? '' : 'draggable="true" '}class="agent pr${a.priority ?? 2} ${a.id === selectedId ? 'selected' : ''} ${idle ? 'idle' : ''}">
626
634
  <div class="line1">${prioChip(a.priority ?? 2)}${a.results?.outcome === 'blocked' ? '<span class="outcome-mark" title="Blocked">⛔</span>' : a.results?.outcome === 'done' ? '<span class="outcome-mark" title="Done">✅</span>' : ''}<span class="name">${esc(a.name)}</span>${flags(a)}<span class="pill ${st}">${pillText(st)}</span></div>
627
635
  <div class="intent">${a.kind === 'general' ? '<span class="tag">general</span> ' : a.kind === 'assistant' ? '<span class="tag assistant">assistant</span> ' : a.startedBy ? '<span class="tag assistant" title="Started by the assistant">🤖 by assistant</span> ' : a.trigger ? `<span class="tag assistant" title="Started from ${a.trigger.source === 'github' ? 'a labeled GitHub issue' : 'the webhook'}${a.trigger.url ? `: ${esc(a.trigger.url)}` : ''}">⚡ ${a.trigger.source === 'github' ? 'GitHub' : 'webhook'}</span> ` : a.playbook ? `<span class="tag assistant" title="Started by the playbook ${esc(a.playbook.name)}">▶ ${esc(a.playbook.name)}</span> ` : ''}${a.engine && a.engine !== 'claude' ? `<span class="tag engine">${esc(ENGINE_LABEL[a.engine] ?? a.engine)}</span> ` : ''}${esc(a.intent || (a.kind === 'assistant' ? 'Finds new work and starts agents for it' : 'General session'))}</div>
628
636
  ${a.status === 'preparing' && a.progress ? `<div class="progress-line">${esc(progressText(a.progress))}</div>` : ''}
@@ -643,6 +651,13 @@ function agentItem(a) {
643
651
  }
644
652
 
645
653
  /** Agents grouped into projects, busiest and most urgent first. */
654
+ // Dragging an agent onto another project moves it there (as Edit → Project does). The list
655
+ // doesn't redraw mid-drag (updates arrive every few seconds), so the drag isn't lost.
656
+ let dragging = null;
657
+ let listStale = false;
658
+ /** What a drop on this project sends as the agent's project: its shortcut, else its name. */
659
+ const dropRef = (g) => (['assistant', 'general', 'other'].includes(g.key) ? '' : g.shortcut || g.name || '');
660
+
646
661
  function projectGroups(list) {
647
662
  const groups = new Map();
648
663
  for (const a of list) {
@@ -686,6 +701,7 @@ function projectGroups(list) {
686
701
  }
687
702
 
688
703
  function renderList() {
704
+ if (dragging) return void (listStale = true);
689
705
  const waiting = [...agents.values()].filter((a) => a.runtime.attention).length;
690
706
  document.title = waiting ? `(${waiting}) AI Agent Manager` : 'AI Agent Manager';
691
707
  // "@cc" shows one project; any other text also matches shortcuts.
@@ -727,7 +743,7 @@ function renderList() {
727
743
  const n = g.agents.length;
728
744
  // While filtering, show every match; otherwise respect collapsed projects.
729
745
  const open = filterText || !collapsed.has(g.key) || g.agents.some((a) => a.id === selectedId);
730
- return `<li class="project ${open ? 'open' : ''}">
746
+ return `<li class="project ${open ? 'open' : ''}" data-key="${esc(g.key)}" data-move="${esc(dropRef(g))}">
731
747
  <button class="project-head" data-toggle="${esc(g.key)}" aria-expanded="${open}" title="${esc(g.full || g.name)}">
732
748
  <span class="caret">${open ? '▾' : '▸'}</span>
733
749
  ${g.shortcut ? `<span class="shortcut" data-rename="${esc(g.shortcut)}" title="Shortcut: type @${esc(g.shortcut)} to filter, or use it as the project when adding agents (click to rename)">${esc(g.shortcut)}</span>` : ''}
@@ -1603,6 +1619,9 @@ function renderDetail() {
1603
1619
  ${it.status === 'review' ? `<span class="spacer"></span><button class="small primary" data-backlog-accept="${it.id}">Accept</button> <button class="small" data-backlog-reopen="${it.id}">Reopen</button>` : ''}</div>`;
1604
1620
  })()}
1605
1621
  ${a.error ? `<div class="error-box">${esc(a.error)}</div>` : ''}
1622
+ ${a.movedTo ? `<p class="hint">Moved to <b>${esc(a.movedTo.machine)}</b> ${ago(a.movedTo.at)}: it continues there with the same conversation and work. This copy stays closed; delete it when you no longer need its folder.</p>` : ''}
1623
+ ${a.movedFrom ? `<p class="hint">Moved here from <b>${esc(a.movedFrom.machine ?? 'another machine')}</b> ${ago(a.movedFrom.at)}, with its conversation and uncommitted work.</p>` : ''}
1624
+ ${!running && a.closedBy === 'cap' ? `<p class="hint">Closed ${ago(a.closedAt)} to stay under the limit on open agents (Settings), as the one idle longest. Resume it any time${a.keep ? '' : ', or mark it Keep'}.</p>` : ''}
1606
1625
  ${!running && a.closedBy === 'cleanup' ? `<p class="hint">Closed by clean up ${ago(a.closedAt)}, after a while with no activity. Resume it any time${a.keep ? '' : ', or mark it Keep'}.</p>` : ''}
1607
1626
  ${a.status === 'preparing'
1608
1627
  ? `<div class="progress-box">
@@ -1788,6 +1807,8 @@ const term = {
1788
1807
  shellExited: false,
1789
1808
 
1790
1809
  open(id, kind = 'claude') {
1810
+ if (kind === 'code') return this.openCode(id);
1811
+ $('#term-wrap').classList.remove('coding');
1791
1812
  if (isBrowser(kind)) return this.openBrowser(id, kind);
1792
1813
  liveView.close();
1793
1814
  $('#term-wrap').classList.remove('browsing');
@@ -1847,6 +1868,10 @@ const term = {
1847
1868
 
1848
1869
  syncOverlay() {
1849
1870
  if (isBrowser(this.kind)) return this.renderTabs();
1871
+ if (this.kind === 'code') {
1872
+ $('#term-overlay').hidden = true;
1873
+ return this.renderTabs();
1874
+ }
1850
1875
  const a = this.agentId && agents.get(this.agentId);
1851
1876
  const shell = isShell(this.kind);
1852
1877
  const closed = shell ? this.shellExited : a && !a.runtime.running && a.status !== 'preparing';
@@ -1887,6 +1912,24 @@ const term = {
1887
1912
  // Browser tabs: one iframe per tab, kept while the panel is open so switching back doesn't reload it.
1888
1913
  frames: new Map(), // "<agent>:<n>" -> iframe
1889
1914
 
1915
+ // Code: the agent's files in an editor, in place of the terminal (like a browser tab).
1916
+ openCode(id) {
1917
+ history.replaceState(null, '', `#${id}/code`);
1918
+ liveView.close();
1919
+ $('#term-wrap').hidden = false;
1920
+ $('#detail').classList.add('with-term');
1921
+ $('#term-wrap').classList.remove('browsing');
1922
+ $('#term-wrap').classList.add('coding');
1923
+ this.ensure();
1924
+ this.disconnect();
1925
+ this.agentId = id;
1926
+ this.kind = 'code';
1927
+ $('#term-overlay').hidden = true;
1928
+ this.renderTabs();
1929
+ $('#term-title').textContent = agents.get(id)?.workdir ?? '';
1930
+ codeUi.open(id);
1931
+ },
1932
+
1890
1933
  openBrowser(id, kind) {
1891
1934
  history.replaceState(null, '', `#${id}/${kind}`);
1892
1935
  $('#term-wrap').hidden = false;
@@ -2842,6 +2885,63 @@ $('#agent-list').addEventListener('click', (e) => {
2842
2885
  if (li) select(li.dataset.id);
2843
2886
  });
2844
2887
 
2888
+ const dropTarget = (e) => {
2889
+ const li = e.target.closest?.('li.project[data-move]');
2890
+ const a = dragging && agents.get(dragging);
2891
+ return li && li.dataset.move && a && (a.projectInfo?.key ?? 'other') !== li.dataset.key ? li : null;
2892
+ };
2893
+ const clearDrop = () => document.querySelectorAll('li.project.drop-target').forEach((x) => x.classList.remove('drop-target'));
2894
+ $('#agent-list').addEventListener('dragstart', (e) => {
2895
+ const li = e.target.closest?.('li[data-id][draggable="true"]');
2896
+ if (!li) return;
2897
+ dragging = li.dataset.id;
2898
+ e.dataTransfer.effectAllowed = 'move';
2899
+ e.dataTransfer.setData('text/plain', agents.get(dragging)?.name ?? dragging);
2900
+ li.classList.add('dragging');
2901
+ document.body.classList.add('dragging-agent');
2902
+ });
2903
+ $('#agent-list').addEventListener('dragover', (e) => {
2904
+ const li = dropTarget(e);
2905
+ if (!li) return;
2906
+ e.preventDefault();
2907
+ e.dataTransfer.dropEffect = 'move';
2908
+ if (!li.classList.contains('drop-target')) {
2909
+ clearDrop();
2910
+ li.classList.add('drop-target');
2911
+ }
2912
+ });
2913
+ $('#agent-list').addEventListener('dragleave', (e) => {
2914
+ const li = e.target.closest?.('li.project');
2915
+ if (li && !li.contains(e.relatedTarget)) li.classList.remove('drop-target');
2916
+ });
2917
+ $('#agent-list').addEventListener('drop', (e) => {
2918
+ const li = dropTarget(e);
2919
+ if (!li) return;
2920
+ e.preventDefault();
2921
+ const a = agents.get(dragging);
2922
+ const to = li.dataset.move;
2923
+ const label = li.querySelector('.pname')?.textContent ?? to;
2924
+ act(async () => upsert(await api('PATCH', `/agents/${a.id}`, { project: to })), `Moved ${a.name} to ${label}`);
2925
+ });
2926
+ document.addEventListener('dragend', () => {
2927
+ if (!dragging) return;
2928
+ dragging = null;
2929
+ clearDrop();
2930
+ document.body.classList.remove('dragging-agent');
2931
+ document.querySelector('li.agent.dragging')?.classList.remove('dragging');
2932
+ if (listStale) {
2933
+ listStale = false;
2934
+ renderList();
2935
+ }
2936
+ });
2937
+
2938
+ // The title goes home: the Agents tab with no agent open.
2939
+ $('#home-link').onclick = (e) => {
2940
+ e.preventDefault();
2941
+ showTab('agents');
2942
+ select(null);
2943
+ };
2944
+
2845
2945
  $('#detail-body').addEventListener('click', async (e) => {
2846
2946
  const a = agents.get(selectedId);
2847
2947
  if (!a) return;
@@ -3243,6 +3343,9 @@ $('#about-check').onclick = async () => {
3243
3343
  out.hidden = false;
3244
3344
  out.textContent = 'Checking…';
3245
3345
  try {
3346
+ // On this computer the daemon can install it; from a phone, just say what is out.
3347
+ const local = await updateUi.check().catch(() => null);
3348
+ if (local) return void (out.innerHTML = local);
3246
3349
  const r = await api('GET', '/about/latest');
3247
3350
  out.innerHTML = r.newer
3248
3351
  ? `Version ${esc(r.latest)} is available. Update with <code>npm install -g ${esc(aboutInfo?.package ?? 'ai-agentmanager')}@latest</code>, then <code>agentmgr restart</code>.`
@@ -3304,7 +3407,7 @@ function handleEvent(msg) {
3304
3407
  msg.agents.forEach(watchDocs);
3305
3408
  if (selectedId && !agents.has(selectedId)) selectedId = null;
3306
3409
  if (openTermOnLoad && selectedId) {
3307
- if (isShell(hashView) || isBrowser(hashView)) term.open(selectedId, hashView);
3410
+ if (isShell(hashView) || isBrowser(hashView) || hashView === 'code') term.open(selectedId, hashView);
3308
3411
  else if (agents.get(selectedId).runtime.running) term.open(selectedId);
3309
3412
  }
3310
3413
  openTermOnLoad = false;