@floless/app 0.54.0 → 0.55.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.
@@ -52856,7 +52856,7 @@ function appVersion() {
52856
52856
  return resolveVersion({
52857
52857
  isSea: isSea2(),
52858
52858
  sqVersionXml: readSqVersionXml(),
52859
- define: true ? "0.54.0" : void 0,
52859
+ define: true ? "0.55.0" : void 0,
52860
52860
  pkgVersion: readPkgVersion()
52861
52861
  });
52862
52862
  }
@@ -52866,7 +52866,7 @@ function resolveChannel(s) {
52866
52866
  return "dev";
52867
52867
  }
52868
52868
  function appChannel() {
52869
- return resolveChannel({ isSea: isSea2(), define: true ? "0.54.0" : void 0 });
52869
+ return resolveChannel({ isSea: isSea2(), define: true ? "0.55.0" : void 0 });
52870
52870
  }
52871
52871
 
52872
52872
  // workflow-update.ts
@@ -60685,8 +60685,10 @@ var PRODUCT_SKILLS = [
60685
60685
  // compose Custom Panels (~/.floless/ui/extensions.json) for the Dashboard
60686
60686
  "floless-app-workflows",
60687
60687
  // author/run .flo workflows
60688
- "reading-structural-drawings"
60688
+ "reading-structural-drawings",
60689
60689
  // the drawing-set reading discipline (non-namespace) the steel guide + reader lean on; shipped so users get it too
60690
+ "steel-drawing-abbreviations"
60691
+ // default US/AISC abbreviations+shapes legend (non-namespace) the steel reader falls back to when a set omits its own legend sheet
60690
60692
  ];
60691
60693
  function selectShippedSkillNames(names) {
60692
60694
  const present = new Set(names);
@@ -63874,6 +63876,13 @@ async function startServer() {
63874
63876
  broadcast({ type: "request-added", request });
63875
63877
  return { ok: true, request };
63876
63878
  });
