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/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.kind === 'general') return { button: '', box: '' };
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.kind === 'general') return '';
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.kind === 'general') return '';
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.kind === 'general' || !a.git?.branch) return '';
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="Group this agent under a different project name">✎</button></dd>
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
- const name = prompt('Project name for this agent (leave empty to use its repo):', a.project ?? a.projectInfo?.name ?? '');
2880
- if (name === null) return;
2881
- act(async () => upsert(await api('PATCH', `/agents/${a.id}`, { project: name })));
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() {