ai-agentmanager 1.0.1 → 1.0.2
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 +9 -0
- package/README.md +21 -4
- package/bin/agentmgr.js +10 -3
- package/docs/images/agents.jpg +0 -0
- package/docs/images/conflict-radar.jpg +0 -0
- package/package.json +1 -1
- package/public/app.js +120 -4
- package/public/icons/icon-192.png +0 -0
- package/public/icons/icon-512.png +0 -0
- package/public/icons/icon.svg +1 -0
- package/public/index.html +7 -0
- package/public/manifest.webmanifest +16 -0
- package/public/style.css +27 -2
- package/src/cli.js +17 -0
- package/src/doctor.js +113 -0
- package/src/engines/claude.js +14 -0
- package/src/engines/index.js +8 -0
- package/src/judge.js +12 -0
- package/src/manager.js +26 -4
- package/src/tracker.js +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.2 (2026-10-28)
|
|
4
|
+
|
|
5
|
+
- **Resize and expand:** drag the edge of the agent list, or the line between the brief and the terminal; double-click a line to reset it. **Expand** (Ctrl+Shift+E) fills the window with the terminal, shell or browser tab, and **Full screen** fills the screen.
|
|
6
|
+
- **Active agents first:** agents that did real work or got your input in the last day sort to the top, and projects with active agents come before idle ones. Idle agents are dimmed and show how long they've been idle. A restart that resumes an agent doesn't count as activity.
|
|
7
|
+
- **`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 what's missing.
|
|
8
|
+
- **Clearer first run:** when Claude Code (or another engine) isn't installed or isn't on the daemon's PATH, the agent says so and how to fix it, instead of `spawn claude ENOENT`. Running from a git checkout without `npm install` says to run it, instead of `Cannot find package`.
|
|
9
|
+
- **AI judge:** no longer reads Claude Code's status bar ("accept edits on", "shift+tab to cycle", "update available") as a question, which raised false "waiting on you" items.
|
|
10
|
+
- **Install as an app:** Chrome and Edge can install AI Agent Manager as an app (the install icon in the address bar), so it opens in its own window with a Dock or taskbar icon.
|
|
11
|
+
|
|
3
12
|
## 1.0.1 (2026-10-26)
|
|
4
13
|
|
|
5
14
|
- **Name:** the product is now called **AI Agent Manager**, matching the `ai-agentmanager` package. The command is still `agentmgr`.
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Run one background [Claude Code](https://claude.com/claude-code) agent per inten
|
|
|
4
4
|
|
|
5
5
|

|
|
6
6
|
|
|
7
|
-

|
|
8
8
|
|
|
9
9
|
- **Intents in, agents out.** Give one intent or a list (one per line). Each becomes its own `claude` process.
|
|
10
10
|
- **Repo or folder.** Give a git URL (cloned once, then each agent gets its own worktree and branch) or a local folder.
|
|
@@ -41,6 +41,8 @@ Run one background [Claude Code](https://claude.com/claude-code) agent per inten
|
|
|
41
41
|
- **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).
|
|
42
42
|
- **CLI sign-in links.** When a terminal prints a sign-in link that returns to a port on this machine (`railway login`, `gh auth login --web`, `gcloud auth login`...), a bar above the terminal offers **Open sign-in**: the link opens in a live browser tab here, so the provider's redirect to `http://127.0.0.1:PORT/callback` reaches the waiting CLI by itself, from any device. It opens on its own if you just typed the command. Without a live browser, it opens a window on your device; from another device you then paste the address you land on (`127.0.0.1:PORT/callback…`) into the bar and AI Agent Manager delivers it to the CLI, only on the port the link named.
|
|
43
43
|
- **Attach files and screenshots.** Drop files on the terminal, paste a screenshot, or use **📎 Attach**. The file is saved on the daemon's machine and its path is pasted into the terminal, which is how Claude Code picks up images and files.
|
|
44
|
+
- **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.
|
|
45
|
+
- **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.
|
|
44
46
|
- **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).
|
|
45
47
|
- **General sessions.** **Ask Claude** starts a session with no repo, folder or intent, for questions. It is tracked and resumable like any other agent.
|
|
46
48
|
|
|
@@ -53,6 +55,18 @@ New in 0.2, for running many agents at once:
|
|
|
53
55
|
- **Checkpoints and forks.** The code (uncommitted work included) and the conversation are saved at the end of every turn. **Fork** starts a new agent from now or from any checkpoint, in its own worktree, to try another direction without losing the first.
|
|
54
56
|
- **Races.** Send one intent to several agents with different models or approaches. Compare their outcome, checks, size of change, cost and a test command run in each worktree, read their diffs side by side, then pick a winner and delete the rest.
|
|
55
57
|
|
|
58
|
+
## When to use it
|
|
59
|
+
|
|
60
|
+
Claude Code on its own, including its built-in ways to run work in parallel, is enough for one repo in one sitting. AI Agent Manager is for when that stops being enough:
|
|
61
|
+
|
|
62
|
+
- **Many sessions across several repos,** running for hours, where you lose track of which one is waiting on you.
|
|
63
|
+
- **Agents that must survive** a closed terminal, a restart or a reboot, and pick up where they left off.
|
|
64
|
+
- **Several agents in one repo,** each on its own worktree and branch, with a warning when two touch the same code.
|
|
65
|
+
- **Keeping up without reading transcripts:** a brief per agent (result, problem, fix, checks) and one inbox for every prompt and question.
|
|
66
|
+
- **From anywhere:** your phone, another computer, or several machines through the hub.
|
|
67
|
+
|
|
68
|
+
**Trying it costs little.** It runs the same `claude` you already use, with your plan and your repo's `CLAUDE.md` and skills. Sessions you started with `claude` in a terminal can be continued in it (**Import** above the agent list), and you can go back to a plain terminal any time with `claude --resume`.
|
|
69
|
+
|
|
56
70
|
## Install
|
|
57
71
|
|
|
58
72
|
Runs on macOS and Linux; on Windows, use WSL2. Requires Node 20+, `git`, and the `claude` CLI, signed in (run `claude` once). Optional: `gh` (authenticated) for PR discovery by branch and PR state, Chrome (or Chromium, Brave, Edge) for live browser tabs, and Codex (`npm i -g @openai/codex`) as a second engine.
|
|
@@ -61,13 +75,13 @@ Runs on macOS and Linux; on Windows, use WSL2. Requires Node 20+, `git`, and the
|
|
|
61
75
|
|
|
62
76
|
```sh
|
|
63
77
|
npm install -g ai-agentmanager # the latest release
|
|
64
|
-
npm install -g ai-agentmanager@1.0.
|
|
78
|
+
npm install -g ai-agentmanager@1.0.2 # or a specific one
|
|
65
79
|
```
|
|
66
80
|
|
|
67
81
|
Or from the repository, at a release tag:
|
|
68
82
|
|
|
69
83
|
```sh
|
|
70
|
-
git clone --branch v1.0.
|
|
84
|
+
git clone --branch v1.0.2 --depth 1 https://github.com/jdmunta/ai-agentmanager-core.git
|
|
71
85
|
cd ai-agentmanager-core
|
|
72
86
|
npm install
|
|
73
87
|
npm link # optional: puts `agentmgr` on your PATH
|
|
@@ -87,9 +101,11 @@ npm link
|
|
|
87
101
|
| Installed with | Update |
|
|
88
102
|
| --- | --- |
|
|
89
103
|
| npm | `npm install -g ai-agentmanager@latest` |
|
|
90
|
-
| A clone at a release tag | `git fetch --tags && git checkout v1.0.
|
|
104
|
+
| A clone at a release tag | `git fetch --tags && git checkout v1.0.2 && npm install` (the tag you want) |
|
|
91
105
|
| A clone of `main` | `git pull && npm install` |
|
|
92
106
|
|
|
107
|
+
**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.
|
|
108
|
+
|
|
93
109
|
## Quick start on a new computer
|
|
94
110
|
|
|
95
111
|
**A computer that runs agents.** After installing:
|
|
@@ -126,6 +142,7 @@ The hub listens on port 4720, on this Mac and on its Tailscale address (`--host`
|
|
|
126
142
|
```sh
|
|
127
143
|
agentmgr ui # start the daemon and open http://127.0.0.1:4717
|
|
128
144
|
agentmgr service install # start the daemon at login (launchd / systemd --user)
|
|
145
|
+
agentmgr doctor # check Node.js, Claude Code, git, gh, the port; says how to fix
|
|
129
146
|
|
|
130
147
|
agentmgr add "Fix the flaky login test" -d ~/src/app
|
|
131
148
|
agentmgr add -r https://github.com/org/repo.git \
|
package/bin/agentmgr.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
main(
|
|
2
|
+
// Dependencies are loaded behind a check, so a git checkout without `npm install` says so.
|
|
3
|
+
try {
|
|
4
|
+
const { main } = await import('../src/cli.js');
|
|
5
|
+
await main(process.argv);
|
|
6
|
+
} catch (err) {
|
|
7
|
+
if (err?.code !== 'ERR_MODULE_NOT_FOUND' || !/Cannot find package/.test(err.message)) throw err;
|
|
8
|
+
const dir = new URL('..', import.meta.url).pathname;
|
|
9
|
+
console.error(`agentmgr: dependencies are missing (${err.message.match(/'([^']+)'/)?.[1] ?? 'a package'}).\nRun \`npm install\` in ${dir} and try again.`);
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
package/docs/images/agents.jpg
CHANGED
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -556,6 +556,11 @@ const copyBtn = (value) => `<button class="link" data-copy="${esc(value)}" title
|
|
|
556
556
|
/** Agents waiting on you first, then running ones, then the rest. */
|
|
557
557
|
const rank = (a) => (a.runtime.attention ? 0 : a.runtime.running ? 1 : 2);
|
|
558
558
|
|
|
559
|
+
// Agents with no work and no input from you for a day sink below the active ones.
|
|
560
|
+
const ACTIVE_MS = 24 * 3600 * 1000;
|
|
561
|
+
const isActive = (a) =>
|
|
562
|
+
Boolean(a.runtime.attention || a.runtime.busy || a.status === 'preparing' || (a.activeAt && Date.now() - Date.parse(a.activeAt) < ACTIVE_MS));
|
|
563
|
+
|
|
559
564
|
// Projects the user collapsed in the tree (kept per browser).
|
|
560
565
|
const collapsed = new Set(
|
|
561
566
|
(() => {
|
|
@@ -578,12 +583,14 @@ const prioChip = (p, { always = false } = {}) =>
|
|
|
578
583
|
|
|
579
584
|
function agentItem(a) {
|
|
580
585
|
const st = statusOf(a);
|
|
581
|
-
|
|
586
|
+
const idle = !isActive(a);
|
|
587
|
+
return `<li data-id="${a.id}" class="agent pr${a.priority ?? 2} ${a.id === selectedId ? 'selected' : ''} ${idle ? 'idle' : ''}">
|
|
582
588
|
<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>
|
|
583
589
|
<div class="intent">${a.kind === 'general' ? '<span class="tag">general</span> ' : ''}${a.engine && a.engine !== 'claude' ? `<span class="tag engine">${esc(ENGINE_LABEL[a.engine] ?? a.engine)}</span> ` : ''}${esc(a.intent || 'General session')}</div>
|
|
584
590
|
${a.status === 'preparing' && a.progress ? `<div class="progress-line">${esc(progressText(a.progress))}</div>` : ''}
|
|
585
591
|
<div class="meta">
|
|
586
592
|
<span class="mono">${a.id}</span>
|
|
593
|
+
${idle && a.activeAt ? `<span title="Idle: no work and no input from you for over a day">💤 ${ago(a.activeAt)}</span>` : ''}
|
|
587
594
|
${a.git?.branch ? `<span class="branch" title="${esc(a.git.branch)}">⎇ ${esc(a.git.branch)}</span>` : ''}
|
|
588
595
|
${a.prs.length ? `<span>${a.prs.length} PR</span>` : ''}
|
|
589
596
|
${a.bugs.length ? `<span>${a.bugs.length} bug</span>` : ''}
|
|
@@ -608,8 +615,15 @@ function projectGroups(list) {
|
|
|
608
615
|
}
|
|
609
616
|
const out = [...groups.values()];
|
|
610
617
|
for (const g of out) {
|
|
611
|
-
//
|
|
612
|
-
g.agents.sort(
|
|
618
|
+
// Active in the last day first; then highest priority, agents needing you, running ones, most recent.
|
|
619
|
+
g.agents.sort(
|
|
620
|
+
(a, b) =>
|
|
621
|
+
isActive(b) - isActive(a) ||
|
|
622
|
+
(a.priority ?? 2) - (b.priority ?? 2) ||
|
|
623
|
+
rank(a) - rank(b) ||
|
|
624
|
+
(b.activeAt ?? b.createdAt).localeCompare(a.activeAt ?? a.createdAt),
|
|
625
|
+
);
|
|
626
|
+
g.active = g.agents.filter(isActive).length;
|
|
613
627
|
g.top = Math.min(...g.agents.map((a) => a.priority ?? 2));
|
|
614
628
|
g.running = g.agents.filter((a) => a.runtime.running).length;
|
|
615
629
|
g.waiting = g.agents.filter((a) => a.runtime.attention).length;
|
|
@@ -623,7 +637,13 @@ function projectGroups(list) {
|
|
|
623
637
|
}
|
|
624
638
|
const special = (g) => (g.key === 'general' ? 1 : g.key === 'other' ? 2 : 0);
|
|
625
639
|
return out.sort(
|
|
626
|
-
(a, b) =>
|
|
640
|
+
(a, b) =>
|
|
641
|
+
special(a) - special(b) ||
|
|
642
|
+
(b.active > 0) - (a.active > 0) ||
|
|
643
|
+
a.top - b.top ||
|
|
644
|
+
b.waiting - a.waiting ||
|
|
645
|
+
b.running - a.running ||
|
|
646
|
+
a.label.localeCompare(b.label),
|
|
627
647
|
);
|
|
628
648
|
}
|
|
629
649
|
|
|
@@ -1725,11 +1745,107 @@ const term = {
|
|
|
1725
1745
|
this.agentId = null;
|
|
1726
1746
|
$('#term-wrap').hidden = true;
|
|
1727
1747
|
$('#detail').classList.remove('with-term');
|
|
1748
|
+
if (document.body.classList.contains('term-max')) setExpanded(false);
|
|
1749
|
+
if (document.fullscreenElement) document.exitFullscreen();
|
|
1728
1750
|
},
|
|
1729
1751
|
};
|
|
1730
1752
|
|
|
1731
1753
|
$('#term-close').onclick = () => term.close();
|
|
1732
1754
|
$('#term-fit').onclick = () => term.refit();
|
|
1755
|
+
|
|
1756
|
+
// ------------------------------------------------------------------ resizing, expanding, full screen
|
|
1757
|
+
|
|
1758
|
+
const layoutPref = {
|
|
1759
|
+
get(key) {
|
|
1760
|
+
try {
|
|
1761
|
+
return localStorage.getItem(`agentmgr.${key}`);
|
|
1762
|
+
} catch {
|
|
1763
|
+
return null;
|
|
1764
|
+
}
|
|
1765
|
+
},
|
|
1766
|
+
set(key, value) {
|
|
1767
|
+
try {
|
|
1768
|
+
value == null ? localStorage.removeItem(`agentmgr.${key}`) : localStorage.setItem(`agentmgr.${key}`, value);
|
|
1769
|
+
} catch {}
|
|
1770
|
+
},
|
|
1771
|
+
};
|
|
1772
|
+
|
|
1773
|
+
function applyLayout() {
|
|
1774
|
+
const side = layoutPref.get('sideWidth');
|
|
1775
|
+
const brief = layoutPref.get('briefHeight');
|
|
1776
|
+
document.documentElement.style.setProperty('--side-w', side ? `${side}px` : '340px');
|
|
1777
|
+
if (brief) document.documentElement.style.setProperty('--brief-h', `${brief}px`);
|
|
1778
|
+
else document.documentElement.style.removeProperty('--brief-h');
|
|
1779
|
+
$('#detail').classList.toggle('brief-sized', Boolean(brief));
|
|
1780
|
+
}
|
|
1781
|
+
applyLayout();
|
|
1782
|
+
|
|
1783
|
+
/** Drag a handle; `move` gets the pointer position and returns the value to keep. */
|
|
1784
|
+
function dragHandle(el, axis, move, key) {
|
|
1785
|
+
el.addEventListener('pointerdown', (e) => {
|
|
1786
|
+
e.preventDefault();
|
|
1787
|
+
el.setPointerCapture(e.pointerId);
|
|
1788
|
+
el.classList.add('dragging');
|
|
1789
|
+
document.body.classList.add('resizing', axis);
|
|
1790
|
+
let value = null;
|
|
1791
|
+
const onMove = (ev) => {
|
|
1792
|
+
value = move(ev);
|
|
1793
|
+
applyLayout();
|
|
1794
|
+
};
|
|
1795
|
+
const onUp = () => {
|
|
1796
|
+
el.classList.remove('dragging');
|
|
1797
|
+
document.body.classList.remove('resizing', axis);
|
|
1798
|
+
el.removeEventListener('pointermove', onMove);
|
|
1799
|
+
el.removeEventListener('pointerup', onUp);
|
|
1800
|
+
if (value != null) layoutPref.set(key, Math.round(value));
|
|
1801
|
+
term.refit();
|
|
1802
|
+
};
|
|
1803
|
+
el.addEventListener('pointermove', onMove);
|
|
1804
|
+
el.addEventListener('pointerup', onUp);
|
|
1805
|
+
});
|
|
1806
|
+
el.addEventListener('dblclick', () => {
|
|
1807
|
+
layoutPref.set(key, null);
|
|
1808
|
+
applyLayout();
|
|
1809
|
+
requestAnimationFrame(() => term.refit());
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
dragHandle($('#split-side'), 'cols', (ev) => {
|
|
1814
|
+
const w = Math.min(Math.max(ev.clientX, 220), window.innerWidth * 0.6);
|
|
1815
|
+
layoutPref.set('sideWidth', Math.round(w));
|
|
1816
|
+
return w;
|
|
1817
|
+
}, 'sideWidth');
|
|
1818
|
+
|
|
1819
|
+
dragHandle($('#split-term'), 'rows', (ev) => {
|
|
1820
|
+
const top = $('#detail').getBoundingClientRect().top;
|
|
1821
|
+
const h = Math.min(Math.max(ev.clientY - top, 60), $('#detail').clientHeight - 160);
|
|
1822
|
+
layoutPref.set('briefHeight', Math.round(h));
|
|
1823
|
+
return h;
|
|
1824
|
+
}, 'briefHeight');
|
|
1825
|
+
|
|
1826
|
+
function setExpanded(on) {
|
|
1827
|
+
document.body.classList.toggle('term-max', on);
|
|
1828
|
+
$('#term-max').classList.toggle('on', on);
|
|
1829
|
+
$('#term-max').textContent = on ? '⤡ Restore' : '⤢ Expand';
|
|
1830
|
+
requestAnimationFrame(() => term.refit());
|
|
1831
|
+
}
|
|
1832
|
+
$('#term-max').onclick = () => setExpanded(!document.body.classList.contains('term-max'));
|
|
1833
|
+
$('#term-full').onclick = () => {
|
|
1834
|
+
if (document.fullscreenElement) document.exitFullscreen();
|
|
1835
|
+
else $('#term-wrap').requestFullscreen?.().catch(() => setExpanded(true));
|
|
1836
|
+
};
|
|
1837
|
+
document.addEventListener('fullscreenchange', () => {
|
|
1838
|
+
const on = document.fullscreenElement === $('#term-wrap');
|
|
1839
|
+
$('#term-full').classList.toggle('on', on);
|
|
1840
|
+
$('#term-full').textContent = on ? '⛶ Exit full screen' : '⛶ Full screen';
|
|
1841
|
+
requestAnimationFrame(() => term.refit());
|
|
1842
|
+
});
|
|
1843
|
+
document.addEventListener('keydown', (e) => {
|
|
1844
|
+
if (e.ctrlKey && e.shiftKey && e.code === 'KeyE' && !$('#term-wrap').hidden) {
|
|
1845
|
+
e.preventDefault();
|
|
1846
|
+
setExpanded(!document.body.classList.contains('term-max'));
|
|
1847
|
+
}
|
|
1848
|
+
}, true);
|
|
1733
1849
|
$('#term-resume').onclick = () => {
|
|
1734
1850
|
const id = term.agentId;
|
|
1735
1851
|
if (isShell(term.kind)) {
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><rect width="512" height="512" rx="112" fill="#151821"/><path d="M256 76l180 180-180 180L76 256z" fill="#6d8bff"/><path d="M256 166l90 90-90 90-90-90z" fill="#151821"/></svg>
|
package/public/index.html
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
7
7
|
<title>AI Agent Manager</title>
|
|
8
8
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 16 16%22%3E%3Cpath d=%22M8 1l7 7-7 7-7-7z%22 fill=%22%233b5bdb%22/%3E%3C/svg%3E" />
|
|
9
|
+
<link rel="manifest" href="/manifest.webmanifest" crossorigin="use-credentials" />
|
|
10
|
+
<meta name="theme-color" content="#151821" />
|
|
11
|
+
<link rel="apple-touch-icon" href="/icons/icon-192.png" />
|
|
9
12
|
<link rel="stylesheet" href="/vendor/xterm/css/xterm.css" />
|
|
10
13
|
<link rel="stylesheet" href="/style.css" />
|
|
11
14
|
</head>
|
|
@@ -44,10 +47,12 @@
|
|
|
44
47
|
<p>Add one or more intents and a repo or folder to get started.</p>
|
|
45
48
|
</div>
|
|
46
49
|
</aside>
|
|
50
|
+
<div class="splitter split-side" id="split-side" role="separator" aria-orientation="vertical" title="Drag to resize the list; double-click to reset"></div>
|
|
47
51
|
|
|
48
52
|
<section class="detail" id="detail">
|
|
49
53
|
<div class="placeholder" id="placeholder">Select an agent to see its session, folder, repo, PRs and bugs.</div>
|
|
50
54
|
<div id="detail-body" hidden></div>
|
|
55
|
+
<div class="splitter split-term" id="split-term" role="separator" aria-orientation="horizontal" title="Drag to resize the terminal; double-click to reset"></div>
|
|
51
56
|
<div class="term-wrap" id="term-wrap" hidden>
|
|
52
57
|
<div class="term-bar">
|
|
53
58
|
<div class="term-tabs" role="tablist">
|
|
@@ -62,6 +67,8 @@
|
|
|
62
67
|
<button id="term-attach" class="ghost small" title="Attach files or images (you can also drop or paste them into the terminal)">📎 Attach</button>
|
|
63
68
|
<input type="file" id="term-file" multiple hidden>
|
|
64
69
|
<button id="term-fit" class="ghost small" title="Refit the terminal to the panel">Fit</button>
|
|
70
|
+
<button id="term-max" class="ghost small desk-only" title="Expand to fill the window (Ctrl+Shift+E)">⤢ Expand</button>
|
|
71
|
+
<button id="term-full" class="ghost small desk-only" title="Full screen (Esc to leave)">⛶ Full screen</button>
|
|
65
72
|
<button id="term-close" class="ghost small" title="Hide the terminal (the agent keeps running)">Hide terminal</button>
|
|
66
73
|
</div>
|
|
67
74
|
<div id="signin-bar" class="signin-bar" hidden></div>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "AI Agent Manager",
|
|
3
|
+
"short_name": "Agent Manager",
|
|
4
|
+
"description": "Run many coding agents in parallel without babysitting terminals.",
|
|
5
|
+
"id": "/",
|
|
6
|
+
"start_url": "/",
|
|
7
|
+
"scope": "/",
|
|
8
|
+
"display": "standalone",
|
|
9
|
+
"background_color": "#0d0f14",
|
|
10
|
+
"theme_color": "#151821",
|
|
11
|
+
"icons": [
|
|
12
|
+
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
|
|
13
|
+
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" },
|
|
14
|
+
{ "src": "/icons/icon.svg", "sizes": "any", "type": "image/svg+xml" }
|
|
15
|
+
]
|
|
16
|
+
}
|
package/public/style.css
CHANGED
|
@@ -88,7 +88,27 @@ input, select, textarea {
|
|
|
88
88
|
textarea { resize: vertical; font-family: var(--mono); font-size: 13px; }
|
|
89
89
|
|
|
90
90
|
/* ---------- layout */
|
|
91
|
-
.layout { flex: 1; display: grid; grid-template-columns: 340px 1fr; min-height: 0; }
|
|
91
|
+
.layout { flex: 1; display: grid; grid-template-columns: var(--side-w, 340px) 0 1fr; min-height: 0; }
|
|
92
|
+
|
|
93
|
+
/* Drag handles: the list's width, and the brief above the terminal. */
|
|
94
|
+
.splitter { position: relative; z-index: 3; }
|
|
95
|
+
.splitter::after { content: ''; position: absolute; }
|
|
96
|
+
.split-side::after { top: 0; bottom: 0; left: -4px; width: 8px; cursor: col-resize; }
|
|
97
|
+
.split-term { display: none; flex: none; height: 0; }
|
|
98
|
+
.detail.with-term > #detail-body:not([hidden]) + .split-term { display: block; }
|
|
99
|
+
.split-term::after { left: 0; right: 0; top: -4px; height: 8px; cursor: row-resize; }
|
|
100
|
+
.splitter:hover::after, .splitter.dragging::after { background: var(--accent); opacity: .5; }
|
|
101
|
+
body.resizing { user-select: none; }
|
|
102
|
+
body.resizing iframe, body.resizing .terminal { pointer-events: none; }
|
|
103
|
+
body.resizing.cols { cursor: col-resize; }
|
|
104
|
+
body.resizing.rows { cursor: row-resize; }
|
|
105
|
+
|
|
106
|
+
/* Expanded: the terminal, shell or browser fills the window. */
|
|
107
|
+
body.term-max .topbar, body.term-max .sidebar, body.term-max .split-side,
|
|
108
|
+
body.term-max #detail-body, body.term-max .split-term, body.term-max #placeholder { display: none !important; }
|
|
109
|
+
body.term-max .layout { grid-template-columns: 1fr; }
|
|
110
|
+
.term-wrap:fullscreen { width: 100vw; height: 100vh; }
|
|
111
|
+
.term-bar button.on { background: #262c38; border-color: #58a6ff; }
|
|
92
112
|
.sidebar { border-right: 1px solid var(--border); overflow-y: auto; background: var(--panel); }
|
|
93
113
|
.side-tabs { display: flex; border-bottom: 1px solid var(--border); position: sticky; top: 0; background: var(--panel); z-index: 2; }
|
|
94
114
|
.side-tab { flex: 1; border: 0; border-radius: 0; background: transparent; padding: 9px; font-weight: 600; color: var(--muted); border-bottom: 2px solid transparent; }
|
|
@@ -172,7 +192,8 @@ textarea { resize: vertical; font-family: var(--mono); font-size: 13px; }
|
|
|
172
192
|
.detail { display: flex; flex-direction: column; min-height: 0; min-width: 0; }
|
|
173
193
|
.placeholder { margin: auto; color: var(--muted); padding: 24px; text-align: center; }
|
|
174
194
|
#detail-body { padding: 16px 20px; overflow-y: auto; flex: 0 1 auto; max-height: 100%; }
|
|
175
|
-
.detail.with-term #detail-body { max-height: 42
|
|
195
|
+
.detail.with-term #detail-body { max-height: var(--brief-h, 42%); border-bottom: 1px solid var(--border); }
|
|
196
|
+
.detail.with-term.brief-sized #detail-body { height: var(--brief-h); flex: none; }
|
|
176
197
|
.d-head { display: flex; align-items: flex-start; gap: 12px; flex-wrap: wrap; }
|
|
177
198
|
.d-title { flex: 1 1 420px; min-width: 0; }
|
|
178
199
|
.d-title h2 { margin: 0 0 4px; font-size: 18px; display: flex; gap: 6px 10px; align-items: center; flex-wrap: wrap; }
|
|
@@ -399,6 +420,7 @@ dialog::backdrop { background: rgba(0, 0, 0, .45); }
|
|
|
399
420
|
.term-bar { flex-wrap: wrap; row-gap: 6px; }
|
|
400
421
|
.term-bar > .term-tabs { flex: 1 0 100%; }
|
|
401
422
|
.layout { grid-template-columns: 1fr; grid-template-rows: auto 1fr; }
|
|
423
|
+
.splitter, .desk-only { display: none !important; }
|
|
402
424
|
.sidebar { max-height: 38vh; border-right: 0; border-bottom: 1px solid var(--border); }
|
|
403
425
|
.top-actions input { width: 130px; }
|
|
404
426
|
.row { grid-template-columns: 1fr; }
|
|
@@ -548,3 +570,6 @@ pre.diff .meta { color: var(--muted); font-weight: 600; }
|
|
|
548
570
|
#import-dialog .import-foot label { display: flex; flex-direction: row; align-items: center; gap: 6px; }
|
|
549
571
|
#import-dialog .import-foot input { width: auto; }
|
|
550
572
|
.judge-line { margin: 14px 12px; line-height: 1.5; }
|
|
573
|
+
|
|
574
|
+
.agent-list li.agent.idle { opacity: .62; }
|
|
575
|
+
.agent-list li.agent.idle:hover, .agent-list li.agent.idle.selected { opacity: 1; }
|
package/src/cli.js
CHANGED
|
@@ -879,6 +879,23 @@ export async function main(argv) {
|
|
|
879
879
|
}
|
|
880
880
|
});
|
|
881
881
|
|
|
882
|
+
program
|
|
883
|
+
.command('doctor')
|
|
884
|
+
.description('Check what AI Agent Manager needs on this machine (Node.js, Claude Code, git, gh, the daemon) and how to fix it')
|
|
885
|
+
.action(async () => {
|
|
886
|
+
const { doctor } = await import('./doctor.js');
|
|
887
|
+
const checks = await doctor();
|
|
888
|
+
const mark = { ok: green('✓'), warn: yellow('!'), fail: red('✗'), info: dim('·') };
|
|
889
|
+
const width = Math.max(...checks.map((c) => c.name.length));
|
|
890
|
+
for (const c of checks) {
|
|
891
|
+
console.log(`${mark[c.status]} ${c.name.padEnd(width)} ${c.status === 'fail' ? red(c.detail) : c.detail}`);
|
|
892
|
+
if (c.fix) console.log(` ${' '.repeat(width)} ${dim('→')} ${c.fix}`);
|
|
893
|
+
}
|
|
894
|
+
const failed = checks.filter((c) => c.status === 'fail').length;
|
|
895
|
+
console.log(failed ? red(`\n${failed} problem${failed === 1 ? '' : 's'} to fix`) : green('\nReady: start with `agentmgr ui`'));
|
|
896
|
+
if (failed) process.exitCode = 1;
|
|
897
|
+
});
|
|
898
|
+
|
|
882
899
|
const remoteCmd = program.command('remote').description('Open AI Agent Manager from a phone or another computer on the same network');
|
|
883
900
|
const printRemote = async (st, { qr = true } = {}) => {
|
|
884
901
|
if (!st.enabled || !st.running) return console.log(`remote access ${dim('off')} (turn on: agentmgr remote on)`);
|
package/src/doctor.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// `agentmgr doctor`: checks what AI Agent Manager needs on this machine and says how to fix
|
|
2
|
+
// what's missing. Each check is { name, status: 'ok' | 'warn' | 'fail' | 'info', detail, fix }.
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import net from 'node:net';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { HOST, PORT, loadConfig, paths } from './config.js';
|
|
9
|
+
import { api, BASE, isRunning } from './client.js';
|
|
10
|
+
import { ENGINES, installed, notInstalled } from './engines/index.js';
|
|
11
|
+
import { findBrowser } from './livebrowser.js';
|
|
12
|
+
|
|
13
|
+
const run = (bin, args, timeout = 8000) =>
|
|
14
|
+
new Promise((resolve) => {
|
|
15
|
+
execFile(bin, args, { timeout }, (err, stdout, stderr) => resolve({ ok: !err, out: `${stdout}${stderr}`.trim(), err }));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const firstLine = (s) => s.split('\n')[0].trim();
|
|
19
|
+
|
|
20
|
+
function portFree(port) {
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
const srv = net.createServer();
|
|
23
|
+
srv.once('error', () => resolve(false));
|
|
24
|
+
srv.listen(port, HOST, () => srv.close(() => resolve(true)));
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Whether Claude Code has credentials, without reading them: an API key, or a signed-in account. */
|
|
29
|
+
function claudeSignedIn() {
|
|
30
|
+
if (process.env.ANTHROPIC_API_KEY) return 'an API key (ANTHROPIC_API_KEY)';
|
|
31
|
+
try {
|
|
32
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.claude.json'), 'utf8'));
|
|
33
|
+
if (cfg.oauthAccount) return 'a Claude account';
|
|
34
|
+
} catch {}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function ptyWorks() {
|
|
39
|
+
try {
|
|
40
|
+
const pty = (await import('node-pty')).default;
|
|
41
|
+
return await new Promise((resolve) => {
|
|
42
|
+
const p = pty.spawn(process.platform === 'win32' ? 'cmd.exe' : '/bin/sh', process.platform === 'win32' ? ['/c', 'exit 0'] : ['-c', 'exit 0'], {});
|
|
43
|
+
const t = setTimeout(() => resolve('started but did not finish'), 5000);
|
|
44
|
+
p.onExit(() => {
|
|
45
|
+
clearTimeout(t);
|
|
46
|
+
resolve(null);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
} catch (err) {
|
|
50
|
+
return err.message;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function doctor() {
|
|
55
|
+
const config = loadConfig();
|
|
56
|
+
const checks = [];
|
|
57
|
+
const add = (name, status, detail, fix) => checks.push({ name, status, detail, ...(fix ? { fix } : {}) });
|
|
58
|
+
|
|
59
|
+
const major = Number(process.versions.node.split('.')[0]);
|
|
60
|
+
add('Node.js', major >= 20 ? 'ok' : 'fail', `${process.versions.node}`, major >= 20 ? null : 'Install Node.js 20 or newer (https://nodejs.org)');
|
|
61
|
+
|
|
62
|
+
const ptyErr = await ptyWorks();
|
|
63
|
+
add('Terminals (node-pty)', ptyErr ? 'fail' : 'ok', ptyErr ? `can't start a terminal: ${ptyErr}` : 'can start terminals',
|
|
64
|
+
ptyErr ? 'Reinstall: npm install -g ai-agentmanager (or npm rebuild node-pty in a git checkout)' : null);
|
|
65
|
+
|
|
66
|
+
// The daemon's view wins: a login service may have a different PATH than this shell.
|
|
67
|
+
const running = await isRunning();
|
|
68
|
+
const daemonEngines = running ? await api('GET', '/engines').catch(() => null) : null;
|
|
69
|
+
for (const engine of Object.values(ENGINES)) {
|
|
70
|
+
const has = daemonEngines ? daemonEngines.find((e) => e.id === engine.id)?.installed : installed(engine, config);
|
|
71
|
+
const main = engine.id === (config.engine ?? 'claude');
|
|
72
|
+
if (!has) {
|
|
73
|
+
add(engine.label, main ? 'fail' : 'info', main ? 'not found' : 'not installed (optional)', main ? notInstalled(engine, config) : null);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const v = await run(engine.bin(config), ['--version']);
|
|
77
|
+
add(engine.label, 'ok', v.ok ? firstLine(v.out) : 'installed');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const signedIn = claudeSignedIn();
|
|
81
|
+
add('Claude sign-in', signedIn ? 'ok' : 'warn', signedIn ? `uses ${signedIn}` : "couldn't tell whether Claude Code is signed in",
|
|
82
|
+
signedIn ? null : 'Run `claude` in a terminal once and sign in');
|
|
83
|
+
|
|
84
|
+
const git = await run('git', ['--version']);
|
|
85
|
+
add('git', git.ok ? 'ok' : 'fail', git.ok ? firstLine(git.out) : 'not found', git.ok ? null : 'Install git (agents work in git worktrees)');
|
|
86
|
+
|
|
87
|
+
const gh = await run('gh', ['auth', 'status']);
|
|
88
|
+
if (gh.err?.code === 'ENOENT') add('GitHub CLI (gh)', 'warn', 'not installed: PRs and checks are not tracked', 'Install it (https://cli.github.com) and run `gh auth login`');
|
|
89
|
+
else add('GitHub CLI (gh)', gh.ok ? 'ok' : 'warn', gh.ok ? 'signed in' : 'not signed in: PRs and checks are not tracked', gh.ok ? null : 'Run `gh auth login`');
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
fs.mkdirSync(paths.home, { recursive: true });
|
|
93
|
+
fs.accessSync(paths.home, fs.constants.W_OK);
|
|
94
|
+
add('Data folder', 'ok', paths.home);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
add('Data folder', 'fail', `${paths.home}: ${err.message}`, 'Make it writable, or point AGENTMGR_HOME at another folder');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (running) {
|
|
100
|
+
const a = await api('GET', '/about').catch(() => null);
|
|
101
|
+
add('Daemon', 'ok', `running at ${BASE}${a ? ` (version ${a.version}, pid ${a.daemon.pid})` : ''}`);
|
|
102
|
+
} else if (await portFree(PORT)) {
|
|
103
|
+
add('Daemon', 'info', `not running; port ${PORT} is free`, 'Start it with `agentmgr ui`');
|
|
104
|
+
} else {
|
|
105
|
+
add('Daemon', 'fail', `port ${PORT} is used by another program`, 'Stop that program, or set AGENTMGR_PORT to another port');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const chrome = findBrowser(config);
|
|
109
|
+
add('Chrome (live browser tabs)', chrome ? 'ok' : 'warn', chrome ?? 'not found: live browser tabs are unavailable (Lite mode still works)',
|
|
110
|
+
chrome ? null : 'Install Chrome, Chromium, Brave or Edge, or set "browserBin" in config.json');
|
|
111
|
+
|
|
112
|
+
return checks;
|
|
113
|
+
}
|
package/src/engines/claude.js
CHANGED
|
@@ -84,6 +84,20 @@ export default {
|
|
|
84
84
|
],
|
|
85
85
|
promptChoices: (screen) => promptChoices(screen),
|
|
86
86
|
|
|
87
|
+
/** Lines of Claude Code's own frame (input box, mode and status bar): never a question to the person. */
|
|
88
|
+
screenChrome: [
|
|
89
|
+
/\b(accept edits|bypass permissions|plan mode|auto mode|auto-accept edits) on\b/i,
|
|
90
|
+
/shift\+tab to cycle/i,
|
|
91
|
+
/\? for shortcuts/i,
|
|
92
|
+
/esc to interrupt/i,
|
|
93
|
+
/ctrl\+[a-z] to (expand|edit|show|hide)/i,
|
|
94
|
+
/update available/i,
|
|
95
|
+
/^[─━═-]{3,}/,
|
|
96
|
+
/^\/\w+$/,
|
|
97
|
+
/^[❯>]\s*$/,
|
|
98
|
+
],
|
|
99
|
+
install: 'Install Claude Code (https://docs.claude.com/en/docs/claude-code) and run `claude` once to sign in',
|
|
100
|
+
|
|
87
101
|
trust: (dir) => trustFolder(dir),
|
|
88
102
|
|
|
89
103
|
/** A one-shot, tool-less call; the prompt goes on stdin and the answer comes back as text. */
|
package/src/engines/index.js
CHANGED
|
@@ -23,6 +23,14 @@ export function installed(engine, config) {
|
|
|
23
23
|
return (process.env.PATH ?? '').split(path.delimiter).some((d) => d && fs.existsSync(path.join(d, bin)));
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/** What to tell the person when an engine's command isn't on this machine. */
|
|
27
|
+
export function notInstalled(engine, config) {
|
|
28
|
+
const bin = engine.bin(config);
|
|
29
|
+
const how = engine.install ?? `Install ${engine.label}`;
|
|
30
|
+
const where = path.isAbsolute(bin) ? `${bin} doesn't exist` : `\`${bin}\` isn't on the daemon's PATH`;
|
|
31
|
+
return `${engine.label} not found: ${where}. ${how}, then run \`agentmgr restart\`. If it's installed somewhere else, set its path in config.json${engine.id === 'claude' ? ' ("claudeBin") or AGENTMGR_CLAUDE_BIN' : ` ("engines": {"${engine.id}": {"bin": "…"}})`}.`;
|
|
32
|
+
}
|
|
33
|
+
|
|
26
34
|
/** Every engine, with whether it can run here, for pickers and the hub. */
|
|
27
35
|
export function listEngines(config) {
|
|
28
36
|
return Object.values(ENGINES).map((e) => ({ id: e.id, label: e.label, installed: installed(e, config), permissionModes: e.permissionModes, supports: e.supports }));
|
package/src/judge.js
CHANGED
|
@@ -78,6 +78,16 @@ export function stuckPrompt(ctx, finding, events, past = []) {
|
|
|
78
78
|
].join('\n');
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/** The screen without the engine's own frame (input box, mode indicators, status bar). */
|
|
82
|
+
export function stripChrome(screen, patterns = []) {
|
|
83
|
+
if (!patterns.length) return screen;
|
|
84
|
+
return screen
|
|
85
|
+
.split('\n')
|
|
86
|
+
.filter((line) => !patterns.some((re) => re.test(line.trim())))
|
|
87
|
+
.join('\n')
|
|
88
|
+
.trim();
|
|
89
|
+
}
|
|
90
|
+
|
|
81
91
|
export function screenPrompt(ctx, screen, past = []) {
|
|
82
92
|
return [
|
|
83
93
|
'You supervise an AI coding agent running in a terminal. It has stopped producing output.',
|
|
@@ -87,6 +97,8 @@ export function screenPrompt(ctx, screen, past = []) {
|
|
|
87
97
|
'- "error": it crashed, hit an error it cannot recover from, or a limit (rate limit, auth, quota)',
|
|
88
98
|
'- "idle": finished a step and is simply waiting for the next instruction, nothing specific to answer',
|
|
89
99
|
'For "question", copy the question in plain words and list the choices it offered, if any.',
|
|
100
|
+
'The tool\'s own input box, mode indicators and status bar (for example "accept edits on", "plan mode on",',
|
|
101
|
+
'"shift+tab to cycle", "? for shortcuts", "update available") are not questions: only the agent\'s own words count.',
|
|
90
102
|
RULES,
|
|
91
103
|
'Shape: {"state": "question|done|error|idle", "confidence": 0.0, "summary": "one line for an inbox", "question": "or empty", "choices": ["..."]}',
|
|
92
104
|
'',
|
package/src/manager.js
CHANGED
|
@@ -4,7 +4,7 @@ import fs from 'node:fs';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import pty from 'node-pty';
|
|
6
6
|
import { expandHome, paths } from './config.js';
|
|
7
|
-
import { DEFAULT_ENGINE, ENGINES, engineOf, installed } from './engines/index.js';
|
|
7
|
+
import { DEFAULT_ENGINE, ENGINES, engineOf, installed, notInstalled } from './engines/index.js';
|
|
8
8
|
import {
|
|
9
9
|
DOC_KINDS,
|
|
10
10
|
briefDir,
|
|
@@ -42,7 +42,7 @@ import * as git from './git.js';
|
|
|
42
42
|
import { conflictMessage, findConflicts, related, repoKey, summarize as summarizeConflicts } from './conflicts.js';
|
|
43
43
|
import { answerMessage, loadQuestions, questionsPath, questionsPrompt } from './inbox.js';
|
|
44
44
|
import { STUCK_DEFAULTS, detectStuck } from './stuck.js';
|
|
45
|
-
import { Judge, clashPrompt, finishMessage, hash, reviewPrompt, screenPrompt, stuckPrompt } from './judge.js';
|
|
45
|
+
import { Judge, clashPrompt, finishMessage, hash, reviewPrompt, screenPrompt, stripChrome, stuckPrompt } from './judge.js';
|
|
46
46
|
import { findLocalUrls, findSignInLinks, parseCallback, parseUrl } from './preview.js';
|
|
47
47
|
import { Plugins } from './plugins.js';
|
|
48
48
|
import { runningClaude, scanClaudeSessions } from './sessions.js';
|
|
@@ -191,6 +191,7 @@ export class AgentManager extends EventEmitter {
|
|
|
191
191
|
this.config = config;
|
|
192
192
|
this.plugins = plugins; // policies before actions, listeners after (src/plugins.js)
|
|
193
193
|
this.runtimes = new Map(); // agent id -> { pty, chunks, size, clients, lastOutputAt, busy, stopping, exited }
|
|
194
|
+
this.inputAt = new Map(); // agent id -> when you last typed into its session or a shell
|
|
194
195
|
this.shells = new Map(); // agent id -> ancillary shell runtime in the agent's folder
|
|
195
196
|
this.cursors = new Map(); // session id -> transcript scan cursor
|
|
196
197
|
this.locks = new Map();
|
|
@@ -268,6 +269,7 @@ export class AgentManager extends EventEmitter {
|
|
|
268
269
|
...agent,
|
|
269
270
|
results: this.results(agent, rt, prs),
|
|
270
271
|
priority: agent.priority ?? 2,
|
|
272
|
+
activeAt: this.activeAt(agent, rt),
|
|
271
273
|
projectInfo: this.projectInfo(agent),
|
|
272
274
|
usage,
|
|
273
275
|
// Claude Code's own cost figure from the transcripts (absent until it records one).
|
|
@@ -295,6 +297,19 @@ export class AgentManager extends EventEmitter {
|
|
|
295
297
|
};
|
|
296
298
|
}
|
|
297
299
|
|
|
300
|
+
/**
|
|
301
|
+
* When the agent last did real work or you last worked with it: its conversation,
|
|
302
|
+
* its brief, your typing, or output while busy (not the redraw after a restart).
|
|
303
|
+
*/
|
|
304
|
+
activeAt(agent, rt) {
|
|
305
|
+
const times = [agent.createdAt, agent.brief?.updatedAt, ...(agent.sessions ?? []).map((s) => s.lastMessageAt ?? s.lastActivity)]
|
|
306
|
+
.map((t) => (t ? Date.parse(t) : 0));
|
|
307
|
+
times.push(this.inputAt.get(agent.id) ?? 0);
|
|
308
|
+
if (rt?.pty && rt.busy && rt.lastOutputAt - (rt.startedAt ?? 0) > 60_000) times.push(rt.lastOutputAt);
|
|
309
|
+
const t = Math.max(0, ...times.filter(Number.isFinite));
|
|
310
|
+
return t ? new Date(t).toISOString() : null;
|
|
311
|
+
}
|
|
312
|
+
|
|
298
313
|
/**
|
|
299
314
|
* The end result / status / blockers for an agent: what the agent wrote under
|
|
300
315
|
* "## Results", plus what AI Agent Manager knows (PRs, checks, prompts, errors).
|
|
@@ -892,6 +907,11 @@ export class AgentManager extends EventEmitter {
|
|
|
892
907
|
}
|
|
893
908
|
}
|
|
894
909
|
|
|
910
|
+
if (!installed(engine, this.config)) {
|
|
911
|
+
const error = notInstalled(engine, this.config);
|
|
912
|
+
this.patch(agent.id, { status: 'error', error });
|
|
913
|
+
throw Object.assign(new Error(error), { status: 400 });
|
|
914
|
+
}
|
|
895
915
|
let proc;
|
|
896
916
|
try {
|
|
897
917
|
proc = pty.spawn(engine.bin(this.config), args, {
|
|
@@ -1198,6 +1218,8 @@ export class AgentManager extends EventEmitter {
|
|
|
1198
1218
|
}
|
|
1199
1219
|
|
|
1200
1220
|
write(id, data, kind = 'claude') {
|
|
1221
|
+
// Focus reports and terminal replies are not you typing.
|
|
1222
|
+
if (!/^(\x1b\[[\d;?>]*[IOcRnt])+$/.test(data)) this.inputAt.set(id, Date.now());
|
|
1201
1223
|
this.termRuntime(id, kind)?.pty?.write(data);
|
|
1202
1224
|
}
|
|
1203
1225
|
|
|
@@ -1898,7 +1920,7 @@ export class AgentManager extends EventEmitter {
|
|
|
1898
1920
|
session.usage = usage;
|
|
1899
1921
|
changed = true;
|
|
1900
1922
|
}
|
|
1901
|
-
for (const key of ['title', 'lastPrompt', 'cost', 'lastActivity']) {
|
|
1923
|
+
for (const key of ['title', 'lastPrompt', 'cost', 'lastActivity', 'lastMessageAt']) {
|
|
1902
1924
|
if (res[key] != null && JSON.stringify(session[key]) !== JSON.stringify(res[key])) {
|
|
1903
1925
|
session[key] = res[key];
|
|
1904
1926
|
changed = true;
|
|
@@ -2253,7 +2275,7 @@ export class AgentManager extends EventEmitter {
|
|
|
2253
2275
|
async judgeScreen(id, rt) {
|
|
2254
2276
|
const agent = this.store.get(id);
|
|
2255
2277
|
if (!agent) return;
|
|
2256
|
-
const screen = this.screenText(rt);
|
|
2278
|
+
const screen = stripChrome(this.screenText(rt), engineOf(agent).screenChrome);
|
|
2257
2279
|
if (!screen) return;
|
|
2258
2280
|
const h = hash(screen);
|
|
2259
2281
|
if (rt.screenVerdict?.hash === h) return this.applyScreen(id, rt, rt.screenVerdict.data);
|
package/src/tracker.js
CHANGED
|
@@ -132,7 +132,7 @@ function textOf(content) {
|
|
|
132
132
|
* subsequent calls only parse newly appended lines.
|
|
133
133
|
*/
|
|
134
134
|
export function scanTranscript(file, cursor, bugPatterns = []) {
|
|
135
|
-
const result = { prs: [], bugs: [], title: null, lastPrompt: null, cost: null, lastActivity: null };
|
|
135
|
+
const result = { prs: [], bugs: [], title: null, lastPrompt: null, cost: null, lastActivity: null, lastMessageAt: null };
|
|
136
136
|
// Token usage per API response, kept on the cursor so incremental scans accumulate.
|
|
137
137
|
cursor.usage ??= new Map();
|
|
138
138
|
let fd;
|
|
@@ -160,6 +160,10 @@ export function scanTranscript(file, cursor, bugPatterns = []) {
|
|
|
160
160
|
} catch {
|
|
161
161
|
continue;
|
|
162
162
|
}
|
|
163
|
+
// The last real message: resuming a session appends bookkeeping lines, which have no timestamp.
|
|
164
|
+
if ((rec.type === 'user' || rec.type === 'assistant') && !rec.isMeta && rec.timestamp > (result.lastMessageAt ?? '')) {
|
|
165
|
+
result.lastMessageAt = rec.timestamp;
|
|
166
|
+
}
|
|
163
167
|
switch (rec.type) {
|
|
164
168
|
case 'pr-link':
|
|
165
169
|
if (rec.prUrl) result.prs.push({ ...prFromUrl(rec.prUrl, 'session'), number: rec.prNumber ?? null });
|