63879
+ app.post("/api/guide", async (req, reply) => {
63880
+ const { appId } = req.body ?? {};
63881
+ if (!appId) return reply.status(400).send({ ok: false, error: "appId required" });
63882
+ const request = addRequest({ type: "guide", appId });
63883
+ broadcast({ type: "request-added", request });
63884
+ return { ok: true, request };
63885
+ });
63877
63886
  app.delete("/api/requests/:id", async (req, reply) => {
63878
63887
  if (!deleteRequest(req.params.id)) return reply.status(404).send({ ok: false, error: "request not found" });
63879
63888
  broadcast({ type: "requests-changed" });
@@ -26,13 +26,16 @@ the run path and no API key.
26
26
  what was found and get an OK before spending effort on the next stage. Resume at any stage on
27
27
  re-invoke ("we already set up the folder, provision the agents").
28
28
 
29
- > Companion skills (both ship with floless.app — invoke them, don't duplicate them):
29
+ > Companion skills (all ship with floless.app — invoke them, don't duplicate them):
30
30
  > - **`reading-structural-drawings`** — the reading *discipline* (massing + coordinate frame FIRST,
31
31
  > members LAST, cross-validate every view). Follow it whenever skimming or classifying the set
32
32
  > (stages 2 and 5) — it stops the classic failures (two buildings read as one, a sloped roof read
33
33
  > as flat, grids conflated across sheets).
34
34
  > - **`floless-app-steel-takeoff`** — reads one framing plan into the contract (the heavy vision
35
35
  > pipeline, the filter pre-stage, the AISC/EN/BS lookup, the contract write + editor hand-off).
36
+ > - **`steel-drawing-abbreviations`** — the default US/AISC abbreviations + shape-designation legend.
37
+ > The fallback for note/profile codes (TYP, TOS, UON, `(E)`/`(N)`, HSS, WT) when the set omits its
38
+ > own abbreviations sheet (stage 2).
36
39
 
37
40
  ---
38
41
 
@@ -66,7 +69,9 @@ the project folder (a few files, **not** a framework):
66
69
 
67
70
  Read the set with the terminal AI's own vision, following the **`reading-structural-drawings`**
68
71
  discipline while skimming (massing + coordinate frame first — it's what makes the sheet
69
- classification reliable).
72
+ classification reliable). If the set has **no abbreviations/legend sheet** (a partial set is common),
73
+ fall back to **`steel-drawing-abbreviations`** — the standard US/AISC abbreviations + shape legend —
74
+ for note/profile codes (TYP, TOS, UON, `(E)`/`(N)`, HSS, WT). The project's own legend always overrides.
70
75
 
71
76
  **Gate:** show `set-index.md` (the sheet map) and confirm the classification looks right — the user
72
77
  knows their set better than any read.
@@ -366,3 +366,8 @@ never committed). Clip title blocks from any embedded preview.
366
366
  sizes or grid positions the drawing does not show.
367
367
  - **Resolve the port** from the running floless.app the same way other floless-app skills do
368
368
  (check `~/.floless/port` or the active server port).
369
+ - **Decode unfamiliar note/profile codes** with **`steel-drawing-abbreviations`** (the default
370
+ US/AISC abbreviations + shape legend) when the set lacks its own abbreviations sheet — for TYP
371
+ profiles, TOS/UON defaults, `(E)`/`(N)` scope, and shape designations. The project's own legend
372
+ always overrides; never expand a member *mark* (`MF`/`BF`/`D1`) from it — resolve those from the
373
+ schedules/elevations.
@@ -46,7 +46,9 @@ one with a curved roof — both obvious in the elevations, invisible in the plan
46
46
  Read the **cover / drawing index** and **general notes** first. Build a sheet list classified by
47
47
  type (index, notes, foundation plan, framing plans per level, frame elevations, sections, details,
48
48
  **schedules**). Confirm the set is complete + current revision. Grab the legend, datum, material
49
- defaults, and the **abbreviations** (FC/MF/BF/(E)/(N)/matchline symbols). Trust PyMuPDF page_count over
49
+ defaults, and the **abbreviations** (FC/MF/BF/(E)/(N)/matchline symbols). If the set ships **no
50
+ abbreviations/legend sheet** (or only partial sheets), fall back to the **`steel-drawing-abbreviations`**
51
+ default US/AISC legend for the standard codes. Trust PyMuPDF page_count over
50
52
  any external count.
51
53
 
52
54
  ### Pass 1 — MASSING (how many buildings, what shape) — from elevations/sections, not plans
@@ -114,6 +116,9 @@ Only then build the scene / overlay / IFC. If unsure, present the **massing + gr
114
116
  skill is the *reading discipline*; that one is the *reader*. The **`floless-app-steel-model`** guide
115
117
  (the front-door orchestrator) wraps the whole flow and references this discipline at the
116
118
  classify-and-verify gates rather than re-deriving it.
119
+ - **`steel-drawing-abbreviations`** — the default US/AISC abbreviations + shape-designation legend.
120
+ Consult it in Pass 0 when the set omits its own legend sheet, and whenever a note code (TYP, TOS,
121
+ UON, `(E)`/`(N)`) or profile designation needs decoding.
117
122
  - There is no off-the-shelf OSS tool that does the whole drawing→model job — floorplan vectorisers
118
123
  (e.g. CubiCasa5k) are residential-trained and won't read steel framing — which is why the order +
119
124
  validation discipline above, not a single tool, is the method.
@@ -0,0 +1,58 @@
1
+ ---
2
+ name: steel-drawing-abbreviations
3
+ description: Default legend of standard US (AISC, imperial) structural-steel drawing abbreviations, note conventions, and shape designations — the fallback meaning when a drawing set omits or only partially supplies its own abbreviations/legend sheet. This skill should be used while reading a structural steel drawing whenever an abbreviation, note code, or profile designation needs expanding (TYP, TOS, UON, EQ, (E)/(N), SIM, HSS, WT, and so on), or when the steel reader (floless-app-steel-takeoff / floless-app-steel-model) meets a note or label whose meaning is not defined on the supplied sheets. The project's own legend always overrides these defaults.
4
+ metadata:
5
+ version: "0.1.0"
6
+ ---
7
+
8
+ # Steel drawing abbreviations — default legend
9
+
10
+ ## What this is
11
+
12
+ A reusable **fallback legend** of the standard abbreviations, note conventions, and steel-shape
13
+ designations that appear on the General Notes / legend sheet (usually `S-001` / `S-100`) of most
14
+ **US (AISC, imperial)** structural sets. These are an industry convention — largely identical from
15
+ project to project — so they fill the gap when a set does not ship its own abbreviations sheet, or
16
+ when only some sheets are provided.
17
+
18
+ The full glossary lives in **`references/standard-us-abbreviations.md`** (abbreviations A–Z, the
19
+ structural-steel shape table, the high-value note conventions for takeoff, and a marks-vs-abbreviations
20
+ warning). Keep this SKILL.md lean; load the reference when an actual lookup is needed.
21
+
22
+ ## When to consult it
23
+
24
+ - A structural plan, section, or **General Note** uses an abbreviation or code that is not spelled
25
+ out on the supplied sheets, and its meaning is needed to read geometry or metadata correctly
26
+ (e.g. a "T.O.S. = 16'-6" UON" note, a "TYP" profile, an `(E)` member).
27
+ - The **steel reader** (`floless-app-steel-takeoff`) or the **guide** (`floless-app-steel-model`) is
28
+ learning a set and the user did **not** provide the abbreviations/legend sheet — use this as the
29
+ default while still reading the rest of the set.
30
+ - A profile designation needs decoding (what `HSS6X6X3/8`, `WT`, `C`, `MC`, `PIPE-XX` mean and which
31
+ ones carry their weight in the name vs. need an AISC table).
32
+
33
+ ## How to use it
34
+
35
+ 1. **Read the project legend first.** If the set HAS an abbreviations sheet or a note that defines the
36
+ code, use *that* — this file is only the fallback. The project's own definition always wins.
37
+ 2. **Look up the code** in `references/standard-us-abbreviations.md` (grep the abbreviation, e.g.
38
+ `grep -i 'TOS' references/standard-us-abbreviations.md`).
39
+ 3. **Respect context over the table.** Several codes are context-dependent (`WT` = WEIGHT or a tee
40
+ shape; `PT` = POST-TENSIONED / PRESSURE-TREATED / POINT). Choose the structurally sensible meaning;
41
+ if it is genuinely ambiguous, flag it as an RFI rather than guess.
42
+ 4. **Do not expand marks.** `MF` / `BF` / `D1` / `C1` / `B12` are project member/frame marks resolved
43
+ from the frame elevations and schedules — never from this glossary (see the reference's
44
+ "Marks vs abbreviations" section).
45
+ 5. **Mind the region.** This is the US/AISC legend. For EU (EN) or UK (BS) sets, defer to the project
46
+ legend and the region-specific detailer agent (`steel-detailer-eu` / `steel-detailer-uk`); the
47
+ shape designations especially differ.
48
+
49
+ ## Companion skills
50
+
51
+ - **`reading-structural-drawings`** — the reading discipline; its Pass 0 (inventory & legend) consults
52
+ this default legend when the set's own abbreviations sheet is missing.
53
+ - **`floless-app-steel-takeoff`** — the reader; uses this while reading notes/labels into the
54
+ `steel.takeoff/v1` contract (TYP profiles, TOS defaults, connection-type words, `(E)`/`(N)` scope).
55
+ - **`floless-app-steel-model`** — the front-door guide; references this at the "learn the set" stage so
56
+ a partial set without a legend sheet still reads correctly.
57
+ - **`steel-detailer-us` / `-eu` / `-uk`** (AWARE agents) — own the section *tables and weights*; this
58
+ skill only decodes the *labels and notes*.
@@ -0,0 +1,296 @@
1
+ # Standard US structural-steel drawing abbreviations & symbols (default legend)
2
+
3
+ These are the **de-facto standard abbreviations** that appear on the General Notes / legend sheet
4
+ (usually `S-001` / `S-100`) of most **US (AISC, imperial)** structural sets. They are an industry
5
+ convention, not any one firm's property, so they serve as a **fallback default** when a drawing set
6
+ does not include its own abbreviations/legend sheet (or only ships partial sheets).
7
+
8
+ **Rules of use (read before applying any expansion):**
9
+ - **The project's own legend ALWAYS wins.** If the set has an abbreviations sheet or a plan/general
10
+ note that defines a code, use *that* meaning — never override it with this list.
11
+ - **Context can override.** The same code means different things by context (e.g. `WT` = WEIGHT or a
12
+ structural-tee shape; `PT` = POST-TENSIONED or PRESSURE-TREATED or POINT). Pick the meaning that
13
+ fits the structural context; if genuinely ambiguous, flag it (RFI) rather than guess.
14
+ - **Region differs.** This is the US/AISC set. EU (EN) and UK (BS) sets use different shape
15
+ designations and some different abbreviations — defer those to the project legend and the
16
+ region-specific detailer agent (`steel-detailer-eu` / `steel-detailer-uk`).
17
+ - **Marks are NOT abbreviations.** Member/frame *marks* like `MF`, `BF`, `D1`, `C1`, `B12` are
18
+ project-specific labels resolved from the frame elevations / schedules — not from this glossary.
19
+ See "Marks vs abbreviations" below.
20
+
21
+ ---
22
+
23
+ ## Abbreviations (A–Z)
24
+
25
+ | Abbr | Meaning |
26
+ |------|---------|
27
+ | `AB` | ANCHOR BOLT |
28
+ | `ACI` | AMERICAN CONCRETE INSTITUTE |
29
+ | `ADD'L` | ADDITIONAL |
30
+ | `ADJ` | ADJACENT / ADJUSTABLE |
31
+ | `AESS` | ARCHITECTURALLY EXPOSED STRUCTURAL STEEL |
32
+ | `AGGR` | AGGREGATE |
33
+ | `AISC` | AMERICAN INSTITUTE OF STEEL CONSTRUCTION |
34
+ | `AITC` | AMERICAN INSTITUTE OF TIMBER CONSTRUCTION |
35
+ | `ALT` | ALTERNATE |
36
+ | `ALUM` | ALUMINUM |
37
+ | `ANCH` | ANCHOR |
38
+ | `ANSI` | AMERICAN NATIONAL STANDARDS INSTITUTE |
39
+ | `APA` | AMERICAN PLYWOOD ASSOCIATION |
40
+ | `APPROX` | APPROXIMATE |
41
+ | `ARCH` | ARCHITECTURAL / ARCHITECT |
42
+ | `ASD` | ALLOWABLE STRENGTH DESIGN |
43
+ | `ASTM` | AMERICAN SOCIETY FOR TESTING AND MATERIALS |
44
+ | `AWPA` | AMERICAN WOOD PRESERVERS ASSOCIATION |
45
+ | `AWS` | AMERICAN WELDING SOCIETY |
46
+ | `AVG` | AVERAGE |
47
+ | `@` | AT |
48
+ | `&` | AND |
49
+ | `BLDG` | BUILDING |
50
+ | `BLK` | BLOCK |
51
+ | `BLKG` | BLOCKING |
52
+ | `BM` | BEAM |
53
+ | `BN` | BOUNDARY NAIL |
54
+ | `BNDRY` | BOUNDARY |
55
+ | `BOT` (or `B`) | BOTTOM |
56
+ | `BRC` | BRACE |
57
+ | `BRG` | BEARING |
58
+ | `BT` | BENT |
59
+ | `BTWN` | BETWEEN |
60
+ | `CANT` | CANTILEVER |
61
+ | `CAM` (or `C`) | CAMBER |
62
+ | `CG` | CENTER OF GRAVITY |
63
+ | `CIP` | CAST-IN-PLACE |
64
+ | `CJ` | CONSTRUCTION JOINT; CONTROL JOINT |
65
+ | `CJP` | COMPLETE JOINT PENETRATION (WELD) |
66
+ | `CL` (or `℄`) | CENTER LINE |
67
+ | `CLR` | CLEAR |
68
+ | `CMU` | CONCRETE MASONRY UNIT |
69
+ | `COL` | COLUMN |
70
+ | `COMP` | COMPRESSION / COMPOSITE |
71
+ | `CONC` | CONCRETE |
72
+ | `CONN` | CONNECTION |
73
+ | `CONST` / `CONSTR` | CONSTRUCTION |
74
+ | `CONT` | CONTINUOUS / CONTINUE |
75
+ | `CTR` | CENTER |
76
+ | `CTSK` | COUNTERSUNK |
77
+ | `CU FT` | CUBIC FOOT |
78
+ | `d` | PENNY (nail size, e.g. `16d`) |
79
+ | `DBL` | DOUBLE |
80
+ | `DEPT` | DEPARTMENT |
81
+ | `DET` | DETAIL |
82
+ | `DIA` (or `Ø`) | DIAMETER |
83
+ | `DIAG` | DIAGONAL |
84
+ | `DIAPH` | DIAPHRAGM |
85
+ | `DIM` | DIMENSION |
86
+ | `DN` | DOWN |
87
+ | `DO` | DITTO |
88
+ | `DWG` | DRAWING |
89
+ | `DWL` | DOWEL |
90
+ | `EA` | EACH |
91
+ | `EF` | EACH FACE |
92
+ | `EJ` | EXPANSION JOINT |
93
+ | `EL` / `ELEV` | ELEVATION |
94
+ | `ELEC` | ELECTRICAL |
95
+ | `EMBED` | EMBEDMENT |
96
+ | `EN` | EDGE NAIL |
97
+ | `EQ` | EQUAL / EQUALLY (SPACED) |
98
+ | `EQUIP` | EQUIPMENT |
99
+ | `ES` | EACH SIDE |
100
+ | `EW` | EACH WAY |
101
+ | `EXP` | EXPANSION |
102
+ | `EXT` | EXTERIOR |
103
+ | `(E)` | EXISTING (often out of new-work scope) |
104
+ | `FDN` | FOUNDATION |
105
+ | `FF` | FAR FACE |
106
+ | `FIN` | FINISH(ED) |
107
+ | `FLR` | FLOOR |
108
+ | `FN` | FIELD NAIL |
109
+ | `FOC` | FACE OF CONCRETE |
110
+ | `FOS` | FACE OF STUD / FACE OF SLAB |
111
+ | `FOW` | FACE OF WALL |
112
+ | `FP` | FIRE PROOFING |
113
+ | `FRMG` | FRAMING |
114
+ | `FS` | FAR SIDE / FULL SIZE |
115
+ | `FT` | FOOT / FEET |
116
+ | `FTG` | FOOTING |
117
+ | `GA` | GAUGE |
118
+ | `GALV` | GALVANIZED |
119
+ | `GB` | GRADE BEAM |
120
+ | `GLB` | GLUE-LAMINATED BEAM |
121
+ | `GR` | GRADE |
122
+ | `GRTG` | GRATING |
123
+ | `HDR` | HEADER |
124
+ | `HGR` | HANGER |
125
+ | `HGT` / `HT` | HEIGHT |
126
+ | `HK` | HOOK |
127
+ | `HORIZ` (or `H`) | HORIZONTAL |
128
+ | `HP` | HIGH POINT |
129
+ | `HS` | HIGH STRENGTH |
130
+ | `HSS` | HOLLOW STRUCTURAL SECTION |
131
+ | `ID` | INSIDE DIAMETER |
132
+ | `IF` | INSIDE FACE |
133
+ | `IN` | INCH |
134
+ | `INCL` | INCLUDE / INCLUDED |
135
+ | `INFO` | INFORMATION |
136
+ | `INSP` | INSPECTION |
137
+ | `INT` | INTERIOR |
138
+ | `JST` | JOIST |
139
+ | `JT` | JOINT |
140
+ | `K` | KIPS (1000 lb) |
141
+ | `KSI` | KIPS PER SQUARE INCH |
142
+ | `LAB` | LABORATORY |
143
+ | `LB(S)` (or `#`) | POUNDS |
144
+ | `LH` | LEFT HAND |
145
+ | `LLH` | LONG LEG HORIZONTAL |
146
+ | `LLV` | LONG LEG VERTICAL |
147
+ | `LP` | LOW POINT |
148
+ | `LT WT` | LIGHTWEIGHT |
149
+ | `MAS` | MASONRY |
150
+ | `MATL` | MATERIAL |
151
+ | `MAX` | MAXIMUM |
152
+ | `MB` | MACHINE BOLT |
153
+ | `MECH` | MECHANICAL |
154
+ | `MFR` | MANUFACTURER |
155
+ | `MIN` | MINIMUM |
156
+ | `MISC` | MISCELLANEOUS |
157
+ | `N` | NORTH |
158
+ | `(N)` | NEW |
159
+ | `NF` | NEAR FACE |
160
+ | `NIC` | NOT IN CONTRACT |
161
+ | `NOM` | NOMINAL |
162
+ | `NO` (or `#`) | NUMBER |
163
+ | `NS` | NEAR SIDE |
164
+ | `NTS` | NOT TO SCALE |
165
+ | `OC` | ON CENTER |
166
+ | `OD` | OUTSIDE DIAMETER |
167
+ | `OF` | OUTSIDE FACE |
168
+ | `OH` | OPPOSITE HAND |
169
+ | `OPNG` | OPENING |
170
+ | `OPP` | OPPOSITE |
171
+ | `OSB` | ORIENTED STRAND BOARD |
172
+ | `PARA` (or `∥`) | PARALLEL |
173
+ | `PERP` (or `⊥`) | PERPENDICULAR |
174
+ | `PJP` | PARTIAL JOINT PENETRATION (WELD) |
175
+ | `PL` | PLATE |
176
+ | `PLF` | POUNDS PER LINEAR FOOT |
177
+ | `PLYWD` | PLYWOOD |
178
+ | `PNL` | PANEL |
179
+ | `PSF` | POUNDS PER SQUARE FOOT |
180
+ | `PSI` | POUNDS PER SQUARE INCH |
181
+ | `PT` | POST-TENSIONED / PRESSURE-TREATED / POINT |
182
+ | `R` / `RAD` | RADIUS |
183
+ | `RD` | ROOF DRAIN |
184
+ | `REF` | REFERENCE |
185
+ | `REINF` | REINFORCING / REINFORCEMENT |
186
+ | `REQ'D` | REQUIRED |
187
+ | `RET` | RETAINING |
188
+ | `RM` | ROOM |
189
+ | `RO` | ROUGH OPENING |
190
+ | `SCHED` | SCHEDULE |
191
+ | `SECT` | SECTION |
192
+ | `SF` | SQUARE FEET |
193
+ | `SHT` | SHEET |
194
+ | `SHTHG` | SHEATHING |
195
+ | `SIM` | SIMILAR |
196
+ | `SLBB` | SHORT LEGS BACK-TO-BACK |
197
+ | `SOG` | SLAB ON GRADE |
198
+ | `SPCG` | SPACING |
199
+ | `SPECS` | SPECIFICATIONS |
200
+ | `SQ` | SQUARE |
201
+ | `SS` | SELECT STRUCTURAL / STAINLESS STEEL |
202
+ | `SSL` | SHORT SLOTTED HOLES |
203
+ | `STAGG` | STAGGERED |
204
+ | `STD` | STANDARD |
205
+ | `STIFF` | STIFFENER |
206
+ | `STIRR` | STIRRUP |
207
+ | `STL` | STEEL |
208
+ | `STRUCT` | STRUCTURAL |
209
+ | `SYM` | SYMMETRICAL |
210
+ | `T&B` | TOP AND BOTTOM |
211
+ | `T&G` | TONGUE AND GROOVE |
212
+ | `TB` | TIE BEAM |
213
+ | `TEMP` | TEMPORARY / TEMPERATURE |
214
+ | `THK` | THICK / THICKNESS |
215
+ | `THR` | THREADED |
216
+ | `THRU` | THROUGH |
217
+ | `TN` | TOE NAIL |
218
+ | `TOC` | TOP OF CONCRETE / TOP OF CURB |
219
+ | `TOF` | TOP OF FOOTING |
220
+ | `TOS` | TOP OF STEEL / TOP OF SLAB |
221
+ | `TOW` | TOP OF WALL |
222
+ | `TYP` | TYPICAL |
223
+ | `UON` / `UNO` | UNLESS OTHERWISE NOTED |
224
+ | `VERT` (or `V`) | VERTICAL |
225
+ | `VIF` | VERIFY IN FIELD |
226
+ | `W` | WEST |
227
+ | `W/` | WITH |
228
+ | `WD` | WOOD |
229
+ | `WP` | WORK POINT (also WATERPROOF) |
230
+ | `WT` | WEIGHT (also a structural-tee shape — see below) |
231
+ | `WWF` | WELDED WIRE FABRIC |
232
+
233
+ ---
234
+
235
+ ## Structural steel shape designations
236
+
237
+ How a profile label decodes (the label sits on/near the member centerline, usually mid-span):
238
+
239
+ | Designation | Shape |
240
+ |-------------|-------|
241
+ | `W` | Wide-flange (W-shape) — e.g. `W16X26` |
242
+ | `M` | Miscellaneous I-shape |
243
+ | `S` | American Standard (S-shape / "I-beam") |
244
+ | `HP` | Bearing pile (HP-shape) |
245
+ | `C` | American Standard Channel |
246
+ | `MC` | Miscellaneous Channel |
247
+ | `L` | Angle (equal/unequal leg) — e.g. `L4X4X1/4` |
248
+ | `WT` / `MT` / `ST` | Structural Tee (cut from W / M / S) |
249
+ | `HSS` | Hollow Structural Section (rectangular/square/round) — e.g. `HSS6X6X3/8` |
250
+ | `PIPE` | Standard pipe; `PIPE-X` extra-strong; `PIPE-XX` double-extra-strong |
251
+ | `TS` | Tube Steel (older designation for HSS) |
252
+ | `PL` | Plate — e.g. `PL1/2X8` |
253
+
254
+ **Reading the size numbers:**
255
+ - **W/M/S/HP/C/MC and WT/MT/ST:** the trailing number after the `X` IS the weight in **lb/ft (plf)** —
256
+ no table needed. `W16X26` = nominal depth **16"** × **26 lb/ft**; `WT6X25` (a tee cut from a W) =
257
+ **25 lb/ft**; `C12X20.7` = **20.7 lb/ft**. (Tees inherit weight-based naming from their parent
258
+ W/M/S shape.)
259
+ - **HSS / PIPE / L / PL:** the numbers are **dimensions**, not the weight (e.g. `HSS6X6X3/8` =
260
+ 6"×6"×3/8" wall, `L4X4X1/4` = 4"×4"×1/4" leg) — look the weight up in an AISC shapes table
261
+ (or via the `steel-detailer-us` agent).
262
+
263
+ ---
264
+
265
+ ## Note conventions most useful to a takeoff
266
+
267
+ When reading the General Notes and plan notes, these drive member geometry/metadata directly:
268
+
269
+ - **`TYP`** — a value/detail shown once applies to all like conditions. A "TYP" profile or spacing in
270
+ the notes is the **default** for unlabeled members of that kind.
271
+ - **`TOS` / `TOC` / `TOF` / `BOS`** — top/bottom-of-steel/concrete/footing elevations. A plan default
272
+ TOS ("ALL STEEL T.O.S. = 16'-6" UON") sets every member's elevation unless a local callout overrides.
273
+ - **`UON` / `UNO`** — the stated value holds everywhere *unless* a local note says otherwise. Treat a
274
+ "TYP … UON" note as the fallback and watch for the exceptions.
275
+ - **`EQ`** — equally-spaced members between two references (no explicit dimension).
276
+ - **`SIM`** — a detail is repeated similarly elsewhere (mirror/rotate as needed).
277
+ - **`(N)` / `(E)`** — new vs existing. `(E)` members are frequently **out of the new-steel takeoff**;
278
+ confirm against line weight/scope before counting them.
279
+ - **Connection-type words in notes** — `MOMENT`, `DRAG`, `SHEAR`, `BRACED`, `PINNED`, `FIXED` name the
280
+ connection type at member ends; feed them into the member-end connection note + the connection
281
+ library (type → design detail# → platform component).
282
+
283
+ ---
284
+
285
+ ## Marks vs abbreviations (do not confuse)
286
+
287
+ A *mark* identifies a specific member or frame and is defined **by the project's schedules/elevations**,
288
+ not by this glossary:
289
+
290
+ - `MF` / `BF` — Moment Frame / Braced Frame member marks (resolved from the Frame Elevation sheets;
291
+ the AISC size comes from the moment-frame schedule, not from "MF").
292
+ - `D1`, `D2`, `BR1` — brace/diagonal marks.
293
+ - `C1`, `B12`, `G3` — column/beam/girder marks keyed to a schedule.
294
+
295
+ Never expand a mark using this abbreviation list. Resolve it from the frame elevations / column &
296
+ beam schedules, and if unresolved, leave it as an RFI.
package/dist/web/app.css CHANGED
@@ -479,6 +479,152 @@
479
479
  .onb-tour-btn:disabled { cursor: default; opacity: 0.85; box-shadow: none; }
480
480
  .onb-tour-ico { font-size: 11px; opacity: 0.7; margin-left: 6px; }
481
481
 
482
+ /* ===== Per-workflow onboarding beacon ("Start here") ===== */
483
+ /* A header pill in the run cluster. Calm by default (accent-soft, like the
484
+ wf-update pill); pulses while the open workflow is new to the user. Accent —
485
+ NOT warn — because it's positive/new, and warn is already the drift colour. */
486
+ .guide-beacon {
487
+ display: inline-flex;
488
+ align-items: center;
489
+ gap: 5px;
490
+ border: 1px solid var(--accent-dim);
491
+ background: var(--accent-soft);
492
+ color: var(--accent-bright);
493
+ font-family: var(--ui);
494
+ font-size: 11px;
495
+ letter-spacing: 0.02em;
496
+ padding: 3px 11px;
497
+ border-radius: 999px;
498
+ cursor: pointer;
499
+ white-space: nowrap;
500
+ flex-shrink: 0; /* the workflow picker shrinks first; the beacon never squashes */
501
+ transition: border-color 0.15s, color 0.15s, background 0.15s;
502
+ }
503
+ .guide-beacon[hidden] { display: none; }
504
+ .guide-beacon:hover { border-color: var(--accent); color: var(--accent); }
505
+ .guide-beacon:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
506
+ .guide-beacon-ico { font-size: 11px; line-height: 1; }
507
+ /* "New to you": a slow box-shadow bloom (GPU-composited, no layout repaint) reading
508
+ as a calm invitation, not an alarm. Same motion dialect as the rest of the app. */
509
+ .guide-beacon.is-new {
510
+ border-color: var(--accent);
511
+ color: var(--accent-bright);
512
+ animation: guide-pulse 2.8s ease-in-out infinite;
513
+ }
514
+ @keyframes guide-pulse {
515
+ 0%, 100% { box-shadow: 0 0 0 0 var(--accent-glow); }
516
+ 50% { box-shadow: 0 0 0 6px rgba(74, 158, 255, 0); }
517
+ }
518
+ @media (prefers-reduced-motion: reduce) {
519
+ /* No pulse — a steady ring (shape + contrast, not colour alone) marks "new". */
520
+ .guide-beacon.is-new { animation: none; box-shadow: 0 0 0 1px var(--accent-dim); }
521
+ }
522
+ /* Spotlit (first-open): raised above the dim, opaque, ringed. The header creates no
523
+ stacking context, so z-index here lifts it above the fixed spotlight overlay. */
524
+ .guide-beacon.spotlit {
525
+ position: relative;
526
+ z-index: 95;
527
+ animation: none;
528
+ background: var(--surface-3);
529
+ border-color: var(--accent);
530
+ color: var(--accent-bright);
531
+ box-shadow: 0 0 0 3px var(--accent-glow), 0 0 20px 4px var(--accent-glow);
532
+ }
533
+
534
+ /* ===== First-open spotlight ===== */
535
+ .guide-spotlight {
536
+ position: fixed;
537
+ inset: 0;
538
+ z-index: 90; /* below modals (100), above the page; the beacon (.spotlit, 95) sits over it */
539
+ background: rgba(7, 10, 15, 0.72);
540
+ backdrop-filter: blur(2px);
541
+ -webkit-backdrop-filter: blur(2px);
542
+ display: none;
543
+ }
544
+ .guide-spotlight:not([hidden]) { display: block; animation: fade-in 0.18s ease-out; }
545
+ .guide-spotlight-bubble {
546
+ position: fixed;
547
+ z-index: 91;
548
+ max-width: 280px;
549
+ background: var(--surface);
550
+ border: 1px solid var(--accent-dim);
551
+ border-radius: 8px;
552
+ padding: 12px 14px;
553
+ box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
554
+ }
555
+ .guide-spotlight-text { font-size: 12.5px; line-height: 1.5; color: var(--text); }
556
+ .guide-spotlight-text strong { color: var(--accent-bright); }
557
+ .guide-spotlight-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 11px; }
558
+ .guide-spotlight-actions button { padding: 5px 13px; font-size: 12px; }
559
+ .guide-spotlight-actions .ghost { background: var(--surface-2); color: var(--text-muted); }
560
+ .guide-spotlight-actions .ghost:hover { color: var(--text); border-color: var(--accent-dim); }
561
+ .guide-spotlight-actions .primary { background: var(--accent); color: #fff; border-color: var(--accent); font-weight: 600; }
562
+ .guide-spotlight-actions .primary:hover { background: var(--accent-bright); box-shadow: 0 0 14px var(--accent-glow); }
563
+
564
+ /* ===== Guide panel (modal) ===== */
565
+ .modal.guide {
566
+ width: 520px;
567
+ max-width: 92vw;
568
+ max-height: 82vh;
569
+ display: flex;
570
+ flex-direction: column;
571
+ }
572
+ .guide-eyebrow {
573
+ font-size: 10px;
574
+ text-transform: uppercase;
575
+ letter-spacing: 0.16em;
576
+ color: var(--accent-bright);
577
+ margin-bottom: 6px;
578
+ }
579
+ /* The body scrolls between the pinned title and the pinned actions; theme its
580
+ scrollbar so no native white bar leaks against the dark surface. */
581
+ #guide-body {
582
+ overflow-y: auto;
583
+ flex: 1 1 auto;
584
+ min-height: 0;
585
+ margin-top: 2px;
586
+ scrollbar-width: thin;
587
+ scrollbar-color: var(--border-strong) transparent;
588
+ }
589
+ #guide-body::-webkit-scrollbar { width: 7px; }
590
+ #guide-body::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 7px; }
591
+ #guide-body::-webkit-scrollbar-track { background: transparent; }
592
+ .guide-section-label {
593
+ font-size: 10px;
594
+ text-transform: uppercase;
595
+ letter-spacing: 0.14em;
596
+ color: var(--text-muted);
597
+ margin: 14px 0 7px;
598
+ }
599
+ .guide-section-label:first-child { margin-top: 2px; }
600
+ .guide-desc { font-size: 12.5px; line-height: 1.55; color: var(--text); }
601
+ .guide-desc p { margin: 0 0 8px; }
602
+ .guide-desc p:last-child { margin-bottom: 0; }
603
+ .guide-desc code { font-family: var(--mono); background: var(--surface-2); padding: 1px 5px; border-radius: 3px; color: var(--text-muted); font-size: 11px; }
604
+ .guide-desc-empty { color: var(--text-muted); font-style: italic; }
605
+ .guide-steps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 9px; }
606
+ .guide-step { display: flex; gap: 10px; align-items: baseline; }
607
+ .guide-box { font-family: var(--mono); font-size: 13px; min-width: 16px; color: var(--text-dim); line-height: 1.3; }
608
+ .guide-step-label { font-size: 12.5px; color: var(--text); }
609
+ .guide-step-note { font-size: 11px; color: var(--text-dim); font-style: italic; }
610
+ /* Done: ✓ in ok-green + struck/dimmed label, so the current step stands out on
611
+ shape + contrast, not colour alone. Current: → in accent + bold label. */
612
+ .guide-step.done .guide-box { color: var(--ok); }
613
+ .guide-step.done .guide-step-label { color: var(--text-dim); text-decoration: line-through; text-decoration-color: var(--text-dim); }
614
+ .guide-step.current .guide-box { color: var(--accent); }
615
+ .guide-step.current .guide-step-label { color: var(--text); font-weight: 600; }
616
+ .guide-walk-hint {
617
+ font-size: 11px;
618
+ line-height: 1.5;
619
+ color: var(--text-muted);
620
+ margin-top: 14px;
621
+ padding-top: 12px;
622
+ border-top: 1px solid var(--border);
623
+ }
624
+ .guide-walk-hint strong { color: var(--text); font-weight: 600; }
625
+ .modal.guide .modal-actions { margin-top: 16px; }
626
+ .modal.guide .modal-actions #guide-got-it { margin-right: auto; }
627
+
482
628
  .agent-card {
483
629
  background: var(--surface);
484
630
  border: 1px solid var(--border-strong);
package/dist/web/aware.js CHANGED
@@ -531,11 +531,193 @@
531
531
  });
532
532
  }
533
533
 
534
+ // ── Per-workflow onboarding beacon ──────────────────────────────────────────
535
+ // "New to you" = the user hasn't completed a first real Run of this workflow and
536
+ // hasn't dismissed its guide. Three bits per app id, persisted per origin; all
537
+ // default false, so every workflow starts new. Never touches the .flo or lock.
538
+ const LS_GUIDE = 'floless:guide';
539
+ function guideStateAll() {
540
+ try { const v = JSON.parse(localStorage.getItem(LS_GUIDE) || '{}'); return (v && typeof v === 'object') ? v : {}; }
541
+ catch { return {}; }
542
+ }
543
+ function guideFor(id) { return guideStateAll()[id] || {}; }
544
+ function setGuide(id, patch) {
545
+ if (!id) return;
546
+ try {
547
+ const all = guideStateAll();
548
+ all[id] = { ...(all[id] || {}), ...patch };
549
+ localStorage.setItem(LS_GUIDE, JSON.stringify(all));
550
+ } catch { /* private mode / quota */ }
551
+ }
552
+ // The beacon pulses while a workflow is new to the user.
553
+ function beaconIsNew(id) { const g = guideFor(id); return !g.firstRunDone && !g.dismissed; }
554
+
555
+ function paintBeacon(app) {
556
+ const $b = document.getElementById('guide-beacon');
557
+ if (!$b) return;
558
+ if (!app) { $b.hidden = true; $b.classList.remove('is-new', 'spotlit'); return; }
559
+ const isNew = beaconIsNew(app.id);
560
+ $b.hidden = false;
561
+ $b.classList.toggle('is-new', isNew);
562
+ const ico = $b.querySelector('.guide-beacon-ico');
563
+ const label = $b.querySelector('.guide-beacon-label');
564
+ // Monochrome dingbats (✦ / ?), not colour emoji — the locked baseline uses text
565
+ // glyphs (★, ▸); the pulse, not an emoji, is the "new" signal.
566
+ if (ico) ico.textContent = isNew ? '✦' : '?';
567
+ if (label) label.textContent = isNew ? 'Start here' : 'Guide';
568
+ $b.dataset.tip = isNew
569
+ ? 'New here? See what this workflow does and how to run it.'
570
+ : 'Open the guide for this workflow.';
571
+ $b.setAttribute('aria-label', (isNew ? 'Start here — guide for ' : 'Guide for ') + (app.displayName || app.id));
572
+ }
573
+
574
+ // Record a completed real run → the workflow is no longer new; calm the beacon (and
575
+ // refresh an open guide so the Run step ticks). Idempotent.
576
+ function markFirstRunDone(id) {
577
+ if (!id || guideFor(id).firstRunDone) return;
578
+ setGuide(id, { firstRunDone: true });
579
+ if (id === currentId) {
580
+ const app = apps.get(id);
581
+ if (app) paintBeacon(app);
582
+ const $m = document.getElementById('guide-modal');
583
+ if ($m && $m.classList.contains('show')) renderGuide();
584
+ }
585
+ }
586
+
587
+ // The guide panel's live checklist — each step ticks from real app state. The
588
+ // "Set inputs" row is omitted for a workflow with no declared inputs.
589
+ function guideSteps(app) {
590
+ const steps = [];
591
+ if (Array.isArray(app.inputs) && app.inputs.length) {
592
+ const vals = currentInputs();
593
+ const allSet = app.inputs.every((i) => {
594
+ const v = vals[i.name];
595
+ return v !== undefined && v !== null && String(v).trim() !== '';
596
+ });
597
+ steps.push({ label: 'Set inputs', note: 'the values this workflow runs with', done: allSet });
598
+ }
599
+ steps.push({ label: 'Compile', note: 'freeze the approved lock', done: app.runState === 'ready' });
600
+ steps.push({ label: 'Run', note: 'against the live host — results render here', done: !!guideFor(app.id).firstRunDone });
601
+ return steps;
602
+ }
603
+
604
+ function renderGuide() {
605
+ const app = currentId && apps.get(currentId);
606
+ const $body = document.getElementById('guide-body');
607
+ const $title = document.getElementById('guide-title');
608
+ const $eyebrow = document.getElementById('guide-eyebrow');
609
+ if (!app || !$body) return;
610
+ if ($title) $title.textContent = app.displayName || app.id;
611
+ if ($eyebrow) $eyebrow.textContent = beaconIsNew(app.id) ? '✦ Start here' : '✦ Workflow guide';
612
+ const desc = (app.description || '').trim();
613
+ const descHtml = desc
614
+ ? `<div class="guide-desc">${desc.split(/\n{2,}/).map((p) => `<p>${mdInline(escapeHtml(p.trim()))}</p>`).join('')}</div>`
615
+ : `<div class="guide-desc guide-desc-empty"><p>A ${app.nodes.length}-step workflow. Set its inputs, Compile to freeze the approved lock, then Run it against your live host.</p></div>`;
616
+ const steps = guideSteps(app);
617
+ const curIdx = steps.findIndex((s) => !s.done);
618
+ const stepsHtml = `<ol class="guide-steps" role="list">${steps.map((s, i) => {
619
+ const state = s.done ? 'done' : i === curIdx ? 'current' : 'todo';
620
+ // Shape carries state, not colour alone: ✓ done · → current · ☐ to-do.
621
+ const box = s.done ? '✓' : i === curIdx ? '→' : '☐';
622
+ const cur = i === curIdx ? ' aria-current="step"' : '';
623
+ return `<li class="guide-step ${state}"${cur}><span class="guide-box" aria-hidden="true">${box}</span>` +
624
+ `<span class="guide-step-text"><span class="guide-step-label">${escapeHtml(s.label)}</span> ` +
625
+ `<span class="guide-step-note">— ${escapeHtml(s.note)}</span></span>` +
626
+ `<span class="sr-only">${s.done ? '(done)' : i === curIdx ? '(next step)' : '(to do)'}</span></li>`;
627
+ }).join('')}</ol>`;
628
+ const walkHint = `<div class="guide-walk-hint">A bigger workflow? <strong>Walk me through it</strong> hands off to your terminal AI for a step-by-step tour using this workflow's own skill.</div>`;
629
+ $body.innerHTML =
630
+ `<div class="guide-section-label">What this does</div>` + descHtml +
631
+ `<div class="guide-section-label">How to go through it</div>` + stepsHtml + walkHint;
632
+ }
633
+
634
+ function openGuide() {
635
+ if (!currentId) { showToast('open a workflow first', 'warn'); return; }
636
+ renderGuide();
637
+ const $m = document.getElementById('guide-modal');
638
+ showModal($m);
639
+ // Focus the dialog shell (not a button) so the checklist is read first; Tab then
640
+ // moves to Got it → Walk me through it. Focus returns to the beacon on close.
641
+ const shell = $m && $m.querySelector('.modal.guide');
642
+ if (shell) { try { shell.focus(); } catch { /* not focusable */ } }
643
+ }
644
+ // Closing the guide (Got it / backdrop / Esc) marks it seen → the beacon stops
645
+ // pulsing even before a first run (the user has looked; don't nag).
646
+ function closeGuide() {
647
+ if (currentId) {
648
+ setGuide(currentId, { dismissed: true });
649
+ const app = apps.get(currentId);
650
+ if (app) paintBeacon(app);
651
+ }
652
+ hideModal(document.getElementById('guide-modal'));
653
+ const $b = document.getElementById('guide-beacon');
654
+ if ($b && !$b.hidden) { try { $b.focus(); } catch { /* */ } }
655
+ }
656
+
657
+ // "Walk me through it" → relay the ask to the terminal AI, the house way: queue a
658
+ // request AND copy the prompt, toast, light the footer requests pill, list it in the
659
+ // Requests window. The UI never guides itself; it hands intent to the assistant.
660
+ async function requestGuide() {
661
+ if (!currentId) { showToast('open a workflow first', 'warn'); return; }
662
+ try {
663
+ const { request } = await api('/api/guide', { method: 'POST', body: JSON.stringify({ appId: currentId }) });
664
+ const copied = await copyToClipboard(markedInstruction(request));
665
+ showToast(copied ? 'Sent to your assistant · copied to clipboard' : 'Sent to your assistant', 'ok');
666
+ appendNarration(`Asked your terminal AI to walk you through <code>${escapeHtml(currentId)}</code> — watch the glowing <strong>requests</strong> pill (bottom-right), or switch to your terminal.${copied ? ' The prompt is also on your clipboard.' : ''}`);
667
+ loadRequests();
668
+ closeGuide(); // engaging with the guidance also calms the beacon
669
+ } catch (e) { reportErr(e); }
670
+ }
671
+
672
+ // ── First-open spotlight (one-time per workflow) ────────────────────────────
673
+ // Dims the canvas, raises the beacon, and points a small caption at it; Esc /
674
+ // click-away / either button dismiss it for good. Slice 2 of the onboarding beacon.
675
+ let _spotlightKey = null;
676
+ function maybeSpotlight(app) {
677
+ if (!app) return;
678
+ const g = guideFor(app.id);
679
+ if (g.spotlightShown || g.dismissed || g.firstRunDone) return;
680
+ if (document.querySelector('.modal-backdrop.show')) return; // don't pile on a dialog
681
+ if ($contractEditor && !$contractEditor.hidden) return;
682
+ setTimeout(() => {
683
+ if (currentId !== app.id) return; // switched away during the delay
684
+ if (guideFor(app.id).spotlightShown) return;
685
+ if (document.querySelector('.modal-backdrop.show')) return;
686
+ showSpotlight(app);
687
+ }, 700);
688
+ }
689
+ function showSpotlight(app) {
690
+ const $sp = document.getElementById('guide-spotlight');
691
+ const $b = document.getElementById('guide-beacon');
692
+ if (!$sp || !$b || $b.hidden) return;
693
+ setGuide(app.id, { spotlightShown: true });
694
+ $b.classList.add('spotlit');
695
+ $sp.hidden = false;
696
+ // Anchor the caption bubble just under the beacon, right-aligned to it.
697
+ const bubble = $sp.querySelector('.guide-spotlight-bubble');
698
+ if (bubble) {
699
+ const r = $b.getBoundingClientRect();
700
+ bubble.style.top = (r.bottom + 10) + 'px';
701
+ bubble.style.right = Math.max(8, window.innerWidth - r.right) + 'px';
702
+ }
703
+ _spotlightKey = (e) => { if (e.key === 'Escape') { e.preventDefault(); hideSpotlight(); } };
704
+ document.addEventListener('keydown', _spotlightKey);
705
+ if (bubble) { try { bubble.focus(); } catch { /* */ } }
706
+ }
707
+ function hideSpotlight() {
708
+ const $sp = document.getElementById('guide-spotlight');
709
+ const $b = document.getElementById('guide-beacon');
710
+ if ($sp) $sp.hidden = true;
711
+ if ($b) $b.classList.remove('spotlit');
712
+ if (_spotlightKey) { document.removeEventListener('keydown', _spotlightKey); _spotlightKey = null; }
713
+ }
714
+
534
715
  // ── load / select / compile / run ───────────────────────────────────────────
535
716
 
536
717
  let _wfUpdates = []; // workflow updates available, keyed by app id — polled by refreshWorkflowUpdates() below
537
718
 
538
719
  async function loadApp(id) {
720
+ hideSpotlight(); // a spotlight pointing at the previous app's beacon must clear on switch
539
721
  const { app } = await api(`/api/app/${encodeURIComponent(id)}`);
540
722
  apps.set(id, app);
541
723
  currentId = id;
@@ -555,8 +737,10 @@
555
737
  seedAppInputs(app);
556
738
  markSpecialNodes();
557
739
  paintGate(app);
740
+ paintBeacon(app); // pulse "Start here" while this workflow is new to the user
558
741
  refreshDirtyIndicator(); // clean app → dot off; switched back to a dirty one → dot on
559
742
  renderWfUpdatePill(); // show the "↑ Update to vX" pill when THIS app has a newer published version
743
+ maybeSpotlight(app); // one-time first-open spotlight pointing at the beacon (slice 2)
560
744
  }
561
745
 
562
746
  async function loadApps() {
@@ -574,6 +758,8 @@
574
758
  if ($menuBakeItem) $menuBakeItem.disabled = true;
575
759
  if ($wfTrigger) $wfTrigger.disabled = true;
576
760
  setComboTriggerLabel('no workflows installed');
761
+ paintBeacon(null); // no workflow open → hide the per-workflow beacon
762
+ hideSpotlight();
577
763
  renderCanvasPlaceholder('empty');
578
764
  return;
579
765
  }
@@ -814,8 +1000,18 @@
814
1000
  if (v != null) localStorage.setItem(lsInputsKey(toId), v);
815
1001
  } catch { /* private mode / quota */ }
