ai-agentmanager 1.1.0 → 1.3.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 +25 -0
- package/README.md +131 -3
- package/docs/plugins.md +4 -1
- package/package.json +1 -1
- package/public/app.js +259 -15
- package/public/index.html +198 -0
- package/public/land.js +149 -0
- package/public/playbooks.js +132 -0
- package/public/review.js +368 -0
- package/public/setup.js +67 -0
- package/public/spend.js +82 -0
- package/public/style.css +116 -1
- package/public/triggers.js +85 -0
- package/src/assistant.js +279 -0
- package/src/budgets.js +166 -0
- package/src/cleanup.js +1 -0
- package/src/cli.js +389 -1
- package/src/client.js +2 -1
- package/src/engines/claude.js +6 -4
- package/src/git.js +4 -4
- package/src/landing.js +212 -0
- package/src/manager.js +168 -78
- package/src/memory.js +106 -0
- package/src/notify.js +3 -1
- package/src/playbooks.js +178 -0
- package/src/review.js +209 -0
- package/src/server.js +108 -5
- package/src/stats.js +107 -0
- package/src/tracker.js +61 -8
- package/src/triggers.js +258 -0
package/public/app.js
CHANGED
|
@@ -4,6 +4,12 @@ import { marked } from '/vendor/marked/marked.esm.js';
|
|
|
4
4
|
import DOMPurify from '/vendor/dompurify/purify.es.mjs';
|
|
5
5
|
import { touchScroll } from '/touchscroll.js';
|
|
6
6
|
import { openStream, post, REMOTE } from '/transport.js';
|
|
7
|
+
import { initReview } from '/review.js';
|
|
8
|
+
import { initLand, prChips } from '/land.js';
|
|
9
|
+
import { initTriggers } from '/triggers.js';
|
|
10
|
+
import { initSpend } from '/spend.js';
|
|
11
|
+
import { initPlaybooks } from '/playbooks.js';
|
|
12
|
+
import { initSetup } from '/setup.js';
|
|
7
13
|
|
|
8
14
|
const $ = (sel) => document.querySelector(sel);
|
|
9
15
|
const agents = new Map();
|
|
@@ -93,6 +99,19 @@ async function act(fn, okMsg) {
|
|
|
93
99
|
}
|
|
94
100
|
}
|
|
95
101
|
|
|
102
|
+
const reviewUi = initReview({ api, esc, toast, agents, $ });
|
|
103
|
+
const triggersUi = initTriggers({ api, esc, toast, $ });
|
|
104
|
+
initSpend({ api, esc, toast, $ });
|
|
105
|
+
initSetup({ api, esc, $ });
|
|
106
|
+
const playbooksUi = initPlaybooks({ api, esc, toast, $, select: (id) => (showTab('agents'), select(id)) });
|
|
107
|
+
$('#new-playbook').onclick = () => {
|
|
108
|
+
const f = $('#new-form');
|
|
109
|
+
$('#new-dialog').close();
|
|
110
|
+
playbooksUi.open({ intent: f.intents.value.trim(), projects: f.project.value.trim() ? [f.project.value.trim()] : [], model: f.model.value.trim(), permissionMode: f.permissionMode.value });
|
|
111
|
+
};
|
|
112
|
+
$('#triggers-open').onclick = () => triggersUi.open();
|
|
113
|
+
const landUi = initLand({ api, esc, toast, agents, $, select: (id) => (showTab('agents'), select(id)), openReview: (id) => reviewUi.open(id), isOpen: () => sideTab === 'land' });
|
|
114
|
+
|
|
96
115
|
function statusOf(a) {
|
|
97
116
|
if (a.runtime.planning) return a.runtime.attention ? 'attention' : 'planning';
|
|
98
117
|
if (a.status === 'running') {
|
|
@@ -200,7 +219,7 @@ function explainBox(a) {
|
|
|
200
219
|
const REVIEW_VERDICT = { complete: '✓ Complete', partial: '≈ Partly done', not_done: '✗ Not done' };
|
|
201
220
|
/** The AI judge's review of the work against the intent and brief: button, and the last result once handled. */
|
|
202
221
|
function reviewBox(a) {
|
|
203
|
-
if (a
|
|
222
|
+
if (noRepo(a)) return { button: '', box: '' };
|
|
204
223
|
const r = a.aiReview;
|
|
205
224
|
const button = `<button class="small" data-act="review" ${r?.pending ? 'disabled' : ''}
|
|
206
225
|
title="Check the work against the intent and brief: what is met, missing or risky. A separate one-off model call; runs by itself when the agent says it is done">${r?.pending ? 'Reviewing…' : 'Review with AI'}</button>`;
|
|
@@ -429,7 +448,7 @@ function openPlan(agentId) {
|
|
|
429
448
|
const PLAN_LABEL = { writing: 'being written', review: 'waiting for your review', changes: 'changes requested', approved: 'approved' };
|
|
430
449
|
/** The plan-review row in the brief card: status, open, approve; or the switch to turn review on. */
|
|
431
450
|
function planRow(a) {
|
|
432
|
-
if (a
|
|
451
|
+
if (noRepo(a)) return '';
|
|
433
452
|
const p = a.planReview;
|
|
434
453
|
if (!p) {
|
|
435
454
|
return a.status === 'done' ? '' : `<div class="plan-row"><button class="small ghost" data-act="plan-on" title="The agent writes an implementation plan and waits for your approval before changing anything">Review its plan first</button></div>`;
|
|
@@ -489,7 +508,7 @@ function resultsCard(a) {
|
|
|
489
508
|
}
|
|
490
509
|
|
|
491
510
|
function briefPanel(a) {
|
|
492
|
-
if (a
|
|
511
|
+
if (noRepo(a)) return '';
|
|
493
512
|
const b = a.brief;
|
|
494
513
|
const ex = explainBox(a);
|
|
495
514
|
const rv = reviewBox(a);
|
|
@@ -598,12 +617,14 @@ function saveCollapsed() {
|
|
|
598
617
|
const prioChip = (p, { always = false } = {}) =>
|
|
599
618
|
p === 2 && !always ? '' : `<span class="prio p${p}" title="Priority P${p}">P${p}</span>`;
|
|
600
619
|
|
|
620
|
+
const noRepo = (a) => a.kind === 'general' || a.kind === 'assistant';
|
|
621
|
+
|
|
601
622
|
function agentItem(a) {
|
|
602
623
|
const st = statusOf(a);
|
|
603
624
|
const idle = !isActive(a);
|
|
604
625
|
return `<li data-id="${a.id}" class="agent pr${a.priority ?? 2} ${a.id === selectedId ? 'selected' : ''} ${idle ? 'idle' : ''}">
|
|
605
626
|
<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>
|
|
606
|
-
<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>
|
|
627
|
+
<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>
|
|
607
628
|
${a.status === 'preparing' && a.progress ? `<div class="progress-line">${esc(progressText(a.progress))}</div>` : ''}
|
|
608
629
|
<div class="meta">
|
|
609
630
|
<span class="mono">${a.id}</span>
|
|
@@ -652,7 +673,7 @@ function projectGroups(list) {
|
|
|
652
673
|
const same = byName ? byName.get(g.name).length : out.filter((x) => x.name === g.name).length;
|
|
653
674
|
g.label = same > 1 && g.full ? g.full : g.name;
|
|
654
675
|
}
|
|
655
|
-
const special = (g) => (g.key === 'general' ? 1 : g.key === 'other' ? 2 : 0);
|
|
676
|
+
const special = (g) => (g.key === 'assistant' ? -1 : g.key === 'general' ? 1 : g.key === 'other' ? 2 : 0);
|
|
656
677
|
return out.sort(
|
|
657
678
|
(a, b) =>
|
|
658
679
|
special(a) - special(b) ||
|
|
@@ -757,6 +778,7 @@ function renderBacklog() {
|
|
|
757
778
|
<span class="spacer"></span>
|
|
758
779
|
<button class="link" data-bl="toggle-run" title="${backlogState.autoRun ? 'Stop starting new items (running ones continue)' : 'Start picking up queued items again'}">${backlogState.autoRun ? '⏸ Pause' : '▶ Resume'}</button>
|
|
759
780
|
<button class="link" data-bl="add">+ Add</button>
|
|
781
|
+
<button class="link" data-bl="playbooks" title="Saved requests you run again, by hand or on a schedule">Playbooks</button>
|
|
760
782
|
</div>
|
|
761
783
|
${!backlogState.autoRun ? '<p class="hint bl-paused">Paused: queued items wait until you resume.</p>' : ''}
|
|
762
784
|
${items.length
|
|
@@ -805,7 +827,9 @@ function showTab(tab) {
|
|
|
805
827
|
$('#agents-view').hidden = tab !== 'agents';
|
|
806
828
|
$('#backlog-view').hidden = tab !== 'backlog';
|
|
807
829
|
$('#inbox-view').hidden = tab !== 'inbox';
|
|
830
|
+
$('#land-view').hidden = tab !== 'land';
|
|
808
831
|
if (tab === 'inbox') loadJudge();
|
|
832
|
+
if (tab === 'land') landUi.load();
|
|
809
833
|
renderBacklog();
|
|
810
834
|
renderInbox();
|
|
811
835
|
}
|
|
@@ -828,6 +852,8 @@ $('#backlog-view').addEventListener('click', async (e) => {
|
|
|
828
852
|
return blApi('POST', '/backlog/settings', { autoRun: !backlogState.autoRun });
|
|
829
853
|
case 'add':
|
|
830
854
|
return openNewDialog('backlog');
|
|
855
|
+
case 'playbooks':
|
|
856
|
+
return playbooksUi.open();
|
|
831
857
|
case 'limit': {
|
|
832
858
|
const n = prompt(`How many "${el.dataset.name}" items should agents work on at once?`, el.textContent.split(' ')[0]);
|
|
833
859
|
if (n === null) return;
|
|
@@ -1119,7 +1145,7 @@ for (const root of [$('#inbox-view'), $('#detail-body')]) {
|
|
|
1119
1145
|
// ------------------------------------------------------------------ checkpoints and forks
|
|
1120
1146
|
|
|
1121
1147
|
function checkpointsCard(a) {
|
|
1122
|
-
if (a
|
|
1148
|
+
if (noRepo(a) || !a.git?.branch) return '';
|
|
1123
1149
|
const cps = [...(a.checkpoints ?? [])].reverse();
|
|
1124
1150
|
const forks = [...agents.values()].filter((x) => x.forkOf?.agentId === a.id);
|
|
1125
1151
|
return `<div class="card">
|
|
@@ -1506,6 +1532,7 @@ function prRow(a, p) {
|
|
|
1506
1532
|
<a class="grow" href="${esc(p.url)}" target="_blank" rel="noopener" title="${esc(p.url)}">${label} ${esc(p.title ?? p.repo ?? p.url)}</a>
|
|
1507
1533
|
<span class="tag" title="How this was found">${esc(p.source)}</span>
|
|
1508
1534
|
<button class="link" data-unlink="pr" data-key="${esc(p.url)}" title="Remove">✕</button>
|
|
1535
|
+
${p.state === 'OPEN' && (p.checks || p.review || p.mergeState) ? `<div class="pr-chips">${prChips(p, esc)}</div>` : ''}
|
|
1509
1536
|
</li>`;
|
|
1510
1537
|
}
|
|
1511
1538
|
|
|
@@ -1545,7 +1572,7 @@ function renderDetail() {
|
|
|
1545
1572
|
${[0, 1, 2, 3].map((p) => `<option value="${p}" ${p === (a.priority ?? 2) ? 'selected' : ''}>P${p}</option>`).join('')}
|
|
1546
1573
|
</select>
|
|
1547
1574
|
<span class="d-name">${esc(a.name)}</span> <span class="pill ${st}">${pillText(st)}</span>${a.kind === 'general' ? ' <span class="tag">general</span>' : ''}</h2>
|
|
1548
|
-
<p class="intent">${esc(a.intent || (a.kind === 'general' ? 'General session: ask Claude anything in the terminal.' : ''))}</p>
|
|
1575
|
+
<p class="intent">${esc(a.intent || (a.kind === 'general' ? 'General session: ask Claude anything in the terminal.' : a.kind === 'assistant' ? 'Your assistant: it checks for new work every few minutes and starts agents for it. Settings and activity: Assistant in the top bar.' : ''))}</p>
|
|
1549
1576
|
</div>
|
|
1550
1577
|
<div class="d-actions">
|
|
1551
1578
|
${a.status === 'error' && !a.workdir
|
|
@@ -1561,7 +1588,9 @@ function renderDetail() {
|
|
|
1561
1588
|
: `<button data-act="start" ${a.status === 'preparing' ? 'disabled' : ''}>Resume in background</button>`}
|
|
1562
1589
|
<button data-act="fresh" ${running || a.status === 'preparing' ? 'disabled' : ''} title="Start over with a new session ID (old ones are kept)">New session</button>
|
|
1563
1590
|
<button data-act="shell" ${a.workdir && a.status !== 'preparing' ? '' : 'disabled'} title="A separate terminal in this agent's folder for running commands by hand">Open shell</button>
|
|
1591
|
+
${noRepo(a) ? '' : `<button data-act="changes" ${a.workdir && a.status !== 'preparing' ? '' : 'disabled'} title="Read its changes, comment on lines for the agent, commit and open a pull request">Review changes${a.git?.dirtyFiles ? ` <span class="count">${a.git.dirtyFiles}</span>` : ''}</button>`}
|
|
1564
1592
|
<button data-act="fork" ${a.workdir && a.git?.branch ? '' : 'disabled'} title="A new agent with this one's code and conversation, in its own worktree, to try another approach">Fork</button>
|
|
1593
|
+
<button data-act="edit" title="Change its name, request, project, priority, permission mode or model">Edit</button>
|
|
1565
1594
|
<button data-act="refresh" title="Re-scan git, PRs and transcripts">Refresh</button>
|
|
1566
1595
|
<button data-act="keep" class="${a.keep ? 'on' : ''}" title="${a.keep ? 'Kept: clean up never closes or deletes it. Click to let clean up handle it again' : 'Clean up never closes or deletes an agent marked Keep'}">${a.keep ? '📌 Kept' : 'Keep'}</button>
|
|
1567
1596
|
<button class="danger" data-act="delete">Delete</button>
|
|
@@ -1617,7 +1646,7 @@ function renderDetail() {
|
|
|
1617
1646
|
<br>${fmtTokens(u.total)} total incl. cache · ${u.requests} requests</div>`
|
|
1618
1647
|
: ''}`
|
|
1619
1648
|
: '—'}</dd>
|
|
1620
|
-
<dt>Cost</dt><dd>${totalCost ? `$${totalCost.toFixed(2)} <span class="hint">(reported by Claude Code)</span>` : '—'}</dd>
|
|
1649
|
+
<dt>Cost</dt><dd>${totalCost ? `$${totalCost.toFixed(2)}${a.budgetUsd ? ` of its $${a.budgetUsd.toFixed(2)} budget` : ''} <span class="hint">(reported by Claude Code)</span>` : a.budgetUsd ? `— <span class="hint">budget $${a.budgetUsd.toFixed(2)}</span>` : '—'}${a.budgetHit ? ' <span class="tag fail" title="Its session was closed at its budget; raise it in Edit to continue">budget reached</span>' : ''}</dd>
|
|
1621
1650
|
<dt>Mode</dt><dd>${esc(a.permissionMode)}${a.model ? ` · ${esc(a.model)}` : ''}</dd>
|
|
1622
1651
|
</dl>
|
|
1623
1652
|
</div>
|
|
@@ -1626,12 +1655,13 @@ function renderDetail() {
|
|
|
1626
1655
|
<h3>Workspace</h3>
|
|
1627
1656
|
<dl class="kv">
|
|
1628
1657
|
<dt>Folder</dt><dd><code>${esc(a.workdir ?? (a.status === 'preparing' ? 'preparing…' : '—'))}</code>${a.workdir ? copyBtn(a.workdir) : ''}</dd>
|
|
1629
|
-
<dt>Project</dt><dd>${esc(a.projectInfo?.full || a.projectInfo?.name || '—')} <button class="link" data-act="project" title="
|
|
1658
|
+
<dt>Project</dt><dd>${esc(a.projectInfo?.full || a.projectInfo?.name || '—')} ${noRepo(a) ? '' : '<button class="link" data-act="project" title="Move this agent under another project">✎ Move</button>'}</dd>
|
|
1630
1659
|
<dt>Repo</dt><dd>${a.repo ? `<code>${esc(a.repo)}</code>` : '—'}</dd>
|
|
1631
1660
|
<dt>Branch</dt><dd>${a.git?.branch ? `<code>${esc(a.git.branch)}</code>` : '—'}${a.git?.dirtyFiles ? ` <span class="tag">${a.git.dirtyFiles} changed</span>` : ''}</dd>
|
|
1632
1661
|
<dt>Head</dt><dd>${a.git?.head ? `<code>${esc(a.git.head.sha)}</code> ${esc(a.git.head.subject)}` : '—'}</dd>
|
|
1633
1662
|
${a.baseRef ? `<dt>Branched from</dt><dd><code>${esc(a.baseRef)}</code></dd>` : ''}
|
|
1634
1663
|
${a.worktree ? `<dt>Worktree of</dt><dd><code>${esc(a.sourceDir)}</code></dd>` : ''}
|
|
1664
|
+
${noRepo(a) ? '' : `<dt>Repo memory</dt><dd><button class="link" data-act="memory" title="What agents learned working in this repo; every agent here gets it">View and edit</button></dd>`}
|
|
1635
1665
|
</dl>
|
|
1636
1666
|
</div>
|
|
1637
1667
|
|
|
@@ -1696,6 +1726,7 @@ function renderDetail() {
|
|
|
1696
1726
|
function render() {
|
|
1697
1727
|
renderList();
|
|
1698
1728
|
renderBacklog();
|
|
1729
|
+
landUi.render();
|
|
1699
1730
|
renderInbox();
|
|
1700
1731
|
renderRace();
|
|
1701
1732
|
renderTeam();
|
|
@@ -2875,12 +2906,10 @@ $('#detail-body').addEventListener('click', async (e) => {
|
|
|
2875
2906
|
term.open(a.id);
|
|
2876
2907
|
}, 'Moved into the repository');
|
|
2877
2908
|
break;
|
|
2878
|
-
case 'project':
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
break;
|
|
2883
|
-
}
|
|
2909
|
+
case 'project':
|
|
2910
|
+
return openEdit(a, 'project');
|
|
2911
|
+
case 'edit':
|
|
2912
|
+
return openEdit(a);
|
|
2884
2913
|
case 'plan-open':
|
|
2885
2914
|
return openPlan(a.id);
|
|
2886
2915
|
case 'plan-on':
|
|
@@ -2905,6 +2934,10 @@ $('#detail-body').addEventListener('click', async (e) => {
|
|
|
2905
2934
|
case 'shell':
|
|
2906
2935
|
term.open(a.id, 'shell:1');
|
|
2907
2936
|
break;
|
|
2937
|
+
case 'changes':
|
|
2938
|
+
return reviewUi.open(a.id);
|
|
2939
|
+
case 'memory':
|
|
2940
|
+
return openMemory(a);
|
|
2908
2941
|
case 'fork':
|
|
2909
2942
|
forkAgent(a.id);
|
|
2910
2943
|
break;
|
|
@@ -3503,6 +3536,217 @@ $('#cleanup-plan').addEventListener('click', (e) => {
|
|
|
3503
3536
|
select(go.dataset.cleanupGoto);
|
|
3504
3537
|
});
|
|
3505
3538
|
|
|
3539
|
+
// ------------------------------------------------------------------ edit an agent
|
|
3540
|
+
|
|
3541
|
+
let editing = null;
|
|
3542
|
+
let enginesCache = null;
|
|
3543
|
+
// The project picked in the dialog: "" while it is listed under its own repo or folder.
|
|
3544
|
+
const editProject = (a) => (a.projectMove || a.project ? (a.projectInfo?.shortcut ?? '') : '');
|
|
3545
|
+
|
|
3546
|
+
let memoryAgent = null;
|
|
3547
|
+
async function openMemory(a) {
|
|
3548
|
+
memoryAgent = a.id;
|
|
3549
|
+
const f = $('#memory-form');
|
|
3550
|
+
$('#memory-error').hidden = true;
|
|
3551
|
+
try {
|
|
3552
|
+
const m = await api('GET', `/agents/${a.id}/memory`);
|
|
3553
|
+
$('#memory-repo').textContent = a.projectInfo?.full || a.projectInfo?.name || a.repo || 'this repo';
|
|
3554
|
+
f.text.value = m.text;
|
|
3555
|
+
$('#memory-meta').textContent = `${m.entries} learning${m.entries === 1 ? '' : 's'} · ${(m.bytes / 1000).toFixed(1)} of ${m.max / 1000} KB`;
|
|
3556
|
+
$('#memory-dialog').showModal();
|
|
3557
|
+
} catch (err) {
|
|
3558
|
+
toast(`Error: ${err.message}`, 5000);
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3561
|
+
$('#memory-form').addEventListener('submit', async (e) => {
|
|
3562
|
+
e.preventDefault();
|
|
3563
|
+
try {
|
|
3564
|
+
await api('PUT', `/agents/${memoryAgent}/memory`, { text: $('#memory-form').text.value });
|
|
3565
|
+
$('#memory-dialog').close();
|
|
3566
|
+
toast('Saved; agents get it from their next start');
|
|
3567
|
+
} catch (err) {
|
|
3568
|
+
$('#memory-error').hidden = false;
|
|
3569
|
+
$('#memory-error').textContent = err.message;
|
|
3570
|
+
}
|
|
3571
|
+
});
|
|
3572
|
+
$('#memory-close').onclick = () => $('#memory-dialog').close();
|
|
3573
|
+
|
|
3574
|
+
async function openEdit(a, focus) {
|
|
3575
|
+
editing = a;
|
|
3576
|
+
const f = $('#edit-form');
|
|
3577
|
+
f.name.value = a.name;
|
|
3578
|
+
f.intent.value = a.intent ?? '';
|
|
3579
|
+
f.tell.checked = false;
|
|
3580
|
+
f.priority.value = String(a.priority ?? 2);
|
|
3581
|
+
f.model.value = a.model ?? '';
|
|
3582
|
+
f.budgetUsd.value = a.budgetUsd ?? '';
|
|
3583
|
+
$('#edit-project-row').hidden = noRepo(a);
|
|
3584
|
+
$('#edit-tell-row').hidden = !a.intent;
|
|
3585
|
+
// Its own repo or folder, every known project, or a new name.
|
|
3586
|
+
f.project.innerHTML = [
|
|
3587
|
+
'<option value="">Its own repo or folder</option>',
|
|
3588
|
+
...projectsState.map((p) => `<option value="${esc(p.shortcut)}">${esc(p.name)} (@${esc(p.shortcut)})</option>`),
|
|
3589
|
+
'<option value="__new">New project name…</option>',
|
|
3590
|
+
].join('');
|
|
3591
|
+
f.project.value = editProject(a);
|
|
3592
|
+
$('#edit-newproject-row').hidden = true;
|
|
3593
|
+
f.newProject.value = '';
|
|
3594
|
+
enginesCache ??= await api('GET', '/engines').catch(() => []);
|
|
3595
|
+
const modes = enginesCache.find((e) => e.id === (a.engine ?? 'claude'))?.permissionModes ?? [];
|
|
3596
|
+
f.permissionMode.innerHTML = ['<option value="">As configured</option>', ...modes.map((m) => `<option value="${esc(m)}">${esc(m)}</option>`)].join('');
|
|
3597
|
+
f.permissionMode.value = a.permissionMode ?? '';
|
|
3598
|
+
$('#edit-error').hidden = true;
|
|
3599
|
+
$('#edit-dialog').showModal();
|
|
3600
|
+
(focus === 'project' ? f.project : f.name).focus();
|
|
3601
|
+
}
|
|
3602
|
+
|
|
3603
|
+
$('#edit-form').project.addEventListener('change', (e) => {
|
|
3604
|
+
$('#edit-newproject-row').hidden = e.target.value !== '__new';
|
|
3605
|
+
if (e.target.value === '__new') $('#edit-form').newProject.focus();
|
|
3606
|
+
});
|
|
3607
|
+
$('#edit-close').onclick = () => $('#edit-dialog').close();
|
|
3608
|
+
$('#edit-form').addEventListener('submit', async (e) => {
|
|
3609
|
+
e.preventDefault();
|
|
3610
|
+
const a = editing;
|
|
3611
|
+
const f = e.target;
|
|
3612
|
+
const patch = {};
|
|
3613
|
+
if (f.name.value.trim() !== a.name) patch.name = f.name.value.trim();
|
|
3614
|
+
if (f.intent.value.trim() !== (a.intent ?? '')) patch.intent = f.intent.value.trim();
|
|
3615
|
+
if (Number(f.priority.value) !== (a.priority ?? 2)) patch.priority = Number(f.priority.value);
|
|
3616
|
+
if ((f.permissionMode.value || null) !== (a.permissionMode ?? null)) patch.permissionMode = f.permissionMode.value || null;
|
|
3617
|
+
if ((f.model.value.trim() || null) !== (a.model ?? null)) patch.model = f.model.value.trim() || null;
|
|
3618
|
+
if ((f.budgetUsd.value === '' ? null : Number(f.budgetUsd.value)) !== (a.budgetUsd ?? null)) patch.budgetUsd = f.budgetUsd.value === '' ? null : Number(f.budgetUsd.value);
|
|
3619
|
+
if (!noRepo(a)) {
|
|
3620
|
+
const target = f.project.value === '__new' ? f.newProject.value.trim() : f.project.value;
|
|
3621
|
+
const was = editProject(a);
|
|
3622
|
+
if (f.project.value === '__new' && !target) {
|
|
3623
|
+
$('#edit-error').textContent = 'Give the new project a name';
|
|
3624
|
+
$('#edit-error').hidden = false;
|
|
3625
|
+
return;
|
|
3626
|
+
}
|
|
3627
|
+
if (target !== was) patch.project = target;
|
|
3628
|
+
}
|
|
3629
|
+
try {
|
|
3630
|
+
if (Object.keys(patch).length) upsert(await api('PATCH', `/agents/${a.id}`, patch));
|
|
3631
|
+
if (patch.intent && f.tell.checked) await api('POST', `/agents/${a.id}/tell`, { text: `The request changed. It is now:\n${patch.intent}` });
|
|
3632
|
+
$('#edit-dialog').close();
|
|
3633
|
+
const relaunch = 'permissionMode' in patch || 'model' in patch;
|
|
3634
|
+
toast(!Object.keys(patch).length ? 'Nothing changed' : `Saved${patch.project !== undefined ? '; moved' : ''}${relaunch ? '. Permission mode and model apply from its next launch' : ''}`);
|
|
3635
|
+
} catch (err) {
|
|
3636
|
+
$('#edit-error').textContent = err.message;
|
|
3637
|
+
$('#edit-error').hidden = false;
|
|
3638
|
+
}
|
|
3639
|
+
});
|
|
3640
|
+
|
|
3641
|
+
// ------------------------------------------------------------------ assistant
|
|
3642
|
+
|
|
3643
|
+
const ASSISTANT_SOURCES = ['jira', 'comments', 'reviews', 'ci', 'slack', 'linear'];
|
|
3644
|
+
let assistantState = null;
|
|
3645
|
+
|
|
3646
|
+
function renderAssistantDot() {
|
|
3647
|
+
const a = assistantState;
|
|
3648
|
+
const dot = $('#assistant-dot');
|
|
3649
|
+
dot.hidden = !a?.agentId;
|
|
3650
|
+
if (a) {
|
|
3651
|
+
dot.className = `assistant-dot ${a.on ? (a.busy ? 'busy' : 'on') : 'off'}`;
|
|
3652
|
+
dot.title = a.on ? (a.busy ? 'Checking now' : 'On') : 'Paused';
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
|
|
3656
|
+
async function loadAssistant({ fill = true } = {}) {
|
|
3657
|
+
const a = await api('GET', '/assistant');
|
|
3658
|
+
assistantState = a;
|
|
3659
|
+
renderAssistantDot();
|
|
3660
|
+
const f = $('#assistant-form');
|
|
3661
|
+
if (fill) {
|
|
3662
|
+
for (const k of ASSISTANT_SOURCES) f[`src_${k}`].checked = a.sources.includes(k);
|
|
3663
|
+
f.everyMin.value = a.everyMin;
|
|
3664
|
+
f.maxPerDay.value = a.maxPerDay;
|
|
3665
|
+
f.maxRunning.value = a.maxRunning;
|
|
3666
|
+
f.notes.value = a.notes ?? '';
|
|
3667
|
+
}
|
|
3668
|
+
const state = !a.agentId ? 'Not set up yet. Start it to create its session.' : a.on ? (a.busy ? 'On: checking now.' : 'On.') : 'Paused: not checking.';
|
|
3669
|
+
$('#assistant-status').innerHTML = `<p><b>${esc(state)}</b>
|
|
3670
|
+
${a.lastCheckAt ? ` Last check ${ago(a.lastCheckAt)}.` : ''}${a.nextAt && a.on ? ` Next ${new Date(a.nextAt) <= new Date() ? 'when it is free' : `at ${new Date(a.nextAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`}.` : ''}
|
|
3671
|
+
${a.agentId ? ` Started ${a.startedToday} agent${a.startedToday === 1 ? '' : 's'} today, ${a.startedTotal} in all.` : ''}</p>
|
|
3672
|
+
${a.started.length ? `<ul class="assistant-started">${a.started.slice(0, 8).map((x) => `<li><a href="#${x.id}" data-assistant-goto="${x.id}">${esc(x.name)}</a> <span class="hint">${ago(x.createdAt)} · ${esc(x.intent.slice(0, 90))}</span></li>`).join('')}</ul>` : ''}`;
|
|
3673
|
+
$('#assistant-activity').innerHTML = a.activity ? `<h4>Activity</h4><div class="md">${md(a.activity.split(/\n(?=## )/).reverse().slice(0, 10).join('\n').replace(/([^\n])\n(?=[^\n#\-*])/g, '$1 \n'))}</div>` : a.agentId ? '<p class="hint">No activity yet. Its first check writes here.</p>' : '';
|
|
3674
|
+
$('#assistant-start').hidden = Boolean(a.on);
|
|
3675
|
+
$('#assistant-start').textContent = a.agentId ? 'Turn on' : 'Start';
|
|
3676
|
+
$('#assistant-pause').hidden = !a.on;
|
|
3677
|
+
$('#assistant-check').hidden = !a.on;
|
|
3678
|
+
$('#assistant-open').hidden = !a.agentId;
|
|
3679
|
+
$('#assistant-error').hidden = true;
|
|
3680
|
+
return a;
|
|
3681
|
+
}
|
|
3682
|
+
|
|
3683
|
+
const assistantPatch = (f) => ({
|
|
3684
|
+
sources: ASSISTANT_SOURCES.filter((k) => f[`src_${k}`].checked),
|
|
3685
|
+
everyMin: Number(f.everyMin.value),
|
|
3686
|
+
maxPerDay: Number(f.maxPerDay.value),
|
|
3687
|
+
maxRunning: Number(f.maxRunning.value),
|
|
3688
|
+
notes: f.notes.value,
|
|
3689
|
+
});
|
|
3690
|
+
const assistantError = (err) => {
|
|
3691
|
+
$('#assistant-error').textContent = err.message;
|
|
3692
|
+
$('#assistant-error').hidden = false;
|
|
3693
|
+
};
|
|
3694
|
+
$('#assistant-btn').onclick = async () => {
|
|
3695
|
+
$('#assistant-dialog').showModal();
|
|
3696
|
+
await loadAssistant().catch(assistantError);
|
|
3697
|
+
};
|
|
3698
|
+
$('#assistant-close').onclick = () => $('#assistant-dialog').close();
|
|
3699
|
+
$('#assistant-form').addEventListener('submit', async (e) => {
|
|
3700
|
+
e.preventDefault();
|
|
3701
|
+
try {
|
|
3702
|
+
await api('PUT', '/assistant', assistantPatch(e.target));
|
|
3703
|
+
toast(assistantState?.running ? 'Saved. A running assistant uses the new watch list and notes after Pause and Turn on.' : 'Assistant settings saved');
|
|
3704
|
+
await loadAssistant();
|
|
3705
|
+
} catch (err) {
|
|
3706
|
+
assistantError(err);
|
|
3707
|
+
}
|
|
3708
|
+
});
|
|
3709
|
+
$('#assistant-start').onclick = async () => {
|
|
3710
|
+
try {
|
|
3711
|
+
if (!assistantState?.agentId) await api('PUT', '/assistant', assistantPatch($('#assistant-form')));
|
|
3712
|
+
await api('POST', '/assistant/start');
|
|
3713
|
+
toast('Assistant on: its first check starts in a moment');
|
|
3714
|
+
await loadAssistant({ fill: false });
|
|
3715
|
+
} catch (err) {
|
|
3716
|
+
assistantError(err);
|
|
3717
|
+
}
|
|
3718
|
+
};
|
|
3719
|
+
$('#assistant-pause').onclick = async () => {
|
|
3720
|
+
try {
|
|
3721
|
+
await api('POST', '/assistant/pause');
|
|
3722
|
+
await loadAssistant({ fill: false });
|
|
3723
|
+
} catch (err) {
|
|
3724
|
+
assistantError(err);
|
|
3725
|
+
}
|
|
3726
|
+
};
|
|
3727
|
+
$('#assistant-check').onclick = async () => {
|
|
3728
|
+
try {
|
|
3729
|
+
await api('POST', '/assistant/check');
|
|
3730
|
+
toast('Asked the assistant to check now');
|
|
3731
|
+
await loadAssistant({ fill: false });
|
|
3732
|
+
} catch (err) {
|
|
3733
|
+
assistantError(err);
|
|
3734
|
+
}
|
|
3735
|
+
};
|
|
3736
|
+
$('#assistant-open').onclick = () => {
|
|
3737
|
+
$('#assistant-dialog').close();
|
|
3738
|
+
if (assistantState?.agentId) select(assistantState.agentId);
|
|
3739
|
+
};
|
|
3740
|
+
$('#assistant-dialog').addEventListener('click', (e) => {
|
|
3741
|
+
const go = e.target.closest('[data-assistant-goto]');
|
|
3742
|
+
if (!go) return;
|
|
3743
|
+
e.preventDefault();
|
|
3744
|
+
$('#assistant-dialog').close();
|
|
3745
|
+
select(go.dataset.assistantGoto);
|
|
3746
|
+
});
|
|
3747
|
+
loadAssistant({ fill: false }).catch(() => {});
|
|
3748
|
+
setInterval(() => loadAssistant({ fill: false }).catch(() => {}), 60_000);
|
|
3749
|
+
|
|
3506
3750
|
// ------------------------------------------------------------------ Slack alerts (from this computer)
|
|
3507
3751
|
|
|
3508
3752
|
async function loadAlerts() {
|