@floless/app 0.19.0 → 0.20.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.
@@ -52683,7 +52683,7 @@ function appVersion() {
52683
52683
  return resolveVersion({
52684
52684
  isSea: isSea2(),
52685
52685
  sqVersionXml: readSqVersionXml(),
52686
- define: true ? "0.19.0" : void 0,
52686
+ define: true ? "0.20.0" : void 0,
52687
52687
  pkgVersion: readPkgVersion()
52688
52688
  });
52689
52689
  }
@@ -52693,7 +52693,7 @@ function resolveChannel(s) {
52693
52693
  return "dev";
52694
52694
  }
52695
52695
  function appChannel() {
52696
- return resolveChannel({ isSea: isSea2(), define: true ? "0.19.0" : void 0 });
52696
+ return resolveChannel({ isSea: isSea2(), define: true ? "0.20.0" : void 0 });
52697
52697
  }
52698
52698
 
52699
52699
  // oauth-presets.ts
@@ -57810,7 +57810,16 @@ async function startServer() {
57810
57810
  broadcast({ type: "run-started", id, dryRun: !!dryRun, simulate: !!simulate, runId: result.runId });
57811
57811
  for (const ev of events) broadcast({ type: "trace", id, event: ev });
57812
57812
  broadcast({ type: "run-ended", id, runId: result.runId });
57813
- const report = withBadge(extractReportHtml(events));
57813
+ const extracted = extractReportHtml(events);
57814
+ let report = null;
57815
+ if (extracted) {
57816
+ let interactive = false;
57817
+ try {
57818
+ interactive = readApp(id).nodes.find((n) => n.id === extracted.nodeId)?.agent === "viewer-3d";
57819
+ } catch {
57820
+ }
57821
+ report = { ...interactive ? extracted : withBadge(extracted), interactive };
57822
+ }
57814
57823
  const credentialIssue = !simulate && hasAuthFailure(events) ? await detectCredentialIssue(id) : null;
57815
57824
  return { ok: true, runId: result.runId, tracePath: result.tracePath, events, report, credentialIssue };
57816
57825
  }
