ai-agentmanager 1.7.0 → 1.9.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 +12 -0
- package/README.md +9 -5
- package/package.json +1 -1
- package/public/app.js +93 -14
- package/public/export.js +110 -0
- package/public/index.html +23 -3
- package/public/menu.js +40 -4
- package/public/review.js +13 -3
- package/public/share.js +1 -1
- package/public/side.js +47 -9
- package/public/speak.js +171 -0
- package/public/style.css +34 -1
- package/public/update.js +3 -5
- package/public/walkthrough.js +16 -0
- package/src/brief.js +79 -0
- package/src/jira.js +100 -0
- package/src/manager.js +69 -1
- package/src/review.js +10 -2
- package/src/server.js +16 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.0 (2026-10-31)
|
|
4
|
+
|
|
5
|
+
- **Intent, spec and plan:** each task agent now has INTENT.md (the request as given, its Jira summary, attachments and who started it; written by AI Agent Manager), SPEC.md (requirements, acceptance criteria, out of scope; the agent writes it before changing code) and PLAN.md (steps it ticks off), next to its brief. **Intent · Spec · Plan** buttons and the split view's Docs menu show them; Explain, the documents and AI review read the spec and plan; **Push & open PR** puts them in the pull request's description (intent in full, spec and plan folded). `"specs": false` in config.json turns the spec and plan instructions off.
|
|
6
|
+
- **Export diagrams:** every drawn diagram has **Copy image** (paste into Slack, a PR comment or a doc), **PNG** and **SVG**; a document with diagrams also downloads as one self-contained **.html**. Diagrams now use SVG text labels so they export as images.
|
|
7
|
+
- **Listen:** **🔊 Listen** reads Explain, the documents and the change walkthrough aloud with the browser's own voices (nothing leaves the device; works on phones). The walkthrough is read change by change, showing each as it's read. Pause, stop and speed (0.85× to 1.75×).
|
|
8
|
+
|
|
9
|
+
## 1.8.0 (2026-10-31)
|
|
10
|
+
|
|
11
|
+
- **Update from your phone:** **Update to …** and **About → Update and restart** now show over remote access too; the install and restart run on the computer and the page reloads when it's back. It installs only the official release, from where this copy came from.
|
|
12
|
+
- **Jira summaries:** an agent created from a Jira link shows the issue's summary as its description (in the list and its header, and as the issue's title under Bugs), fetched in the background from Jira Cloud. Until then, or when it can't be fetched, the words after the link. **Settings → Jira** takes your site, email and API token for private issues (also `JIRA_SITE` / `JIRA_EMAIL` / `JIRA_API_TOKEN`); only `*.atlassian.net` and that site are contacted, and the token goes only to that site. Saving fills in agents already created.
|
|
13
|
+
- **Theme:** Auto / Light / Dark in the ☰ menu, remembered in the browser; diagrams redraw in the theme's colors.
|
|
14
|
+
|
|
3
15
|
## 1.7.0 (2026-10-31)
|
|
4
16
|
|
|
5
17
|
- **Split view:** **◫ Split** in the terminal bar: the terminal (or a shell, or a browser tab) on the left, and Code, Docs (Diagrams by default, or any other document) or Changes on the right. Resizable and remembered.
|
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@ Run one background [Claude Code](https://claude.com/claude-code) agent per inten
|
|
|
7
7
|

|
|
8
8
|
|
|
9
9
|
- **Intents in, agents out.** Give a list, one per line (say, a ticket each), and each becomes its own `claude` process. Or pick **One agent for all of it** and a long description goes to a single agent as written.
|
|
10
|
+
- **Issue links.** A request that starts with a Jira link is named after the issue (`ABC-123`) and described by its summary, fetched from Jira Cloud (public issues as they are; your company's site after **Settings → Jira**: site, email and API token, kept on this computer and sent only to that site). Without access, the words after the link describe it.
|
|
10
11
|
- **Repo or folder.** Give a git URL (cloned once, then each agent gets its own worktree and branch) or a local folder.
|
|
11
12
|
- **Fast clones.** URLs are cloned as partial clones (`--filter=blob:none`): history now, file contents only for what's checked out.
|
|
12
13
|
- **Progress.** Clone and checkout progress shows in the UI and `agentmgr ls`.
|
|
@@ -84,13 +85,13 @@ Runs on macOS and Linux; on Windows, use WSL2. Requires Node 20+, `git`, and the
|
|
|
84
85
|
|
|
85
86
|
```sh
|
|
86
87
|
npm install -g ai-agentmanager # the latest release
|
|
87
|
-
npm install -g ai-agentmanager@1.
|
|
88
|
+
npm install -g ai-agentmanager@1.9.0 # or a specific one
|
|
88
89
|
```
|
|
89
90
|
|
|
90
91
|
Or from the repository, at a release tag:
|
|
91
92
|
|
|
92
93
|
```sh
|
|
93
|
-
git clone --branch v1.
|
|
94
|
+
git clone --branch v1.9.0 --depth 1 https://github.com/jdmunta/ai-agentmanager-core.git
|
|
94
95
|
cd ai-agentmanager-core
|
|
95
96
|
npm install
|
|
96
97
|
npm link # optional: puts `agentmgr` on your PATH
|
|
@@ -105,12 +106,12 @@ npm install
|
|
|
105
106
|
npm link
|
|
106
107
|
```
|
|
107
108
|
|
|
108
|
-
**Updating.** `agentmgr update` installs the newest release and restarts onto it; running agents pause briefly and resume. In the app, **Update to …** appears in the top bar when a release is out (checked twice a day; turn it off in **☰ → Settings** or with `agentmgr update --auto-check off`), and **About → Update and restart** does the same. A clone at a release tag (or one you `npm link`ed) moves to the newest tag; a clone on a branch or with uncommitted changes is left alone. By hand, use the command for how you installed, then `agentmgr restart`:
|
|
109
|
+
**Updating.** `agentmgr update` installs the newest release and restarts onto it; running agents pause briefly and resume. In the app, **Update to …** appears in the top bar when a release is out (checked twice a day; turn it off in **☰ → Settings** or with `agentmgr update --auto-check off`), and **About → Update and restart** does the same. Both work from a signed-in phone too: the install and restart run on the computer, and the page reloads when it's back. A clone at a release tag (or one you `npm link`ed) moves to the newest tag; a clone on a branch or with uncommitted changes is left alone. By hand, use the command for how you installed, then `agentmgr restart`:
|
|
109
110
|
|
|
110
111
|
| Installed with | Update |
|
|
111
112
|
| --- | --- |
|
|
112
113
|
| npm | `npm install -g ai-agentmanager@latest` |
|
|
113
|
-
| A clone at a release tag | `git fetch --tags && git checkout v1.
|
|
114
|
+
| A clone at a release tag | `git fetch --tags && git checkout v1.9.0 && npm install` (the tag you want) |
|
|
114
115
|
| A clone of `main` | `git pull && npm install` |
|
|
115
116
|
|
|
116
117
|
**Check your setup.** `agentmgr doctor` checks Node.js, Claude Code and its sign-in, git, `gh`, the data folder, the daemon's port and Chrome, and says how to fix anything missing.
|
|
@@ -330,6 +331,8 @@ The Brief panel's **Share for review** buttons, or `agentmgr doc <agent> <kind>`
|
|
|
330
331
|
| **Human vs AI** | Your top 5 decisions, each with why it needs you, the stakes, options and what the work suggests, plus a box for your answer. Up to 5 questions the AI answers, each with its confidence, evidence and how to check it. A second model's verdict on each. |
|
|
331
332
|
| **Diagrams** | Diagrams only, for a quick grasp: the problem, the architecture (with what changes highlighted), the design, the data flow (animated) and the key interaction, plus the plan. Drawn from the documents already written and the agent's work. Optional: **Write all** leaves it out. |
|
|
332
333
|
|
|
334
|
+
Each drawn diagram has **Copy image** (paste it into Slack, a PR comment or a doc), **PNG** and **SVG**, and a document with diagrams also downloads as one **.html** file. **🔊 Listen** reads Explain, documents and the change walkthrough aloud with the browser's own voices; the walkthrough is read change by change, showing each as it goes.
|
|
335
|
+
|
|
333
336
|
**Human vs AI.** This review takes two separate read-only calls.
|
|
334
337
|
1. **The split.** The agent's engine splits the open questions. *For you to decide* covers judgment calls such as intent, priorities, risk appetite and trade-offs. *Answered by AI* covers reasoning and evaluation: correctness, edge cases, test coverage and what's left.
|
|
335
338
|
2. **The second opinion.** Another model reviews that split against the same context. It marks each AI answer *agrees*, *partly* or *disagrees*, with a correction where needed. It marks each of your questions *keep*, *AI could answer* or *reword*, and lists questions both lists missed.
|
|
@@ -386,7 +389,8 @@ Each card has at most **three points of about 12 words each**, so they're easy t
|
|
|
386
389
|
- **Line comments.** Click **+** next to a line (or its line number) to comment on it, and add an overall comment if you like. **Send to agent** types them into the agent as one message, each with its file, line and the code it is about, and asks it to address them. Comments not sent yet are kept in this browser.
|
|
387
390
|
- **Explain changes** explains each change one by one: a title, what it changes, why (from the request and the conversation), the risk and what to check, in the order a reviewer should read them. Each explanation sits above its hunk, and **◀ / ▶** (or **[** and **]**) step through them, opening folded files on the way. When the agent changes the code afterwards, the page says so and **Explain again** brings it up to date. From the CLI: `agentmgr doc <agent> walkthrough`.
|
|
388
391
|
- **Commit all** commits everything in the worktree, new files included, with your git name and email.
|
|
389
|
-
- **
|
|
392
|
+
- **Intent, spec and plan.** Each agent keeps INTENT.md (what was asked, written by AI Agent Manager), SPEC.md (requirements and acceptance criteria, written by the agent before it changes code) and PLAN.md (steps, ticked off as it goes) next to its brief. Open them with **Intent · Spec · Plan** or in the split view's Docs; Explain, the documents and AI review read them too. `"specs": false` in config.json stops asking agents for the spec and plan.
|
|
393
|
+
- **Push & open PR** pushes the branch and opens a pull request with `gh`, against the branch the agent started from (title from your text, or from the commits; **Draft** if you like). Its description carries the intent, and the spec and plan in folded sections. With a pull request already open, it pushes to it. The pull request is linked to the agent.
|
|
390
394
|
|
|
391
395
|
The same from the API: `GET /api/agents/:id/changes`, `POST …/changes/comments`, `…/changes/commit`, `…/changes/pr`. Plugins can refuse a commit or a pull request (`agent.commit`, `agent.pr`).
|
|
392
396
|
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -6,6 +6,8 @@ import { touchScroll } from '/touchscroll.js';
|
|
|
6
6
|
import { openStream, post, REMOTE } from '/transport.js';
|
|
7
7
|
import { attachBox } from '/attach.js';
|
|
8
8
|
import { initSide } from '/side.js';
|
|
9
|
+
import { addTools, download, htmlFile, initExport } from '/export.js';
|
|
10
|
+
import { bindListen, listenHtml, play, speakable, now as speaking, stop as stopSpeaking } from '/speak.js';
|
|
9
11
|
import { initReview } from '/review.js';
|
|
10
12
|
import { initLand, prChips } from '/land.js';
|
|
11
13
|
import { initTriggers } from '/triggers.js';
|
|
@@ -164,6 +166,21 @@ function checksSummary(b) {
|
|
|
164
166
|
}
|
|
165
167
|
|
|
166
168
|
const diagrams = new Map(); // mermaid source -> rendered SVG (or an error)
|
|
169
|
+
/** Whether the page is dark: the menu's choice, else this device's setting. */
|
|
170
|
+
const isDark = () => {
|
|
171
|
+
const t = document.documentElement.dataset.theme;
|
|
172
|
+
return t ? t === 'dark' : matchMedia('(prefers-color-scheme: dark)').matches;
|
|
173
|
+
};
|
|
174
|
+
// SVG text labels rather than HTML ones: a diagram then exports to PNG (the browser won't turn
|
|
175
|
+
// HTML-in-SVG into an image).
|
|
176
|
+
const mermaidConfig = () => ({ startOnLoad: false, securityLevel: 'strict', theme: isDark() ? 'dark' : 'default', htmlLabels: false, flowchart: { htmlLabels: false } });
|
|
177
|
+
// Diagrams are drawn in the theme's colors: redraw them when it changes.
|
|
178
|
+
window.addEventListener('agentmgr-theme', () => {
|
|
179
|
+
diagrams.clear();
|
|
180
|
+
mermaidLib?.initialize(mermaidConfig());
|
|
181
|
+
for (const el of document.querySelectorAll('[data-mermaid][data-filled]')) delete el.dataset.filled;
|
|
182
|
+
renderDiagrams(document);
|
|
183
|
+
});
|
|
167
184
|
const expanded = new Set(); // "<agent id>:<section>" the user opened with Show more
|
|
168
185
|
|
|
169
186
|
/** Long sections are clamped; Show more toggles them. */
|
|
@@ -189,8 +206,7 @@ async function renderDiagrams(root) {
|
|
|
189
206
|
try {
|
|
190
207
|
if (!mermaidLib) {
|
|
191
208
|
mermaidLib = (await import('/vendor/mermaid/mermaid.esm.min.mjs')).default;
|
|
192
|
-
|
|
193
|
-
mermaidLib.initialize({ startOnLoad: false, securityLevel: 'strict', theme: dark ? 'dark' : 'default' });
|
|
209
|
+
mermaidLib.initialize(mermaidConfig());
|
|
194
210
|
}
|
|
195
211
|
const { svg } = await mermaidLib.render(`mmd-${diagrams.size}-${Date.now()}`, src);
|
|
196
212
|
diagrams.set(src, { svg });
|
|
@@ -203,7 +219,10 @@ async function renderDiagrams(root) {
|
|
|
203
219
|
for (const el of document.querySelectorAll('[data-mermaid]')) {
|
|
204
220
|
if (el.dataset.mermaid !== src || el.dataset.filled) continue;
|
|
205
221
|
el.dataset.filled = '1';
|
|
206
|
-
if (out.svg)
|
|
222
|
+
if (out.svg) {
|
|
223
|
+
el.innerHTML = out.svg;
|
|
224
|
+
addTools(el);
|
|
225
|
+
}
|
|
207
226
|
else el.innerHTML = `<div class="hint">Diagram could not be drawn (${esc(out.error)}). Source:</div><pre>${esc(src)}</pre>`;
|
|
208
227
|
}
|
|
209
228
|
}
|
|
@@ -221,7 +240,7 @@ function explainBox(a) {
|
|
|
221
240
|
${ex.markdown
|
|
222
241
|
? `${stale ? '<p class="hint">The brief has changed since this explanation; use <b>Explain again</b> to refresh it.</p>' : ''}
|
|
223
242
|
${clamp(a, 'explain', md(ex.markdown))}
|
|
224
|
-
<p class="hint">Explained ${ago(ex.at)}
|
|
243
|
+
<p class="hint explain-foot">Explained ${ago(ex.at)}. ${listenHtml(`explain:${a.id}`, { title: 'Read the explanation aloud' })}</p>`
|
|
225
244
|
: ''}
|
|
226
245
|
</div>`;
|
|
227
246
|
return { button, box };
|
|
@@ -251,9 +270,18 @@ const INSIGHT_ICONS = { why: '🎯', what: '📦', how: '🛠', now: '📍' };
|
|
|
251
270
|
const DOCS = { analysis: 'Analysis', design: 'Design', flow: 'Flow', proposal: 'Proposal', humanai: 'Human vs AI', diagrams: 'Diagrams' };
|
|
252
271
|
// Optional extras that "Write all" leaves out.
|
|
253
272
|
const DOC_EXTRAS = new Set(['diagrams']);
|
|
254
|
-
const DOC_TITLES = { ...DOCS, plan: '
|
|
273
|
+
const DOC_TITLES = { ...DOCS, plan: 'Plan', intent: 'Intent', spec: 'Spec' };
|
|
274
|
+
// The agent's intent, spec and plan files (read from the daemon when opened; kept here by agent and kind).
|
|
275
|
+
const SPEC_KINDS = { intent: 'Intent', spec: 'Spec', plan: 'Plan' };
|
|
276
|
+
const specCache = new Map(); // "<agent>:<kind>" -> { markdown, at }
|
|
277
|
+
const isSpecKind = (a, kind) => Boolean(SPEC_KINDS[kind]) && !(kind === 'plan' && a?.planReview);
|
|
278
|
+
async function loadSpec(agentId, kind) {
|
|
279
|
+
const r = await api('GET', `/agents/${agentId}/specs/${kind}`).catch(() => null);
|
|
280
|
+
specCache.set(`${agentId}:${kind}`, { markdown: r?.markdown ?? null, at: r?.updatedAt ?? null, loaded: true });
|
|
281
|
+
}
|
|
255
282
|
/** What the document dialog shows: a stakeholder document, or the agent's plan under review. */
|
|
256
|
-
const docOf = (a, kind) =>
|
|
283
|
+
const docOf = (a, kind) =>
|
|
284
|
+
isSpecKind(a, kind) ? specCache.get(`${a?.id}:${kind}`) : kind === 'plan' ? a?.planReview && { markdown: a.planReview.markdown, at: a.planReview.updatedAt } : a?.docs?.[kind];
|
|
257
285
|
|
|
258
286
|
const VERDICT = { agree: '✓ Agrees', partly: '≈ Partly agrees', disagree: '✗ Disagrees', keep: '✓ Keep', ai: '🤖 AI could answer', reword: '✎ Reword' };
|
|
259
287
|
const CONFIDENCE = { high: 'High confidence', medium: 'Medium confidence', low: 'Low confidence' };
|
|
@@ -324,6 +352,17 @@ function docButtons(a) {
|
|
|
324
352
|
return `<button class="small doc-btn ${d?.markdown ? 'has' : ''}" data-doc="${k}" ${d?.pending ? 'disabled' : ''}
|
|
325
353
|
title="${k === 'diagrams' ? (d?.markdown ? `Open the diagrams${stale ? ' (the brief changed since)' : ''}` : 'Draw the problem and its solution for a quick grasp: architecture, design, data flow (animated) and the key interaction, diagrams only. Built from the documents already written and the agent\'s work') : d?.markdown ? `Open the ${label.toLowerCase()} document${stale ? ' (the brief changed since)' : ''}` : `Write a ${label.toLowerCase()} document for stakeholders`}">${d?.pending ? `${label}…` : label}${d?.markdown ? (stale ? ' •' : ' ✓') : ''}</button>`;
|
|
326
354
|
})
|
|
355
|
+
.join('')}</span>${specButtons(a)}`;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Intent · Spec · Plan: the files kept for reviewers (and put in the pull request's description). */
|
|
359
|
+
function specButtons(a) {
|
|
360
|
+
if (!a.specDocs) return '';
|
|
361
|
+
return `<span class="doc-buttons spec-buttons" title="For reviewers, people and agents: what was asked, what done means, and the plan. Push & open PR puts them in the description">${Object.entries(SPEC_KINDS)
|
|
362
|
+
.map(([k, label]) => {
|
|
363
|
+
const at = a.specDocs[k] ?? (k === 'plan' ? a.planReview?.updatedAt : null);
|
|
364
|
+
return `<button class="small doc-btn ${at ? 'has' : ''}" data-doc="${k}" title="${at ? `Open ${label} (updated ${ago(at)})` : k === 'intent' ? 'Written when the agent is created' : `The agent writes it ${k === 'spec' ? 'before changing code' : 'and keeps it current'}`}">${label}${at ? ' ✓' : ''}</button>`;
|
|
365
|
+
})
|
|
327
366
|
.join('')}</span>`;
|
|
328
367
|
}
|
|
329
368
|
|
|
@@ -334,7 +373,9 @@ function renderDocDialog() {
|
|
|
334
373
|
const a = agents.get(docView.agentId);
|
|
335
374
|
const d = docOf(a, docView.kind);
|
|
336
375
|
const label = DOC_TITLES[docView.kind];
|
|
337
|
-
|
|
376
|
+
// The plan under review (approve / send back); otherwise a plain file to read.
|
|
377
|
+
const plan = docView.kind === 'plan' && Boolean(a?.planReview);
|
|
378
|
+
const specKind = isSpecKind(a, docView.kind);
|
|
338
379
|
$('#doc-title').textContent = `${label} · ${a?.name ?? ''}`;
|
|
339
380
|
const body = $('#doc-body');
|
|
340
381
|
body.classList.toggle('diagrams-view', docView.kind === 'diagrams');
|
|
@@ -343,6 +384,8 @@ function renderDocDialog() {
|
|
|
343
384
|
body.dataset.rendered = '';
|
|
344
385
|
} else if (d?.error) body.innerHTML = `<p class="error">Couldn't write it: ${esc(d.error)}</p>`;
|
|
345
386
|
else if (plan && !d?.markdown) body.innerHTML = '<p class="hint">The agent is investigating and hasn\'t written its plan yet. You\'ll see it here (and in the inbox) when it has.</p>';
|
|
387
|
+
else if (specKind && !d?.loaded) body.innerHTML = '<p class="hint">Loading…</p>';
|
|
388
|
+
else if (specKind && !d?.markdown) body.innerHTML = `<p class="hint">${docView.kind === 'intent' ? 'No intent file for this agent (agents created before 1.9 have none).' : `The agent hasn't written its ${label.toLowerCase()} yet. It writes ${docView.kind === 'spec' ? 'the spec before changing code' : 'the plan and keeps it current'}.`}</p>`;
|
|
346
389
|
else if (d?.markdown) {
|
|
347
390
|
if (body.dataset.rendered !== d.at) {
|
|
348
391
|
body.innerHTML = d.data ? humanAiHtml(d) : docHtml(d.markdown);
|
|
@@ -352,10 +395,13 @@ function renderDocDialog() {
|
|
|
352
395
|
}
|
|
353
396
|
const stale = !plan && d?.markdown && a.brief?.updatedAt && d.briefUpdatedAt !== a.brief.updatedAt;
|
|
354
397
|
const by = d?.writer ? ` by ${d.writer}${d.critique ? `, checked by ${d.critique.label}` : ''}${d.from ? ` from ${d.from.length ? d.from.join(', ') : "the agent's work"}` : ''}` : '';
|
|
355
|
-
$('#doc-meta').textContent = d?.markdown ? `${plan ? `Plan · ${PLAN_LABEL[a.planReview.status]} · updated` : docView.kind === 'diagrams' ? 'Drawn' : 'Written'} ${ago(d.at)}${by}${stale ? ' · the brief has changed since; Regenerate for the latest' : ''}` : '';
|
|
398
|
+
$('#doc-meta').textContent = d?.markdown ? `${plan ? `Plan · ${PLAN_LABEL[a.planReview.status]} · updated` : specKind ? 'Updated' : docView.kind === 'diagrams' ? 'Drawn' : 'Written'} ${ago(d.at)}${by}${stale && !specKind ? ' · the brief has changed since; Regenerate for the latest' : ''}${specKind ? ' · goes into the pull request description' : ''}` : '';
|
|
356
399
|
for (const id of ['doc-copy', 'doc-download', 'doc-print']) $(`#${id}`).disabled = !d?.markdown;
|
|
357
|
-
|
|
358
|
-
$('#doc-
|
|
400
|
+
// Diagrams: the whole document as one HTML file with every diagram (each also has Copy / PNG / SVG).
|
|
401
|
+
$('#doc-html').hidden = !/```mermaid/.test(d?.markdown ?? '');
|
|
402
|
+
$('#doc-listen').innerHTML = d?.markdown && docView.kind !== 'diagrams' ? listenHtml(`doc:${docView.agentId}:${docView.kind}`, { title: 'Read the document aloud' }) : '';
|
|
403
|
+
$('#doc-print').hidden = plan || specKind;
|
|
404
|
+
$('#doc-regen').hidden = plan || specKind;
|
|
359
405
|
$('#doc-regen').disabled = Boolean(d?.pending);
|
|
360
406
|
$('#doc-bg').hidden = !d?.pending;
|
|
361
407
|
// Footer: send your answers (Human vs AI), or approve / send back the plan.
|
|
@@ -376,7 +422,10 @@ function openDoc(agentId, kind) {
|
|
|
376
422
|
$('#doc-body').dataset.rendered = '';
|
|
377
423
|
$('#doc-dialog').showModal();
|
|
378
424
|
const a = agents.get(agentId);
|
|
379
|
-
if (
|
|
425
|
+
if (isSpecKind(a, kind)) {
|
|
426
|
+
specCache.delete(`${agentId}:${kind}`);
|
|
427
|
+
loadSpec(agentId, kind).then(() => docView?.agentId === agentId && docView.kind === kind && renderDocDialog());
|
|
428
|
+
} else if (kind !== 'plan' && !a.docs?.[kind]?.markdown && !a.docs?.[kind]?.pending) generateDoc();
|
|
380
429
|
renderDocDialog();
|
|
381
430
|
}
|
|
382
431
|
|
|
@@ -386,6 +435,7 @@ function generateDoc() {
|
|
|
386
435
|
}
|
|
387
436
|
|
|
388
437
|
$('#doc-close').onclick = () => {
|
|
438
|
+
if (speaking()?.id.startsWith('doc:')) stopSpeaking();
|
|
389
439
|
docView = null;
|
|
390
440
|
$('#doc-dialog').close();
|
|
391
441
|
};
|
|
@@ -472,6 +522,25 @@ function planRow(a) {
|
|
|
472
522
|
${p.status !== 'approved' ? '<button class="small ghost" data-act="plan-skip" title="Stop waiting for review; the agent goes ahead">Skip review</button>' : ''}</div>`;
|
|
473
523
|
}
|
|
474
524
|
$('#doc-print').onclick = () => window.open(`/share.html#${docView.agentId}/${docView.kind}`, '_blank', 'noopener');
|
|
525
|
+
$('#doc-html').onclick = () => {
|
|
526
|
+
const a = agents.get(docView.agentId);
|
|
527
|
+
download(htmlFile($('#doc-body'), `${DOC_TITLES[docView.kind]}: ${a?.name ?? ''}`), `${a?.slug ?? 'agent'}-${docView.kind}.html`);
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
// Diagram export: files named after the agent they belong to.
|
|
531
|
+
initExport({ toast, base: (slot) => (slot.closest('#doc-body') && docView ? agents.get(docView.agentId)?.slug : agents.get(term.agentId)?.slug ?? agents.get(selectedId)?.slug) ?? '' });
|
|
532
|
+
|
|
533
|
+
// Listening: what each Listen control reads.
|
|
534
|
+
bindListen((id) => {
|
|
535
|
+
const [what, agentId, kind] = id.split(':');
|
|
536
|
+
const a = agents.get(agentId);
|
|
537
|
+
let parts = null;
|
|
538
|
+
if (what === 'explain') parts = [speakable(a?.explanation?.markdown)];
|
|
539
|
+
else if (what === 'doc') parts = [speakable(kind === 'explain' ? a?.explanation?.markdown : docOf(a, kind)?.markdown)];
|
|
540
|
+
else if (what === 'walk') parts = reviewUi.listenParts();
|
|
541
|
+
else if (what === 'sidewalk') parts = sidePane?.listenParts();
|
|
542
|
+
if (!parts?.length || !play(id, parts)) toast('Nothing to read aloud here (or this browser has no speech)');
|
|
543
|
+
});
|
|
475
544
|
|
|
476
545
|
/** Why / What / How / Now as four cards. */
|
|
477
546
|
function insightsBox(a) {
|
|
@@ -638,7 +707,7 @@ function agentItem(a) {
|
|
|
638
707
|
const idle = !isActive(a);
|
|
639
708
|
return `<li data-id="${a.id}" ${a.kind === 'general' || a.kind === 'assistant' ? '' : 'draggable="true" '}class="agent pr${a.priority ?? 2} ${a.id === selectedId ? 'selected' : ''} ${idle ? 'idle' : ''}">
|
|
640
709
|
<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>
|
|
641
|
-
<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
|
|
710
|
+
<div class="intent" title="${esc(describeTitle(a) ?? '')}">${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(describe(a) || (a.kind === 'assistant' ? 'Finds new work and starts agents for it' : 'General session'))}</div>
|
|
642
711
|
${a.status === 'preparing' && a.progress ? `<div class="progress-line">${esc(progressText(a.progress))}</div>` : ''}
|
|
643
712
|
<div class="meta">
|
|
644
713
|
<span class="mono">${a.id}</span>
|
|
@@ -1594,7 +1663,7 @@ function renderDetail() {
|
|
|
1594
1663
|
${[0, 1, 2, 3].map((p) => `<option value="${p}" ${p === (a.priority ?? 2) ? 'selected' : ''}>P${p}</option>`).join('')}
|
|
1595
1664
|
</select>
|
|
1596
1665
|
<span class="d-name">${esc(a.name)}</span> <span class="pill ${st}">${pillText(st)}</span>${a.kind === 'general' ? ' <span class="tag">general</span>' : ''}</h2>
|
|
1597
|
-
<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>
|
|
1666
|
+
<p class="intent">${a.synopsis?.text ? `<b>${esc(a.synopsis.text)}</b>${a.synopsis.source === 'jira' ? ` <span class="hint">· ${esc(a.synopsis.key)}</span>` : ''}<br><span class="hint">${esc(a.intent)}</span>` : 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>
|
|
1598
1667
|
</div>
|
|
1599
1668
|
<div class="d-actions">
|
|
1600
1669
|
${a.status === 'error' && !a.workdir
|
|
@@ -2108,6 +2177,7 @@ sidePane = initSide({
|
|
|
2108
2177
|
codeUi,
|
|
2109
2178
|
docHtml,
|
|
2110
2179
|
renderDiagrams,
|
|
2180
|
+
listenHtml,
|
|
2111
2181
|
mainKind: () => term.kind,
|
|
2112
2182
|
setMain: (kind) => term.agentId && term.open(term.agentId, kind),
|
|
2113
2183
|
refit: () => requestAnimationFrame(() => term.refit()),
|
|
@@ -2897,6 +2967,10 @@ function upsert(a) {
|
|
|
2897
2967
|
/** Views that show one agent's details outside the main render: keep them current. */
|
|
2898
2968
|
function agentChanged(a) {
|
|
2899
2969
|
if (a.id === term.agentId) term.renderTabs();
|
|
2970
|
+
// An open intent / spec / plan that the agent just updated: read it again.
|
|
2971
|
+
if (docView?.agentId === a.id && isSpecKind(a, docView.kind) && specCache.get(`${a.id}:${docView.kind}`)?.at !== (a.specDocs?.[docView.kind] ?? null)) {
|
|
2972
|
+
loadSpec(a.id, docView.kind).then(renderDocDialog);
|
|
2973
|
+
}
|
|
2900
2974
|
reviewUi.agentChanged(a);
|
|
2901
2975
|
sidePane?.agentChanged(a);
|
|
2902
2976
|
}
|
|
@@ -3450,7 +3524,8 @@ $('#about-check').onclick = async () => {
|
|
|
3450
3524
|
out.hidden = false;
|
|
3451
3525
|
out.textContent = 'Checking…';
|
|
3452
3526
|
try {
|
|
3453
|
-
//
|
|
3527
|
+
// The daemon checks and can install it (from this computer or a signed-in phone); an older
|
|
3528
|
+
// daemon without /update gets the manual steps.
|
|
3454
3529
|
const local = await updateUi.check().catch(() => null);
|
|
3455
3530
|
if (local) return void (out.innerHTML = local);
|
|
3456
3531
|
const r = await api('GET', '/about/latest');
|
|
@@ -4070,6 +4145,10 @@ $('#login-form').addEventListener('submit', async (e) => {
|
|
|
4070
4145
|
});
|
|
4071
4146
|
|
|
4072
4147
|
// Engines this machine can run; the New agent form offers a choice once there's more than Claude.
|
|
4148
|
+
/** What an agent is about: the issue's summary when its request starts with an issue link, else the request. */
|
|
4149
|
+
const describe = (a) => a.synopsis?.text || a.intent;
|
|
4150
|
+
const describeTitle = (a) => (a.synopsis?.text ? `${a.synopsis.source === 'jira' ? `${a.synopsis.key} in Jira` : `${a.synopsis.key} (summary not fetched; your words after the link)`}. Request: ${a.intent}` : a.intent);
|
|
4151
|
+
|
|
4073
4152
|
const ENGINE_LABEL = { claude: 'Claude', codex: 'Codex', antigravity: 'Antigravity', muse: 'Muse Code' };
|
|
4074
4153
|
async function loadEngines() {
|
|
4075
4154
|
try {
|
package/public/export.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Taking diagrams out of the page, for a pull request or Slack: each drawn diagram gets Copy image,
|
|
2
|
+
// PNG and SVG; a whole document of diagrams downloads as one self-contained HTML file. Diagrams are
|
|
3
|
+
// drawn with SVG text labels (not HTML in foreignObject), so they turn into PNGs without the
|
|
4
|
+
// browser refusing a "tainted" canvas.
|
|
5
|
+
|
|
6
|
+
const TOOLS = `<span class="dg-tools"><button type="button" class="small ghost" data-dg="copy" title="Copy as an image: paste into Slack, a GitHub comment or a doc">Copy image</button><button type="button" class="small ghost" data-dg="png" title="Download as a PNG">PNG</button><button type="button" class="small ghost" data-dg="svg" title="Download as an SVG (sharp at any size)">SVG</button></span>`;
|
|
7
|
+
|
|
8
|
+
/** Add the export buttons to a drawn diagram slot. */
|
|
9
|
+
export function addTools(slot) {
|
|
10
|
+
if (slot.querySelector('.dg-tools') || !slot.querySelector('svg')) return;
|
|
11
|
+
slot.insertAdjacentHTML('afterbegin', TOOLS);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const pageBg = () => getComputedStyle(document.body).backgroundColor || '#ffffff';
|
|
15
|
+
|
|
16
|
+
/** A standalone copy of a diagram's SVG: sized, namespaced, on the page's background. */
|
|
17
|
+
export function svgText(svg, { background = pageBg() } = {}) {
|
|
18
|
+
const copy = svg.cloneNode(true);
|
|
19
|
+
copy.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
20
|
+
copy.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
|
|
21
|
+
const vb = svg.viewBox?.baseVal;
|
|
22
|
+
const box = svg.getBoundingClientRect();
|
|
23
|
+
const w = Math.ceil(vb?.width || box.width || 800);
|
|
24
|
+
const h = Math.ceil(vb?.height || box.height || 600);
|
|
25
|
+
copy.setAttribute('width', w);
|
|
26
|
+
copy.setAttribute('height', h);
|
|
27
|
+
copy.removeAttribute('style'); // mermaid's max-width would shrink it outside the page
|
|
28
|
+
if (background) {
|
|
29
|
+
const bg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
30
|
+
Object.entries({ x: vb?.x ?? 0, y: vb?.y ?? 0, width: '100%', height: '100%', fill: background }).forEach(([k, v]) => bg.setAttribute(k, v));
|
|
31
|
+
if (vb) {
|
|
32
|
+
bg.setAttribute('width', vb.width);
|
|
33
|
+
bg.setAttribute('height', vb.height);
|
|
34
|
+
}
|
|
35
|
+
copy.insertBefore(bg, copy.firstChild);
|
|
36
|
+
}
|
|
37
|
+
return { text: new XMLSerializer().serializeToString(copy), w, h };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The diagram as a PNG blob, at twice its size so it stays sharp on retina screens and in PRs. */
|
|
41
|
+
export function pngBlob(svg, scale = 2) {
|
|
42
|
+
const { text, w, h } = svgText(svg);
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const img = new Image();
|
|
45
|
+
img.onload = () => {
|
|
46
|
+
const canvas = Object.assign(document.createElement('canvas'), { width: w * scale, height: h * scale });
|
|
47
|
+
const ctx = canvas.getContext('2d');
|
|
48
|
+
ctx.scale(scale, scale);
|
|
49
|
+
ctx.drawImage(img, 0, 0, w, h);
|
|
50
|
+
canvas.toBlob((b) => (b ? resolve(b) : reject(new Error('Could not make the image'))), 'image/png');
|
|
51
|
+
};
|
|
52
|
+
img.onerror = () => reject(new Error('Could not draw the diagram as an image'));
|
|
53
|
+
img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(text)}`;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function download(blob, name) {
|
|
58
|
+
const link = Object.assign(document.createElement('a'), { href: URL.createObjectURL(blob), download: name });
|
|
59
|
+
document.body.append(link);
|
|
60
|
+
link.click();
|
|
61
|
+
link.remove();
|
|
62
|
+
setTimeout(() => URL.revokeObjectURL(link.href), 2000);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const slugOf = (s) => String(s || 'diagram').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 60) || 'diagram';
|
|
66
|
+
|
|
67
|
+
/** The diagram's name for files: its heading in the document, or "diagram". */
|
|
68
|
+
function nameOf(slot, base) {
|
|
69
|
+
let el = slot.previousElementSibling;
|
|
70
|
+
while (el && !/^H[1-4]$/.test(el.tagName)) el = el.previousElementSibling;
|
|
71
|
+
return `${base ? `${slugOf(base)}-` : ''}${slugOf(el?.textContent)}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** One page with every diagram (and heading) of a rendered document, to keep or attach. */
|
|
75
|
+
export function htmlFile(article, title) {
|
|
76
|
+
const copy = article.cloneNode(true);
|
|
77
|
+
copy.querySelectorAll('.dg-tools').forEach((t) => t.remove());
|
|
78
|
+
copy.querySelectorAll('svg').forEach((svg, i) => {
|
|
79
|
+
const live = article.querySelectorAll('svg')[i];
|
|
80
|
+
if (live) svg.outerHTML = svgText(live, { background: null }).text;
|
|
81
|
+
});
|
|
82
|
+
const css = `body{font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;max-width:1000px;margin:32px auto;padding:0 16px;color:#1c2330;background:#fff}
|
|
83
|
+
h1{font-size:24px}h2{font-size:18px;margin-top:28px;border-top:1px solid #dde1e7;padding-top:14px}
|
|
84
|
+
.diagram{display:flex;justify-content:center;margin:12px 0;overflow-x:auto}svg{max-width:100%;height:auto}`;
|
|
85
|
+
const html = `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${title.replace(/</g, '<')}</title><style>${css}</style></head><body>${copy.innerHTML}</body></html>`;
|
|
86
|
+
return new Blob([html], { type: 'text/html' });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Clicks on any diagram's buttons, wherever it is drawn. `base` names the files (the agent). */
|
|
90
|
+
export function initExport({ toast, base = () => '' }) {
|
|
91
|
+
document.addEventListener('click', async (e) => {
|
|
92
|
+
const b = e.target.closest('[data-dg]');
|
|
93
|
+
if (!b) return;
|
|
94
|
+
const slot = b.closest('.diagram');
|
|
95
|
+
const svg = slot?.querySelector('svg');
|
|
96
|
+
if (!svg) return;
|
|
97
|
+
const name = nameOf(slot, base(slot));
|
|
98
|
+
try {
|
|
99
|
+
if (b.dataset.dg === 'svg') download(new Blob([svgText(svg).text], { type: 'image/svg+xml' }), `${name}.svg`);
|
|
100
|
+
else if (b.dataset.dg === 'png') download(await pngBlob(svg), `${name}.png`);
|
|
101
|
+
else {
|
|
102
|
+
// Safari wants the promise handed over within the click.
|
|
103
|
+
await navigator.clipboard.write([new ClipboardItem({ 'image/png': pngBlob(svg) })]);
|
|
104
|
+
toast('Diagram copied as an image: paste it into Slack, a PR comment or a doc');
|
|
105
|
+
}
|
|
106
|
+
} catch (err) {
|
|
107
|
+
toast(b.dataset.dg === 'copy' ? `Couldn't copy (${err.message}); use PNG instead` : `Couldn't export: ${err.message}`, 5000);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
package/public/index.html
CHANGED
|
@@ -5,6 +5,13 @@
|
|
|
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
7
|
<title>AI Agent Manager</title>
|
|
8
|
+
<script>
|
|
9
|
+
// The theme chosen in the menu (Auto / Light / Dark), applied before anything draws.
|
|
10
|
+
try {
|
|
11
|
+
const t = localStorage.getItem('agentmgr.theme');
|
|
12
|
+
if (t === 'light' || t === 'dark') document.documentElement.dataset.theme = t;
|
|
13
|
+
} catch {}
|
|
14
|
+
</script>
|
|
8
15
|
<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
16
|
<link rel="manifest" href="/manifest.webmanifest" crossorigin="use-credentials" />
|
|
10
17
|
<meta name="theme-color" content="#151821" />
|
|
@@ -17,7 +24,7 @@
|
|
|
17
24
|
<div class="brand">
|
|
18
25
|
<a href="#" id="home-link" class="home-link" title="Home: all agents"><span class="logo" aria-hidden="true">◆</span><h1>AI Agent Manager</h1></a>
|
|
19
26
|
<button id="about-btn" class="about-btn" type="button" title="About AI Agent Manager: version, release, links">About</button>
|
|
20
|
-
<button id="update-btn" class="update-btn
|
|
27
|
+
<button id="update-btn" class="update-btn" type="button" hidden></button>
|
|
21
28
|
<span id="conn" class="conn" title="Daemon connection">connecting…</span>
|
|
22
29
|
</div>
|
|
23
30
|
<div class="top-actions">
|
|
@@ -42,6 +49,8 @@
|
|
|
42
49
|
<button role="menuitem" id="alerts-btn" class="local-only" title="Slack alerts when an agent needs you">Alerts…</button>
|
|
43
50
|
<button role="menuitem" id="remote-btn" class="local-only" title="Open AI Agent Manager from your phone">Remote access…</button>
|
|
44
51
|
<div class="menu-sep"></div>
|
|
52
|
+
<div class="menu-theme"><span>Theme</span><span class="seg" role="radiogroup" aria-label="Theme"><button type="button" data-theme-set="auto" title="Follow this device's setting">Auto</button><button type="button" data-theme-set="light">Light</button><button type="button" data-theme-set="dark">Dark</button></span></div>
|
|
53
|
+
<div class="menu-sep"></div>
|
|
45
54
|
<button role="menuitem" id="menu-about">About and updates…</button>
|
|
46
55
|
</div>
|
|
47
56
|
</div>
|
|
@@ -369,7 +378,9 @@
|
|
|
369
378
|
<button type="button" id="doc-bg" class="small" hidden title="Close this and keep writing; you'll be told when it's ready">Continue in background</button>
|
|
370
379
|
<button type="button" id="doc-regen" class="small" title="Write it again from the agent's latest work">Regenerate</button>
|
|
371
380
|
<button type="button" id="doc-copy" class="small" title="Copy as Markdown">Copy Markdown</button>
|
|
381
|
+
<span id="doc-listen"></span>
|
|
372
382
|
<button type="button" id="doc-download" class="small">Download .md</button>
|
|
383
|
+
<button type="button" id="doc-html" class="small" hidden title="One HTML file with every diagram: open it anywhere, attach it, or print it">Download .html</button>
|
|
373
384
|
<button type="button" id="doc-print" class="small primary" title="Open a print-ready page (Save as PDF)">Print / PDF</button>
|
|
374
385
|
<button type="button" id="doc-close" class="small ghost">Close</button>
|
|
375
386
|
</div>
|
|
@@ -691,9 +702,9 @@
|
|
|
691
702
|
<dl id="about-list" class="about-list"></dl>
|
|
692
703
|
<p id="about-update" class="hint" hidden></p>
|
|
693
704
|
<div class="dialog-actions">
|
|
694
|
-
<label class="about-auto
|
|
705
|
+
<label class="about-auto" title="Twice a day, ask for the newest release (GitHub for a git checkout, npm otherwise)"><input type="checkbox" id="about-auto" /> Check automatically</label>
|
|
695
706
|
<button type="button" id="about-check" class="ghost" title="Asks for the newest release">Check for updates</button>
|
|
696
|
-
<button type="button" id="about-install" class="primary
|
|
707
|
+
<button type="button" id="about-install" class="primary" hidden>Update and restart</button>
|
|
697
708
|
<button type="button" id="about-close" class="primary">Close</button>
|
|
698
709
|
</div>
|
|
699
710
|
</dialog>
|
|
@@ -712,6 +723,15 @@
|
|
|
712
723
|
<p class="hint">After a restart or a crash, the daemon comes back by itself and running agents resume; without it, it starts only when you run <code>agentmgr</code>. <span id="settings-service-state"></span></p>
|
|
713
724
|
</fieldset>
|
|
714
725
|
<fieldset class="local-only">
|
|
726
|
+
<legend>Jira</legend>
|
|
727
|
+
<p class="hint">For an agent created from a Jira link, its description shows the issue's summary. Public issues on Atlassian Cloud need nothing here; for your company's site, add it with your email and an <a href="https://id.atlassian.com/manage-profile/security/api-tokens" target="_blank" rel="noopener">API token</a>. The token is kept on this computer and sent only to that site.</p>
|
|
728
|
+
<div class="row three">
|
|
729
|
+
<label>Site <input name="jiraSite" placeholder="https://yourcompany.atlassian.net" autocomplete="off" /></label>
|
|
730
|
+
<label>Email <input name="jiraEmail" type="email" autocomplete="off" /></label>
|
|
731
|
+
<label>API token <input name="jiraToken" type="password" autocomplete="new-password" /></label>
|
|
732
|
+
</div>
|
|
733
|
+
</fieldset>
|
|
734
|
+
<fieldset>
|
|
715
735
|
<legend>Updates</legend>
|
|
716
736
|
<label class="check"><input type="checkbox" name="updateCheck" /> Check for a new release twice a day</label>
|
|
717
737
|
<p class="hint">Shows <b>Update to …</b> in the top bar; one click installs it and restarts, and running agents resume.</p>
|
package/public/menu.js
CHANGED
|
@@ -22,7 +22,8 @@ export function initMenu({ api, esc, toast, $ }) {
|
|
|
22
22
|
});
|
|
23
23
|
// Any item closes the menu; its own handler does the rest.
|
|
24
24
|
menu.addEventListener('click', (e) => {
|
|
25
|
-
|
|
25
|
+
// The theme buttons keep it open, to compare.
|
|
26
|
+
if (e.target.closest('button') && !e.target.closest('[data-theme-set]')) open(false);
|
|
26
27
|
});
|
|
27
28
|
$('#menu-about').onclick = () => $('#about-btn').click();
|
|
28
29
|
|
|
@@ -53,6 +54,7 @@ export function initMenu({ api, esc, toast, $ }) {
|
|
|
53
54
|
const dialog = $('#settings-dialog');
|
|
54
55
|
const form = $('#settings-form');
|
|
55
56
|
let service = null; // the login service as the dialog opened
|
|
57
|
+
let jira = null; // the Jira settings as the dialog opened
|
|
56
58
|
$('#settings-btn').onclick = async () => {
|
|
57
59
|
$('#settings-error').hidden = true;
|
|
58
60
|
try {
|
|
@@ -60,9 +62,14 @@ export function initMenu({ api, esc, toast, $ }) {
|
|
|
60
62
|
form.maxRunning.value = c.maxRunning ?? '';
|
|
61
63
|
$('#settings-running').textContent = `Open now: ${c.running}.`;
|
|
62
64
|
service = null;
|
|
65
|
+
const u = await api('GET', '/update').catch(() => null);
|
|
66
|
+
form.updateCheck.checked = u?.check !== false;
|
|
63
67
|
if (!remote()) {
|
|
64
|
-
|
|
65
|
-
form.
|
|
68
|
+
jira = await api('GET', '/jira').catch(() => null);
|
|
69
|
+
form.jiraSite.value = jira?.site ?? '';
|
|
70
|
+
form.jiraEmail.value = jira?.email ?? '';
|
|
71
|
+
form.jiraToken.value = '';
|
|
72
|
+
form.jiraToken.placeholder = jira?.hasToken ? 'saved (type to replace)' : '';
|
|
66
73
|
service = await api('GET', '/service').catch(() => null);
|
|
67
74
|
$('#settings-service').hidden = !service?.supported;
|
|
68
75
|
form.startAtLogin.checked = Boolean(service?.installed);
|
|
@@ -84,7 +91,11 @@ export function initMenu({ api, esc, toast, $ }) {
|
|
|
84
91
|
e.preventDefault();
|
|
85
92
|
try {
|
|
86
93
|
const c = await api('PUT', '/cleanup', { maxRunning: form.maxRunning.value.trim() || null });
|
|
87
|
-
|
|
94
|
+
await api('PUT', '/update', { check: form.updateCheck.checked }).catch(() => {});
|
|
95
|
+
if (!remote() && jira && (form.jiraSite.value.trim() !== (jira.site ?? '') || form.jiraEmail.value.trim() !== (jira.email ?? '') || form.jiraToken.value.trim())) {
|
|
96
|
+
await api('PUT', '/jira', { site: form.jiraSite.value.trim(), email: form.jiraEmail.value.trim(), token: form.jiraToken.value.trim() || undefined });
|
|
97
|
+
toast('Jira saved; summaries for agents from Jira links are being fetched', 4000);
|
|
98
|
+
}
|
|
88
99
|
// Only when it changed: installing touches launchd / systemd.
|
|
89
100
|
if (!remote() && service?.supported && form.startAtLogin.checked !== Boolean(service.installed)) {
|
|
90
101
|
const s = await api('PUT', '/service', { enabled: form.startAtLogin.checked });
|
|
@@ -98,6 +109,31 @@ export function initMenu({ api, esc, toast, $ }) {
|
|
|
98
109
|
}
|
|
99
110
|
});
|
|
100
111
|
|
|
112
|
+
// ------------------------------------------------------------ Theme
|
|
113
|
+
const THEME_KEY = 'agentmgr.theme';
|
|
114
|
+
const theme = () => {
|
|
115
|
+
try {
|
|
116
|
+
return localStorage.getItem(THEME_KEY) || 'auto';
|
|
117
|
+
} catch {
|
|
118
|
+
return 'auto';
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
function applyTheme(t) {
|
|
122
|
+
if (t === 'light' || t === 'dark') document.documentElement.dataset.theme = t;
|
|
123
|
+
else delete document.documentElement.dataset.theme;
|
|
124
|
+
for (const b of menu.querySelectorAll('[data-theme-set]')) b.classList.toggle('on', b.dataset.themeSet === t);
|
|
125
|
+
window.dispatchEvent(new CustomEvent('agentmgr-theme'));
|
|
126
|
+
}
|
|
127
|
+
menu.addEventListener('click', (e) => {
|
|
128
|
+
const b = e.target.closest('[data-theme-set]');
|
|
129
|
+
if (!b) return;
|
|
130
|
+
try {
|
|
131
|
+
localStorage.setItem(THEME_KEY, b.dataset.themeSet);
|
|
132
|
+
} catch {}
|
|
133
|
+
applyTheme(b.dataset.themeSet);
|
|
134
|
+
});
|
|
135
|
+
applyTheme(theme());
|
|
136
|
+
|
|
101
137
|
loadMe();
|
|
102
138
|
return { loadMe };
|
|
103
139
|
}
|