ai-agentmanager 1.0.0 โ†’ 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/public/app.js CHANGED
@@ -464,7 +464,7 @@ function resultsCard(a) {
464
464
  const [icon, label] = OUTCOME[r.outcome] ?? ['๐Ÿ”„', r.outcome];
465
465
  const item = (x, cls) => `<li class="${cls}">${x.url ? `<a href="${esc(x.url)}" target="_blank" rel="noopener">${md(x.text).replace(/^<p>|<\/p>\s*$/g, '')}</a>` : md(x.text).replace(/^<p>|<\/p>\s*$/g, '')}</li>`;
466
466
  return `<div class="results ${r.outcome.replace(' ', '-')}">
467
- <h4>Results <span class="outcome">${icon} ${label}</span>${r.fromAgent ? '' : ' <span class="hint">(from what Agent Manager can see; the agent adds its own when it next updates its brief)</span>'}</h4>
467
+ <h4>Results <span class="outcome">${icon} ${label}</span>${r.fromAgent ? '' : ' <span class="hint">(from what AI Agent Manager can see; the agent adds its own when it next updates its brief)</span>'}</h4>
468
468
  ${r.blockers.length ? `<ul class="blockers">${r.blockers.map((x) => item(x, 'blocker')).join('')}</ul>` : ''}
469
469
  ${r.delivered.length ? `<ul class="delivered">${r.delivered.map((x) => item(x, 'delivered')).join('')}</ul>` : '<p class="hint">Nothing delivered yet.</p>'}
470
470
  ${r.notes ? `<div class="md">${md(r.notes)}</div>` : ''}
@@ -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
- return `<li data-id="${a.id}" class="agent pr${a.priority ?? 2} ${a.id === selectedId ? 'selected' : ''}">
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
- // Highest priority first, then agents needing you, then running ones, then newest.
612
- g.agents.sort((a, b) => (a.priority ?? 2) - (b.priority ?? 2) || rank(a) - rank(b) || b.createdAt.localeCompare(a.createdAt));
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,13 +637,19 @@ 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) => special(a) - special(b) || a.top - b.top || b.waiting - a.waiting || b.running - a.running || a.label.localeCompare(b.label),
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
 
