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/index.html CHANGED
@@ -22,6 +22,8 @@
22
22
  </div>
23
23
  <div class="top-actions">
24
24
  <input id="filter" type="search" placeholder="Filter agents" aria-label="Filter agents" />
25
+ <button id="assistant-btn" title="An always-on session that finds new work and starts agents for it">Assistant <span id="assistant-dot" class="assistant-dot" hidden></span></button>
26
+ <button id="spend-btn" title="Results across agents (merge rate, time to PR, cost per merged PR), what they cost, and spending limits">Results <span id="spend-today" class="spend-today"></span></button>
25
27
  <button id="cleanup-btn" title="Close agents idle for a while, and delete old closed ones">Clean up</button>
26
28
  <button id="alerts-btn" class="local-only" title="Slack alerts when an agent needs you">Alerts</button>
27
29
  <button id="remote-btn" class="local-only" title="Open AI Agent Manager from your phone">Remote access</button>
@@ -36,6 +38,7 @@
36
38
  <button class="side-tab active" data-tab="agents" role="tab">Agents</button>
37
39
  <button class="side-tab" data-tab="inbox" role="tab" title="Everything waiting on you: prompts, questions, stuck agents, overlaps">Inbox <span id="inbox-count" class="badge"></span></button>
38
40
  <button class="side-tab" data-tab="backlog" role="tab">Backlog <span id="backlog-count" class="badge"></span></button>
41
+ <button class="side-tab" data-tab="land" role="tab" title="Agents' open pull requests: what each needs to get merged">Land <span id="land-count" class="badge"></span></button>
39
42
  </div>
40
43
  <div id="agents-view">
41
44
  <div class="counts" id="counts"></div>
@@ -50,6 +53,7 @@
50
53
  </div>
51
54
  <div id="backlog-view" hidden></div>
52
55
  <div id="inbox-view" hidden></div>
56
+ <div id="land-view" hidden></div>
53
57
  <div id="empty" class="empty" hidden>
54
58
  <p>No agents yet.</p>
55
59
  <p>Add one or more intents and a repo or folder to get started.</p>
@@ -268,6 +272,8 @@
268
272
  </label>
269
273
  <p id="new-error" class="error" hidden></p>
270
274
  <div class="dialog-actions">
275
+ <button type="button" id="new-playbook" class="ghost" title="Keep this request to run again, in several projects or on a schedule">Save as playbook…</button>
276
+ <span class="spacer"></span>
271
277
  <button type="button" id="new-cancel" class="ghost">Cancel</button>
272
278
  <button type="submit" class="primary">Create agents</button>
273
279
  </div>
@@ -354,6 +360,15 @@
354
360
  <div id="team-body" class="race-body team-body"></div>
355
361
  </dialog>
356
362
 
363
+ <dialog id="review-dialog" class="doc-dialog review-dialog">
364
+ <div class="doc-head">
365
+ <h2 id="review-title">Review</h2>
366
+ <span class="spacer"></span>
367
+ <button type="button" id="review-close" class="small ghost">Close</button>
368
+ </div>
369
+ <div id="review-body" class="review-body"></div>
370
+ </dialog>
371
+
357
372
  <dialog id="race-dialog" class="doc-dialog">
358
373
  <div class="doc-head">
359
374
  <h2 id="race-title">Race</h2>
@@ -383,6 +398,8 @@
383
398
  <label class="check"><input type="checkbox" name="ev_stuck" /> seems stuck (same failure, undoing its edits, no progress)</label>
384
399
  <label class="check"><input type="checkbox" name="ev_conflict" /> changes the same files as another agent</label>
385
400
  <label class="check"><input type="checkbox" name="ev_question" /> asks a question in the inbox</label>
401
+ <label class="check"><input type="checkbox" name="ev_assistant" /> is started for new work (by the assistant, a GitHub label or a webhook)</label>
402
+ <label class="check"><input type="checkbox" name="ev_budget" /> nears or reaches its budget, or today's spend reaches the daily limit</label>
386
403
  <label class="check"><input type="checkbox" name="ev_waiting" /> finished its turn and is idle after <input name="waitingDelaySec" type="number" min="0" class="num" />s</label>
387
404
  <label class="check"><input type="checkbox" name="skipWhenWatching" /> not while I have its terminal open</label>
388
405
  <label class="check"><input type="checkbox" name="includeIntent" /> include the intent text in the message</label>