816
1002
  }
1003
+ // A rename is the SAME workflow under a new id → carry its onboarding-seen state so it
1004
+ // doesn't re-pulse "Start here" / re-show the spotlight. (Not done on duplicate: a copy
1005
+ // is a genuinely new workflow that the user hasn't run, so it should start new.)
1006
+ function migrateGuideLS(fromId, toId) {
1007
+ try {
1008
+ const all = guideStateAll();
1009
+ if (all[fromId]) { all[toId] = all[fromId]; localStorage.setItem(LS_GUIDE, JSON.stringify(all)); }
1010
+ } catch { /* private mode / quota */ }
1011
+ }
817
1012
  function dropClientAppState(id) {
818
1013
  try { localStorage.removeItem(lsInputsKey(id)); } catch { /* ignore */ }
1014
+ try { const all = guideStateAll(); if (all[id]) { delete all[id]; localStorage.setItem(LS_GUIDE, JSON.stringify(all)); } } catch { /* ignore */ }
819
1015
  appInputValues.delete(id); dirtyBaseline.delete(id); apps.delete(id);
820
1016
  }
821
1017
 
@@ -843,6 +1039,7 @@
843
1039
  // so a rename never silently drops the user's inputs (#85 review). loadApp() only seeds
844
1040
  // inputs when the id is absent, so pre-setting them here preserves unsaved edits.