630
650
  function renderList() {
631
651
  const waiting = [...agents.values()].filter((a) => a.runtime.attention).length;
632
- document.title = waiting ? `(${waiting}) Agent Manager` : 'Agent Manager';
652
+ document.title = waiting ? `(${waiting}) AI Agent Manager` : 'AI Agent Manager';
633
653
  // "@cc" shows one project; any other text also matches shortcuts.
634
654
  const at = filterText.startsWith('@') ? filterText.slice(1) : null;
635
655
  const list = [...agents.values()].filter((a) =>
@@ -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)) {
@@ -1995,7 +2111,7 @@ function renderImport() {
1995
2111
  .join('')}</div>`,
1996
2112
  )
1997
2113
  .join('')
1998
- : `<p class="hint">${importSessions.length ? 'Nothing matches.' : 'No Claude sessions started in a terminal in this period (Agent Manager\'s own agents are left out).'}</p>`;
2114
+ : `<p class="hint">${importSessions.length ? 'Nothing matches.' : 'No Claude sessions started in a terminal in this period (AI Agent Manager\'s own agents are left out).'}</p>`;
1999
2115
  const picked = importSessions.filter((s) => importPicked.has(s.sessionId));
2000
2116
  const open = picked.filter((s) => s.inUse).length;
2001
2117
  Object.assign($('#import-go'), {
@@ -2562,8 +2678,8 @@ $('#detail-body').addEventListener('click', async (e) => {
2562
2678
  break;
2563
2679
  case 'delete': {
2564
2680
  const cancelNote = a.status === 'preparing' ? '\n\nThis also cancels the workspace setup (clone / checkout) in progress.' : '';
2565
- if (!confirm(`Delete agent "${a.name}"? Its session IDs will be forgotten by Agent Manager.${cancelNote}`)) return;
2566
- const del = a.worktree && confirm('Also remove the git worktree Agent Manager created for it?\n(Uncommitted changes there will be lost.)');
2681
+ if (!confirm(`Delete agent "${a.name}"? Its session IDs will be forgotten by AI Agent Manager.${cancelNote}`)) return;
2682
+ const del = a.worktree && confirm('Also remove the git worktree AI Agent Manager created for it?\n(Uncommitted changes there will be lost.)');
2567
2683
  act(async () => {
2568
2684
  const res = await api('DELETE', `/agents/${a.id}${del ? '?deleteWorkspace=1' : ''}`);
2569
2685
  if (res.warnings?.length) toast(res.warnings.join('; '), 6000);
@@ -2730,6 +2846,61 @@ $('#new-form').addEventListener('submit', async (e) => {
2730
2846
 
2731
2847
  // ------------------------------------------------------------------ ask (general session)
2732
2848
 
2849
+ // ---------------------------------------------------------------- about
2850
+ const aboutDialog = $('#about-dialog');
2851
+ let aboutInfo = null;
2852
+ async function loadAbout() {
2853
+ aboutInfo = await api('GET', '/about');
2854
+ $('#about-btn').textContent = `v${aboutInfo.version}`;
2855
+ return aboutInfo;
2856
+ }
2857
+ loadAbout().catch(() => {});
2858
+ function renderAbout(a) {
2859
+ const link = (url, text = url.replace(/^https:\/\//, '')) => `<a href="${esc(url)}" target="_blank" rel="noopener">${esc(text)}</a>`;
2860
+ const when = (iso) => new Date(iso).toLocaleString();
2861
+ const rows = [
2862
+ ['Version', `${esc(a.version)}${a.releaseDate ? ` <span class="hint">released ${esc(a.releaseDate)}</span>` : ''}`],
2863
+ ['Author', esc(a.author ?? 'โ€”')],
2864
+ ['License', esc(a.license ?? 'โ€”')],
2865
+ ['Install', `<code>npm install -g ${esc(a.package)}</code>, then <code>${esc(a.command ?? 'agentmgr')}</code>`],
2866
+ ['Code', a.links.repository ? link(a.links.repository) : 'โ€”'],
2867
+ ['Releases', a.links.releases ? link(a.links.releases, 'Release notes') : 'โ€”'],
2868
+ ['Report a problem', a.links.issues ? link(a.links.issues, 'GitHub issues') : 'โ€”'],
2869
+ null,
2870
+ ['Daemon', `pid ${esc(a.daemon.pid)} ยท port ${esc(a.daemon.port ?? 'โ€”')} ยท running since ${esc(when(a.daemon.startedAt))}`],
2871
+ ['Data folder', `<code>${esc(a.daemon.home ?? 'โ€”')}</code>`],
2872
+ ['Node.js', esc(a.daemon.node)],
2873
+ ['System', esc(a.daemon.platform)],
2874
+ ['Claude Code', `<code>${esc(a.daemon.claudeBin ?? 'claude')}</code>`],
2875
+ ];
2876
+ $('#about-list').innerHTML = rows.map((r) => (r ? `<dt>${r[0]}</dt><dd>${r[1]}</dd>` : '<div class="sep"></div>')).join('');
2877
+ }
2878
+ $('#about-btn').onclick = async () => {
2879
+ $('#about-update').hidden = true;
2880
+ if (aboutInfo) renderAbout(aboutInfo);
2881
+ else $('#about-list').innerHTML = '<dt>Loadingโ€ฆ</dt><dd></dd>';
2882
+ aboutDialog.showModal();
2883
+ try {
2884
+ renderAbout(await loadAbout()); // fresh daemon details (uptime, pid) each time it opens
2885
+ } catch (err) {
2886
+ $('#about-list').innerHTML = `<dt>Error</dt><dd>${esc(err.message)}</dd>`;
2887
+ }
2888
+ };
2889
+ $('#about-close').onclick = () => aboutDialog.close();
2890
+ $('#about-check').onclick = async () => {
2891
+ const out = $('#about-update');
2892
+ out.hidden = false;
2893
+ out.textContent = 'Checkingโ€ฆ';
2894
+ try {
2895
+ const r = await api('GET', '/about/latest');
2896
+ out.innerHTML = r.newer
2897
+ ? `Version ${esc(r.latest)} is available. Update with <code>npm install -g ${esc(aboutInfo?.package ?? 'ai-agentmanager')}@latest</code>, then <code>agentmgr restart</code>.`
2898
+ : `You have the latest version (${esc(r.current)}).`;
2899
+ } catch (err) {
2900
+ out.textContent = `Couldn't check: ${err.message}`;
2901
+ }
2902
+ };
2903
+
2733
2904
  const askDialog = $('#ask-dialog');