@@ -399,6 +416,187 @@
399
416
  </form>
400
417
  </dialog>
401
418
 
419
+ <dialog id="edit-dialog">
420
+ <form id="edit-form" method="dialog" class="alerts-body">
421
+ <h2>Edit agent</h2>
422
+ <label>Name <input name="name" required maxlength="80" /></label>
423
+ <label>Request <span class="hint">what the agent was asked to do</span>
424
+ <textarea name="intent" rows="4"></textarea>
425
+ </label>
426
+ <label class="check" id="edit-tell-row"><input type="checkbox" name="tell" /> Also tell the agent about the changed request</label>
427
+ <label id="edit-project-row">Project
428
+ <select name="project"></select>
429
+ </label>
430
+ <label id="edit-newproject-row" hidden>New project name <input name="newProject" maxlength="60" /></label>
431
+ <div class="row three">
432
+ <label>Priority
433
+ <select name="priority"><option value="0">P0 urgent</option><option value="1">P1 high</option><option value="2">P2 normal</option><option value="3">P3 low</option></select>
434
+ </label>
435
+ <label>Permission mode <select name="permissionMode"></select></label>
436
+ <label>Model <input name="model" placeholder="as configured" /></label>
437
+ </div>
438
+ <label>Budget <span class="hint">US dollars; empty for the default limit</span><input name="budgetUsd" type="number" min="0.01" step="0.01" placeholder="default" /></label>
439
+ <p class="hint" id="edit-hint">Moving changes only where the agent is listed; its code stays in its own repo and branch. Permission mode and model apply from its next launch.</p>
440
+ <p id="edit-error" class="error" hidden></p>
441
+ <div class="dialog-actions">
442
+ <span class="spacer"></span>
443
+ <button type="button" id="edit-close" class="ghost">Cancel</button>
444
+ <button class="primary">Save</button>
445
+ </div>
446
+ </form>
447
+ </dialog>
448
+
449
+ <dialog id="assistant-dialog">
450
+ <form id="assistant-form" method="dialog" class="alerts-body">
451
+ <h2>Assistant</h2>
452
+ <p class="hint">An always-on Claude session that works on your behalf. Every few minutes it checks for new work with its own Jira, GitHub and Slack tools, and starts an agent for each new item, or passes news to the agent already on it. It never merges, approves, comments or sends messages as you.</p>
453
+ <div id="assistant-status" class="assistant-status"></div>
454
+ <fieldset>
455
+ <legend>Watch</legend>
456
+ <label class="check"><input type="checkbox" name="src_jira" /> Jira issues newly assigned to me</label>
457
+ <label class="check"><input type="checkbox" name="src_comments" /> New comments and mentions on my Jira issues (passed to the agent on it)</label>
458
+ <label class="check"><input type="checkbox" name="src_reviews" /> GitHub pull requests waiting for my review</label>
459
+ <label class="check"><input type="checkbox" name="src_ci" /> Failing checks on my open pull requests</label>
460
+ <label class="check"><input type="checkbox" name="src_slack" /> Slack messages to me that ask for work</label>
461
+ <label class="check"><input type="checkbox" name="src_linear" /> Linear issues newly assigned to me</label>
462
+ </fieldset>
463
+ <div class="row three">
464
+ <label>Check every <span class="hint">minutes</span><input name="everyMin" type="number" min="1" /></label>
465
+ <label>New agents a day <span class="hint">at most</span><input name="maxPerDay" type="number" min="0" /></label>
466
+ <label>Running at once <span class="hint">at most</span><input name="maxRunning" type="number" min="0" /></label>
467
+ </div>
468
+ <label>Notes for the assistant <span class="hint">e.g. "Jira project OPS is the @cc repo; skip issues labeled design"</span>
469
+ <textarea name="notes" rows="3"></textarea>
470
+ </label>
471
+ <div id="assistant-activity" class="assistant-activity"></div>
472
+ <p id="assistant-error" class="error" hidden></p>
473
+ <div class="dialog-actions">
474
+ <button type="button" id="assistant-start" class="primary">Start</button>
475
+ <button type="button" id="assistant-check">Check now</button>
476
+ <button type="button" id="assistant-pause">Pause</button>
477
+ <button type="button" id="assistant-open" class="ghost">Open its session</button>
478
+ <button type="button" id="triggers-open" class="ghost" title="Start agents from GitHub labels, /agent PR comments and a webhook, with no session watching">Triggers…</button>
479
+ <span class="spacer"></span>
480
+ <button type="button" id="assistant-close" class="ghost">Close</button>
481
+ <button id="assistant-save">Save</button>
482
+ </div>
483
+ </form>
484
+ </dialog>
485
+
486
+ <dialog id="triggers-dialog">
487
+ <form id="triggers-form" method="dialog" class="alerts-body">
488
+ <h2>Triggers</h2>
489
+ <p class="hint">Start agents from where the work is, with no model watching (no tokens spent on it). Agents started this way are marked <b>⚡ GitHub</b> or <b>⚡ webhook</b>.</p>
490
+ <fieldset>
491
+ <legend>GitHub label</legend>
492
+ <label class="check"><input type="checkbox" name="gh_enabled" /> Start an agent for each open issue with this label</label>
493
+ <div class="row">
494
+ <label>Label <input name="gh_label" placeholder="agentmgr" /></label>
495
+ <label>Check every <span class="hint">minutes</span><input name="gh_everyMin" type="number" min="1" /></label>
496
+ </div>
497
+ <label>In these repos <span class="hint">owner/name, comma or space separated</span><input name="gh_repos" placeholder="acme/web acme/api" /></label>
498
+ <label>Or any repo of these owners <input name="gh_owners" placeholder="acme" /></label>
499
+ <label class="check"><input type="checkbox" name="gh_comment" /> Comment on the issue that an agent is on it</label>
500
+ </fieldset>
501
+ <fieldset>
502
+ <legend>Pull request comments</legend>
503
+ <label class="check"><input type="checkbox" name="pc_enabled" /> Pass comments that start with <input name="pc_prefix" class="inline" placeholder="/agent" /> on an agent's pull request to that agent</label>
504
+ <p class="hint">Only from the repo's owners, members and collaborators.</p>
505
+ </fieldset>
506
+ <fieldset>
507
+ <legend>Webhook</legend>
508
+ <label class="check"><input type="checkbox" name="wh_enabled" /> Accept <code>POST /api/hooks/agents</code> with the token</label>
509
+ <div id="triggers-token" class="hint"></div>
510
+ <button type="button" id="triggers-newtoken" class="small">New token</button>
511
+ <label>Only these repos <span class="hint">optional</span><input name="wh_repos" /></label>
512
+ </fieldset>
513
+ <label>Agents started by triggers a day <span class="hint">at most</span><input name="maxPerDay" type="number" min="0" /></label>
514
+ <div id="triggers-status" class="hint"></div>
515
+ <p id="triggers-error" class="error" hidden></p>
516
+ <div class="dialog-actions">
517
+ <button type="button" id="triggers-poll">Check now</button>
518
+ <span class="spacer"></span>
519
+ <button type="button" id="triggers-close" class="ghost">Close</button>
520
+ <button class="primary">Save</button>
521
+ </div>
522
+ </form>
523
+ </dialog>
524
+
525
+ <dialog id="memory-dialog">
526
+ <form id="memory-form" method="dialog" class="alerts-body">
527
+ <h2>Repo memory</h2>
528
+ <p class="hint">What agents learned working in <b id="memory-repo"></b>. Every agent in this repo gets it when it starts, and adds to it with <code>agentmgr learn</code>. Tidy it here: keep it short and true.</p>
529
+ <textarea name="text" rows="14" class="memory-text" placeholder="- npm test needs a running Postgres: docker compose up db (you, 2026-10-31)"></textarea>
530
+ <p id="memory-meta" class="hint"></p>
531
+ <p id="memory-error" class="error" hidden></p>
532
+ <div class="dialog-actions">
533
+ <span class="spacer"></span>
534
+ <button type="button" id="memory-close" class="ghost">Cancel</button>
535
+ <button class="primary">Save</button>
536
+ </div>
537
+ </form>
538
+ </dialog>
539
+
540
+ <dialog id="playbooks-dialog" class="doc-dialog">
541
+ <div class="doc-head">
542
+ <h2>Playbooks</h2>
543
+ <span class="spacer"></span>
544
+ <button type="button" id="pb-new" class="small">+ New playbook</button>
545
+ <button type="button" id="pb-close" class="small ghost">Close</button>
546
+ </div>
547
+ <div class="race-body">
548
+ <p class="hint">Saved requests you run again, in one or more projects (one agent each), by hand or on a schedule. Write <code>{{name}}</code> for a value you fill in when it runs.</p>
549
+ <div id="pb-list"></div>
550
+ <form id="pb-form" class="alerts-body pb-form" hidden>
551
+ <input type="hidden" name="id" />
552
+ <label>Name <input name="name" required maxlength="80" placeholder="Upgrade a dependency" /></label>
553
+ <label>Request <textarea name="intent" rows="4" required placeholder="Upgrade {{package}} to the latest version, fix what breaks, run the tests and open a pull request."></textarea></label>
554
+ <label>Run in these projects <span class="hint">shortcuts, comma or space separated; one agent each</span><input name="projects" placeholder="web api" list="project-options" /></label>
555
+ <div id="pb-defaults"></div>
556
+ <div class="row three">
557
+ <label>Model <input name="model" placeholder="as configured" /></label>
558
+ <label>Permission mode <select name="permissionMode"><option value="">As configured</option><option value="acceptEdits">Accept edits</option><option value="plan">Plan</option><option value="auto">Auto</option><option value="bypassPermissions">Bypass permissions</option></select></label>
559
+ <label>Priority <select name="priority"><option value="0">P0</option><option value="1">P1</option><option value="2" selected>P2</option><option value="3">P3</option></select></label>
560
+ </div>
561
+ <div class="row three">
562
+ <label>Schedule <select name="every"><option value="">By hand only</option><option value="hour">Every hour</option><option value="day">Every day</option><option value="weekday">Every weekday</option><option value="week">Every week</option></select></label>
563
+ <label>At <input name="at" type="time" value="09:00" /></label>
564
+ <label>On <select name="day"><option value="mon">Monday</option><option value="tue">Tuesday</option><option value="wed">Wednesday</option><option value="thu">Thursday</option><option value="fri">Friday</option><option value="sat">Saturday</option><option value="sun">Sunday</option></select></label>
565
+ </div>
566
+ <label class="check"><input type="checkbox" name="enabled" checked /> Schedule on</label>
567
+ <p id="pb-error" class="error" hidden></p>
568
+ <div class="dialog-actions">
569
+ <button type="button" id="pb-delete" class="danger ghost">Delete</button>
570
+ <span class="spacer"></span>
571
+ <button type="button" id="pb-cancel" class="ghost">Cancel</button>
572
+ <button class="primary">Save</button>
573
+ </div>
574
+ </form>
575
+ </div>
576
+ </dialog>
577
+
578
+ <dialog id="spend-dialog">
579
+ <form id="spend-form" method="dialog" class="alerts-body">
580
+ <h2>Results <select id="stats-days" class="inline-select"><option value="7">last 7 days</option><option value="30" selected>last 30 days</option><option value="90">last 90 days</option><option value="0">all time</option></select></h2>
581
+ <div id="stats-body"></div>
582
+ <h3 class="spend-h">Spend</h3>
583
+ <div id="spend-summary"></div>
584
+ <div class="row">
585
+ <label>Each agent at most <span class="hint">US dollars; empty for no limit</span><input name="perAgentUsd" type="number" min="0.01" step="0.01" placeholder="no limit" /></label>
586
+ <label>All agents a day at most <span class="hint">US dollars</span><input name="perDayUsd" type="number" min="0.01" step="0.01" placeholder="no limit" /></label>
587
+ </div>
588
+ <p class="hint">At 80% of an agent's budget you get an alert; at its budget its session is closed until you raise it (Edit on the agent). At the daily limit no new agents start or resume until tomorrow; running ones go on.</p>
589
+ <div id="spend-agents"></div>
590
+ <p id="spend-note" class="hint"></p>
591
+ <p id="spend-error" class="error" hidden></p>
592
+ <div class="dialog-actions">
593
+ <span class="spacer"></span>
594
+ <button type="button" id="spend-close" class="ghost">Close</button>
595
+ <button class="primary">Save limits</button>
596
+ </div>
597
+ </form>
598
+ </dialog>
599
+
402
600
  <dialog id="cleanup-dialog">