845
1041
  migrateInputsLS(oldId, r.id);
1042
+ migrateGuideLS(oldId, r.id); // carry onboarding-seen state so the renamed workflow doesn't re-pulse
846
1043
  if (appInputValues.has(oldId)) appInputValues.set(r.id, appInputValues.get(oldId));
847
1044
  if (dirtyBaseline.has(oldId)) dirtyBaseline.set(r.id, dirtyBaseline.get(oldId));
848
1045
  const wasOpen = currentId === oldId;
@@ -1767,6 +1964,7 @@
1767
1964
  $reportSub.innerHTML = `Live 3D view from <code>${escapeHtml(nodeId)}</code>${inputBadge ? ' · ' + escapeHtml(inputBadge) : ''} — drag to orbit, scroll to zoom, click an element to inspect.`;
1768
1965
  }
1769
1966
  paintReport(html, interactive);
1967
+ markFirstRunDone(app.id); // a real run rendered → this workflow is no longer "new" (app captured pre-await; a mid-run switch must not mark the wrong workflow)
1770
1968
  lastReportByApp.set(currentId, { nodeId, label: inputBadge, html, interactive });
1771
1969
  $reportModal.dataset.html = '1';
1772
1970
  appendNarration(`Rendered the ${interactive ? '3D view' : 'report'} from <strong>${escapeHtml(nodeId)}</strong>${inputBadge ? ' (' + escapeHtml(inputBadge) + ')' : ''} — live run against the model.`);