2734
2905
  $('#ask-btn').onclick = () => {
2735
2906
  $('#ask-error').hidden = true;
@@ -4,7 +4,7 @@
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <meta name="referrer" content="no-referrer" />
7
- <title>Agent Manager Hub</title>
7
+ <title>AI Agent Manager Hub</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
9
  <link rel="stylesheet" href="/vendor/xterm/css/xterm.css" />
10
10
  <link rel="stylesheet" href="/hub.css" />
@@ -13,7 +13,7 @@
13
13
  <header class="topbar">
14
14
  <div class="brand">
15
15
  <span class="logo" aria-hidden="true">โ—†</span>
16
- <h1>Agent Manager <span class="muted">Hub</span></h1>
16
+ <h1>AI Agent Manager <span class="muted">Hub</span></h1>
17
17
  <span id="hub-name" class="muted"></span>
18
18
  </div>
19
19
  <div class="top-actions" id="top-actions" hidden>
@@ -62,7 +62,7 @@
62
62
  <dialog id="invite-dialog">
63
63
  <form method="dialog" class="dialog">
64
64
  <h2>Add a machine</h2>
65
- <p class="muted">On the Mac to add (with Agent Manager installed), run:</p>
65
+ <p class="muted">On the Mac to add (with AI Agent Manager installed), run:</p>
66
66
  <div class="cmd"><code id="invite-cmd">โ€ฆ</code><button type="button" id="copy-btn" class="ghost">Copy</button></div>
67
67
  <p class="muted small" id="invite-expiry"></p>
68
68
  <p class="error small" id="invite-warning" hidden></p>
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
@@ -4,8 +4,11 @@
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
6
6
  <meta name="apple-mobile-web-app-capable" content="yes" />
7
- <title>Agent Manager</title>
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>
@@ -13,13 +16,14 @@
13
16
  <header class="topbar">
14
17
  <div class="brand">
15
18
  <span class="logo" aria-hidden="true">โ—†</span>
16
- <h1>Agent Manager</h1>
19
+ <h1>AI Agent Manager</h1>
20
+ <button id="about-btn" class="about-btn" type="button" title="About AI Agent Manager: version, release, links">About</button>
17
21
  <span id="conn" class="conn" title="Daemon connection">connectingโ€ฆ</span>
18
22
  </div>
19
23
  <div class="top-actions">
20
24
  <input id="filter" type="search" placeholder="Filter agents" aria-label="Filter agents" />
21
25
  <button id="alerts-btn" class="local-only" title="Slack alerts when an agent needs you">Alerts</button>
22
- <button id="remote-btn" class="local-only" title="Open Agent Manager from your phone">Remote access</button>
26
+ <button id="remote-btn" class="local-only" title="Open AI Agent Manager from your phone">Remote access</button>
23
27
  <button id="ask-btn" title="A general Claude session with no repo or folder">Ask Claude</button>
24
28
  <button id="new-btn" class="primary">+ New agents</button>
25
29
  </div>
@@ -43,10 +47,12 @@
43
47
  <p>Add one or more intents and a repo or folder to get started.</p>
44
48
  </div>
45
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>
46
51
 
47
52
  <section class="detail" id="detail">
48
53
  <div class="placeholder" id="placeholder">Select an agent to see its session, folder, repo, PRs and bugs.</div>
49
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>
50
56
  <div class="term-wrap" id="term-wrap" hidden>
51
57
  <div class="term-bar">
52
58
  <div class="term-tabs" role="tablist">
@@ -61,6 +67,8 @@
61
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>
62
68
  <input type="file" id="term-file" multiple hidden>
63
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>
64
72
  <button id="term-close" class="ghost small" title="Hide the terminal (the agent keeps running)">Hide terminal</button>
65
73
  </div>
66
74
  <div id="signin-bar" class="signin-bar" hidden></div>
@@ -79,12 +87,12 @@
79
87
  </form>
80
88
  <div id="browser-hint" class="browser-hint" hidden>Blank? Many sites refuse to show inside another page. Open it with โ†— instead.</div>
81
89
  <div id="browser-cert" class="browser-start" hidden>
82
- <p><b>One more step on this device.</b> Each browser tab goes through its own port, and this device hasn't accepted Agent Manager's certificate for this one yet.</p>
83
- <p>Open it, accept the warning the same way you did for Agent Manager (Safari: Show Details โ†’ visit this website), then come back here. You only do this once per site; it keeps its port.</p>
90
+ <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>
91
+ <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>
84
92
  <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>
85
93
  </div>
86
94
  <div id="browser-start" class="browser-start" hidden>
87
- <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: Agent Manager relays it over its own secure connection, for signed-in devices only.</p>
95
+ <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>
88
96
  <div id="browser-suggest"></div>
89
97
  </div>
90
98
  <div id="browser-frames" class="browser-frames"></div>
@@ -337,6 +345,17 @@
337
345
  </div>
338
346
  </dialog>
339
347
 
348
+ <dialog id="about-dialog" class="about-dialog">
349
+ <h2>AI Agent Manager</h2>
350
+ <p class="hint">Run and track background coding agents, one per intent.</p>
351
+ <dl id="about-list" class="about-list"></dl>
352
+ <p id="about-update" class="hint" hidden></p>
353
+ <div class="dialog-actions">
354
+ <button type="button" id="about-check" class="ghost" title="Asks the npm registry for the newest version">Check for updates</button>
355
+ <button type="button" id="about-close" class="primary">Close</button>
356
+ </div>
357
+ </dialog>
358
+
340
359
  <script type="module" src="/app.js"></script>
341
360
  </body>
342
361
  </html>
@@ -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/share.js CHANGED
@@ -9,7 +9,7 @@ $('#print').onclick = () => window.print();
9
9
 
10
10
  async function main() {
11
11
  const res = await fetch(`/api/agents/${encodeURIComponent(agentId)}`, { credentials: 'same-origin' });
12
- if (!res.ok) throw new Error(res.status === 401 ? 'Sign in to Agent Manager first, then reopen this page.' : `HTTP ${res.status}`);
12
+ if (!res.ok) throw new Error(res.status === 401 ? 'Sign in to AI Agent Manager first, then reopen this page.' : `HTTP ${res.status}`);
13
13
  const a = await res.json();
14
14
  const d = a.docs?.[kind];
15
15
  if (!d?.markdown) throw new Error('This document has not been written yet.');
package/public/style.css CHANGED
@@ -56,6 +56,15 @@ a:hover { text-decoration: underline; }
56
56
  .brand h1 { font-size: 16px; margin: 0; font-weight: 600; }
57
57
  .logo { color: var(--accent); }
58
58
  .conn { font-size: 12px; color: var(--muted); }
59
+ .about-btn { font-size: 12px; padding: 2px 8px; color: var(--muted); background: transparent; border: 1px solid var(--border); border-radius: 999px; font-variant-numeric: tabular-nums; }
60
+ .about-btn:hover, .about-btn:focus-visible { color: var(--text); border-color: var(--accent); }
61
+ .about-dialog { width: min(560px, calc(100vw - 32px)); padding: 20px; }
62
+ .about-dialog h2 { margin: 0 0 2px; font-size: 17px; }
63
+ .about-dialog .hint { margin: 0; }
64
+ .about-list { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 6px 16px; margin: 14px 0; font-size: 13px; }
65
+ .about-list dt { color: var(--muted); }
66
+ .about-list dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
67
+ .about-list .sep { grid-column: 1 / -1; border-top: 1px solid var(--border); margin: 4px 0; }
59
68
  .conn.ok::before { content: 'โ— '; color: var(--green); }
60
69
  .conn.bad::before { content: 'โ— '; color: var(--red); }
61
70
  .top-actions { display: flex; gap: 8px; }
@@ -79,7 +88,27 @@ input, select, textarea {
79
88
  textarea { resize: vertical; font-family: var(--mono); font-size: 13px; }
80
89
 
81
90
  /* ---------- layout */
82
- .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; }
83
112
  .sidebar { border-right: 1px solid var(--border); overflow-y: auto; background: var(--panel); }
84
113
  .side-tabs { display: flex; border-bottom: 1px solid var(--border); position: sticky; top: 0; background: var(--panel); z-index: 2; }
85
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; }
@@ -163,7 +192,8 @@ textarea { resize: vertical; font-family: var(--mono); font-size: 13px; }
163
192
  .detail { display: flex; flex-direction: column; min-height: 0; min-width: 0; }
164
193
  .placeholder { margin: auto; color: var(--muted); padding: 24px; text-align: center; }
165
194
  #detail-body { padding: 16px 20px; overflow-y: auto; flex: 0 1 auto; max-height: 100%; }
166
- .detail.with-term #detail-body { max-height: 42%; border-bottom: 1px solid var(--border); }
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; }
167
197
  .d-head { display: flex; align-items: flex-start; gap: 12px; flex-wrap: wrap; }
