@floless/app 0.31.1 → 0.31.2

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.
@@ -52835,7 +52835,7 @@ function appVersion() {
52835
52835
  return resolveVersion({
52836
52836
  isSea: isSea2(),
52837
52837
  sqVersionXml: readSqVersionXml(),
52838
- define: true ? "0.31.1" : void 0,
52838
+ define: true ? "0.31.2" : void 0,
52839
52839
  pkgVersion: readPkgVersion()
52840
52840
  });
52841
52841
  }
@@ -52845,7 +52845,7 @@ function resolveChannel(s) {
52845
52845
  return "dev";
52846
52846
  }
52847
52847
  function appChannel() {
52848
- return resolveChannel({ isSea: isSea2(), define: true ? "0.31.1" : void 0 });
52848
+ return resolveChannel({ isSea: isSea2(), define: true ? "0.31.2" : void 0 });
52849
52849
  }
52850
52850
 
52851
52851
  // oauth-presets.ts
package/dist/web/app.css CHANGED
@@ -566,6 +566,7 @@
566
566
  font-size: 12px;
567
567
  background: var(--accent-soft);
568
568
  flex-shrink: 0;
569
+ cursor: help;
569
570
  }
570
571
  .agent-card .fav-btn {
571
572
  position: absolute;
@@ -1124,6 +1125,10 @@
1124
1125
  never deletes — matching the app's other destructive affordances. */
1125
1126
  .modal-actions button.danger { border-color: var(--border-strong); color: var(--text); font-weight: 600; }
1126
1127
  .modal-actions button.danger:hover { color: var(--err); border-color: var(--err); background: rgba(248, 113, 113, 0.1); box-shadow: none; }
1128
+ /* #125: form modal with many fields must not overflow the viewport — cap + scroll
1129
+ the body between the pinned title and actions, mirroring .modal.routines. */
1130
+ #form-modal .modal { max-height: 86vh; display: flex; flex-direction: column; }
1131
+ #form-modal-body { overflow-y: auto; flex: 1 1 auto; min-height: 0; }
1127
1132
  .modal.library {
1128
1133
  width: 720px;
1129
1134
  max-height: 82vh;
package/dist/web/app.js CHANGED
@@ -352,7 +352,7 @@ function cardEl(id, ports) {
352
352
  <div class="kind">${a.kind}</div>
353
353
  <div class="ver">${a.version}</div>
354
354
  </div>
355
- <div class="icon">${a.icon}</div>
355
+ <div class="icon"${a.iconTip ? ` data-tip="${escapeAttr(a.iconTip)}"` : ''}>${a.icon}</div>
356
356
  </div>
357
357
  <div class="title">${a.title}</div>
358
358
  <div class="subtitle">${a.subtitle}</div>
package/dist/web/aware.js CHANGED
@@ -94,10 +94,28 @@
94
94
  }
95
95
 
96
96
  const AGENT_ICONS = {
97
+ tekla: '<line x1="4" y1="5" x2="20" y2="5"/><line x1="12" y1="5" x2="12" y2="20"/>',
98
+ file: '<path d="M13 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V9z"/><polyline points="13 2 13 9 20 9"/>',
99
+ excel: '<rect x="2" y="3" width="20" height="18" rx="2"/><line x1="12" y1="3" x2="12" y2="21"/><line x1="2" y1="12" x2="22" y2="12"/>',
100
+ revit: '<rect x="3" y="4" width="18" height="17" rx="1"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="12" y1="21" x2="12" y2="12"/>',
101
+ slack: '<line x1="4" y1="10" x2="20" y2="10"/><line x1="4" y1="14" x2="20" y2="14"/><line x1="10" y1="4" x2="8" y2="20"/><line x1="16" y1="4" x2="14" y2="20"/>',
102
+ 'trimble-connect': '<path d="M17.5 10a4.5 4.5 0 00-9 0A4 4 0 009 18h6a4 4 0 002.5-7z"/><line x1="12" y1="18" x2="12" y2="12"/><polyline points="9 15 12 12 15 15"/>',
103
+ 'microsoft-365': '<rect x="2" y="2" width="8" height="8" rx="1"/><rect x="14" y="2" width="8" height="8" rx="1"/><rect x="2" y="14" width="8" height="8" rx="1"/><rect x="14" y="14" width="8" height="8" rx="1"/>',
104
+ email: '<rect x="2" y="4" width="20" height="16" rx="2"/><path d="M2 7l10 7 10-7"/>',
105
+ http: '<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>',
106
+ };
107
+ const AGENT_NAMES = {
108
+ tekla: 'Tekla', file: 'File', excel: 'Excel', revit: 'Revit', slack: 'Slack',
109
+ 'trimble-connect': 'Trimble Connect', 'microsoft-365': 'Microsoft 365', email: 'Email', http: 'HTTP',
110
+ };
111
+ const _svgWrap = (inner) => `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${inner}</svg>`;
112
+ const iconFor = (agent) => _svgWrap(AGENT_ICONS[agent] || '<path d="M12 2l8 10-8 10L4 12z"/>');
113
+ const iconTipFor = (agent) => AGENT_NAMES[agent] || (agent ? agent.replace(/-/g, ' ') : '');
114
+ const AGENT_ICONS_TEXT = {
97
115
  tekla: '⊞', file: '🗀', excel: '◵', revit: '◰', slack: '✦',
98
116
  'trimble-connect': '☁', 'microsoft-365': '✉', email: '✉', http: '⇄',
99
117
  };
100
- const iconFor = (agent) => AGENT_ICONS[agent] || '◈';
118
+ const iconTextFor = (agent) => AGENT_ICONS_TEXT[agent] || '◈';
101
119
 
102
120
  // The demo's renderChat staggers narration via setTimeout; its bootstrap call
103
121
  // (fired before this script runs) would otherwise drip stale demo lines into
@@ -289,6 +307,7 @@
289
307
  // Description tab: a plain-English explanation + a small input table that
290
308
  // EXCLUDES the code (code belongs to the Code tab, never dumped here).
291
309
  const plainDesc = execSource ? leadingComment(execSource) : '';
310
+ const blurbText = plainDesc && (s => s.length > 80 ? s.slice(0, 79) + '…' : s)(plainDesc.replace(/\.\s+.*/, '.').trim());
292
311
  const inputsNoCode = {};
293
312
  for (const [k, v] of Object.entries(inputs || {})) if (k !== 'code') inputsNoCode[k] = v;
294
313
  return {
@@ -296,11 +315,12 @@
296
315
  _runtimeModel: !!n.runtimeModel, // B2: lock stamped runtime-model → card badge
297
316
  _frozen: !!n.frozen, // source node carries a `frozen:` block → card badge + Unfreeze menu
298
317
  icon: iconFor(n.agent),
318
+ iconTip: iconTipFor(n.agent),
299
319
  kind: n.kind === 'agent' && n.agent ? `${agentLabel} agent` : escapeHtml(n.kind),
300
320
  version: pin ? `v${escapeHtml(String(pin))}` : '—',
301
321
  title: escapeHtml(n.id),
302
322
  subtitle: `${agentLabel}${cmd ? '/' + cmd : ''} · ${mode}`,
303
- blurb: n.notes[0] ? escapeHtml(humanizeNote(n.notes[0].text)) : `${mode}-mode ${cmd ? '<code>' + cmd + '</code>' : escapeHtml(n.kind)} node`,
323
+ blurb: blurbText ? escapeHtml(blurbText) : n.notes[0] ? escapeHtml(humanizeNote(n.notes[0].text)) : `${mode}-mode ${cmd ? '<code>' + cmd + '</code>' : escapeHtml(n.kind)} node`,
304
324
  description: `${plainDesc ? `<p>${escapeHtml(plainDesc)}</p>` : ''}<p>Resolves to ${modeBadge} via <code>${escapeHtml(n.agent || n.kind)}${n.command ? '.' + escapeHtml(n.command) : ''}</code>.</p>${Object.keys(inputsNoCode).length ? `<p><strong>Inputs</strong></p>${kvTable(inputsNoCode)}` : ''}${execSource ? `<p class="dim-note">Full source in the <strong>Code</strong> tab.</p>` : ''}${notesHtml}`,
305
325
  // Rich agent/command detail when the manifest is installed (n.skill); else
306
326
  // the lock-pointer fallback below (exec/agent-less nodes, uninstalled agents).
@@ -1624,7 +1644,7 @@
1624
1644
  card.setAttribute('role', 'status');
1625
1645
 
1626
1646
  const status = mkEl('div', 'cred-fail-status');
1627
- const icon = mkEl('span', 'cred-fail-icon', iconFor(cred.id));
1647
+ const icon = mkEl('span', 'cred-fail-icon', iconTextFor(cred.id));
1628
1648
  icon.setAttribute('aria-hidden', 'true');
1629
1649
  status.append(icon, document.createTextNode('SESSION EXPIRED'));
1630
1650
 
@@ -1774,6 +1794,7 @@
1774
1794
  const first = $body.querySelector('input:not(.fm-file-input),textarea');
1775
1795
  if (first) { first.focus(); if (first.select) first.select(); }
1776
1796
  else if (danger) $cancel.focus();
1797
+ else $ok.focus();
1777
1798
  // ── images-field setup ──────────────────────────────────────────────────────
1778
1799
  const imageStates = new Map();
1779
1800
  $body.querySelectorAll('.fm-images').forEach((box) => {
@@ -2563,6 +2584,7 @@
2563
2584
  // popover → Update now → POST /api/update/apply, which downloads + relaunches into
2564
2585
  // the new version. Hidden in dev/npm (supported:false).
2565
2586
  const $appUpdate = document.getElementById('app-update');
2587
+ let _appUpdateRecoveryTimer = null; // cleared before each new apply attempt
2566
2588
  // The apply body, extracted so the popover's "Update now" can drive it with the
2567
2589
  // polled target version (no textContent scrape). Preserves the npm-vs-desktop
2568
2590
  // branch verbatim: npm copies the command; desktop confirms → relaunch.
@@ -2578,18 +2600,32 @@
2578
2600
  }
2579
2601
  // desktop (Velopack): one-click download + relaunch
2580
2602
  const v = version || '';
2581
- if (!window.confirm('Download v' + v + ' and relaunch floless.app now?')) return;
2603
+ const go = await formModal({ title: 'Update and relaunch', sub: 'FloLess will download v' + v + ', close, and relaunch into the new version.', fields: [], okLabel: 'Update now' });
2604
+ if (!go) return;
2582
2605
  // Record the pending version so the new build's first paint can reveal the
2583
2606
  // what's-new panel (survives the relaunch via localStorage; cleared once shown).
2584
2607
  try { localStorage.setItem('floless.whatsNew.pending', v); } catch { /* private mode */ }
2608
+ if (_appUpdateRecoveryTimer) { clearTimeout(_appUpdateRecoveryTimer); _appUpdateRecoveryTimer = null; }
2585
2609
  $appUpdate.disabled = true;
2586
2610
  $appUpdate.textContent = '↑ Updating…';
2587
2611
  try {
2588
- const r = await fetch('/api/update/apply', { method: 'POST', headers: { 'content-type': 'application/json' } });
2612
+ const r = await fetch('/api/update/apply', { method: 'POST', headers: { 'content-type': 'application/json' }, signal: AbortSignal.timeout(5 * 60 * 1000) });
2589
2613
  const d = await r.json().catch(() => ({}));
2590
2614
  if (!r.ok || !d.ok) throw new Error(d.error || 'update failed');
2591
2615
  // Success: the server is exiting + Update.exe relaunches the new build. The health
2592
2616
  // poll flips to offline (R1 overlay), then the new version reconnects on its own.
2617
+ // Recovery guard: if the relaunch never happens and this page stays alive (e.g. a UAC
2618
+ // prompt was declined after the server already responded), re-enable the pill after 90 s
2619
+ // so the user can retry — the page won't go offline in that case.
2620
+ _appUpdateRecoveryTimer = setTimeout(() => {
2621
+ _appUpdateRecoveryTimer = null;
2622
+ if ($appUpdate && $appUpdate.disabled) {
2623
+ $appUpdate.disabled = false;
2624
+ $appUpdate.textContent = '↑ Relaunch failed — retry';
2625
+ try { localStorage.removeItem('floless.whatsNew.pending'); } catch { /* private mode */ }
2626
+ refreshUpdate(); // re-poll so the pill state reflects reality, same as catch path
2627
+ }
2628
+ }, 90_000);
2593
2629
  } catch (e) {
2594
2630
  $appUpdate.disabled = false;
2595
2631
  try { localStorage.removeItem('floless.whatsNew.pending'); } catch { /* private mode */ }
@@ -2667,7 +2703,8 @@
2667
2703
  async function applyAwareUpdate(version) {
2668
2704
  if (!$awareUpdate) return;
2669
2705
  const v = version || '';
2670
- if (!window.confirm('Upgrade AWARE runtime to v' + v + '? This reinstalls the npm package in place the app stays open and the version restamps automatically.')) return;
2706
+ const go = await formModal({ title: 'Upgrade AWARE runtime', sub: 'Reinstalls @aware-aeco/cli to v' + v + ' in place. The app stays open the version updates automatically when the install finishes.', fields: [], okLabel: 'Upgrade now' });
2707
+ if (!go) return;
2671
2708
  $awareUpdate.disabled = true;
2672
2709
  $awareUpdate.textContent = '↑ Upgrading…';
2673
2710
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floless/app",
3
- "version": "0.31.1",
3
+ "version": "0.31.2",
4
4
  "type": "module",
5
5
  "description": "Thin localhost host for floless.app — serves web/ and shells the aware CLI. No engine, no LLM.",
6
6
  "bin": {