@@ -3215,6 +3413,7 @@
3215
3413
  appendNarration(simulate
3216
3414
  ? `Simulated run complete · ${steps} node${steps === 1 ? '' : 's'} stubbed (no host needed) · see the <strong>Execution</strong> tab.`
3217
3415
  : `Run complete · ${steps} node${steps === 1 ? '' : 's'} against the live model · see the <strong>Execution</strong> tab.`);
3416
+ if (!simulate) markFirstRunDone(app.id); // a real run completed → calm the beacon (app captured pre-await)
3218
3417
  } catch (e) {
3219
3418
  const msg = (e && e.message) ? e.message : String(e);
3220
3419
  if (cancelRequested) {
@@ -4116,6 +4315,9 @@
4116
4315
  : '';
4117
4316
  return `In floless app "${req.appId}", re-read & re-bake${where} per the floless-app-rebake skill: ${req.instruction}${snaps}`;
4118
4317
  }
4318
+ if (req.type === 'guide') {
4319
+ return `Walk me through the floless workflow "${req.appId}" — explain in plain English what it does and how to run it (set its inputs → Compile → ▶ Run). Use your floless-app-${req.appId} skill if one exists; otherwise read the workflow's own description and node notes from GET /api/app/${req.appId} and explain from those. This is a guided explanation — don't change anything.`;
4320
+ }
4119
4321
  return '';
4120
4322
  }