168
198
  .d-title { flex: 1 1 420px; min-width: 0; }
169
199
  .d-title h2 { margin: 0 0 4px; font-size: 18px; display: flex; gap: 6px 10px; align-items: center; flex-wrap: wrap; }
@@ -390,6 +420,7 @@ dialog::backdrop { background: rgba(0, 0, 0, .45); }
390
420
  .term-bar { flex-wrap: wrap; row-gap: 6px; }
391
421
  .term-bar > .term-tabs { flex: 1 0 100%; }
392
422
  .layout { grid-template-columns: 1fr; grid-template-rows: auto 1fr; }
423
+ .splitter, .desk-only { display: none !important; }
393
424
  .sidebar { max-height: 38vh; border-right: 0; border-bottom: 1px solid var(--border); }
394
425
  .top-actions input { width: 130px; }
395
426
  .row { grid-template-columns: 1fr; }
@@ -539,3 +570,6 @@ pre.diff .meta { color: var(--muted); font-weight: 600; }
539
570
  #import-dialog .import-foot label { display: flex; flex-direction: row; align-items: center; gap: 6px; }
540
571
  #import-dialog .import-foot input { width: auto; }
541
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/about.js ADDED
@@ -0,0 +1,70 @@
1
+ // What the About box and "agentmgr about" show: the release, who makes it, where to get help,
2
+ // and how this daemon runs. Nothing here leaves the machine; the update check is a separate,
3
+ // user-triggered request to the npm registry.
4
+ import fs from 'node:fs';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
10
+ const read = (f) => {
11
+ try {
12
+ return fs.readFileSync(path.join(root, f), 'utf8');
13
+ } catch {
14
+ return '';
15
+ }
16
+ };
17
+ const pkg = JSON.parse(read('package.json'));
18
+ const startedAt = new Date().toISOString();
19
+
20
+ /** The date in a changelog heading like "## 1.0.0 (2026-10-25)", or null. */
21
+ export function releaseDate(changelog, version) {
22
+ const esc = version.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
23
+ return changelog.match(new RegExp(`^##\\s+v?${esc}\\s+\\((\\d{4}-\\d{2}-\\d{2})\\)`, 'm'))?.[1] ?? null;
24
+ }
25
+
26
+ const authorName = (a) => (typeof a === 'string' ? a.replace(/\s*[<(].*$/, '') : a?.name) || null;
27
+ const repoUrl = (r) => String((typeof r === 'string' ? r : r?.url) ?? '').replace(/^git\+/, '').replace(/\.git$/, '') || null;
28
+
29
+ export function about({ port, config = {} } = {}) {
30
+ return {
31
+ product: 'AI Agent Manager',
32
+ package: pkg.name,
33
+ command: Object.keys(pkg.bin ?? {})[0] ?? null,
34
+ version: pkg.version,
35
+ releaseDate: releaseDate(read('CHANGELOG.md'), pkg.version),
36
+ author: authorName(pkg.author),
37
+ license: pkg.license,
38
+ links: {
39
+ repository: repoUrl(pkg.repository),
40
+ issues: pkg.bugs?.url ?? null,
41
+ releases: repoUrl(pkg.repository) ? `${repoUrl(pkg.repository)}/releases` : null,
42
+ npm: `https://www.npmjs.com/package/${pkg.name}`,
43
+ },
44
+ daemon: {
45
+ pid: process.pid,
46
+ port: port ?? null,
47
+ home: config.home ?? null,
48
+ startedAt,
49
+ node: process.version,
50
+ platform: `${os.type()} ${os.release()} (${process.arch})`,
51
+ claudeBin: config.claudeBin ?? null,
52
+ },
53
+ };
54
+ }
55
+
56
+ /** The newest version on npm. Only called when someone clicks "Check for updates". */
57
+ export async function latestRelease({ fetchImpl = fetch, timeoutMs = 5000 } = {}) {
58
+ const res = await fetchImpl(`https://registry.npmjs.org/${encodeURIComponent(pkg.name)}/latest`, { signal: AbortSignal.timeout(timeoutMs) });
59
+ if (!res.ok) throw Object.assign(new Error(`npm answered ${res.status}`), { status: 502 });
60
+ const { version } = await res.json();
61
+ return { current: pkg.version, latest: version, newer: newerThan(version, pkg.version) };
62
+ }
63
+
64
+ /** True when semver `a` is newer than `b` (pre-releases count as older than the release). */
65
+ export function newerThan(a, b) {
66
+ const parse = (v) => String(v).split('-')[0].split('.').map(Number);
67
+ const [x, y] = [parse(a), parse(b)];
68
+ for (let i = 0; i < 3; i++) if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) > (y[i] ?? 0);
69
+ return String(b).includes('-') && !String(a).includes('-');
70
+ }
package/src/brief.js CHANGED
@@ -15,7 +15,7 @@ export function briefPath(agentId) {
15
15
  }