403
601
  <form id="cleanup-form" method="dialog" class="alerts-body">
404
602
  <h2>Clean up old agents</h2>
package/public/land.js ADDED
@@ -0,0 +1,149 @@
1
+ // Land tab: every agent's open pull request, grouped by what it needs to get merged, with
2
+ // Review, Update, Ask to fix and Merge. Settings for sending failures back to agents at the bottom.
3
+
4
+ const SHORT = { ready: 'ready', fix: 'to fix', behind: 'behind', waiting: 'waiting', draft: 'draft' };
5
+ const GROUPS = [
6
+ ['ready', 'Ready to merge'],
7
+ ['fix', 'Needs fixes'],
8
+ ['behind', 'Behind its base'],
9
+ ['waiting', 'Waiting for checks or review'],
10
+ ['draft', 'Drafts'],
11
+ ];
12
+
13
+ /** Checks, review and merge state of an open pull request, as small tags. */
14
+ export function prChips(pr, esc) {
15
+ const c = pr.checks;
16
+ const out = [];
17
+ if (c?.state === 'failed') out.push(`<span class="tag fail" title="${esc(c.failed.join(', '))}">✗ ${c.failed.length} failing</span>`);
18
+ else if (c?.state === 'running') out.push(`<span class="tag">◔ ${c.running} running</span>`);
19
+ else if (c?.state === 'passed') out.push('<span class="tag open">✓ checks</span>');
20
+ if (pr.review === 'APPROVED') out.push('<span class="tag open">Approved</span>');
21
+ else if (pr.review === 'CHANGES_REQUESTED') out.push('<span class="tag fail">Changes requested</span>');
22
+ else if (pr.review === 'REVIEW_REQUIRED') out.push('<span class="tag">Review required</span>');
23
+ if (pr.mergeable === 'CONFLICTING' || pr.mergeState === 'DIRTY') out.push('<span class="tag fail">Conflicts</span>');
24
+ else if (pr.mergeState === 'BEHIND') out.push('<span class="tag">Behind</span>');
25
+ return out.join(' ');
26
+ }
27
+
28
+ export function initLand({ api, esc, toast, agents, $, select, openReview, isOpen }) {
29
+ const view = $('#land-view');
30
+ let settings = null;
31
+
32
+ const rows = () => {
33
+ const out = [];
34
+ for (const a of agents.values()) {
35
+ for (const pr of a.prs ?? []) {
36
+ if (pr.state !== 'OPEN' || pr.source === 'mentioned') continue;
37
+ out.push({ a, pr, land: pr.land ?? 'waiting' });
38
+ }
39
+ }
40
+ return out.sort((x, y) => (x.a.priority ?? 2) - (y.a.priority ?? 2));
41
+ };
42
+
43
+ function rowHtml({ a, pr, land }) {
44
+ const canFix = land === 'fix' || land === 'behind';
45
+ return `<li class="agent land-row" data-agent="${a.id}" data-url="${esc(pr.url)}">
46
+ <div class="line1"><span class="prio p${a.priority ?? 2}">P${a.priority ?? 2}</span>
47
+ <a class="name" href="${esc(pr.url)}" target="_blank" rel="noopener" title="${esc(pr.title ?? pr.url)}">#${esc(pr.number ?? '')} ${esc(pr.title ?? pr.url)}</a></div>
48
+ <div class="line2"><button class="link" data-land="goto">${esc(a.name)}</button>${a.runtime?.busy ? ' <span class="hint">working</span>' : ''}</div>
49
+ <div class="land-chips">${prChips(pr, esc)}</div>
50
+ <div class="bl-actions">
51
+ <button class="small" data-land="review" title="Its diff, line comments for the agent, commit and push">Review</button>
52
+ ${land === 'behind' || pr.mergeable === 'CONFLICTING' ? '<button class="small" data-land="update" title="Merge the base branch in here (when the agent is idle and its work is committed) and push; otherwise, or on conflicts, ask the agent">Update</button>' : ''}
53
+ ${canFix ? '<button class="small" data-land="fix" title="Send the failures, requested changes or conflicts to the agent">Ask to fix</button>' : ''}
54
+ ${land === 'ready' ? '<button class="small primary" data-land="merge">Merge</button>' : ''}
55
+ ${land === 'waiting' || land === 'behind' ? '<button class="small" data-land="auto" title="GitHub merges it once checks and reviews allow">Auto-merge</button>' : ''}
56
+ </div>
57
+ </li>`;
58
+ }
59
+
60
+ function render() {
61
+ const list = rows();
62
+ const need = list.filter((r) => r.land === 'ready' || r.land === 'fix').length;
63
+ $('#land-count').textContent = list.length ? String(list.length) : '';
64
+ $('#land-count').className = `badge ${list.some((r) => r.land === 'ready') ? 'ready' : ''}`;
65
+ $('#land-count').title = need ? `${need} ready or needing fixes` : '';
66
+ if (!isOpen()) return;
67
+ const s = settings;
68
+ view.innerHTML = `
69
+ <div class="counts">${GROUPS.map(([k, label]) => {
70
+ const n = list.filter((r) => r.land === k).length;
71
+ return n ? `<span class="${k === 'fix' ? 'fail' : ''}">${n} ${SHORT[k]}</span>` : '';
72
+ }).join('')}<span class="spacer"></span><button class="link" data-land="refresh" title="Ask GitHub again for every open pull request">↻ Refresh</button></div>
73
+ ${list.length
74
+ ? `<ul class="agent-list">${GROUPS.map(([k, label]) => {
75
+ const g = list.filter((r) => r.land === k);
76
+ return g.length ? `<li class="project open"><div class="project-head bl-head"><span class="pname">${label}</span><span class="pcount">${g.length}</span></div><ul>${g.map(rowHtml).join('')}</ul></li>` : '';
77
+ }).join('')}</ul>`
78
+ : '<p class="hint land-empty">No open pull requests from agents. Open one from <b>Review changes</b> on an agent, or let the agent open it.</p>'}
79
+ ${s ? `<details class="land-settings"><summary>When checks fail or changes are requested</summary>
80
+ <label class="check"><input type="checkbox" data-set="fixChecks" ${s.fixChecks ? 'checked' : ''}> Send failing checks to the agent</label>
81
+ <label class="check"><input type="checkbox" data-set="fixReviews" ${s.fixReviews ? 'checked' : ''}> Send requested changes to the agent</label>
82
+ <label>At most <input type="number" min="0" max="20" data-set="maxFixes" value="${s.maxFixes}"> times per pull request</label>
83
+ <label>Merge with <select data-set="mergeMethod">${['squash', 'merge', 'rebase'].map((m) => `<option ${m === s.mergeMethod ? 'selected' : ''}>${m}</option>`).join('')}</select></label>
84
+ <label class="check"><input type="checkbox" data-set="deleteBranch" ${s.deleteBranch ? 'checked' : ''}> Delete the branch on GitHub after merging</label>
85
+ </details>` : ''}`;
86
+ }
87
+
88
+ async function load() {
89
+ try {
90
+ settings = (await api('GET', '/landing')).settings;
91
+ } catch {}
92
+ render();
93
+ }
94
+
95
+ view.addEventListener('click', async (e) => {
96
+ const b = e.target.closest('[data-land]');
97
+ if (!b) return;
98
+ const row = b.closest('[data-agent]');
99
+ const id = row?.dataset.agent;
100
+ const url = row?.dataset.url;
101
+ const run = async (fn, ok) => {
102
+ b.disabled = true;
103
+ try {
104
+ const r = await fn();
105
+ const msg = typeof ok === 'function' ? ok(r) : ok;
106
+ if (msg) toast(msg, 4000);
107
+ } catch (err) {
108
+ toast(`Error: ${err.message}`, 6000);
109
+ } finally {
110
+ b.disabled = false;
111
+ }
112
+ };
113
+ switch (b.dataset.land) {
114
+ case 'goto':
115
+ return select(id);
116
+ case 'review':
117
+ return openReview(id);
118
+ case 'refresh':
119
+ return run(async () => {
120
+ const ids = [...new Set(rows().map((r) => r.a.id))];
121
+ await Promise.all(ids.map((x) => api('POST', `/agents/${x}/refresh`).catch(() => null)));
122
+ }, 'Asked GitHub again');
123
+ case 'update':
124
+ return run(() => api('POST', `/agents/${id}/land/update`, {}), (r) =>
125
+ r.upToDate ? `Already up to date with ${r.target}` : r.merged ? `Merged ${r.merged} commit${r.merged === 1 ? '' : 's'} from ${r.target}${r.pushed ? ' and pushed' : ''}` : `Asked the agent to update (${r.why})`);
126
+ case 'fix':
127
+ return run(() => api('POST', `/agents/${id}/land/fix`, { url }), 'Sent to the agent');
128
+ case 'merge':
129
+ if (!confirm(`Merge this pull request with ${settings?.mergeMethod ?? 'squash'}?`)) return;
130
+ return run(() => api('POST', `/agents/${id}/land/merge`, { url }), 'Merged');
131
+ case 'auto':
132
+ return run(() => api('POST', `/agents/${id}/land/merge`, { url, auto: true }), 'Auto-merge is on: GitHub merges it once it may');
133
+ }
134
+ });
135
+
136
+ view.addEventListener('change', async (e) => {
137
+ const k = e.target.dataset.set;
138
+ if (!k) return;
139
+ const v = e.target.type === 'checkbox' ? e.target.checked : e.target.type === 'number' ? Number(e.target.value) : e.target.value;
140
+ try {
141
+ settings = (await api('PUT', '/landing', { [k]: v })).settings;
142
+ toast('Saved');
143
+ } catch (err) {
144
+ toast(`Error: ${err.message}`, 5000);
145
+ }
146
+ });
147
+
148
+ return { render, load };
149
+ }
@@ -0,0 +1,132 @@
1
+ // Playbooks dialog: list, edit, run now (asking for {{placeholders}}), schedule.
2
+
3
+ const placeholders = (text) => [...new Set([...String(text ?? '').matchAll(/\{\{\s*([\w-]+)\s*\}\}/g)].map((m) => m[1]))];
4
+ const EVERY = { hour: 'every hour', day: 'every day', weekday: 'every weekday', week: 'every week' };
5
+
6
+ export function initPlaybooks({ api, esc, toast, $, select }) {
7
+ const dlg = $('#playbooks-dialog');
8
+ const f = $('#pb-form');
9
+ let list = [];
10
+
11
+ const when = (iso) => (iso ? new Date(iso).toLocaleString([], { weekday: 'short', hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' }) : '');
12
+
13
+ function renderList() {
14
+ $('#pb-list').innerHTML = list.length
15
+ ? `<table class="spend-table pb-table"><tr><th>Playbook</th><th>Projects</th><th>Schedule</th><th>Last run</th><th></th></tr>${list
16
+ .map(
17
+ (p) => `<tr data-pb="${p.id}"><td><b>${esc(p.name)}</b><div class="hint pb-intent">${esc(p.intent.slice(0, 140))}</div></td>
18
+ <td>${p.projects.map((x) => `<code>${esc(x)}</code>`).join(' ')}</td>
19
+ <td>${p.schedule ? `${EVERY[p.schedule.every]}${p.schedule.every === 'hour' ? ` at :${p.schedule.at.slice(3)}` : ` at ${p.schedule.at}`}${p.schedule.day ? ` (${p.schedule.day})` : ''}${p.enabled ? `<div class="hint">next ${when(p.nextRunAt)}</div>` : ' <span class="tag">off</span>'}` : '<span class="hint">by hand</span>'}</td>
20
+ <td>${p.lastRunAt ? `${when(p.lastRunAt)}<div class="hint">${(p.runs?.at(-1)?.agents ?? []).map((a) => `<a href="#${a.id}" data-pb-goto="${a.id}">${esc(a.name)}</a>`).join(', ')}${p.runs?.at(-1)?.failed?.length ? ` <span class="error">${p.runs.at(-1).failed.length} failed</span>` : ''}</div>` : '—'}</td>
21
+ <td class="pb-actions"><button type="button" class="small primary" data-pb-run>Run now</button> <button type="button" class="small" data-pb-edit>Edit</button></td></tr>`,
22
+ )
23
+ .join('')}</table>`
24
+ : '<p class="hint">No playbooks yet. <b>+ New playbook</b>, or <b>Save as playbook</b> in New agents.</p>';
25
+ }
26
+
27
+ async function load() {
28
+ list = await api('GET', '/playbooks');
29
+ renderList();
30
+ }
31
+
32
+ function fillDefaults(values = {}) {
33
+ const keys = placeholders(f.intent.value);
34
+ $('#pb-defaults').innerHTML = keys.length
35
+ ? `<div class="row">${keys.map((k) => `<label>Default for <code>{{${esc(k)}}}</code> <span class="hint">needed for a schedule</span><input data-default="${esc(k)}" value="${esc(values[k] ?? '')}" /></label>`).join('')}</div>`
36
+ : '';
37
+ }
38
+
39
+ function edit(p = {}) {
40
+ f.hidden = false;
41
+ $('#pb-error').hidden = true;
42
+ f.id.value = p.id ?? '';
43
+ f.name.value = p.name ?? '';
44
+ f.intent.value = p.intent ?? '';
45
+ f.projects.value = (p.projects ?? []).join(' ');
46
+ f.model.value = p.model ?? '';
47
+ f.permissionMode.value = p.permissionMode ?? '';
48
+ f.priority.value = String(p.priority ?? 2);
49
+ f.every.value = p.schedule?.every ?? '';
50
+ f.at.value = p.schedule?.at ?? '09:00';
51
+ f.day.value = p.schedule?.day ?? 'mon';
52
+ f.enabled.checked = p.enabled ?? true;
53
+ $('#pb-delete').hidden = !p.id;
54
+ fillDefaults(p.defaults);
55
+ f.name.focus();
56
+ }
57
+
58
+ f.intent.addEventListener('input', () => {
59
+ const cur = Object.fromEntries([...f.querySelectorAll('[data-default]')].map((el) => [el.dataset.default, el.value]));
60
+ fillDefaults(cur);
61
+ });
62
+
63
+ f.addEventListener('submit', async (e) => {
64
+ e.preventDefault();
65
+ const body = {
66
+ name: f.name.value,
67
+ intent: f.intent.value,
68
+ projects: f.projects.value,
69
+ model: f.model.value.trim(),
70
+ permissionMode: f.permissionMode.value,
71
+ priority: Number(f.priority.value),
72
+ defaults: Object.fromEntries([...f.querySelectorAll('[data-default]')].map((el) => [el.dataset.default, el.value.trim()]).filter(([, v]) => v)),
73
+ schedule: f.every.value ? { every: f.every.value, at: f.at.value, day: f.day.value } : null,
74
+ enabled: f.enabled.checked,
75
+ };
76
+ try {
77
+ await api(f.id.value ? 'PUT' : 'POST', f.id.value ? `/playbooks/${f.id.value}` : '/playbooks', body);
78
+ f.hidden = true;
79
+ toast('Playbook saved');
80
+ await load();
81
+ } catch (err) {
82
+ $('#pb-error').hidden = false;
83
+ $('#pb-error').textContent = err.message;
84
+ }
85
+ });
86
+
87
+ $('#pb-list').addEventListener('click', async (e) => {
88
+ const go = e.target.closest('[data-pb-goto]');
89
+ if (go) {
90
+ e.preventDefault();
91
+ dlg.close();
92
+ return select(go.dataset.pbGoto);
93
+ }
94
+ const row = e.target.closest('[data-pb]');
95
+ const p = row && list.find((x) => x.id === row.dataset.pb);
96
+ if (!p) return;
97
+ if (e.target.closest('[data-pb-edit]')) return edit(p);
98
+ if (e.target.closest('[data-pb-run]')) {
99
+ const vars = {};
100
+ for (const k of p.placeholders) {
101
+ const v = prompt(`${p.name}: value for {{${k}}}`, p.defaults?.[k] ?? '');
102
+ if (v === null) return;
103
+ vars[k] = v;
104
+ }
105
+ try {
106
+ const r = await api('POST', `/playbooks/${p.id}/run`, { vars });
107
+ toast(`Started ${r.agents.length} agent${r.agents.length === 1 ? '' : 's'}${r.failed.length ? `; ${r.failed.length} failed: ${r.failed[0].reason}` : ''}`, 5000);
108
+ await load();
109
+ } catch (err) {
110
+ toast(`Error: ${err.message}`, 5000);
111
+ }
112
+ }
113
+ });
114
+
115
+ $('#pb-new').onclick = () => edit();
116
+ $('#pb-cancel').onclick = () => (f.hidden = true);
117
+ $('#pb-delete').onclick = async () => {
118
+ if (!confirm(`Delete the playbook "${f.name.value}"? Agents it started stay.`)) return;
119
+ await api('DELETE', `/playbooks/${f.id.value}`).catch((err) => toast(`Error: ${err.message}`));
120
+ f.hidden = true;
121
+ await load();
122
+ };
123
+ $('#pb-close').onclick = () => dlg.close();
124
+
125
+ async function open(prefill) {
126
+ f.hidden = true;
127
+ if (!dlg.open) dlg.showModal();
128
+ await load();
129
+ if (prefill) edit(prefill);
130
+ }
131
+ return { open };
132
+ }