@floless/app 0.29.0 → 0.30.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 +77 -2
- package/dist/web/app.css +27 -0
- package/dist/web/app.js +4 -1
- package/dist/web/aware.js +112 -0
- package/package.json +1 -1
package/dist/floless-server.cjs
CHANGED
|
@@ -51016,6 +51016,8 @@ function readApp(id) {
|
|
|
51016
51016
|
kind: locked && typeof locked.kind === "string" ? locked.kind : "agent",
|
|
51017
51017
|
mode: lockMode === "write" || lockMode === "read" ? lockMode : "unknown",
|
|
51018
51018
|
runtimeModel: !!locked && locked["runtime-model"] === true,
|
|
51019
|
+
// `frozen:` lives on the SOURCE node (the user froze it); present (non-null) = frozen.
|
|
51020
|
+
frozen: n.frozen != null,
|
|
51019
51021
|
inputs: asRecord(locked?.inputs),
|
|
51020
51022
|
config: asRecord(n.config),
|
|
51021
51023
|
notes: notes2,
|
|
@@ -51627,6 +51629,10 @@ function assertId(id) {
|
|
|
51627
51629
|
if (!APP_ID.test(id)) throw new AwareError(`invalid app id: ${JSON.stringify(id)}`);
|
|
51628
51630
|
return id;
|
|
51629
51631
|
}
|
|
51632
|
+
function assertNodeId(id) {
|
|
51633
|
+
if (!APP_ID.test(id)) throw new AwareError(`invalid node id: ${JSON.stringify(id)}`);
|
|
51634
|
+
return id;
|
|
51635
|
+
}
|
|
51630
51636
|
var SOURCE_EXT = /\.(flo|app|flow|aware)$/i;
|
|
51631
51637
|
function assertSourcePath(p) {
|
|
51632
51638
|
if (!SOURCE_EXT.test(p)) throw new AwareError(`source must be a .flo/.app file: ${JSON.stringify(p)}`);
|
|
@@ -52008,6 +52014,39 @@ var aware = {
|
|
|
52008
52014
|
}
|
|
52009
52015
|
return { compiled: /lock refreshed/i.test(stdout), output: stdout.trim() };
|
|
52010
52016
|
},
|
|
52017
|
+
/**
|
|
52018
|
+
* Freeze a node (`aware app freeze <app> <node>`, CLI ≥ 0.77, aware-aeco#261). Pins the
|
|
52019
|
+
* node's LAST run output into the source as a `frozen:` block so Run skips the agent
|
|
52020
|
+
* (replays the pinned value, never re-runs it) and recompiles the lock. Fails when the
|
|
52021
|
+
* node has never produced output (nothing to pin) — the route relays that as "run it
|
|
52022
|
+
* once first". Throws AwareUnsupportedError on a pre-0.77 CLI so the route can say "update
|
|
52023
|
+
* AWARE" instead of treating an old CLI as a hard fault (mirrors rename/duplicate).
|
|
52024
|
+
*/
|
|
52025
|
+
async freeze(appId, nodeId) {
|
|
52026
|
+
const { code, stdout, stderr } = await runRaw(["app", "freeze", assertId(appId), assertNodeId(nodeId)]);
|
|
52027
|
+
if (code !== 0) {
|
|
52028
|
+
if (isInvokeUnsupported({ stdout, stderr })) {
|
|
52029
|
+
throw new AwareUnsupportedError("aware CLI does not support `app freeze` (needs \u2265 0.77) \u2014 update AWARE", { stdout, stderr });
|
|
52030
|
+
}
|
|
52031
|
+
throw new AwareError(`aware app freeze ${nodeId} failed (exit ${code})`, { stdout, stderr, code });
|
|
52032
|
+
}
|
|
52033
|
+
return { output: stdout.trim() };
|
|
52034
|
+
},
|
|
52035
|
+
/**
|
|
52036
|
+
* Unfreeze a node (`aware app unfreeze <app> <node>`, CLI ≥ 0.77). Removes the node's
|
|
52037
|
+
* `frozen:` block so it runs normally again and recompiles. Same unsupported-degradation
|
|
52038
|
+
* contract as freeze.
|
|
52039
|
+
*/
|
|
52040
|
+
async unfreeze(appId, nodeId) {
|
|
52041
|
+
const { code, stdout, stderr } = await runRaw(["app", "unfreeze", assertId(appId), assertNodeId(nodeId)]);
|
|
52042
|
+
if (code !== 0) {
|
|
52043
|
+
if (isInvokeUnsupported({ stdout, stderr })) {
|
|
52044
|
+
throw new AwareUnsupportedError("aware CLI does not support `app unfreeze` (needs \u2265 0.77) \u2014 update AWARE", { stdout, stderr });
|
|
52045
|
+
}
|
|
52046
|
+
throw new AwareError(`aware app unfreeze ${nodeId} failed (exit ${code})`, { stdout, stderr, code });
|
|
52047
|
+
}
|
|
52048
|
+
return { output: stdout.trim() };
|
|
52049
|
+
},
|
|
52011
52050
|
/**
|
|
52012
52051
|
* Install a first-party agent by registry id (`aware agent install <id>`). The
|
|
52013
52052
|
* registry install pulls a large tarball (~200s observed) — far over runRaw's 60s
|
|
@@ -52796,7 +52835,7 @@ function appVersion() {
|
|
|
52796
52835
|
return resolveVersion({
|
|
52797
52836
|
isSea: isSea2(),
|
|
52798
52837
|
sqVersionXml: readSqVersionXml(),
|
|
52799
|
-
define: true ? "0.
|
|
52838
|
+
define: true ? "0.30.0" : void 0,
|
|
52800
52839
|
pkgVersion: readPkgVersion()
|
|
52801
52840
|
});
|
|
52802
52841
|
}
|
|
@@ -52806,7 +52845,7 @@ function resolveChannel(s) {
|
|
|
52806
52845
|
return "dev";
|
|
52807
52846
|
}
|
|
52808
52847
|
function appChannel() {
|
|
52809
|
-
return resolveChannel({ isSea: isSea2(), define: true ? "0.
|
|
52848
|
+
return resolveChannel({ isSea: isSea2(), define: true ? "0.30.0" : void 0 });
|
|
52810
52849
|
}
|
|
52811
52850
|
|
|
52812
52851
|
// oauth-presets.ts
|
|
@@ -58225,6 +58264,42 @@ async function startServer() {
|
|
|
58225
58264
|
broadcast({ type: "apps-changed", id: req.params.id });
|
|
58226
58265
|
return { ok: true };
|
|
58227
58266
|
});
|
|
58267
|
+
app.post("/api/app/:id/freeze", async (req, reply) => {
|
|
58268
|
+
const nodeId = typeof req.body?.nodeId === "string" ? req.body.nodeId.trim() : "";
|
|
58269
|
+
if (!nodeId) return reply.status(400).send({ ok: false, error: "nodeId required" });
|
|
58270
|
+
try {
|
|
58271
|
+
await aware.freeze(req.params.id, nodeId);
|
|
58272
|
+
} catch (e) {
|
|
58273
|
+
if (e instanceof AwareUnsupportedError) {
|
|
58274
|
+
return reply.status(422).send({ ok: false, error: "Freezing needs AWARE 0.77 or newer \u2014 update AWARE from the footer." });
|
|
58275
|
+
}
|
|
58276
|
+
if (e instanceof AwareError) {
|
|
58277
|
+
app.log.warn({ id: req.params.id, nodeId, detail: e.detail }, "freeze failed");
|
|
58278
|
+
return reply.status(422).send({ ok: false, error: "Couldn't freeze this node \u2014 it may have no saved output yet (run it once first).", reason: awareStderr(e) });
|
|
58279
|
+
}
|
|
58280
|
+
throw e;
|
|
58281
|
+
}
|
|
58282
|
+
broadcast({ type: "apps-changed", id: req.params.id });
|
|
58283
|
+
return { ok: true };
|
|
58284
|
+
});
|
|
58285
|
+
app.post("/api/app/:id/unfreeze", async (req, reply) => {
|
|
58286
|
+
const nodeId = typeof req.body?.nodeId === "string" ? req.body.nodeId.trim() : "";
|
|
58287
|
+
if (!nodeId) return reply.status(400).send({ ok: false, error: "nodeId required" });
|
|
58288
|
+
try {
|
|
58289
|
+
await aware.unfreeze(req.params.id, nodeId);
|
|
58290
|
+
} catch (e) {
|
|
58291
|
+
if (e instanceof AwareUnsupportedError) {
|
|
58292
|
+
return reply.status(422).send({ ok: false, error: "Freezing needs AWARE 0.77 or newer \u2014 update AWARE from the footer." });
|
|
58293
|
+
}
|
|
58294
|
+
if (e instanceof AwareError) {
|
|
58295
|
+
app.log.warn({ id: req.params.id, nodeId, detail: e.detail }, "unfreeze failed");
|
|
58296
|
+
return reply.status(422).send({ ok: false, error: "Could not unfreeze this node.", reason: awareStderr(e) });
|
|
58297
|
+
}
|
|
58298
|
+
throw e;
|
|
58299
|
+
}
|
|
58300
|
+
broadcast({ type: "apps-changed", id: req.params.id });
|
|
58301
|
+
return { ok: true };
|
|
58302
|
+
});
|
|
58228
58303
|
app.post("/api/compile", async (req, reply) => {
|
|
58229
58304
|
const { id, sourcePath } = req.body ?? {};
|
|
58230
58305
|
const path = sourcePath ?? (id ? readApp(id).source.path : void 0);
|
package/dist/web/app.css
CHANGED
|
@@ -608,6 +608,20 @@
|
|
|
608
608
|
border: 1px solid var(--accent-dim); border-radius: 999px;
|
|
609
609
|
cursor: help;
|
|
610
610
|
}
|
|
611
|
+
/* Frozen node (aware app freeze): its last output is pinned and Run skips this node.
|
|
612
|
+
A quiet, INERT read — badge + a one-step-cooler surface — NOT a disabled state (the
|
|
613
|
+
card stays fully interactive; it's just static). Mirrors the rt-model-badge recipe but
|
|
614
|
+
with neutral-cool tokens so it reads distinct from the accent-blue runtime-model badge.
|
|
615
|
+
(No left ::after ribbon: that pseudo-element is already the node mode stripe.) */
|
|
616
|
+
.agent-card.frozen { background: var(--surface-2); }
|
|
617
|
+
.agent-card .frozen-badge {
|
|
618
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
619
|
+
margin: 0 0 8px; padding: 2px 8px;
|
|
620
|
+
font: 600 10px/1.4 var(--ui); letter-spacing: 0.01em;
|
|
621
|
+
color: var(--text-muted); background: var(--surface-3);
|
|
622
|
+
border: 1px solid var(--border-strong); border-radius: 999px;
|
|
623
|
+
cursor: help;
|
|
624
|
+
}
|
|
611
625
|
.agent-card .footer-row {
|
|
612
626
|
display: flex;
|
|
613
627
|
justify-content: space-between;
|
|
@@ -1855,6 +1869,19 @@
|
|
|
1855
1869
|
background: var(--border);
|
|
1856
1870
|
margin: 5px 4px;
|
|
1857
1871
|
}
|
|
1872
|
+
/* Cursor-anchored node context menu (right-click a card). Reuses .menu/.menu-item; only
|
|
1873
|
+
the fixed 280px width is wrong for it — the pointer anchor (top/left) is set inline. */
|
|
1874
|
+
.menu.context-menu { width: auto; min-width: 168px; }
|
|
1875
|
+
/* Non-interactive node-name header line atop the context menu. */
|
|
1876
|
+
.menu-head {
|
|
1877
|
+
padding: 6px 9px 4px;
|
|
1878
|
+
font-size: 11px;
|
|
1879
|
+
color: var(--text-dim);
|
|
1880
|
+
max-width: 220px;
|
|
1881
|
+
overflow: hidden;
|
|
1882
|
+
text-overflow: ellipsis;
|
|
1883
|
+
white-space: nowrap;
|
|
1884
|
+
}
|
|
1858
1885
|
.menu-theme {
|
|
1859
1886
|
display: flex;
|
|
1860
1887
|
align-items: center;
|
package/dist/web/app.js
CHANGED
|
@@ -339,7 +339,9 @@ function cardEl(id, ports) {
|
|
|
339
339
|
const a = AGENTS[id];
|
|
340
340
|
ports = ports || { in: 0, out: 0 };
|
|
341
341
|
const div = document.createElement('div');
|
|
342
|
-
|
|
342
|
+
// `frozen` = the node's output is pinned and Run skips it (aware app freeze). A quiet,
|
|
343
|
+
// inert card treatment + badge so it reads as "static / won't re-run" at a glance.
|
|
344
|
+
div.className = 'agent-card' + (a._frozen ? ' frozen' : '');
|
|
343
345
|
div.dataset.agentId = id;
|
|
344
346
|
div.draggable = true;
|
|
345
347
|
const faved = state.favorites.some(f => f.id === id);
|
|
@@ -355,6 +357,7 @@ function cardEl(id, ports) {
|
|
|
355
357
|
<div class="title">${a.title}</div>
|
|
356
358
|
<div class="subtitle">${a.subtitle}</div>
|
|
357
359
|
${a._runtimeModel ? '<div class="rt-model-badge" title="This node calls a model at run time (vision.extract). Output is content-cached and sits behind an approve gate — review the extraction before any write.">⚡ calls a model at run time</div>' : ''}
|
|
360
|
+
${a._frozen ? '<div class="frozen-badge" title="Frozen — its last result is pinned and Run skips this node. Right-click to unfreeze.">❄ Frozen</div>' : ''}
|
|
358
361
|
<div class="blurb">${a.blurb}</div>
|
|
359
362
|
<div class="footer-row">
|
|
360
363
|
<span class="ports"><span class="ports-num">${ports.in}</span> in <span class="ports-arrow">·</span> <span class="ports-num">${ports.out}</span> out</span>
|
package/dist/web/aware.js
CHANGED
|
@@ -294,6 +294,7 @@
|
|
|
294
294
|
return {
|
|
295
295
|
_mode: n.mode,
|
|
296
296
|
_runtimeModel: !!n.runtimeModel, // B2: lock stamped runtime-model → card badge
|
|
297
|
+
_frozen: !!n.frozen, // source node carries a `frozen:` block → card badge + Unfreeze menu
|
|
297
298
|
icon: iconFor(n.agent),
|
|
298
299
|
kind: n.kind === 'agent' && n.agent ? `${agentLabel} agent` : escapeHtml(n.kind),
|
|
299
300
|
version: pin ? `v${escapeHtml(String(pin))}` : '—',
|
|
@@ -1392,6 +1393,106 @@
|
|
|
1392
1393
|
}
|
|
1393
1394
|
}
|
|
1394
1395
|
|
|
1396
|
+
// ── Right-click Freeze / Unfreeze a node (aware-aeco#261) ─────────────────────
|
|
1397
|
+
// A general per-node toggle: right-click a canvas node → Freeze output (Run then
|
|
1398
|
+
// REPLAYS the node's last result instead of re-running its agent — so e.g. a
|
|
1399
|
+
// drawing-reader node won't re-read & wipe the user's edits, "whatever's there is
|
|
1400
|
+
// there") or Unfreeze (runs normally again). Needs AWARE ≥ 0.77; on an older runtime
|
|
1401
|
+
// the menu simply doesn't appear and the native menu passes through (the app keeps
|
|
1402
|
+
// AWARE at latest, so that's rare/transient — never a dead/disabled control).
|
|
1403
|
+
let _freezeMenuEl = null, _freezeMenuEsc = null, _freezeMenuOutside = null;
|
|
1404
|
+
|
|
1405
|
+
// numeric major.minor.patch compare; an empty/unparseable version sorts BELOW (-1),
|
|
1406
|
+
// so a missing awareVersion safely reads as "freeze unsupported". `Number` (not parseInt)
|
|
1407
|
+
// is strict — a prerelease segment like "0-rc1" → NaN → unsupported (defer to the server
|
|
1408
|
+
// backstop), never silently treated as the release.
|
|
1409
|
+
function cmpSemver(a, b) {
|
|
1410
|
+
const pa = String(a || '').split('.').map((x) => Number(x));
|
|
1411
|
+
const pb = String(b || '').split('.').map((x) => Number(x));
|
|
1412
|
+
if (pa.length < 3 || pa.some(Number.isNaN)) return -1;
|
|
1413
|
+
for (let i = 0; i < 3; i++) {
|
|
1414
|
+
const d = (pa[i] || 0) - (pb[i] || 0);
|
|
1415
|
+
if (d !== 0) return d < 0 ? -1 : 1;
|
|
1416
|
+
}
|
|
1417
|
+
return 0;
|
|
1418
|
+
}
|
|
1419
|
+
function awareSupportsFreeze() { return cmpSemver(awareRuntimeVersion, '0.77.0') >= 0; }
|
|
1420
|
+
|
|
1421
|
+
function closeNodeMenu() {
|
|
1422
|
+
if (_freezeMenuEsc) { document.removeEventListener('keydown', _freezeMenuEsc, true); _freezeMenuEsc = null; }
|
|
1423
|
+
if (_freezeMenuOutside) { document.removeEventListener('mousedown', _freezeMenuOutside, true); _freezeMenuOutside = null; }
|
|
1424
|
+
if (_freezeMenuEl) { _freezeMenuEl.remove(); _freezeMenuEl = null; }
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
// Build + open the cursor-anchored menu over a node card. Reuses the hamburger
|
|
1428
|
+
// `.menu`/`.menu-item` vocabulary (+ a `.context-menu` width modifier) so it matches
|
|
1429
|
+
// the existing popover language; dismiss mirrors openNotesPopover (Escape + click-away).
|
|
1430
|
+
function openNodeMenu(card, x, y) {
|
|
1431
|
+
closeNodeMenu();
|
|
1432
|
+
const nodeId = card.dataset.agentId;
|
|
1433
|
+
if (!nodeId) return;
|
|
1434
|
+
const node = AGENTS[nodeId];
|
|
1435
|
+
const frozen = !!(node && node._frozen);
|
|
1436
|
+
// A 4-line snowflake glyph as a stroke SVG — matches `.menu-icon svg` (Lucide family).
|
|
1437
|
+
const ice = '<span class="menu-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><line x1="12" y1="2" x2="12" y2="22"/><line x1="2" y1="12" x2="22" y2="12"/><line x1="4.6" y1="4.6" x2="19.4" y2="19.4"/><line x1="19.4" y1="4.6" x2="4.6" y2="19.4"/></svg></span>';
|
|
1438
|
+
const menu = document.createElement('div');
|
|
1439
|
+
menu.className = 'menu context-menu show';
|
|
1440
|
+
menu.setAttribute('role', 'menu');
|
|
1441
|
+
menu.setAttribute('aria-label', 'Node actions');
|
|
1442
|
+
menu.innerHTML =
|
|
1443
|
+
`<div class="menu-head" role="none">${escapeHtml(nodeId)}</div>`
|
|
1444
|
+
+ `<div class="menu-divider"></div>`
|
|
1445
|
+
+ `<button type="button" class="menu-item" role="menuitem">${ice}<span class="menu-label">${frozen ? 'Unfreeze' : 'Freeze output'}</span></button>`;
|
|
1446
|
+
document.body.appendChild(menu);
|
|
1447
|
+
// Anchor at the pointer, then clamp so a card near the right/bottom edge can't push
|
|
1448
|
+
// the menu off-screen (same intent as openNotesPopover's edge-clamp).
|
|
1449
|
+
const r = menu.getBoundingClientRect();
|
|
1450
|
+
menu.style.left = Math.max(8, Math.min(x, window.innerWidth - r.width - 8)) + 'px';
|
|
1451
|
+
menu.style.top = Math.max(8, Math.min(y, window.innerHeight - r.height - 8)) + 'px';
|
|
1452
|
+
const item = menu.querySelector('.menu-item');
|
|
1453
|
+
item.onclick = () => { closeNodeMenu(); toggleFreeze(nodeId, frozen); };
|
|
1454
|
+
try { item.focus(); } catch { /* container handles focus */ }
|
|
1455
|
+
_freezeMenuEl = menu;
|
|
1456
|
+
_freezeMenuEsc = (e) => { if (e.key === 'Escape') { e.preventDefault(); closeNodeMenu(); try { card.focus(); } catch { /* card not focusable */ } } };
|
|
1457
|
+
document.addEventListener('keydown', _freezeMenuEsc, true);
|
|
1458
|
+
_freezeMenuOutside = (e) => { if (_freezeMenuEl && !_freezeMenuEl.contains(e.target)) closeNodeMenu(); };
|
|
1459
|
+
document.addEventListener('mousedown', _freezeMenuOutside, true);
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
// Toggle a node's frozen state via the server (which shells `aware app freeze/unfreeze`
|
|
1463
|
+
// + recompiles), then re-read the app so the badge + state repaint. api() throws on any
|
|
1464
|
+
// failure (non-2xx OR in-band ok:false) with the server's plain-English message — e.g.
|
|
1465
|
+
// "No output to freeze — run this node at least once first." — so one catch covers it.
|
|
1466
|
+
async function toggleFreeze(nodeId, currentlyFrozen) {
|
|
1467
|
+
if (!currentId) return;
|
|
1468
|
+
const verb = currentlyFrozen ? 'unfreeze' : 'freeze';
|
|
1469
|
+
try {
|
|
1470
|
+
await api(`/api/app/${encodeURIComponent(currentId)}/${verb}`, { method: 'POST', body: JSON.stringify({ nodeId }) });
|
|
1471
|
+
await loadApp(currentId); // re-read → frozen badge/state repaints
|
|
1472
|
+
showToast(currentlyFrozen
|
|
1473
|
+
? 'Node unfrozen — it will run normally.'
|
|
1474
|
+
: 'Output frozen — this node will replay its last result on the next run.',
|
|
1475
|
+
currentlyFrozen ? 'info' : 'ok');
|
|
1476
|
+
} catch (e) {
|
|
1477
|
+
// Surface the real aware reason the route attached as body.reason — otherwise the friendly
|
|
1478
|
+
// lead hides the actual cause. First non-empty line, capped, keeps the warn toast sane.
|
|
1479
|
+
const lead = (e && e.message) || `Could not ${verb} this node.`;
|
|
1480
|
+
const raw = e && e.body && typeof e.body.reason === 'string' ? e.body.reason.trim() : '';
|
|
1481
|
+
const reason = raw ? (raw.split('\n').find((l) => l.trim()) || '').trim().slice(0, 200) : '';
|
|
1482
|
+
showToast(reason ? `${lead} — ${reason}` : lead, 'warn');
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
// Right-click a node card → the Freeze/Unfreeze menu. preventDefault is scoped to cards
|
|
1487
|
+
// (right-click stays normal everywhere else), and only when AWARE supports it — on an
|
|
1488
|
+
// older runtime the native menu passes through, so there's no dead interaction.
|
|
1489
|
+
document.addEventListener('contextmenu', (e) => {
|
|
1490
|
+
const card = e.target.closest && e.target.closest('.agent-card');
|
|
1491
|
+
if (!card || !awareSupportsFreeze()) return;
|
|
1492
|
+
e.preventDefault();
|
|
1493
|
+
openNodeMenu(card, e.clientX, e.clientY);
|
|
1494
|
+
});
|
|
1495
|
+
|
|
1395
1496
|
// Approve & bake — freezes the edited contract into the .lock, arming Run.
|
|
1396
1497
|
// Flushes the editor's pending debounce save first so Approve always bakes the
|
|
1397
1498
|
// latest in-memory edits, not a stale snapshot. api() throws on non-2xx, so
|
|
@@ -3284,6 +3385,8 @@
|
|
|
3284
3385
|
}
|
|
3285
3386
|
let shownVersion = false;
|
|
3286
3387
|
let loadedAppVersion = null; // the build this tab is running against (first health wins)
|
|
3388
|
+
let awareRuntimeVersion = null; // installed AWARE version (from /api/health) — gates the freeze menu
|
|
3389
|
+
let freezeHintAppended = false; // append the right-click-to-freeze canvas hint once, when supported
|
|
3287
3390
|
function startHealthPoll() {
|
|
3288
3391
|
const tick = async () => {
|
|
3289
3392
|
let ok = false;
|
|
@@ -3326,6 +3429,15 @@
|
|
|
3326
3429
|
// After the build version is stamped, reveal the relaunch-surviving what's-new
|
|
3327
3430
|
// panel iff this is the build we just self-updated into (guarded to once).
|
|
3328
3431
|
maybeShowWhatsNew();
|
|
3432
|
+
// Track the installed AWARE runtime version so the right-click freeze menu can
|
|
3433
|
+
// gate itself (needs ≥ 0.77) and the canvas hint can advertise it once available.
|
|
3434
|
+
if (h && h.awareVersion) {
|
|
3435
|
+
awareRuntimeVersion = h.awareVersion;
|
|
3436
|
+
if (!freezeHintAppended && awareSupportsFreeze()) {
|
|
3437
|
+
const ch = document.getElementById('canvas-hint');
|
|
3438
|
+
if (ch) { ch.textContent += ' Right-click a node to freeze its output.'; freezeHintAppended = true; }
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3329
3441
|
const wv = document.getElementById('aware-version');
|
|
3330
3442
|
if (wv && h && h.awareVersion) {
|
|
3331
3443
|
const next = 'AWARE ' + h.awareVersion;
|