16
16
 
17
17
  export const briefPrompt = (file) =>
18
- `Keep a short, living brief of this task in ${file} (Markdown). Agent Manager shows it to the user ` +
18
+ `Keep a short, living brief of this task in ${file} (Markdown). AI Agent Manager shows it to the user ` +
19
19
  'next to your terminal. Start it with one status line, then use exactly these four sections:\n' +
20
20
  'Status: planning | working | blocked | done\n' +
21
21
  'Set "done" only when the task is complete and its checks pass; set "blocked" when you need the user.\n' +
package/src/cli.js CHANGED
@@ -4,6 +4,7 @@ import { Command, Option } from 'commander';
4
4
  import WebSocket from 'ws';
5
5
  import { HOST, PORT } from './config.js';
6
6
  import { api, BASE, ensureDaemon, isRunning } from './client.js';
7
+ import { about, latestRelease } from './about.js';
7
8
 
8
9
  const tty = process.stdout.isTTY;
9
10
  const c = (code) => (s) => (tty ? `\x1b[${code}m${s}\x1b[0m` : String(s));
@@ -855,7 +856,47 @@ export async function main(argv) {
855
856
  console.log(j.enabled ? `AI judge ${green('on')} ${j.calls} of ${j.cap} calls today (on your own engine plan)` : `AI judge ${dim('off')} (fixed rules only; turn on: agentmgr judge on)`);
856
857
  });
