@floless/app 0.74.0 → 0.76.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.
@@ -236,19 +236,38 @@
236
236
  },
237
237
  "joint": {
238
238
  "type": "object",
239
- "required": ["id", "kind", "main"],
239
+ "required": ["id", "kind"],
240
240
  "additionalProperties": true,
241
- "description": "A placed connection the geometry-bearing recipe the connection engine expands into real 3D parts bound to its member(s). Distinct from the `connections[]` library (vendor-neutral type/detail/targets identities a joint can cite). The engine derives all geometry from this recipe + the live member positions, so a joint moves/re-solves with its members. v1 kinds: base-plate, shear-plate.",
241
+ "description": "A placed connection the connection engine expands into real 3D parts. A parametric RECIPE kind (base-plate, shear-plate) derives all geometry from its `params` + the live member positions, so it moves/re-solves with its members. A 'custom' kind is an imported connection (read from IFC via AWARE's connection-reader) that carries its own opaque tessellated `geometry` and a `place` point — move / replace only, no per-dimension edit. Distinct from the `connections[]` library (vendor-neutral type/detail/targets identities a joint can cite). v1 kinds: base-plate, shear-plate, custom.",
242
242
  "properties": {
243
- "id": { "type": "string", "description": "Stable joint id, e.g. j-bp-1 / sp-b3-e0." },
244
- "kind": { "type": "string", "description": "Connection kind the engine knows how to expand: base-plate, shear-plate (v1). end-plate / cap-plate / … follow." },
245
- "main": { "type": "string", "description": "Id of the host member this joint hangs off. For a base-plate this is the column; for a shear-plate this is the SUPPORTED BEAM (the joint sits at one of the beam's ends — see `at`)." },
243
+ "id": { "type": "string", "description": "Stable joint id, e.g. j-bp-1 / sp-b3-e0 / cx-<id>." },
244
+ "kind": { "type": "string", "description": "Connection kind: base-plate, shear-plate (parametric recipes the engine expands), or custom (imported IFC geometry). end-plate / cap-plate / … follow." },
245
+ "main": { "type": "string", "description": "Id of the host member this joint hangs off. For a base-plate this is the column; for a shear-plate this is the SUPPORTED BEAM (the joint sits at one of the beam's ends — see `at`). Required for the recipe kinds; optional for 'custom' (an imported connection carries its own geometry rather than deriving it from a member)." },
246
246
  "secondaries": { "type": "array", "items": { "type": "string" }, "description": "Ids of the other members this joint binds — e.g. the SUPPORT a shear-plate frames into. Optional and inferred by geometry; the shear-plate engine does not require it (the fin plate is placed off the beam end + beam axis). Empty for a base-plate." },
247
247
  "at": { "type": "string", "description": "Where on the main member the joint sits (kind-dependent): base-plate = base; shear-plate = end0 | end1 — the beam end framing into the support (end0 ↔ ends[0]/start, end1 ↔ ends[1]/end)." },
248
248
  "conn": { "type": "string", "description": "Optional `connections[]` row id this joint cites for its type/detail identity (keeps the connection library + confidence scoring in play)." },
249
- "params": { "type": "object", "additionalProperties": true, "description": "Geometry settings (mm) with engine defaults; absent = all defaults. base-plate: plateWidth/plateDepth/thickness, boltCols/boltRows/boltDia/edgeDist, weldLeg, anchor kit, levelingNut. shear-plate: plateThickness, plateWidth (along the beam), plateHeight (vertical), boltCols/boltRows/boltDia/boltPitch/edgeDist, weldLeg, clearance (the erection gap so the beam clears the support, ~½–¾\"; pulls the beam box back), webSide (+1/-1 — which face of the beam web the plate laps), boltGrade (A325 | A490 — the AISC bolt length auto-sizes from the grip), stiffener (bool — a full-height stiffener on the far side of a girder web)." },
249
+ "params": { "type": "object", "additionalProperties": true, "description": "Geometry settings (mm) with engine defaults; absent = all defaults. base-plate: plateWidth/plateDepth/thickness, boltCols/boltRows/boltDia/edgeDist, weldLeg, anchor kit, levelingNut. shear-plate: plateThickness, plateWidth (along the beam), plateHeight (vertical), boltCols/boltRows/boltDia/boltPitch/edgeDist, weldLeg, clearance (the erection gap so the beam clears the support, ~½–¾\"; pulls the beam box back), webSide (+1/-1 — which face of the beam web the plate laps), boltGrade (A325 | A490 — the AISC bolt length auto-sizes from the grip), stiffener (bool — a full-height stiffener on the far side of a girder web). Not used by 'custom'." },
250
250
  "source": { "enum": ["auto", "user"], "description": "auto = proposed by auto-detail from the takeoff; user = placed/edited by hand." },
251
- "verified": { "type": "boolean", "description": "True = a human confirmed this joint." }
251
+ "verified": { "type": "boolean", "description": "True = a human confirmed this joint." },
252
+ "name": { "type": "string", "description": "custom only: display label carried from the source IFC (e.g. \"COLUMN C102\")." },
253
+ "place": { "$ref": "#/$defs/point3", "description": "custom only: the world-mm point the connection's LOCAL `geometry` is translated by — its placement in the model." },
254
+ "geometry": { "type": "array", "items": { "$ref": "#/$defs/meshPart" }, "description": "custom only: imported mesh parts (one per plate/bolt/weld) in the connection's LOCAL mm frame. Read once from IFC by AWARE's connection-reader; opaque tessellated geometry (no per-dimension edit)." }
255
+ },
256
+ "allOf": [
257
+ { "if": { "properties": { "kind": { "enum": ["base-plate", "shear-plate"] } } }, "then": { "required": ["main"] } },
258
+ { "if": { "properties": { "kind": { "const": "custom" } } }, "then": { "required": ["geometry"] } }
259
+ ]
260
+ },
261
+ "meshPart": {
262
+ "type": "object",
263
+ "required": ["positions", "indices"],
264
+ "additionalProperties": true,
265
+ "description": "One tessellated mesh part of a 'custom' connection (a plate / bolt / weld), in the connection's LOCAL mm frame: flat x,y,z position triples + 0-based triangle indices — AWARE's generic kind:\"mesh\" scene primitive. Read from IFC by the connection-reader; never hand-authored.",
266
+ "properties": {
267
+ "id": { "type": "string", "description": "Source part id (the IFC element GlobalId)." },
268
+ "role": { "type": "string", "description": "Hardware role from the source IFC: plate | bolt | weld." },
269
+ "positions": { "type": "array", "items": { "type": "number" }, "description": "Flat x,y,z vertex coordinates (mm), local to the connection; length is a multiple of 3." },
270
+ "indices": { "type": "array", "items": { "type": "number" }, "description": "0-based triangle vertex indices (triples) into positions." }
252
271
  }
253
272
  },
