@floless/app 0.26.0 → 0.28.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/dist/floless-server.cjs +40 -15
- package/dist/web/app.css +47 -1
- package/dist/web/app.js +55 -0
- package/dist/web/aware.js +125 -26
- package/package.json +1 -1
package/dist/floless-server.cjs
CHANGED
|
@@ -51710,8 +51710,11 @@ function parseBuildOutput(stdout) {
|
|
|
51710
51710
|
function agentInstallArgv(id) {
|
|
51711
51711
|
return ["agent", "install", assertId(id)];
|
|
51712
51712
|
}
|
|
51713
|
+
function agentUpdateArgv(id) {
|
|
51714
|
+
return ["agent", "update", assertId(id)];
|
|
51715
|
+
}
|
|
51713
51716
|
function parseAgentInstallOutput(stdout) {
|
|
51714
|
-
const m = stdout.match(/✓\s+installed\s+(\S+)/);
|
|
51717
|
+
const m = stdout.match(/✓\s+(?:installed|updated)\s+(\S+)/);
|
|
51715
51718
|
return { installed: m?.[1] ?? null };
|
|
51716
51719
|
}
|
|
51717
51720
|
function isAgentInstalled(agents, id) {
|
|
@@ -52023,6 +52026,18 @@ var aware = {
|
|
|
52023
52026
|
if (isAgentInstalled(agents, id)) return { installed: id, output: "already installed" };
|
|
52024
52027
|
return this.agentInstall(id);
|
|
52025
52028
|
},
|
|
52029
|
+
/**
|
|
52030
|
+
* Update an installed agent to the latest registry version (`aware agent update
|
|
52031
|
+
* <id>`). Unlike `ensureAgentInstalled`, this always re-pulls — it is the Agent
|
|
52032
|
+
* Library's in-place "Update" action for a card whose installed version trails the
|
|
52033
|
+
* catalog. Like install, the pull fetches a tarball (slow), so use the generous
|
|
52034
|
+
* timeout. The token is NOT involved — only the agent manifest is fetched.
|
|
52035
|
+
*/
|
|
52036
|
+
async agentUpdate(id) {
|
|
52037
|
+
const { code, stdout, stderr } = await runRawWithEnv(agentUpdateArgv(id), void 0, 6e5);
|
|
52038
|
+
if (code !== 0) throw new AwareError(`aware agent update ${id} failed (exit ${code})`, { stdout, stderr });
|
|
52039
|
+
return { ...parseAgentInstallOutput(stdout), output: stdout.trim() };
|
|
52040
|
+
},
|
|
52026
52041
|
/**
|
|
52027
52042
|
* POST /api/run — installed ID. `run` ignores --json and prints
|
|
52028
52043
|
* "✓ run complete; trace at <path>"; we parse the path and read the JSONL.
|
|
@@ -52777,7 +52792,7 @@ function appVersion() {
|
|
|
52777
52792
|
return resolveVersion({
|
|
52778
52793
|
isSea: isSea2(),
|
|
52779
52794
|
sqVersionXml: readSqVersionXml(),
|
|
52780
|
-
define: true ? "0.
|
|
52795
|
+
define: true ? "0.28.0" : void 0,
|
|
52781
52796
|
pkgVersion: readPkgVersion()
|
|
52782
52797
|
});
|
|
52783
52798
|
}
|
|
@@ -52787,7 +52802,7 @@ function resolveChannel(s) {
|
|
|
52787
52802
|
return "dev";
|
|
52788
52803
|
}
|
|
52789
52804
|
function appChannel() {
|
|
52790
|
-
return resolveChannel({ isSea: isSea2(), define: true ? "0.
|
|
52805
|
+
return resolveChannel({ isSea: isSea2(), define: true ? "0.28.0" : void 0 });
|
|
52791
52806
|
}
|
|
52792
52807
|
|
|
52793
52808
|
// oauth-presets.ts
|
|
@@ -57673,28 +57688,38 @@ async function startServer() {
|
|
|
57673
57688
|
throw err;
|
|
57674
57689
|
}
|
|
57675
57690
|
});
|
|
57676
|
-
const
|
|
57677
|
-
let
|
|
57678
|
-
|
|
57679
|
-
|
|
57680
|
-
|
|
57681
|
-
if (installingAgents.has(id)) return reply.status(202).send({ ok: true, started: true });
|
|
57682
|
-
installingAgents.add(id);
|
|
57683
|
-
installQueue = installQueue.then(async () => {
|
|
57691
|
+
const agentJobsInFlight = /* @__PURE__ */ new Set();
|
|
57692
|
+
let agentJobQueue = Promise.resolve();
|
|
57693
|
+
function startAgentJob(id, action, run) {
|
|
57694
|
+
agentJobsInFlight.add(id);
|
|
57695
|
+
agentJobQueue = agentJobQueue.then(async () => {
|
|
57684
57696
|
let result;
|
|
57685
57697
|
try {
|
|
57686
|
-
const res = await
|
|
57687
|
-
result = { type: "agent-install-result", id, ok: true, installed: res.installed ?? id };
|
|
57698
|
+
const res = await run();
|
|
57699
|
+
result = { type: "agent-install-result", action, id, ok: true, installed: res.installed ?? id };
|
|
57688
57700
|
} catch (err) {
|
|
57689
|
-
result = { type: "agent-install-result", id, ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
57701
|
+
result = { type: "agent-install-result", action, id, ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
57690
57702
|
} finally {
|
|
57691
|
-
|
|
57703
|
+
agentJobsInFlight.delete(id);
|
|
57692
57704
|
}
|
|
57693
57705
|
try {
|
|
57694
57706
|
broadcast(result);
|
|
57695
57707
|
} catch {
|
|
57696
57708
|
}
|
|
57697
57709
|
});
|
|
57710
|
+
}
|
|
57711
|
+
app.post("/api/agents/install", async (req, reply) => {
|
|
57712
|
+
const id = req.body?.id;
|
|
57713
|
+
if (!id || typeof id !== "string") return reply.status(400).send({ ok: false, error: "id is required" });
|
|
57714
|
+
if (agentJobsInFlight.has(id)) return reply.status(202).send({ ok: true, started: true });
|
|
57715
|
+
startAgentJob(id, "install", () => aware.ensureAgentInstalled(id));
|
|
57716
|
+
return reply.status(202).send({ ok: true, started: true });
|
|
57717
|
+
});
|
|
57718
|
+
app.post("/api/agents/update", async (req, reply) => {
|
|
57719
|
+
const id = req.body?.id;
|
|
57720
|
+
if (!id || typeof id !== "string") return reply.status(400).send({ ok: false, error: "id is required" });
|
|
57721
|
+
if (agentJobsInFlight.has(id)) return reply.status(202).send({ ok: true, started: true });
|
|
57722
|
+
startAgentJob(id, "update", () => aware.agentUpdate(id));
|
|
57698
57723
|
return reply.status(202).send({ ok: true, started: true });
|
|
57699
57724
|
});
|
|
57700
57725
|
app.get("/api/integrations", async () => {
|
package/dist/web/app.css
CHANGED
|
@@ -344,6 +344,9 @@
|
|
|
344
344
|
overflow: hidden;
|
|
345
345
|
min-width: 0;
|
|
346
346
|
position: relative;
|
|
347
|
+
/* Touch: the canvas is a one-finger-pan + pinch-zoom surface on phones (handled in app.js);
|
|
348
|
+
touch-action:none stops the browser claiming the gesture first. .dashboard re-enables scroll. */
|
|
349
|
+
touch-action: none;
|
|
347
350
|
/* The whole canvas is a pannable infinite surface (drag the background / middle-mouse),
|
|
348
351
|
so the grab affordance covers the entire grid-area — not just .topology, which is
|
|
349
352
|
sized around the node cards and left distant background areas as a plain arrow (#115).
|
|
@@ -1165,6 +1168,17 @@
|
|
|
1165
1168
|
text-overflow: ellipsis;
|
|
1166
1169
|
white-space: nowrap;
|
|
1167
1170
|
}
|
|
1171
|
+
/* Version as a small dim pill on the meta line — matches the canvas node-card
|
|
1172
|
+
`.ver` token, so the version no longer competes with the agent name (#119). */
|
|
1173
|
+
.lib-item .info .meta .lib-ver {
|
|
1174
|
+
font-family: var(--mono);
|
|
1175
|
+
font-size: 9px;
|
|
1176
|
+
color: var(--text-dim);
|
|
1177
|
+
background: var(--surface-2);
|
|
1178
|
+
padding: 1px 5px;
|
|
1179
|
+
border-radius: 3px;
|
|
1180
|
+
margin-right: 4px;
|
|
1181
|
+
}
|
|
1168
1182
|
.lib-item .info .blurb { font-size: 11px; color: var(--text-muted); line-height: 1.4; }
|
|
1169
1183
|
.lib-item .lib-fav {
|
|
1170
1184
|
background: transparent;
|
|
@@ -1180,6 +1194,22 @@
|
|
|
1180
1194
|
}
|
|
1181
1195
|
.lib-item .lib-fav.faved { color: var(--star); border-color: var(--star); }
|
|
1182
1196
|
|
|
1197
|
+
/* Installed-tab action cluster: the transient "Update" action sits LEFT of the
|
|
1198
|
+
persistent ⌄ "show commands" caret (#119). */
|
|
1199
|
+
.lib-item .lib-actions { display: flex; gap: 6px; align-items: center; flex-shrink: 0; }
|
|
1200
|
+
/* "Update" affordance — outline-accent-dim: signals "an action is available / newer
|
|
1201
|
+
version" without the full-accent weight reserved for primary/selected. Shown only
|
|
1202
|
+
when the registry is strictly ahead of the installed version; hidden when current. */
|
|
1203
|
+
.lib-update {
|
|
1204
|
+
background: transparent; border: 1px solid var(--accent-dim); color: var(--accent-dim);
|
|
1205
|
+
padding: 4px 9px; font-size: 11px; cursor: pointer; border-radius: 3px; flex-shrink: 0;
|
|
1206
|
+
line-height: 1; transition: color 0.15s, border-color 0.15s; font-family: var(--ui);
|
|
1207
|
+
display: inline-flex; align-items: center; gap: 6px; white-space: nowrap;
|
|
1208
|
+
}
|
|
1209
|
+
.lib-update:hover { color: var(--accent); border-color: var(--accent); }
|
|
1210
|
+
.lib-update[aria-busy="true"] { color: var(--text-dim); border-color: var(--border-strong); cursor: default; pointer-events: none; }
|
|
1211
|
+
.lib-update .rtn-spinner { width: 11px; height: 11px; border-width: 2px; }
|
|
1212
|
+
|
|
1183
1213
|
/* Two-tab Agent Library: the tab bar sits between title and panes; each pane is a
|
|
1184
1214
|
flex column so its inner .lib-list keeps scrolling within the modal's max-height.
|
|
1185
1215
|
The hidden pane uses [hidden] (display:none) so it's skipped by assistive tech. */
|
|
@@ -2873,7 +2903,7 @@ body {
|
|
|
2873
2903
|
/* In dashboard view the pannable topology is hidden, so the canvas isn't a drag surface —
|
|
2874
2904
|
drop the grab affordance the .canvas rule sets for the normal view (#115 follow-up). */
|
|
2875
2905
|
.canvas.view-dashboard { cursor: default; }
|
|
2876
|
-
.dashboard { flex: 1; min-height: 0; overflow-y: auto; padding: 18px 24px 24px; }
|
|
2906
|
+
.dashboard { flex: 1; min-height: 0; overflow-y: auto; padding: 18px 24px 24px; touch-action: pan-y; }
|
|
2877
2907
|
.dashboard[hidden] { display: none; }
|
|
2878
2908
|
|
|
2879
2909
|
/* Notices above the panels: dim note (degraded validation / warnings) and the
|
|
@@ -2979,3 +3009,19 @@ body {
|
|
|
2979
3009
|
|
|
2980
3010
|
/* Inspect-tab panels reuse the same block styles inside the inspect body. */
|
|
2981
3011
|
.ext-inspect { display: flex; flex-direction: column; gap: 10px; overflow-y: auto; min-height: 0; }
|
|
3012
|
+
|
|
3013
|
+
/* ===== MOBILE REFLOW =====
|
|
3014
|
+
Phone/touch access (e.g. over Tailscale). The desktop 3-column shell is wider than a phone
|
|
3015
|
+
and html,body{overflow:hidden} (top of file) clips the off-screen columns with no way to pan
|
|
3016
|
+
to them — single-finger AND pinch do nothing. On narrow screens, WITHIN the locked skin:
|
|
3017
|
+
(1) let the shell scroll so nothing is ever unreachable;
|
|
3018
|
+
(2) collapse both side panels by default (see app.js) — canvas full-width, tap the existing
|
|
3019
|
+
48px stubs to reveal chat/inspect one at a time;
|
|
3020
|
+
(3) let the header wrap and the workflow picker shrink so it fits one phone-width row.
|
|
3021
|
+
Guarded by the media query → desktop layout is 100% untouched. No new fonts/colors/components. */
|
|
3022
|
+
@media (max-width: 760px) {
|
|
3023
|
+
html, body { overflow: auto; }
|
|
3024
|
+
.app { height: auto; min-height: 100vh; grid-template-rows: auto 1fr 44px; }
|
|
3025
|
+
header { flex-wrap: wrap; gap: 8px; padding: 8px 14px; }
|
|
3026
|
+
select, .wf-combo { min-width: 0; }
|
|
3027
|
+
}
|
package/dist/web/app.js
CHANGED
|
@@ -31,6 +31,13 @@ function loadState() {
|
|
|
31
31
|
if (c.left !== undefined) state.collapse.left = c.left;
|
|
32
32
|
if (c.right !== undefined) state.collapse.right = c.right;
|
|
33
33
|
} catch {}
|
|
34
|
+
// Mobile/touch: start with both side panels collapsed so the canvas is full-width and the
|
|
35
|
+
// desktop 3-column shell fits a phone. In-memory only — NOT persisted, so it never clobbers
|
|
36
|
+
// the desktop collapse preference. The user can still tap the stubs to reveal chat/inspect.
|
|
37
|
+
if (window.matchMedia('(max-width: 760px)').matches) {
|
|
38
|
+
state.collapse.left = true;
|
|
39
|
+
state.collapse.right = true;
|
|
40
|
+
}
|
|
34
41
|
}
|
|
35
42
|
function saveFavs() {
|
|
36
43
|
document.getElementById('fav-count').textContent = state.favorites.length;
|
|
@@ -1448,6 +1455,54 @@ window.addEventListener('mouseup', () => {
|
|
|
1448
1455
|
});
|
|
1449
1456
|
$canvasEl.addEventListener('auxclick', (e) => { if (e.button === 1) e.preventDefault(); });
|
|
1450
1457
|
|
|
1458
|
+
// ----- Touch: one-finger pan + two-finger pinch-zoom (phones). The mouse handlers above are
|
|
1459
|
+
// untouched (touch events never fire from a mouse), so desktop is unaffected. touch-action:none
|
|
1460
|
+
// on .canvas (CSS) stops the browser hijacking the gesture and suppresses synthetic mouse events.
|
|
1461
|
+
let tPan = false, tPinch = false, tFromX = 0, tFromY = 0, tBaseX = 0, tBaseY = 0, pinchDist0 = 0, pinchZoom0 = 1;
|
|
1462
|
+
const pinchSpan = (ts) => Math.hypot(ts[0].clientX - ts[1].clientX, ts[0].clientY - ts[1].clientY);
|
|
1463
|
+
$canvasEl.addEventListener('touchstart', (e) => {
|
|
1464
|
+
if (e.touches.length === 2) {
|
|
1465
|
+
e.preventDefault();
|
|
1466
|
+
tPinch = true; tPan = false;
|
|
1467
|
+
pinchDist0 = pinchSpan(e.touches) || 1; pinchZoom0 = zoom;
|
|
1468
|
+
} else if (e.touches.length === 1) {
|
|
1469
|
+
// Let taps on interactive chrome through (node select, zoom toolbar, fav-bar, find, dashboard).
|
|
1470
|
+
if (e.target.closest('.agent-card, .canvas-toolbar, .fav-bar, .find-overlay, .dashboard')) return;
|
|
1471
|
+
e.preventDefault();
|
|
1472
|
+
tPan = true; tPinch = false;
|
|
1473
|
+
tFromX = e.touches[0].clientX; tFromY = e.touches[0].clientY;
|
|
1474
|
+
tBaseX = panX; tBaseY = panY;
|
|
1475
|
+
$canvasEl.classList.add('panning');
|
|
1476
|
+
}
|
|
1477
|
+
}, { passive: false });
|
|
1478
|
+
$canvasEl.addEventListener('touchmove', (e) => {
|
|
1479
|
+
if (tPinch && e.touches.length === 2) {
|
|
1480
|
+
e.preventDefault();
|
|
1481
|
+
const ratio = pinchSpan(e.touches) / pinchDist0;
|
|
1482
|
+
zoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, pinchZoom0 * ratio));
|
|
1483
|
+
autoFit = false;
|
|
1484
|
+
applyTransform();
|
|
1485
|
+
} else if (tPan && e.touches.length === 1) {
|
|
1486
|
+
e.preventDefault();
|
|
1487
|
+
panX = tBaseX + (e.touches[0].clientX - tFromX);
|
|
1488
|
+
panY = tBaseY + (e.touches[0].clientY - tFromY);
|
|
1489
|
+
autoFit = false;
|
|
1490
|
+
applyTransform();
|
|
1491
|
+
}
|
|
1492
|
+
}, { passive: false });
|
|
1493
|
+
function endTouch(e) {
|
|
1494
|
+
if (e.touches.length === 0) {
|
|
1495
|
+
tPan = false; tPinch = false; $canvasEl.classList.remove('panning');
|
|
1496
|
+
} else if (e.touches.length === 1 && tPinch) {
|
|
1497
|
+
// 2->1 finger: hand off from pinch to pan without a jump.
|
|
1498
|
+
tPinch = false; tPan = true;
|
|
1499
|
+
tFromX = e.touches[0].clientX; tFromY = e.touches[0].clientY;
|
|
1500
|
+
tBaseX = panX; tBaseY = panY;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
$canvasEl.addEventListener('touchend', endTouch);
|
|
1504
|
+
$canvasEl.addEventListener('touchcancel', endTouch);
|
|
1505
|
+
|
|
1451
1506
|
// Keep the auto-fitted view fitted as the canvas resizes (a panel toggling/dragging,
|
|
1452
1507
|
// the window resizing) — but never override a view the user has zoomed/panned. A
|
|
1453
1508
|
// trailing debounce fires ONE clean re-fit after the resize settles (rather than on
|
package/dist/web/aware.js
CHANGED
|
@@ -3296,21 +3296,31 @@
|
|
|
3296
3296
|
if ($libModal && $libModal.classList.contains('show')) renderLibrary();
|
|
3297
3297
|
}).catch(() => {});
|
|
3298
3298
|
} else if (m.type === 'agent-install-result') {
|
|
3299
|
-
// A background `aware agent install` (
|
|
3300
|
-
//
|
|
3301
|
-
//
|
|
3299
|
+
// A background `aware agent install` (Available tab) or `aware agent update`
|
|
3300
|
+
// (Installed tab) finished — `m.action` says which. Clear its in-flight state,
|
|
3301
|
+
// then on success refresh the catalog (install: drops out of Available, appears
|
|
3302
|
+
// under Installed; update: the new version replaces the old → its Update button
|
|
3303
|
+
// disappears). On failure restore the card + surface why.
|
|
3304
|
+
const isUpdate = m.action === 'update';
|
|
3302
3305
|
installingIds.delete(m.id);
|
|
3303
3306
|
if (m.ok) {
|
|
3304
|
-
|
|
3305
|
-
|
|
3307
|
+
if (isUpdate) {
|
|
3308
|
+
showToast(`Updated “${m.id}”`, 'ok');
|
|
3309
|
+
} else {
|
|
3310
|
+
const a = availableCatalog.find((x) => x.id === m.id);
|
|
3311
|
+
showToast(`Installed “${a && a.display_name ? a.display_name : m.id}” — find it in Installed`, 'ok');
|
|
3312
|
+
}
|
|
3306
3313
|
loadAgentCatalog().then(() => {
|
|
3307
3314
|
if (!($libModal && $libModal.classList.contains('show'))) return;
|
|
3308
3315
|
renderAvailable();
|
|
3309
3316
|
if (libTab === 'installed') renderLibrary();
|
|
3310
3317
|
}).catch(() => {});
|
|
3311
3318
|
} else {
|
|
3312
|
-
showToast(
|
|
3313
|
-
if ($libModal && $libModal.classList.contains('show'))
|
|
3319
|
+
showToast(`${isUpdate ? 'Update' : 'Install'} failed: ${String(m.error || '').slice(0, 80)}`, 'err');
|
|
3320
|
+
if ($libModal && $libModal.classList.contains('show')) {
|
|
3321
|
+
if (isUpdate) renderLibrary();
|
|
3322
|
+
else renderAvailable();
|
|
3323
|
+
}
|
|
3314
3324
|
}
|
|
3315
3325
|
} else if (m.type === 'request-added' || m.type === 'requests-changed') {
|
|
3316
3326
|
loadRequests();
|
|
@@ -3726,29 +3736,85 @@
|
|
|
3726
3736
|
renderLibrary();
|
|
3727
3737
|
$libModal.classList.add('show');
|
|
3728
3738
|
if (!agentCatalog.length) loadAgentCatalog().then(renderLibrary).catch(reportErr);
|
|
3739
|
+
// Load the registry catalog too, so the Installed tab can flag outdated agents
|
|
3740
|
+
// with an "Update" button (and the Available tab is instant when switched to).
|
|
3741
|
+
ensureAvailableLoaded(() => {
|
|
3742
|
+
renderLibrary();
|
|
3743
|
+
if (libTab === 'available') renderAvailable();
|
|
3744
|
+
});
|
|
3729
3745
|
};
|
|
3730
3746
|
|
|
3747
|
+
// Compare two dotted version strings numerically, segment by segment (missing
|
|
3748
|
+
// segments = 0). Returns 1 if a>b, -1 if a<b, 0 if equal — so the Installed tab
|
|
3749
|
+
// only offers an "Update" when the registry is STRICTLY ahead (never a downgrade).
|
|
3750
|
+
// Handles the registry's mixed schemes (e.g. "0.2.0" vs "1.0.0", "0.1.0" vs
|
|
3751
|
+
// "2025.0.1"). ponytail: numeric-only compare — registry versions are pure dotted
|
|
3752
|
+
// integers; a pre-release/build suffix (none today) compares by its leading int.
|
|
3753
|
+
function cmpVer(a, b) {
|
|
3754
|
+
const pa = String(a).split('.');
|
|
3755
|
+
const pb = String(b).split('.');
|
|
3756
|
+
const n = Math.max(pa.length, pb.length);
|
|
3757
|
+
for (let i = 0; i < n; i++) {
|
|
3758
|
+
const x = parseInt(pa[i], 10) || 0;
|
|
3759
|
+
const y = parseInt(pb[i], 10) || 0;
|
|
3760
|
+
if (x !== y) return x > y ? 1 : -1;
|
|
3761
|
+
}
|
|
3762
|
+
return 0;
|
|
3763
|
+
}
|
|
3764
|
+
|
|
3765
|
+
// Latest installable version for an installed id, from availableCatalog (lazy-loaded
|
|
3766
|
+
// on library open). Uses the catalog's `manifest_version` — the agent's manifest
|
|
3767
|
+
// version for the latest registry entry, i.e. the version an in-place `aware agent
|
|
3768
|
+
// update` actually pulls. This is the SAME axis as the installed agent's version
|
|
3769
|
+
// (agentCatalog/`agent list` also reports manifest.version), so the comparison is
|
|
3770
|
+
// valid — the catalog's `version` field is the registry INDEX key (the install
|
|
3771
|
+
// spec, a different axis) and must NOT be compared. null when the registry hasn't
|
|
3772
|
+
// loaded, the agent isn't in it (e.g. a grafted/local agent), or the CLI predates
|
|
3773
|
+
// manifest_version (aware < the catalog-manifest-version release) → no Update shown.
|
|
3774
|
+
function latestVersionFor(id) {
|
|
3775
|
+
const a = availableCatalog.find((x) => x.id === id);
|
|
3776
|
+
return a && a.manifest_version ? a.manifest_version : null;
|
|
3777
|
+
}
|
|
3778
|
+
|
|
3731
3779
|
renderLibrary = function renderLibraryReal() {
|
|
3732
3780
|
const q = ($libSearch.value || '').toLowerCase().trim();
|
|
3733
3781
|
const items = agentCatalog.filter((a) => !q || a.id.toLowerCase().includes(q) || (a.kind || '').toLowerCase().includes(q));
|
|
3734
3782
|
// Each agent is ONE grid cell: the card header + its (lazy) command list live
|
|
3735
3783
|
// in the same `.lib-card`, so expanding nests the commands directly under the
|
|
3736
3784
|
// picked card instead of scattering them into a neighbouring grid slot.
|
|
3737
|
-
$libList.innerHTML = items.map((a) =>
|
|
3738
|
-
|
|
3785
|
+
$libList.innerHTML = items.map((a) => {
|
|
3786
|
+
const id = escapeAttr(a.id);
|
|
3787
|
+
// "Update" shows only when the registry is strictly ahead of the installed
|
|
3788
|
+
// version AND the registry catalog has loaded (else we can't know) — hidden,
|
|
3789
|
+
// not disabled, when up to date. A pull in flight shows a spinner instead.
|
|
3790
|
+
const latest = availableLoaded ? latestVersionFor(a.id) : null;
|
|
3791
|
+
const canUpdate = !!latest && cmpVer(latest, a.version) > 0;
|
|
3792
|
+
const updateBtn = installingIds.has(a.id)
|
|
3793
|
+
? `<button class="lib-update" aria-busy="true" aria-label="Updating ${escapeAttr(a.id)}…" data-tip="Downloading update from the registry — this can take a minute"><span class="rtn-spinner"></span></button>`
|
|
3794
|
+
: canUpdate
|
|
3795
|
+
? `<button class="lib-update" data-update="${id}" aria-label="Update ${escapeAttr(a.id)} to v${escapeAttr(String(latest))}" data-tip="Update to v${escapeAttr(String(latest))}">↑ v${escapeHtml(String(latest))}</button>`
|
|
3796
|
+
: '';
|
|
3797
|
+
return `
|
|
3798
|
+
<div class="lib-card" data-agent="${id}">
|
|
3739
3799
|
<div class="lib-item">
|
|
3740
3800
|
<div class="info">
|
|
3741
|
-
<div class="name">${escapeHtml(a.id)}
|
|
3742
|
-
<div class="meta"
|
|
3801
|
+
<div class="name">${escapeHtml(a.id)}</div>
|
|
3802
|
+
<div class="meta"><span class="lib-ver">v${escapeHtml(String(a.version))}</span> ${escapeHtml(a.kind || 'agent')} · ${a.commands} command${a.commands === 1 ? '' : 's'} · ${a.skills} skill${a.skills === 1 ? '' : 's'}</div>
|
|
3803
|
+
</div>
|
|
3804
|
+
<div class="lib-actions">
|
|
3805
|
+
${updateBtn}
|
|
3806
|
+
<button class="lib-fav" data-expand="${id}" data-tip="Show commands">⌄</button>
|
|
3743
3807
|
</div>
|
|
3744
|
-
<button class="lib-fav" data-expand="${escapeAttr(a.id)}" data-tip="Show commands">⌄</button>
|
|
3745
3808
|
</div>
|
|
3746
|
-
<div class="lib-commands" id="cmds-${
|
|
3747
|
-
</div
|
|
3748
|
-
|
|
3809
|
+
<div class="lib-commands" id="cmds-${id}" hidden></div>
|
|
3810
|
+
</div>`;
|
|
3811
|
+
}).join('') || '<div class="empty-state">No agents match.</div>';
|
|
3749
3812
|
$libList.querySelectorAll('[data-expand]').forEach((btn) => {
|
|
3750
3813
|
btn.onclick = () => expandAgent(btn.dataset.expand);
|
|
3751
3814
|
});
|
|
3815
|
+
$libList.querySelectorAll('[data-update]').forEach((btn) => {
|
|
3816
|
+
btn.onclick = () => updateAgent(btn.dataset.update, btn);
|
|
3817
|
+
});
|
|
3752
3818
|
};
|
|
3753
3819
|
|
|
3754
3820
|
async function expandAgent(id) {
|
|
@@ -3948,6 +4014,49 @@
|
|
|
3948
4014
|
});
|
|
3949
4015
|
}
|
|
3950
4016
|
|
|
4017
|
+
// Update an installed agent in place to the latest registry version (Installed tab
|
|
4018
|
+
// "Update" button). Mirrors installAgent: the server runs `aware agent update`
|
|
4019
|
+
// asynchronously (a registry pull, ~a minute) and reports completion over SSE
|
|
4020
|
+
// (`agent-install-result`, action:'update'), so we flip the card to a busy spinner
|
|
4021
|
+
// and let the SSE handler refresh the catalog + re-render. `installingIds` is the
|
|
4022
|
+
// shared in-flight set (an id can't be installing and updating at once).
|
|
4023
|
+
function updateAgent(id, btn) {
|
|
4024
|
+
if (installingIds.has(id)) return; // already in flight (server dedupes too)
|
|
4025
|
+
installingIds.add(id);
|
|
4026
|
+
if (btn) { // immediate feedback before the next render
|
|
4027
|
+
btn.setAttribute('aria-busy', 'true');
|
|
4028
|
+
btn.setAttribute('aria-label', `Updating ${id}…`);
|
|
4029
|
+
btn.removeAttribute('data-update'); // a stray re-wire can't re-trigger it
|
|
4030
|
+
btn.innerHTML = '<span class="rtn-spinner"></span>';
|
|
4031
|
+
}
|
|
4032
|
+
showToast(`Updating “${id}” — this can take a minute or two`, 'info');
|
|
4033
|
+
api('/api/agents/update', { method: 'POST', body: JSON.stringify({ id }) })
|
|
4034
|
+
.catch((e) => { // the START failed (bad id / server) — completion errors arrive via SSE
|
|
4035
|
+
installingIds.delete(id);
|
|
4036
|
+
if ($libModal && $libModal.classList.contains('show')) renderLibrary();
|
|
4037
|
+
reportErr(e);
|
|
4038
|
+
});
|
|
4039
|
+
}
|
|
4040
|
+
|
|
4041
|
+
// Lazy-load the registry catalog once. Used by the Available tab (to list agents)
|
|
4042
|
+
// AND the Installed tab (to detect outdated agents → show "Update"). `onLoaded`
|
|
4043
|
+
// fires after the data is available (immediately if already loaded), so the caller
|
|
4044
|
+
// can re-render whichever view needs it. No-op on a pre-0.54 CLI (no catalog).
|
|
4045
|
+
function ensureAvailableLoaded(onLoaded) {
|
|
4046
|
+
if (availableUnsupported) return;
|
|
4047
|
+
if (availableLoaded) { if (onLoaded) onLoaded(); return; }
|
|
4048
|
+
if (availableLoading) return; // a load is already in flight; its onLoaded re-renders
|
|
4049
|
+
availableLoading = true;
|
|
4050
|
+
loadAvailableAgents()
|
|
4051
|
+
.then(() => { if (onLoaded) onLoaded(); })
|
|
4052
|
+
.catch((e) => {
|
|
4053
|
+
$libAvailList.innerHTML = '<div class="empty-state">Could not load available agents.</div>';
|
|
4054
|
+
$libAvailCount.textContent = '';
|
|
4055
|
+
reportErr(e);
|
|
4056
|
+
})
|
|
4057
|
+
.finally(() => { availableLoading = false; });
|
|
4058
|
+
}
|
|
4059
|
+
|
|
3951
4060
|
function switchLibTab(tab) {
|
|
3952
4061
|
libTab = tab;
|
|
3953
4062
|
$libTabs.querySelectorAll('[data-libtab]').forEach((b) => {
|
|
@@ -3959,17 +4068,7 @@
|
|
|
3959
4068
|
$libPaneAvailable.hidden = tab !== 'available';
|
|
3960
4069
|
if (tab === 'available') {
|
|
3961
4070
|
renderAvailable(); // shows the loading spinner first if not yet loaded
|
|
3962
|
-
|
|
3963
|
-
availableLoading = true;
|
|
3964
|
-
loadAvailableAgents()
|
|
3965
|
-
.then(renderAvailable)
|
|
3966
|
-
.catch((e) => {
|
|
3967
|
-
$libAvailList.innerHTML = '<div class="empty-state">Could not load available agents.</div>';
|
|
3968
|
-
$libAvailCount.textContent = '';
|
|
3969
|
-
reportErr(e);
|
|
3970
|
-
})
|
|
3971
|
-
.finally(() => { availableLoading = false; });
|
|
3972
|
-
}
|
|
4071
|
+
ensureAvailableLoaded(() => { renderAvailable(); renderLibrary(); });
|
|
3973
4072
|
setTimeout(() => { if (!$libAvailSearch.hidden) $libAvailSearch.focus(); }, 0);
|
|
3974
4073
|
} else {
|
|
3975
4074
|
renderLibrary(); // repaint from agentCatalog — it may have grown via an Install on the other tab
|