857
858
 
858
- const remoteCmd = program.command('remote').description('Open Agent Manager from a phone or another computer on the same network');
859
+ program
860
+ .command('about')
861
+ .description('Version, release date, author, license and links; the running daemon, if any')
862
+ .option('--check', 'also ask the npm registry for the newest version')
863
+ .action(async (opts) => {
864
+ // An older daemon has no /about; this install's own details still apply.
865
+ const a = (await isRunning()) ? await api('GET', '/about').catch(() => about()) : about();
866
+ const row = (k, v) => console.log(`${dim(k.padEnd(13))} ${v}`);
867
+ console.log(`${a.product} ${green(a.version)}${a.releaseDate ? dim(` released ${a.releaseDate}`) : ''}`);
868
+ row('Author', a.author ?? 'โ€”');
869
+ row('License', a.license ?? 'โ€”');
870
+ row('Package', `${a.package} (npm install -g ${a.package})`);
871
+ row('Code', a.links.repository ?? 'โ€”');
872
+ row('Issues', a.links.issues ?? 'โ€”');
873
+ if (a.daemon.port) row('Daemon', `pid ${a.daemon.pid}, port ${a.daemon.port}, since ${new Date(a.daemon.startedAt).toLocaleString()}, data in ${a.daemon.home}`);
874
+ else row('Daemon', dim('not running (start: agentmgr ui)'));
875
+ row('Node.js', `${a.daemon.node} on ${a.daemon.platform}`);
876
+ if (opts.check) {
877
+ const r = await latestRelease();
878
+ row('Latest', r.newer ? yellow(`${r.latest} is available: npm install -g ${a.package}@latest`) : `${r.latest} (up to date)`);
879
+ }
880
+ });
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
+
899
+ const remoteCmd = program.command('remote').description('Open AI Agent Manager from a phone or another computer on the same network');
859
900
  const printRemote = async (st, { qr = true } = {}) => {
860
901
  if (!st.enabled || !st.running) return console.log(`remote access ${dim('off')} (turn on: agentmgr remote on)`);
861
902
  console.log(`remote access ${green('on')} HTTPS port ${st.port}`);
package/src/conflicts.js CHANGED
@@ -95,11 +95,11 @@ export function conflictMessage(self, group) {
95
95
  const what = files ? `is changing the same files as you in this repository: ${files}.` : `is working on related code in this repository.`;
96
96
  const order =
97
97
  group.mergeFirst === self.id
98
- ? `Agent Manager suggests your change lands first; ${group.agent.name} will rebase onto it.`
99
- : `Agent Manager suggests ${group.agent.name}'s change lands first: when it does, rebase onto it and resolve the overlap.`;
98
+ ? `AI Agent Manager suggests your change lands first; ${group.agent.name} will rebase onto it.`
99
+ : `AI Agent Manager suggests ${group.agent.name}'s change lands first: when it does, rebase onto it and resolve the overlap.`;
100
100
  const ai = group.ai?.reason ? ` Why this matters: ${group.ai.reason}${group.ai.advice ? ` ${group.ai.advice}` : ''}` : '';
101
101
  return (
102
- `Heads-up from Agent Manager: another agent, ${group.agent.name}, ${what} ${order}${ai} ` +
102
+ `Heads-up from AI Agent Manager: another agent, ${group.agent.name}, ${what} ${order}${ai} ` +
103
103
  `Keep your edits in those files focused, avoid unrelated reformatting there, and mention the overlap in your PR description.`
104
104
  );
105
105
  }