package/dist/web/app.css CHANGED
@@ -2209,7 +2209,9 @@ body {
2209
2209
  .report-actions button.report-stop:hover { background: color-mix(in srgb, var(--warn) 16%, transparent); border-color: var(--warn); color: var(--text); }
2210
2210
  #report-close { font-size: 18px; line-height: 1; padding: 4px 10px; }
2211
2211
  .report-stage { position: relative; flex: 1; background: #0f172a; }
2212
- #report-frame { width: 100%; height: 100%; border: 0; display: block; background: #0f172a; }
2212
+ /* Only the SHOWN frame fills the stage `:not([hidden])` so `display:block` never defeats the
2213
+ `hidden` attribute on the inactive frame (else it keeps its height and pushes the other off-screen). */
2214
+ #report-frame:not([hidden]), #report-frame-3d:not([hidden]) { width: 100%; height: 100%; border: 0; display: block; background: #0f172a; }
2213
2215
 
2214
2216
  /* Live "On trigger" report: a pulsing pip in the viewer subtitle + a brief edge
2215
2217
  flash on the stage when a trigger routine (e.g. tekla-selection-report) pushes a
package/dist/web/aware.js CHANGED
@@ -18,6 +18,7 @@
18
18
  const $notesStrip = document.getElementById('notes-strip');
19
19
  const $reportModal = document.getElementById('report-modal');
20
20
  const $reportFrame = document.getElementById('report-frame');
21
+ const $reportFrame3d = document.getElementById('report-frame-3d');
21
22
  const $reportOverlay = document.getElementById('report-overlay');
22
23
  const $reportTitle = document.getElementById('report-title');
23
24
  const $reportSub = document.getElementById('report-sub');
@@ -838,10 +839,10 @@
838
839
  function reportNodeId() {
839
840
  const app = currentId && apps.get(currentId);
840
841
  if (!app || !app.nodes.length) return null;
841
- // An html-report agent node renders report HTML — its `render` command returns
842
- // an `html` field, which is exactly what extractReportHtml keys on. So it's a
843
- // report producer regardless of exec code; prefer it directly as the viewer node.
844
- const htmlReportNode = app.nodes.find((n) => n.agent === 'html-report');
842
+ // An html-report (static) or viewer-3d (interactive 3D) agent node renders HTML — its
843
+ // `render` command returns an `html` field, exactly what extractReportHtml keys on. So
844
+ // it's a report producer regardless of exec code; prefer it directly as the viewer node.
845
+ const htmlReportNode = app.nodes.find((n) => n.agent === 'html-report' || n.agent === 'viewer-3d');
845
846
  if (htmlReportNode) return htmlReportNode.id;
846
847
  // Otherwise, qualify the app only if some node actually PRODUCES report HTML — its exec
847
848
  // code returns an `html` field (the `data.result.html` extractReportHtml keys
@@ -953,7 +954,11 @@
953
954
  // clear anything we injected last render, so re-renders never stack
954
955
  card.querySelectorAll('.node-action, .node-inputs').forEach((b) => b.remove());
955
956
  if (isInput) addNodeInputs(card); // current values, above the button
956
- if (isReport) addNodeAction(card, 'View report ▸', () => showReport(id));
957
+ if (isReport) {
958
+ const rNode = app && app.nodes.find((n) => n.id === rid);
959
+ const viewLabel = rNode && rNode.agent === 'viewer-3d' ? 'View 3D ▸' : 'View report ▸';
960
+ addNodeAction(card, viewLabel, () => showReport(id));
961
+ }
957
962
  if (isInput) addNodeAction(card, 'Set inputs ▸', () => openInputsDialog());
958
963
  if (isRebake) addNodeAction(card, 'Re-read & re-bake ▸', () => openRebakeDialog());
959
964
  });
@@ -1245,7 +1250,37 @@
1245
1250
 
1246
1251
  // Share is visible exactly when the frame holds a report (the empty state HIDES it —
1247
1252
  // never a disabled affordance with no path forward).
1248
- function paintReport(html) { $reportFrame.srcdoc = html; $reportOverlay.hidden = true; $reportOverlay.replaceChildren(); $reportShare.hidden = false; }
1253
+ // The last HTML painted (static or 3D) so Open works for both surfaces.
1254
+ let lastPaintedHtml = '';
1255
+ // Paint a report into the viewer. A STATIC report uses the scriptless srcdoc frame; an
1256
+ // INTERACTIVE 3D doc (viewer-3d) uses the script-enabled frame via a data: URL (opaque
1257
+ // origin, cannot touch the app page) and keeps a "Loading 3D view…" overlay until the doc
1258
+ // posts `viewer-ready`. Exactly one frame is shown at a time.
1259
+ function paintReport(html, interactive) {
1260
+ lastPaintedHtml = html || '';
1261
+ if (interactive) {
1262
+ $reportFrame.hidden = true; $reportFrame.srcdoc = '';
1263
+ $reportFrame3d.hidden = false;
1264
+ $reportFrame3d.src = 'data:text/html;charset=utf-8,' + encodeURIComponent(html);
1265
+ $reportShare.hidden = true; // a script doc can't be statically hosted — hide, don't disable
1266
+ $reportOverlay.hidden = false; $reportOverlay.replaceChildren();
1267
+ $reportOverlay.append(mkEl('div', 'spinner'), mkEl('div', null, 'Loading 3D view…'));
1268
+ } else {
1269
+ $reportFrame3d.hidden = true; $reportFrame3d.removeAttribute('src');
1270
+ $reportFrame.hidden = false; $reportFrame.srcdoc = html;
1271
+ $reportOverlay.hidden = true; $reportOverlay.replaceChildren();
1272
+ $reportShare.hidden = false;
1273
+ }
1274
+ }
1275
+
1276
+ // Reset the viewer to "no report shown": clear BOTH frames + the Open buffer, so a stale
1277
+ // interactive 3D doc can't linger (its WebGL loop running) behind an overlay, and ↗ Open
1278
+ // can't reopen a previous app's report. Used by every empty / transition state.
1279
+ function clearReportFrames() {
1280
+ $reportFrame.srcdoc = ''; $reportFrame.hidden = false;
1281
+ $reportFrame3d.hidden = true; $reportFrame3d.removeAttribute('src');
1282
+ lastPaintedHtml = '';
1283
+ }
1249
1284
 
1250
1285
  // Double-click the HTML Viewer node → LOAD the last report (no run; running is
1251
1286
  // the header "▶ Run workflow" button's job). Shows a prompt if nothing has run yet.
@@ -1258,10 +1293,16 @@
1258
1293
  if (reportRunning) return; // a run is in flight — its overlay already shows progress
1259
1294
  const cached = lastReportByApp.get(currentId);
1260
1295
  if (cached && cached.html) {
1261
- $reportSub.innerHTML = `Last report${cached.label ? ' · ' + escapeHtml(cached.label) : ''} — rendered from the run's output, never composed by the UI.`;
1262
- paintReport(cached.html);
1296
+ const sfx = cached.label ? ' · ' + cached.label : '';
1297
+ if (cached.interactive) {
1298
+ $reportTitle.textContent = `3D Viewer · ${app.displayName}`;
1299
+ $reportSub.textContent = `Last 3D view${sfx} — drag to orbit, scroll to zoom, click an element to inspect.`;
1300
+ } else {
1301
+ $reportSub.textContent = `Last report${sfx} — rendered from the run's output, never composed by the UI.`;
1302
+ }
1303
+ paintReport(cached.html, cached.interactive);
1263
1304
  } else {
1264
- $reportFrame.srcdoc = '';
1305
+ clearReportFrames();
1265
1306
  $reportShare.hidden = true; // nothing to share — hide, don't disable
1266
1307
  $reportOverlay.hidden = false;
1267
1308
  $reportOverlay.innerHTML = `<div>No report yet. Click <strong>▶ Run workflow</strong> (top right) to run it against the live model, then double-click to view it any time.</div>`;
@@ -1286,7 +1327,7 @@
1286
1327
  const inputBadge = Object.entries(inputs).map(([k, v]) => `${k}=${v}`).join(' · ');
1287
1328
  $reportTitle.textContent = `HTML Viewer · ${app.displayName}`;
1288
1329
  $reportSub.innerHTML = `Live run of <code>${escapeHtml(nodeId)}</code>${inputBadge ? ' · ' + escapeHtml(inputBadge) : ''} — rendered from the node's output, never composed by the UI.`;
1289
- $reportFrame.srcdoc = ''; // clear any previously rendered report while this run is in flight
1330
+ clearReportFrames(); // clear any previous report (static or 3D) before this run paints a fresh one
1290
1331
  $reportShare.hidden = true; // the frame is blank — only a painted report is shareable
1291
1332
  $reportOverlay.hidden = false;
1292
1333
  $reportOverlay.innerHTML = opts.debug
@@ -1307,14 +1348,19 @@
1307
1348
  // body, not real data) — prompt reconnect instead of rendering the noise.
1308
1349
  if (res.credentialIssue) { surfaceCredentialIssue(res.credentialIssue); return; }
1309
1350
  const html = res.report && res.report.html;
1351
+ const interactive = !!(res.report && res.report.interactive);
1310
1352
  if (!html) {
1311
1353
  $reportOverlay.innerHTML = `<div>Run completed but <code>${escapeHtml(nodeId)}</code> returned no <code>html</code>. Check the Execution tab.</div>`;
1312
1354
  return;
1313
1355
  }
1314
- paintReport(html);
1315
- lastReportByApp.set(currentId, { nodeId, label: inputBadge, html });
1356
+ if (interactive) {
1357
+ $reportTitle.textContent = `3D Viewer · ${app.displayName}`;
1358
+ $reportSub.innerHTML = `Live 3D view from <code>${escapeHtml(nodeId)}</code>${inputBadge ? ' · ' + escapeHtml(inputBadge) : ''} — drag to orbit, scroll to zoom, click an element to inspect.`;
1359
+ }
1360
+ paintReport(html, interactive);
1361
+ lastReportByApp.set(currentId, { nodeId, label: inputBadge, html, interactive });
1316
1362
  $reportModal.dataset.html = '1';
1317
- appendNarration(`Rendered the report from <strong>${escapeHtml(nodeId)}</strong>${inputBadge ? ' (' + escapeHtml(inputBadge) + ')' : ''} — live run against the model.`);
1363
+ appendNarration(`Rendered the ${interactive ? '3D view' : 'report'} from <strong>${escapeHtml(nodeId)}</strong>${inputBadge ? ' (' + escapeHtml(inputBadge) + ')' : ''} — live run against the model.`);
1318
1364
  } catch (e) {
1319
1365
  const msg = e && e.message ? e.message : String(e);
1320
1366
  if (cancelRequested) {
@@ -1362,7 +1408,7 @@
1362
1408
  const CRED_BODY_IDLE = 'Sign in again and then run the workflow — it takes about 30 seconds.';
1363
1409
 
1364
1410
  function showCredentialReconnect(cred) {
1365
- $reportFrame.srcdoc = '';
1411
+ clearReportFrames();
1366
1412
  $reportShare.hidden = true;
1367
1413
  $reportOverlay.hidden = false;
1368
1414
  $reportOverlay.replaceChildren();
@@ -1674,10 +1720,22 @@
1674
1720
  // The Stop button is rebuilt into the overlay each run — delegate so one
1675
1721
  // listener survives every innerHTML swap.
1676
1722
  $reportOverlay.addEventListener('click', (e) => { if (e.target.closest('.overlay-stop')) stopRun(); });
1723
+ // The interactive 3D doc (viewer-3d) posts a load handshake from its opaque-origin frame:
1724
+ // `viewer-ready` clears the loading overlay; `viewer-error` (CDN/script/timeout) shows a
1725
+ // retry. Guard: only when the 3D frame is the active surface and the message is from it.
1726
+ window.addEventListener('message', (e) => {
1727
+ if ($reportFrame3d.hidden || e.source !== $reportFrame3d.contentWindow) return;
1728
+ const t = e.data && e.data.type;
1729
+ if (t === 'viewer-ready') { $reportOverlay.hidden = true; $reportOverlay.replaceChildren(); }
1730
+ else if (t === 'viewer-error') {
1731
+ $reportOverlay.hidden = false; $reportOverlay.replaceChildren();
1732
+ $reportOverlay.append(mkEl('div', null, '3D view failed to load — check your connection, then click ▶ Run workflow to try again.'));
1733
+ }
1734
+ });
1677
1735
  // The header ■ Stop run — the always-reachable twin of the overlay Stop (#39).
1678
1736
  if ($stopRunBtn) $stopRunBtn.onclick = () => stopRun();
1679
1737
  $reportOpen.onclick = () => {
1680
- const html = $reportFrame.srcdoc;
1738
+ const html = lastPaintedHtml || $reportFrame.srcdoc;
1681
1739
  if (!html) return;
1682
1740
  // Open via a blob: URL — an opaque origin, so the report can't script the
1683
1741
  // app origin (mirrors the iframe's sandbox intent).
@@ -4033,7 +4091,7 @@
4033
4091
  $reportModal.dataset.nodeId = reportNodeId() || '';
4034
4092
  const cached = lastReportByApp.get(id);
4035
4093
  if (cached && cached.html) paintReport(cached.html);
4036
- else { $reportFrame.srcdoc = ''; $reportShare.hidden = true; }
4094
+ else { clearReportFrames(); $reportShare.hidden = true; }
4037
4095
  $reportOverlay.hidden = false;
4038
4096
  $reportOverlay.replaceChildren();
4039
4097
  $reportOverlay.append(mkEl('div', 'spinner'), mkEl('div', null, 'Starting live session…'));
@@ -4118,7 +4176,7 @@
4118
4176
  $reportShare.hidden = false; // the cached report is the primary content
4119
4177
  $reportSub.innerHTML = `<span class="live-pip" aria-hidden="true"></span>Live · waiting for the next change — change your selection in Tekla to refresh the report.`;
4120
4178
  } else {
4121
- $reportFrame.srcdoc = '';
4179
+ clearReportFrames();
4122
4180
  $reportShare.hidden = true;
4123
4181
  $reportOverlay.hidden = false;
4124
4182
  $reportOverlay.replaceChildren();
@@ -4146,7 +4204,7 @@
4146
4204
  ? `Run complete · last report above — click <strong>▶ Run workflow</strong> to run again.`
4147
4205
  : `Live session stopped · last report above — click <strong>▶ Run workflow</strong> to start again.`;
4148
4206
  } else {
4149
- $reportFrame.srcdoc = '';
4207
+ clearReportFrames();
4150
4208
  $reportShare.hidden = true;
4151
4209
  $reportOverlay.hidden = false;
4152
4210
  $reportOverlay.replaceChildren();
@@ -319,6 +319,13 @@
319
319
  Scripts stay BLOCKED (no allow-scripts), so even though the report is now
320
320
  same-origin it has no JS to touch the app page — static reports only. -->
321
321
  <iframe id="report-frame" sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox" title="Report"></iframe>
322
+ <!-- Interactive output (the viewer-3d agent) needs scripts, so it cannot use the
323
+ scriptless static frame above. Loaded via a `data:text/html` URL → an opaque
324
+ origin (cannot reach the app page, cookies, or same-origin APIs). Minimal
325
+ sandbox: `allow-scripts` ONLY — the 3D doc has no links/popups of its own
326
+ (↗ Open is a parent-side action), and NO allow-same-origin. Shown only for
327
+ interactive reports; the static frame is used otherwise. -->
328
+ <iframe id="report-frame-3d" sandbox="allow-scripts" title="3D view" hidden></iframe>
322
329
  <div class="report-overlay" id="report-overlay" hidden></div>
323
330
  </div>
324
331
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floless/app",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
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": {