wisp-agents 0.11.0 → 0.12.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 +16 -1
- package/README.md +3 -3
- package/package.json +1 -1
- package/server/adapters/index.js +4 -1
- package/server/index.js +6 -1
- package/server/machines.js +18 -1
- package/server/openapi.js +2 -1
- package/web/css/app.css +15 -2
- package/web/js/app.js +57 -4
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.12.0] - 2026-10-18
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Settings → Agents → Re-check** looks for harnesses and models again, on this computer and on every online machine. Install a CLI, sign in or add a provider (say a new opencode provider), come back to wisp and click it: the list, the model counts and the launcher's harness picker update without restarting wisp, and a toast says what changed ("opencode +12 models", "Codex found"). Each harness now shows how many models it has, and one that isn't installed says to install its CLI and sign in. Before, wisp kept the first detection until it restarted. API: `POST /api/agents/recheck`, or `GET /api/agents?fresh=1` for this computer only.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- The calm feed (nothing needs you) looks tidier. **Nothing needs you** gets a green dot and lines up with the **Done** and **Archived** headings, and folded groups now sit close together instead of a full section gap apart. Folded headings highlight on hover so they read as buttons.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- In **Settings → Machines**, the add/edit machine form lays out properly again: labels sit left above their fields, and the **SSH host** and **Name** inputs are full height instead of squashed and centred. The Settings row style was leaking into it.
|
|
21
|
+
|
|
8
22
|
## [0.11.0] - 2026-10-18
|
|
9
23
|
|
|
10
24
|
### Added
|
|
@@ -206,7 +220,8 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
|
206
220
|
- Session queue with follow-ups, answers to agent questions, terminal takeover and mark-as-done.
|
|
207
221
|
- Older turns are folded, with a floating collapse-all button and back-to-top.
|
|
208
222
|
|
|
209
|
-
[Unreleased]: https://github.com/M0xoo/wisp/compare/v0.
|
|
223
|
+
[Unreleased]: https://github.com/M0xoo/wisp/compare/v0.12.0...HEAD
|
|
224
|
+
[0.12.0]: https://github.com/M0xoo/wisp/compare/v0.11.0...v0.12.0
|
|
210
225
|
[0.11.0]: https://github.com/M0xoo/wisp/compare/v0.10.0...v0.11.0
|
|
211
226
|
[0.10.0]: https://github.com/M0xoo/wisp/compare/v0.9.0...v0.10.0
|
|
212
227
|
[0.9.0]: https://github.com/M0xoo/wisp/compare/v0.8.0...v0.9.0
|
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ From there you can **follow up**, **answer its questions**, **take it over** in
|
|
|
53
53
|
| 🤝 **Subagents** | Give the lead agent a team on other harnesses (e.g. Claude Code hands tests to Codex ×3). It delegates through a `delegate` tool; their edits land in its worktree. See [Subagents](#subagents). |
|
|
54
54
|
| ⏪ **Checkpoints & rollback** | Every turn is snapshotted first. Restore to before any turn, revert a single file, or re-run a command block from the presentation. |
|
|
55
55
|
| ⚖️ **Compare** | Run one task on two harnesses side by side, and keep the better run. |
|
|
56
|
-
| 🧾 **Recipes & triggers** | Saved prompts with `{params}` (`wisp run release 0.
|
|
56
|
+
| 🧾 **Recipes & triggers** | Saved prompts with `{params}` (`wisp run release 0.12.0`), run by hand, on a schedule, when CI fails, or when an issue is labelled. |
|
|
57
57
|
| 📊 **Usage** | Spend, success rate and questions per project and per harness · model. |
|
|
58
58
|
| 📱 **Phone** | Questions and finished tasks on ntfy or Telegram; answer from there. |
|
|
59
59
|
| 🔐 **Permission modes** | Choose **Plan** (read-only), **Edits** (file edits auto-accepted), or **Full auto** for each launch. `Shift+Tab` in the prompt cycles modes. |
|
|
@@ -125,7 +125,7 @@ wisp reply <id> "also do X" # follow up (answers a pending question)
|
|
|
125
125
|
wisp stop <id>
|
|
126
126
|
wisp ship <id> [--draft] # commit, push and open a PR for a worktree task
|
|
127
127
|
wisp merge <id> # squash-merge its PR, fast-forward your local main
|
|
128
|
-
wisp run release 0.
|
|
128
|
+
wisp run release 0.12.0 # run a recipe; values fill its {params} (or version=0.12.0)
|
|
129
129
|
wisp recipes # list recipes
|
|
130
130
|
wisp usage [--days 7] # spend and outcomes
|
|
131
131
|
wisp mcp # MCP server for other agents and editors
|
|
@@ -305,7 +305,7 @@ Before each turn, wisp snapshots the git working tree (pinned at `refs/wisp/chec
|
|
|
305
305
|
Prepare release {version}: update CHANGELOG, README and docs/, bump package.json, tag v{version}.
|
|
306
306
|
```
|
|
307
307
|
|
|
308
|
-
Make one from ⌘K → *Recipes*, or *Save as recipe…* on any card. A recipe may pin a project, harness, model and mode. Run it from ⌘K (*Run: Release*), from the Recipes view, or from a terminal: `wisp run release 0.
|
|
308
|
+
Make one from ⌘K → *Recipes*, or *Save as recipe…* on any card. A recipe may pin a project, harness, model and mode. Run it from ⌘K (*Run: Release*), from the Recipes view, or from a terminal: `wisp run release 0.12.0` fills the fields in order, `wisp run release version=0.12.0` by name.
|
|
309
309
|
|
|
310
310
|
**Triggers** launch sessions on their own while wisp is open (⌘K → *Triggers*):
|
|
311
311
|
|
package/package.json
CHANGED
package/server/adapters/index.js
CHANGED
|
@@ -35,7 +35,10 @@ export const adapters = Object.fromEntries([claude, codex, opencode, cursor, ant
|
|
|
35
35
|
|
|
36
36
|
const detected = new Map();
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
// fresh: detect again even where a hit is cached (Settings → Agents → Re-check), so a newly
|
|
39
|
+
// signed-in provider's models or an upgraded CLI show up without restarting wisp.
|
|
40
|
+
export async function listAgents({ fresh = false } = {}) {
|
|
41
|
+
if (fresh) detected.clear();
|
|
39
42
|
return Promise.all(
|
|
40
43
|
Object.values(adapters).map(async (a) => {
|
|
41
44
|
if (!detected.has(a.id)) detected.set(a.id, a.detect());
|
package/server/index.js
CHANGED
|
@@ -429,7 +429,12 @@ export async function startServer({ port = 4477, host = '127.0.0.1', handleSigna
|
|
|
429
429
|
}
|
|
430
430
|
if (root === 'openapi.json' && m === 'GET') return send(res, 200, openapi({ version: VERSION, port }));
|
|
431
431
|
|
|
432
|
-
if (root === 'agents' && m === 'GET') return send(res, 200, await listAgents());
|
|
432
|
+
if (root === 'agents' && m === 'GET') return send(res, 200, await listAgents({ fresh: url.searchParams.has('fresh') }));
|
|
433
|
+
// Re-check: detect harnesses and models again, here and on every online machine.
|
|
434
|
+
if (root === 'agents' && id === 'recheck' && m === 'POST') {
|
|
435
|
+
const [agents] = await Promise.all([listAgents({ fresh: true }), machines.recheckAgents()]);
|
|
436
|
+
return send(res, 200, agents);
|
|
437
|
+
}
|
|
433
438
|
|
|
434
439
|
// Bridge for the ask_user MCP server running inside an agent process.
|
|
435
440
|
if (root === 'internal') {
|
package/server/machines.js
CHANGED
|
@@ -156,6 +156,9 @@ export function sshHosts(file = path.join(os.homedir(), '.ssh', 'config'), seen
|
|
|
156
156
|
return [...uniq.values()];
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
// What the UI needs of a machine's harness list.
|
|
160
|
+
const remoteAgent = ({ id, name, available, version, models, efforts, delegates }) => ({ id, name, available, version, models, efforts, delegates });
|
|
161
|
+
|
|
159
162
|
// Code hosts you push to, not computers to run agents on.
|
|
160
163
|
const FORGE = /(^|\.)(github\.com|gitlab\.com|bitbucket\.org|ssh\.dev\.azure\.com|vs-ssh\.visualstudio\.com|heroku\.com|codeberg\.org|sr\.ht)$/i;
|
|
161
164
|
|
|
@@ -437,7 +440,7 @@ export class Machines extends EventEmitter {
|
|
|
437
440
|
this.#step(c, 'tunnel', 'done', `localhost:${c.localPort} → ${m.port}`);
|
|
438
441
|
const [agents, config] = await Promise.all([this.#raw(m, c, 'GET', '/agents').catch(() => []), this.#raw(m, c, 'GET', '/config').catch(() => ({}))]);
|
|
439
442
|
const ready = agents.filter((a) => a.available);
|
|
440
|
-
c.remote = { ...c.remote, version: health.version, pid: health.pid, home: config.home || '', agents: agents.map(
|
|
443
|
+
c.remote = { ...c.remote, version: health.version, pid: health.pid, home: config.home || '', agents: agents.map(remoteAgent) };
|
|
441
444
|
this.#step(c, 'agents', ready.length ? 'done' : 'warn', ready.length ? ready.map((a) => a.name).join(', ') : 'None found. Install a harness CLI there (claude, codex, opencode…) and sign in.');
|
|
442
445
|
clearTimeout(c.readyTimer);
|
|
443
446
|
Object.assign(c, { state: 'online', error: '', hint: null, attempt: 0, wasOnline: true, since: Date.now() });
|
|
@@ -447,6 +450,20 @@ export class Machines extends EventEmitter {
|
|
|
447
450
|
this.emit('online', m);
|
|
448
451
|
}
|
|
449
452
|
|
|
453
|
+
// Detect harnesses and models again on every online machine (after installing or signing in
|
|
454
|
+
// to something there). A machine that doesn't answer keeps what it had.
|
|
455
|
+
async recheckAgents() {
|
|
456
|
+
const online = this.list.map((m) => [m, this.conns.get(m.id)]).filter(([, c]) => c?.state === 'online');
|
|
457
|
+
await Promise.all(
|
|
458
|
+
online.map(async ([m, c]) => {
|
|
459
|
+
const agents = await this.#raw(m, c, 'GET', '/agents?fresh=1').catch(() => null);
|
|
460
|
+
if (!Array.isArray(agents) || this.conns.get(m.id) !== c) return;
|
|
461
|
+
c.remote = { ...c.remote, agents: agents.map(remoteAgent) };
|
|
462
|
+
}),
|
|
463
|
+
);
|
|
464
|
+
if (online.length) this.onChange();
|
|
465
|
+
}
|
|
466
|
+
|
|
450
467
|
#fail(c, msg, hint, { keep = false } = {}) {
|
|
451
468
|
if (this.conns.get(c.id) !== c) return;
|
|
452
469
|
if (c.error && !keep) return;
|
package/server/openapi.js
CHANGED
|
@@ -44,7 +44,8 @@ export function openapi({ version, port }) {
|
|
|
44
44
|
responses: { 200: json({ type: 'object' }), 400: err[400] },
|
|
45
45
|
},
|
|
46
46
|
},
|
|
47
|
-
'/agents': { get: { summary: 'Installed harnesses with their models and thinking levels', responses: { 200: json({ type: 'array', items: ref('Agent') }) } } },
|
|
47
|
+
'/agents': { get: { summary: 'Installed harnesses with their models and thinking levels (?fresh=1 detects again)', responses: { 200: json({ type: 'array', items: ref('Agent') }) } } },
|
|
48
|
+
'/agents/recheck': { post: { summary: 'Detect harnesses and models again, here and on every online machine', responses: { 200: json({ type: 'array', items: ref('Agent') }) } } },
|
|
48
49
|
'/config': {
|
|
49
50
|
get: { summary: 'Launcher defaults, projects, webhooks', responses: { 200: json({ type: 'object' }) } },
|
|
50
51
|
patch: {
|
package/web/css/app.css
CHANGED
|
@@ -434,13 +434,13 @@ body:has(.launcher.docked:is(:focus-within, :has(.open))) .to-top { display: non
|
|
|
434
434
|
}
|
|
435
435
|
|
|
436
436
|
/* ---------- feed ---------- */
|
|
437
|
-
.feed-head { display: flex; align-items: center; gap: 14px; min-height: 22px; margin: 26px
|
|
438
|
-
.fh-status { display: flex; gap: 14px; }
|
|
437
|
+
.feed-head { display: flex; align-items: center; gap: 14px; min-height: 22px; margin: 26px 0 10px; padding: 0 4px; font-size: 13px; }
|
|
439
438
|
.feed-head[hidden] + .feed { margin-top: 26px; }
|
|
440
439
|
.hs { display: inline-flex; align-items: center; gap: 7px; color: var(--ink-2); }
|
|
441
440
|
.hs i { width: 7px; height: 7px; border-radius: 50%; }
|
|
442
441
|
.hs.working i { background: var(--accent); animation: pulse 1.6s infinite; }
|
|
443
442
|
.hs.needs i { background: var(--warn); }
|
|
443
|
+
.hs.calm i { background: var(--good); }
|
|
444
444
|
|
|
445
445
|
.f-group { margin-bottom: 22px; }
|
|
446
446
|
.f-group h4 {
|
|
@@ -462,6 +462,11 @@ body:has(.launcher.docked:is(:focus-within, :has(.open))) .to-top { display: non
|
|
|
462
462
|
.toggle-done:hover { color: var(--ink-2); }
|
|
463
463
|
.toggle-done .chev { width: 12px; height: 12px; transition: transform 0.2s; }
|
|
464
464
|
.f-group.done:not(.collapsed) .chev { transform: rotate(90deg); }
|
|
465
|
+
/* Folded groups are just a row of headings: stack them tight instead of leaving a section gap each. */
|
|
466
|
+
.f-group.collapsed { margin-bottom: 2px; }
|
|
467
|
+
.f-group.collapsed h4 { margin: 0; }
|
|
468
|
+
.f-group h4.toggle-done { width: fit-content; min-height: 24px; margin-left: -4px; padding: 0 8px; border-radius: 6px; transition: background 0.15s, color 0.15s; }
|
|
469
|
+
.f-group h4.toggle-done:hover { background: var(--bg-2); }
|
|
465
470
|
.f-list { display: flex; flex-direction: column; gap: 6px; }
|
|
466
471
|
.feed-empty { padding: 48px 0; text-align: center; color: var(--faint); font-size: 13.5px; }
|
|
467
472
|
|
|
@@ -1200,6 +1205,8 @@ pre.code { margin: 0 0 12px; padding: 12px 14px; border-radius: 10px; background
|
|
|
1200
1205
|
.sx-ctl .input { width: 300px; }
|
|
1201
1206
|
.sx-state { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--muted); }
|
|
1202
1207
|
.sx-state.bad { color: var(--bad); }
|
|
1208
|
+
#recheck.busy { cursor: progress; }
|
|
1209
|
+
#recheck.busy .i { animation: spin 0.8s linear infinite reverse; }
|
|
1203
1210
|
.sx-usage .it-loading { grid-column: 1 / -1; }
|
|
1204
1211
|
/* a view (Memory, Automations, Usage, Phone) opened inside Settings: no dialog chrome of its own */
|
|
1205
1212
|
.modal.embedded { width: auto; max-height: none; overflow: visible; padding: 0; border: 0; border-radius: 0; box-shadow: none; background: none; transform: none; }
|
|
@@ -1476,6 +1483,12 @@ pre.code { margin: 0 0 12px; padding: 12px 14px; border-radius: 10px; background
|
|
|
1476
1483
|
.mc-adv summary { cursor: pointer; width: max-content; font-size: 12.5px; color: var(--muted); }
|
|
1477
1484
|
.mc-adv[open] summary { margin-bottom: 8px; }
|
|
1478
1485
|
.mc-check { margin-top: 8px; }
|
|
1486
|
+
/* Hosted inside Settings: undo its row styling for labels (centred, padded, bordered, input flexed to 0 tall). */
|
|
1487
|
+
.mc-form label { padding: 0; border-bottom: 0; }
|
|
1488
|
+
.mc-form .mc-field { align-items: stretch; justify-content: flex-start; gap: 5px; }
|
|
1489
|
+
.mc-form .mc-field > span { text-align: left; }
|
|
1490
|
+
.mc-form .mc-field > .input { flex: none; max-width: none; }
|
|
1491
|
+
.mc-form .mc-check { display: inline-flex; justify-content: flex-start; gap: 8px; width: max-content; }
|
|
1479
1492
|
.mc-foot { display: flex; align-items: center; gap: 10px; }
|
|
1480
1493
|
.mc-foot .hint { flex: 1; margin: 0 !important; font-size: 12px !important; }
|
|
1481
1494
|
.mc-foot .primary { flex: none; }
|
package/web/js/app.js
CHANGED
|
@@ -269,7 +269,7 @@ function renderFeedHead(needs, working) {
|
|
|
269
269
|
if (working.length) parts.push(`<span class="hs working"><i></i>${working.length} working</span>`);
|
|
270
270
|
if (needs.length) parts.push(`<span class="hs needs"><i></i>${needs.length} need${needs.length === 1 ? 's' : ''} you</span>`);
|
|
271
271
|
// Group headings already carry the counts; only say something when the feed is calm.
|
|
272
|
-
$('#feedHead').innerHTML = parts.length ? '' : '<span class="
|
|
272
|
+
$('#feedHead').innerHTML = parts.length ? '' : '<span class="hs calm"><i></i>Nothing needs you</span>';
|
|
273
273
|
$('#feedHead').hidden = !!parts.length;
|
|
274
274
|
placeCollapseAll();
|
|
275
275
|
}
|
|
@@ -4020,6 +4020,22 @@ const SETTINGS_PANES = [
|
|
|
4020
4020
|
];
|
|
4021
4021
|
let settingsPane = 'general';
|
|
4022
4022
|
|
|
4023
|
+
// What a re-check turned up: harnesses found or gone, and model lists that grew or shrank.
|
|
4024
|
+
function recheckSummary(before, after) {
|
|
4025
|
+
const count = (a) => (a?.models || []).filter((m) => m.id).length;
|
|
4026
|
+
const notes = [];
|
|
4027
|
+
for (const a of after) {
|
|
4028
|
+
const was = before.get(a.id);
|
|
4029
|
+
if (a.available && !was?.available) notes.push(`<b>${esc(a.name)}</b> found`);
|
|
4030
|
+
else if (!a.available && was?.available) notes.push(`<b>${esc(a.name)}</b> not found`);
|
|
4031
|
+
else if (a.available && count(a) !== count(was)) {
|
|
4032
|
+
const d = count(a) - count(was);
|
|
4033
|
+
notes.push(`<b>${esc(a.name)}</b> ${d > 0 ? '+' : '−'}${Math.abs(d)} model${Math.abs(d) === 1 ? '' : 's'}`);
|
|
4034
|
+
} else if (a.available && a.version !== was?.version) notes.push(`<b>${esc(a.name)}</b> updated`);
|
|
4035
|
+
}
|
|
4036
|
+
return notes.length ? notes.join(' · ') : 'Up to date: nothing new';
|
|
4037
|
+
}
|
|
4038
|
+
|
|
4023
4039
|
function openSettings(opts) {
|
|
4024
4040
|
if (SETTINGS_PANES.some((p) => p.id === opts?.pane)) settingsPane = opts.pane;
|
|
4025
4041
|
const d = S.config.defaults;
|
|
@@ -4056,9 +4072,10 @@ function openSettings(opts) {
|
|
|
4056
4072
|
</div>
|
|
4057
4073
|
</section>
|
|
4058
4074
|
<section class="sx-pane" data-pane="agents">
|
|
4059
|
-
<div class="sx-group"
|
|
4060
|
-
|
|
4061
|
-
|
|
4075
|
+
<div class="sx-group sx-agents"></div>
|
|
4076
|
+
<div class="sx-group">
|
|
4077
|
+
${row('Installed something new?', '<span class="recheck-hint"></span>', `<button class="ghost sm" id="recheck">${icon('replay')}Re-check</button>`)}
|
|
4078
|
+
</div>
|
|
4062
4079
|
</section>
|
|
4063
4080
|
<section class="sx-pane" data-pane="machines">
|
|
4064
4081
|
<div class="sx-group">
|
|
@@ -4092,6 +4109,41 @@ function openSettings(opts) {
|
|
|
4092
4109
|
</div>`);
|
|
4093
4110
|
const close = overlay(node);
|
|
4094
4111
|
$('.close', node).onclick = close;
|
|
4112
|
+
const paintAgents = () => {
|
|
4113
|
+
$('.sx-agents', node).innerHTML = S.agents.length
|
|
4114
|
+
? S.agents.map((a) => {
|
|
4115
|
+
const n = (a.models || []).filter((m) => m.id).length;
|
|
4116
|
+
const desc = a.available ? [a.version, n && `${n} model${n === 1 ? '' : 's'}`].filter(Boolean).join(' · ') : `Install the ${a.name} CLI and sign in, then re-check`;
|
|
4117
|
+
return row(`<span class="model-logo">${harnessLogo(a.id)}</span>${esc(a.name)}`, esc(desc), `<span class="sx-state ${a.available ? 'ok' : 'bad'}" ${a.error ? `title="${esc(a.error)}"` : ''}><i class="dot ${a.available ? 'ok' : 'bad'}"></i>${a.available ? 'Ready' : 'Not found'}</span>`);
|
|
4118
|
+
}).join('')
|
|
4119
|
+
: '<div class="sx-row"><div class="it-loading"><span class="orb tiny"></span>Looking for harnesses…</div></div>';
|
|
4120
|
+
const missing = S.agents.filter((a) => !a.available).length;
|
|
4121
|
+
const dot = $('.sx-nav [data-pane="agents"] .dot', node);
|
|
4122
|
+
if (dot && !missing) dot.remove();
|
|
4123
|
+
if (!dot && missing) $('.sx-nav [data-pane="agents"]', node).insertAdjacentHTML('beforeend', `<i class="dot bad" title="${missing} not found"></i>`);
|
|
4124
|
+
const online = S.machines.filter((m) => m.state === 'online').length;
|
|
4125
|
+
$('.recheck-hint', node).textContent = `After installing a CLI, signing in or adding a provider, look for harnesses and models again${online ? `, here and on ${online} machine${online === 1 ? '' : 's'}` : ''}`;
|
|
4126
|
+
};
|
|
4127
|
+
paintAgents();
|
|
4128
|
+
node.paintAgents = paintAgents;
|
|
4129
|
+
$('#recheck', node).onclick = async (e) => {
|
|
4130
|
+
const btn = e.currentTarget;
|
|
4131
|
+
btn.disabled = true;
|
|
4132
|
+
btn.classList.add('busy');
|
|
4133
|
+
const before = new Map(S.agents.map((a) => [a.id, a]));
|
|
4134
|
+
try {
|
|
4135
|
+
S.agents = await api('/agents/recheck', { method: 'POST', body: {} });
|
|
4136
|
+
if (!node.isConnected) return;
|
|
4137
|
+
paintAgents();
|
|
4138
|
+
$('#launcher')?.placesChanged?.();
|
|
4139
|
+
toast(recheckSummary(before, S.agents));
|
|
4140
|
+
} catch (err) {
|
|
4141
|
+
toast(esc(err.message), { tone: 'bad' });
|
|
4142
|
+
} finally {
|
|
4143
|
+
btn.disabled = false;
|
|
4144
|
+
btn.classList.remove('busy');
|
|
4145
|
+
}
|
|
4146
|
+
};
|
|
4095
4147
|
const pick = (id, focus = false) => {
|
|
4096
4148
|
settingsPane = id;
|
|
4097
4149
|
const p = SETTINGS_PANES.find((x) => x.id === id);
|
|
@@ -4820,6 +4872,7 @@ window.addEventListener('focus', () => {
|
|
|
4820
4872
|
(async () => {
|
|
4821
4873
|
[S.agents] = await Promise.all([api('/agents').catch(() => []), loadConfig(), loadSessions(), loadMachines()]);
|
|
4822
4874
|
renderLauncher();
|
|
4875
|
+
$('.settings-modal')?.paintAgents?.();
|
|
4823
4876
|
connect();
|
|
4824
4877
|
route();
|
|
4825
4878
|
$('#prompt')?.focus();
|