254
273
  "point2": {
package/dist/web/app.css CHANGED
@@ -3344,6 +3344,8 @@ body {
3344
3344
  .app.mode-workspaces .notes-strip, .app.mode-workspaces .find-overlay,
3345
3345
  /* the ws surfaces carry their own headings — hide the canvas's "Canvas · transparency layer" label */
3346
3346
  .app.mode-workspaces .canvas > .panel-label { display:none !important; }
3347
+ /* …but Find in model reuses that overlay — un-hide it while open (Ctrl+F searches editor members). */
3348
+ .app.mode-workspaces .find-overlay.show { display: flex !important; }
3347
3349
  /* the editor owns center+right in a project — collapse the inspect column entirely */
3348
3350
  .app.mode-workspaces { --right-width:0px !important; }
3349
3351
  .app.mode-workspaces .inspect { display:none; }
@@ -3473,9 +3475,26 @@ table.hist tr.current td { background:var(--accent-soft); }
3473
3475
  .hist .gate-badge { display:inline-block; padding:2px 7px; border-radius:4px; font-size:9.5px; font-weight:600;
3474
3476
  text-transform:uppercase; letter-spacing:.05em; background:transparent; border:1px solid var(--border-strong); color:var(--text-dim); }
3475
3477
  .hist .gate-badge.model { background:var(--accent-soft); border-color:var(--accent-dim); color:var(--accent-bright); }
3476
- .hist .btn-mini { background:none; border:1px solid var(--border-strong); color:var(--text-muted); border-radius:6px;
3478
+ .hist .btn-mini, .ws-drawings-bar .btn-mini, .ws-revision-card .btn-mini { background:none; border:1px solid var(--border-strong); color:var(--text-muted); border-radius:6px;
3477
3479
  padding:4px 10px; font-size:11px; cursor:pointer; white-space:nowrap; }
3478
- .hist .btn-mini:hover { color:var(--text); border-color:var(--accent); }
3480
+ .hist .btn-mini:hover, .ws-drawings-bar .btn-mini:hover, .ws-revision-card .btn-mini:hover { color:var(--text); border-color:var(--accent); }
3481
+
3482
+ /* ── Workspaces: revision re-read (Slice 4b) — attach band + pending/failed card ── */
3483
+ .ws-drawings-bar { flex:none; display:flex; align-items:center; gap:10px; padding:8px 14px; border-bottom:1px solid var(--border); background:var(--surface); }
3484
+ .ws-drawings-bar[hidden] { display:none !important; }
3485
+ .ws-drawings-bar-label { font-size:10px; color:var(--text-dim); text-transform:uppercase; letter-spacing:.1em; }
3486
+ .ws-drawings-bar-sep { flex:1; }
3487
+ .ws-revision-card { flex:none; display:flex; align-items:center; gap:10px; padding:9px 14px; border-bottom:1px solid var(--border); background:var(--surface-2); font-size:12px; color:var(--text-muted); }
3488
+ .ws-revision-card[hidden] { display:none !important; }
3489
+ .ws-revision-card b { color:var(--text); font-weight:600; }
3490
+ .ws-revision-card .wrc-text { flex:1; min-width:0; }
3491
+ .ws-revision-card .wrc-spin { flex:none; width:14px; height:14px; border:2px solid var(--border-strong); border-top-color:var(--accent); border-radius:50%; animation:spin .8s linear infinite; }
3492
+ .ws-revision-card.state-failed { border-left:3px solid var(--err); }
3493
+ .ws-revision-card.state-failed .wrc-err-ico { color:var(--err); flex:none; }
3494
+ .ws-revision-card.state-failed .wrc-err-detail { color:var(--text-dim); }
3495
+ .ws-revision-card .wrc-actions { display:flex; gap:6px; flex:none; }
3496
+ @media (prefers-reduced-motion: reduce) { .ws-revision-card .wrc-spin { animation:none; } }
3497
+ .hist .gate-badge.ai-read { background:var(--accent-soft); border-color:var(--accent-dim); color:var(--accent-bright); }
3479
3498
 
3480
3499
  /* Rollback confirm — a styled anchored popover (NEVER a native dialog); names the Exports re-lock. */
3481
3500
  .hist-confirm { position:absolute; z-index:20; width:264px; padding:12px 13px; background:var(--surface-3);
package/dist/web/app.js CHANGED
@@ -1094,6 +1094,10 @@ const $findInput = document.getElementById('find-input');
1094
1094
  const $findCount = document.getElementById('find-count');
1095
1095
 
1096
1096
  function openFind() {
1097
+ const ws = window.flolessWorkspaces && window.flolessWorkspaces.findActive();
1098
+ // In Workspaces mode there's nothing to find off the Model step — don't pop a dead overlay.
1099
+ if (document.getElementById('app').classList.contains('mode-workspaces') && !ws) return;
1100
+ $findInput.placeholder = ws ? 'Find in model…' : 'Find agent…';
1097
1101
  $findOverlay.classList.add('show');
1098
1102
  $findInput.value = '';
1099
1103
  $findCount.textContent = '';
@@ -1101,13 +1105,21 @@ function openFind() {
1101
1105
  }
1102
1106
  function closeFind() {
1103
1107
  $findOverlay.classList.remove('show');
1108
+ if (window.flolessWorkspaces && window.flolessWorkspaces.clearFind) window.flolessWorkspaces.clearFind();
1104
1109
  document.querySelectorAll('.agent-card').forEach(c => {
1105
1110
  c.classList.remove('find-dim', 'find-match');
1106
1111
  });
1107
1112
  }
1108
1113
 
1109
1114
  $findInput.addEventListener('input', () => {
1110
- const q = $findInput.value.toLowerCase().trim();
1115
+ const raw = $findInput.value;
1116
+ const q = raw.toLowerCase().trim();
1117
+ // Workspaces + Model step: search members in the embedded editor, not the canvas.
1118
+ if (window.flolessWorkspaces && window.flolessWorkspaces.findActive()) {
1119
+ const r = window.flolessWorkspaces.find(raw) || { count: 0 };
1120
+ $findCount.textContent = q ? `${r.count} match${r.count === 1 ? '' : 'es'}` : '';
1121
+ return;
1122
+ }
1111
1123
  let matches = 0;
1112
1124
  document.querySelectorAll('.agent-card').forEach(card => {
1113
1125
  if (!q) {
package/dist/web/aware.js CHANGED
@@ -4655,6 +4655,13 @@
4655
4655
  if (req.type === 'new-workflow') {
4656
4656
  return `Help me build a brand-new floless workflow from scratch, step by step, with your floless-app-new-workflow skill. Ask me what I want it to do, then before writing any node confirm the agents it needs are installed (offer to install a missing one from the catalogue, or to report a not-yet-existing agent as an idea). Author the .flo node by node, verifying each step is correct (plain-English descriptions, validate, compile, a real run), and finish by installing → Compile → ▶ Run so I can see it work and approve.`;
4657
4657
  }
4658
+ if (req.type === 'revision-read') {
4659
+ const snaps = req.snapshots && req.snapshots.length
4660
+ ? `\nRevised drawing${req.snapshots.length > 1 ? 's' : ''} (read these): ${req.snapshots.join(', ')}`
4661
+ : '';
4662
+ const note = req.instruction ? `\nMy note: ${req.instruction}` : '';
4663
+ return `In floless project "${req.project}" (app "${req.appId}"), a REVISED drawing set was attached — re-read it into the SAME project as a new version (do NOT create a new project). Read the drawings per the floless-app-steel-takeoff skill, compose the updated steel.takeoff/v1 contract, then POST /api/projects/${req.project}/revision-read with { contract, message: "<plain-English what changed>", requestId: "${req.id}" }. The server derives the base version + source provenance from this queued request, records a "revision-read" version, and clears the request — do NOT DELETE the request yourself. If you can't read the set, POST /api/projects/${req.project}/revision-requests/${req.id}/fail with { error }.${snaps}${note}`;
4664
+ }
4658
4665
  return '';
4659
4666
  }
4660
4667
 
@@ -4665,6 +4672,7 @@
4665
4672
  tweak: 'floless-app-workflows',
4666
4673
  'ui-customize': 'floless-app-ui',
4667
4674
  rebake: 'floless-app-rebake',
4675
+ 'revision-read': 'floless-app-steel-takeoff',
4668
4676
  // The guided-tour ask is picked up by onboarding; the instruction body names the
4669
4677
  // per-app floless-app-<appId> skill for depth when one exists.
4670
4678
  guide: 'floless-app-onboarding',
@@ -4682,10 +4690,15 @@
4682
4690
  if (!body) return '';
4683
4691
  const skill = REQUEST_SKILL[req.type] || 'floless-app-workflows';
4684
4692
  const base = (typeof location !== 'undefined' && location.origin) ? location.origin : 'http://127.0.0.1:4317';
4693
+ // A revision-read is cleared by its own /revision-read POST — a separate DELETE would 404. Every
4694
+ // other type is applied-then-DELETEd by the terminal AI.
4695
+ const clears = req.type === 'revision-read'
4696
+ ? `apply it via the route in the line below (that POST clears this request — do NOT DELETE it separately).`
4697
+ : `apply that request, then DELETE ${base}/api/requests/${req.id}.`;
4685
4698
  const marker =
4686
4699
  `[floless-request type=${req.type} id=${req.id}] — queued from the FloLess Dashboard. ` +
4687
4700
  `Apply it with your ${skill} skill: fetch the authoritative spec from GET ${base}/api/requests, ` +
4688
- `apply that request, then DELETE ${base}/api/requests/${req.id}. Don't run the line below verbatim.`;
4701
+ `${clears} Don't run the line below verbatim.`;
4689
4702
  return `${marker}\n${body}`;
4690
4703
  }
4691
4704
 
@@ -4764,8 +4777,8 @@
4764
4777
  return;
4765
4778
  }
4766
4779
  $list.innerHTML = pendingRequests.map((r) => {
4767
- const label = r.type === 'use-template' ? 'template' : r.type === 'ui-customize' ? 'dashboard' : r.type === 'rebake' ? 're-bake' : r.type === 'guide' ? 'guide' : r.type === 'new-workflow' ? 'new workflow' : 'tweak';
4768
- const badgeCls = r.type === 'tweak' || r.type === 'ui-customize' || r.type === 'rebake' ? 'req-type req-type-tweak' : 'req-type';
4780
+ const label = r.type === 'use-template' ? 'template' : r.type === 'ui-customize' ? 'dashboard' : r.type === 'rebake' ? 're-bake' : r.type === 'revision-read' ? 'revision' : r.type === 'guide' ? 'guide' : r.type === 'new-workflow' ? 'new workflow' : 'tweak';
4781
+ const badgeCls = r.type === 'tweak' || r.type === 'ui-customize' || r.type === 'rebake' || r.type === 'revision-read' ? 'req-type req-type-tweak' : 'req-type';
4769
4782
  const target = r.type === 'tweak' && r.nodeId
4770
4783
  ? ` · node <code>${escapeHtml(r.nodeId)}</code>`
4771
4784
  : r.type === 'rebake' && r.inputName
@@ -170,6 +170,15 @@
170
170
  <button type="button" data-step="exports" role="tab" aria-selected="false">Exports</button>
171
171
  <button type="button" data-step="history" role="tab" aria-selected="false">History</button>
172
172
  </div>
173
+ <!-- Drawings step: a shell toolbar band + revision-read status card ABOVE the filter iframe
174
+ (chrome around it, never inside — the filter owns its own layout). Shown only on Drawings. -->
175
+ <div class="ws-drawings-bar" id="ws-drawings-bar" hidden>
176
+ <span class="ws-drawings-bar-label">Drawing set</span>
177
+ <span class="ws-drawings-bar-sep"></span>
178
+ <button type="button" id="ws-attach-revision" class="btn-mini" data-tip="Attach a revised drawing set — your terminal AI reads it into a new version">⤒ Attach revised drawings…</button>
179
+ <input type="file" id="ws-revision-file" accept=".pdf,image/*" multiple hidden>
180
+ </div>
181
+ <div class="ws-revision-card" id="ws-revision-card" hidden></div>
173
182
  <!-- Two lazily-srced iframes so switching steps never reloads the editor's 3D state.
174
183
  Same-origin like #contract-editor-frame (they call /api/contract directly). -->
175
184
  <iframe id="ws-frame-model" class="ws-frame" title="Project model editor" hidden></iframe>
@@ -422,6 +422,18 @@ function placeWasher(mesh, el) {
422
422
  mesh.position.copy(A); // extrudes from `from` toward `to`
423
423
  }
424
424
 
425
+ // An imported connection part (slice B): AWARE's generic kind:"mesh" primitive — flat world-mm
426
+ // positions + 0-based triangle indices straight from the connection-reader. Absolute coordinates, so
427
+ // the mesh sits at the root origin like a member box (no local transform). Winding order isn't trusted
428
+ // (its material is double-sided) — computeVertexNormals so it still shades.
429
+ function placeMesh(mesh, el) {
430
+ const g = new THREE.BufferGeometry();
431
+ g.setAttribute('position', new THREE.Float32BufferAttribute(el.positions || [], 3));
432
+ if (Array.isArray(el.indices) && el.indices.length) g.setIndex(el.indices);
433
+ g.computeVertexNormals();
434
+ mesh.geometry = g;
435
+ }
436
+
425
437
  // Build a mesh for one scene element by kind: a member box (default) or a connection part. Returns
426
438
  // false for an unknown kind so the caller skips it (forward-compatible with later part kinds).
427
439
  function placeElement(mesh, el) {
@@ -434,6 +446,7 @@ function placeElement(mesh, el) {
434
446
  case 'nut': placeNut(mesh, el); return true;
435
447
  case 'washer': placeWasher(mesh, el); return true;
436
448
  case 'weld': placeWeld(mesh, el); return true;
449
+ case 'mesh': placeMesh(mesh, el); return true;
437
450
  default: return false;
438
451
  }
439
452
  }
@@ -444,7 +457,10 @@ function materialFor(groupKey) {
444
457
  // Fasteners (anchor rods / bolts / nuts / washers) read more metallic (zinc-plated); plates/welds/members stay matte.
445
458
  const metallic = groupKey === 'anchor' || groupKey === 'bolt' || groupKey === 'nut' || groupKey === 'washer';
446
459
  const metal = metallic ? 0.5 : 0.1, rough = metallic ? 0.4 : 0.75;
447
- baseMat.set(groupKey, new THREE.MeshStandardMaterial({ color: col, metalness: metal, roughness: rough }));
460
+ // Imported (custom) connection meshes are tessellated with untrusted winding order → render both
461
+ // faces so no triangle drops out; matte steel like a plate.
462
+ const dbl = groupKey === 'custom';
463
+ baseMat.set(groupKey, new THREE.MeshStandardMaterial({ color: col, metalness: metal, roughness: rough, side: dbl ? THREE.DoubleSide : THREE.FrontSide }));
448
464
  }
449
465
  return baseMat.get(groupKey);
450
466
  }