4121
4323
 
@@ -4126,6 +4328,9 @@
4126
4328
  tweak: 'floless-app-workflows',
4127
4329
  'ui-customize': 'floless-app-ui',
4128
4330
  rebake: 'floless-app-rebake',
4331
+ // The guided-tour ask is picked up by onboarding; the instruction body names the
4332
+ // per-app floless-app-<appId> skill for depth when one exists.
4333
+ guide: 'floless-app-onboarding',
4129
4334
  };
4130
4335
 
4131
4336
  // The COPIED form of a request: instructionFor() prefixed with a self-identifying
@@ -4220,7 +4425,7 @@
4220
4425
  return;
4221
4426
  }
4222
4427
  $list.innerHTML = pendingRequests.map((r) => {
4223
- const label = r.type === 'use-template' ? 'template' : r.type === 'ui-customize' ? 'dashboard' : r.type === 'rebake' ? 're-bake' : 'tweak';
4428
+ const label = r.type === 'use-template' ? 'template' : r.type === 'ui-customize' ? 'dashboard' : r.type === 'rebake' ? 're-bake' : r.type === 'guide' ? 'guide' : 'tweak';
4224
4429
  const badgeCls = r.type === 'tweak' || r.type === 'ui-customize' || r.type === 'rebake' ? 'req-type req-type-tweak' : 'req-type';
4225
4430
  const target = r.type === 'tweak' && r.nodeId
4226
4431
  ? ` · node <code>${escapeHtml(r.nodeId)}</code>`
@@ -5034,6 +5239,7 @@
5034
5239
  try {
5035
5240
  const res = await api('/api/trigger-run', { method: 'POST', body: JSON.stringify({ id, inputs: currentInputs() }) });
5036
5241
  foregroundTrigger = { appId: id, sessionId: res.sessionId, firedCount: 0 };
5242
+ markFirstRunDone(id); // a real live session started → a streaming workflow's "first run" is no longer new
5037
5243
  syncRunControls(); // header ■ Stop run becomes reachable; Run disabled while live
5038
5244
  paintForegroundListening();
5039
5245
  appendNarration(`Live session started for <strong>${escapeHtml(id)}</strong> — the report updates on every event. Click <strong>■ Stop run</strong> (top right) to end it.`);
@@ -5718,6 +5924,25 @@
5718
5924
  if (e.key === 'Escape' && $reqModal && $reqModal.classList.contains('show')) $reqModal.classList.remove('show');
5719
5925
  });
5720
5926
 
5927
+ // Per-workflow onboarding beacon + guide panel + first-open spotlight.
5928
+ const $guideBeaconBtn = document.getElementById('guide-beacon');
5929
+ if ($guideBeaconBtn) $guideBeaconBtn.onclick = () => { hideSpotlight(); openGuide(); };
5930
+ const $guideModal = document.getElementById('guide-modal');
5931
+ const $guideGot = document.getElementById('guide-got-it');
5932
+ if ($guideGot) $guideGot.onclick = () => closeGuide();
5933
+ const $guideWalk = document.getElementById('guide-walk');
5934
+ if ($guideWalk) $guideWalk.onclick = () => requestGuide();
5935
+ if ($guideModal) onBackdropDismiss($guideModal, () => closeGuide());
5936
+ document.addEventListener('keydown', (e) => {
5937
+ if (e.key === 'Escape' && $guideModal && $guideModal.classList.contains('show')) closeGuide();
5938
+ });
5939
+ const $spotGot = document.getElementById('guide-spotlight-got');
5940
+ if ($spotGot) $spotGot.onclick = () => hideSpotlight();
5941
+ const $spotShow = document.getElementById('guide-spotlight-show');
5942
+ if ($spotShow) $spotShow.onclick = () => { hideSpotlight(); openGuide(); };
5943
+ const $spotlightEl = document.getElementById('guide-spotlight');
5944
+ if ($spotlightEl) $spotlightEl.onclick = (e) => { if (e.target === $spotlightEl) hideSpotlight(); };
5945
+
5721
5946
  // Pull all workspace data (agents/templates/requests + the apps list). Factored
5722
5947
  // out of bootWorkspace so the health poll can re-pull it after the server comes
5723
5948
  // back from a boot-time outage (setServerStatus → recovery).
@@ -58,6 +58,13 @@
58
58
  <!-- Agents (⊞) and Routines (⏱) buttons moved into the ≡ menu in #149
59
59
  (Ctrl+G / Ctrl+R); the toolbar keeps only the run-critical controls. -->
60
60
  <span class="ctl-sep" aria-hidden="true"></span>
61
+ <!-- Per-workflow onboarding beacon. Pulses "✨ Start here" while this workflow is
62
+ new to the user (no first Run completed, not dismissed); calms to a quiet
63
+ "❔ Guide" after — always clickable for re-reference. Hidden until an app loads. -->
64
+ <button id="guide-beacon" class="guide-beacon" type="button" hidden aria-haspopup="dialog" data-tip="New here? See what this workflow does and how to run it.">
65
+ <span class="guide-beacon-ico" aria-hidden="true">✦</span>
66
+ <span class="guide-beacon-label">Start here</span>
67
+ </button>
61
68
  <span class="run-state" id="run-state" role="status" aria-live="polite"></span>
62
69
  <button id="compile-btn" data-tip="Compile + approve → freeze the .lock">⎙ Compile</button>
63
70
  <button id="sim-btn" data-tip="Simulate: stub every node from its output schema — no live host is contacted. Validates the workflow's composition end-to-end, even when the real agents aren't connected yet.">Simulate</button>
@@ -511,6 +518,37 @@
511
518
  </div>
512
519
  </div>
513
520
 
521
+ <!-- Per-workflow onboarding "Start here" guide. Surfaces the workflow's plain-English
522
+ description + a live "Set inputs → Compile → Run" checklist (ticks from real state),
523
+ and relays a "walk me through it" ask to the terminal AI. A styled modal — never a
524
+ native dialog; body (#guide-body) is rendered per-open in aware.js. -->
525
+ <div class="modal-backdrop" id="guide-modal">
526
+ <div class="modal guide" role="dialog" aria-modal="true" aria-labelledby="guide-title" tabindex="-1">
527
+ <div class="guide-eyebrow" id="guide-eyebrow">✦ Start here</div>
528
+ <div class="modal-title" id="guide-title">Workflow</div>
529
+ <div class="modal-sub">What this workflow does, and how to go through it.</div>
530
+ <div id="guide-body"></div>
531
+ <div class="modal-actions">
532
+ <button id="guide-got-it">Got it</button>
533
+ <button id="guide-walk" class="primary">Walk me through it →</button>
534
+ </div>
535
+ </div>
536
+ </div>
537
+
538
+ <!-- One-time first-open spotlight: dims the canvas and points at the "Start here"
539
+ beacon, then collapses into it. Shown once per workflow; Esc / click-away / either
540
+ button dismiss it. The beacon gets .spotlit so it sits above the dim. The bubble is
541
+ positioned next to the beacon at open time (aware.js). -->
542
+ <div id="guide-spotlight" class="guide-spotlight" hidden>
543
+ <div class="guide-spotlight-bubble" role="dialog" aria-modal="false" aria-label="New workflow tip" tabindex="-1">
544
+ <div class="guide-spotlight-text">New to this workflow? <strong>Start here</strong> — see what it does and how to run it.</div>
545
+ <div class="guide-spotlight-actions">
546
+ <button type="button" id="guide-spotlight-got" class="ghost">Got it</button>
547
+ <button type="button" id="guide-spotlight-show" class="primary">Show me</button>
548
+ </div>
549
+ </div>
550
+ </div>
551
+
514
552
  <!-- Routines — scheduled .flo runs. The list; the add/edit form is its own modal. -->
515
553
  <div class="modal-backdrop" id="routines-modal">
516
554
  <div class="modal routines">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floless/app",
3
- "version": "0.54.0",
3
+ "version": "0.55.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": {