@floless/app 0.34.2 → 0.35.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/floless-server.cjs +89 -32
- package/dist/schemas/steel.takeoff.v1.schema.json +1 -0
- package/dist/skills/floless-app-steel-takeoff/SKILL.md +6 -3
- package/dist/skills/floless-app-workflows/references/dev-server-and-run-trace.md +4 -2
- package/dist/web/app.css +24 -23
- package/dist/web/app.js +7 -3
- package/dist/web/aware.js +47 -5
- package/dist/web/index.html +22 -9
- package/dist/web/package.json +5 -0
- package/dist/web/panels.js +25 -11
- package/dist/web/steel-3d-core.js +137 -0
- package/dist/web/steel-3d-view.js +422 -0
- package/dist/web/steel-editor.html +140 -6
- package/dist/web/vendor/OrbitControls.js +1963 -0
- package/dist/web/vendor/README.md +15 -0
- package/dist/web/vendor/three.core.js +59732 -0
- package/dist/web/vendor/three.module.js +19552 -0
- package/package.json +1 -1
package/dist/floless-server.cjs
CHANGED
|
@@ -50822,7 +50822,7 @@ function deriveSandboxLicenseDir() {
|
|
|
50822
50822
|
deriveSandboxLicenseDir();
|
|
50823
50823
|
|
|
50824
50824
|
// main.ts
|
|
50825
|
-
var
|
|
50825
|
+
var import_node_child_process9 = require("node:child_process");
|
|
50826
50826
|
var import_node_readline2 = require("node:readline");
|
|
50827
50827
|
|
|
50828
50828
|
// index.ts
|
|
@@ -50830,7 +50830,7 @@ var import_node_url4 = require("node:url");
|
|
|
50830
50830
|
var import_node_path26 = require("node:path");
|
|
50831
50831
|
var import_node_os18 = require("node:os");
|
|
50832
50832
|
var import_node_fs28 = require("node:fs");
|
|
50833
|
-
var
|
|
50833
|
+
var import_node_child_process7 = require("node:child_process");
|
|
50834
50834
|
|
|
50835
50835
|
// log.mjs
|
|
50836
50836
|
var import_node_path = require("node:path");
|
|
@@ -52856,7 +52856,7 @@ function appVersion() {
|
|
|
52856
52856
|
return resolveVersion({
|
|
52857
52857
|
isSea: isSea2(),
|
|
52858
52858
|
sqVersionXml: readSqVersionXml(),
|
|
52859
|
-
define: true ? "0.
|
|
52859
|
+
define: true ? "0.35.1" : 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.
|
|
52869
|
+
return resolveChannel({ isSea: isSea2(), define: true ? "0.35.1" : void 0 });
|
|
52870
52870
|
}
|
|
52871
52871
|
|
|
52872
52872
|
// oauth-presets.ts
|
|
@@ -53948,6 +53948,42 @@ function writeTeklaApp(dir, appId, scene, teklaVersion = "2026.0") {
|
|
|
53948
53948
|
return flo;
|
|
53949
53949
|
}
|
|
53950
53950
|
|
|
53951
|
+
// tekla-version.ts
|
|
53952
|
+
var import_node_child_process3 = require("node:child_process");
|
|
53953
|
+
var import_node_util = require("node:util");
|
|
53954
|
+
var pexec = (0, import_node_util.promisify)(import_node_child_process3.execFile);
|
|
53955
|
+
var DEFAULT_TEKLA_VERSION = "2026.0";
|
|
53956
|
+
function parseTeklaVersionFromExePath(p) {
|
|
53957
|
+
const m = /Tekla Structures[\\/]+([^\\/]+)[\\/]+bin\b/i.exec(p);
|
|
53958
|
+
return m ? m[1].trim() : null;
|
|
53959
|
+
}
|
|
53960
|
+
async function detectRunningTeklaVersion() {
|
|
53961
|
+
if (process.platform !== "win32") return null;
|
|
53962
|
+
try {
|
|
53963
|
+
const { stdout } = await pexec(
|
|
53964
|
+
"powershell",
|
|
53965
|
+
[
|
|
53966
|
+
"-NoProfile",
|
|
53967
|
+
"-NonInteractive",
|
|
53968
|
+
"-Command",
|
|
53969
|
+
`Get-CimInstance Win32_Process -Filter "Name='TeklaStructures.exe'" | Select-Object -First 1 -ExpandProperty ExecutablePath`
|
|
53970
|
+
],
|
|
53971
|
+
{ timeout: 5e3, windowsHide: true }
|
|
53972
|
+
);
|
|
53973
|
+
const exePath = stdout.trim();
|
|
53974
|
+
if (!exePath) return null;
|
|
53975
|
+
const version = parseTeklaVersionFromExePath(exePath);
|
|
53976
|
+
if (!version) console.warn(`tekla-version: Tekla is running but no version parsed from "${exePath}" \u2014 baking ${DEFAULT_TEKLA_VERSION}`);
|
|
53977
|
+
return version;
|
|
53978
|
+
} catch (e) {
|
|
53979
|
+
console.warn(`tekla-version: could not detect the running Tekla version (${e instanceof Error ? e.message : String(e)}) \u2014 baking ${DEFAULT_TEKLA_VERSION}`);
|
|
53980
|
+
return null;
|
|
53981
|
+
}
|
|
53982
|
+
}
|
|
53983
|
+
async function teklaVersionToBake() {
|
|
53984
|
+
return await detectRunningTeklaVersion() ?? DEFAULT_TEKLA_VERSION;
|
|
53985
|
+
}
|
|
53986
|
+
|
|
53951
53987
|
// bom-format.ts
|
|
53952
53988
|
var round1 = (n) => Math.round(n * 10) / 10;
|
|
53953
53989
|
function contractToBom(contractInput) {
|
|
@@ -55187,7 +55223,7 @@ function isGatedAwareRoute(url, method) {
|
|
|
55187
55223
|
}
|
|
55188
55224
|
|
|
55189
55225
|
// autostart.mjs
|
|
55190
|
-
var
|
|
55226
|
+
var import_node_child_process4 = require("node:child_process");
|
|
55191
55227
|
var import_node_fs22 = require("node:fs");
|
|
55192
55228
|
var import_node_os14 = require("node:os");
|
|
55193
55229
|
var import_node_path19 = require("node:path");
|
|
@@ -55296,7 +55332,7 @@ function currentUserId() {
|
|
|
55296
55332
|
}
|
|
55297
55333
|
function removeLegacyRunKey() {
|
|
55298
55334
|
try {
|
|
55299
|
-
(0,
|
|
55335
|
+
(0, import_node_child_process4.execFileSync)("reg", ["delete", RUN_KEY, "/v", RUN_VALUE, "/f"], { stdio: "ignore", windowsHide: true });
|
|
55300
55336
|
} catch {
|
|
55301
55337
|
}
|
|
55302
55338
|
}
|
|
@@ -55314,7 +55350,7 @@ function registerAutostart(exePath) {
|
|
|
55314
55350
|
const tmp = (0, import_node_path19.join)((0, import_node_os14.tmpdir)(), `floless-autostart-${process.pid}-${Date.now()}.xml`);
|
|
55315
55351
|
(0, import_node_fs22.writeFileSync)(tmp, "\uFEFF" + xml, { encoding: "utf16le" });
|
|
55316
55352
|
try {
|
|
55317
|
-
(0,
|
|
55353
|
+
(0, import_node_child_process4.execFileSync)("schtasks", ["/Create", "/TN", TASK_NAME, "/XML", tmp, "/F"], {
|
|
55318
55354
|
stdio: ["ignore", "ignore", "ignore"],
|
|
55319
55355
|
windowsHide: true
|
|
55320
55356
|
});
|
|
@@ -55332,7 +55368,7 @@ function registerAutostart(exePath) {
|
|
|
55332
55368
|
function autostartPresent() {
|
|
55333
55369
|
if (!isWin) return false;
|
|
55334
55370
|
try {
|
|
55335
|
-
(0,
|
|
55371
|
+
(0, import_node_child_process4.execFileSync)("schtasks", ["/query", "/tn", TASK_NAME], { stdio: "ignore", windowsHide: true });
|
|
55336
55372
|
return true;
|
|
55337
55373
|
} catch {
|
|
55338
55374
|
return false;
|
|
@@ -55349,14 +55385,14 @@ function ensureAutostart(exePath) {
|
|
|
55349
55385
|
function unregisterAutostart() {
|
|
55350
55386
|
if (!isWin) return;
|
|
55351
55387
|
try {
|
|
55352
|
-
(0,
|
|
55388
|
+
(0, import_node_child_process4.execFileSync)("schtasks", ["/delete", "/tn", TASK_NAME, "/f"], { stdio: "ignore", windowsHide: true });
|
|
55353
55389
|
} catch {
|
|
55354
55390
|
}
|
|
55355
55391
|
removeLegacyRunKey();
|
|
55356
55392
|
}
|
|
55357
55393
|
|
|
55358
55394
|
// updater.ts
|
|
55359
|
-
var
|
|
55395
|
+
var import_node_child_process5 = require("node:child_process");
|
|
55360
55396
|
var import_node_crypto6 = require("node:crypto");
|
|
55361
55397
|
var import_node_fs24 = require("node:fs");
|
|
55362
55398
|
var import_node_stream = require("node:stream");
|
|
@@ -55547,7 +55583,7 @@ async function applyUpdate(check, opts) {
|
|
|
55547
55583
|
const pkg = await downloadPackage(check.asset);
|
|
55548
55584
|
if (opts?.onBeforeApply) await opts.onBeforeApply();
|
|
55549
55585
|
await new Promise((resolve6, reject) => {
|
|
55550
|
-
const child = (0,
|
|
55586
|
+
const child = (0, import_node_child_process5.spawn)(exe, ["apply", "--package", pkg, "--waitPid", String(process.pid)], {
|
|
55551
55587
|
cwd: installRoot(),
|
|
55552
55588
|
detached: true,
|
|
55553
55589
|
stdio: "ignore",
|
|
@@ -55780,7 +55816,7 @@ function isTraceCorrupt(events) {
|
|
|
55780
55816
|
}
|
|
55781
55817
|
|
|
55782
55818
|
// launch.mjs
|
|
55783
|
-
var
|
|
55819
|
+
var import_node_child_process6 = require("node:child_process");
|
|
55784
55820
|
var import_node_path22 = require("node:path");
|
|
55785
55821
|
var import_node_url2 = require("node:url");
|
|
55786
55822
|
var import_node_fs25 = require("node:fs");
|
|
@@ -55866,7 +55902,7 @@ function startServerDetached() {
|
|
|
55866
55902
|
const { cmd, args, shell } = resolveServerStart();
|
|
55867
55903
|
rotateLog();
|
|
55868
55904
|
const fd = openLogFd();
|
|
55869
|
-
const child = (0,
|
|
55905
|
+
const child = (0, import_node_child_process6.spawn)(cmd, args, {
|
|
55870
55906
|
cwd: __dirname2,
|
|
55871
55907
|
detached: true,
|
|
55872
55908
|
stdio: fd == null ? "ignore" : ["ignore", fd, fd],
|
|
@@ -55876,13 +55912,13 @@ function startServerDetached() {
|
|
|
55876
55912
|
child.unref();
|
|
55877
55913
|
}
|
|
55878
55914
|
function openBrowser2(url) {
|
|
55879
|
-
if (isWin2) (0,
|
|
55880
|
-
else (0,
|
|
55915
|
+
if (isWin2) (0, import_node_child_process6.spawn)("cmd", ["/c", "start", "", url], { windowsHide: true }).unref();
|
|
55916
|
+
else (0, import_node_child_process6.spawn)(process.platform === "darwin" ? "open" : "xdg-open", [url], { detached: true }).unref();
|
|
55881
55917
|
}
|
|
55882
55918
|
function stopServer() {
|
|
55883
55919
|
if (!isWin2) {
|
|
55884
55920
|
try {
|
|
55885
|
-
(0,
|
|
55921
|
+
(0, import_node_child_process6.execSync)(`bash -lc "fuser -k ${PORT}/tcp"`, { stdio: "ignore" });
|
|
55886
55922
|
return true;
|
|
55887
55923
|
} catch {
|
|
55888
55924
|
return false;
|
|
@@ -55890,7 +55926,7 @@ function stopServer() {
|
|
|
55890
55926
|
}
|
|
55891
55927
|
try {
|
|
55892
55928
|
const ps = `$p = Get-NetTCPConnection -LocalPort ${PORT} -State Listen -ErrorAction SilentlyContinue | Select-Object -Expand OwningProcess -Unique; if ($p) { $p | ForEach-Object { taskkill /PID $_ /T /F } } else { exit 9 }`;
|
|
55893
|
-
(0,
|
|
55929
|
+
(0, import_node_child_process6.execSync)(`powershell -NoProfile -Command "${ps}"`, { stdio: "ignore" });
|
|
55894
55930
|
return true;
|
|
55895
55931
|
} catch {
|
|
55896
55932
|
return false;
|
|
@@ -55942,7 +55978,7 @@ async function cmdSupervise() {
|
|
|
55942
55978
|
if (isSeaChannel && !process.env[SUPERVISE_RESPAWN_ENV]) {
|
|
55943
55979
|
rotateLog();
|
|
55944
55980
|
const fd = openLogFd();
|
|
55945
|
-
const child = (0,
|
|
55981
|
+
const child = (0, import_node_child_process6.spawn)(process.execPath, ["--supervise"], {
|
|
55946
55982
|
detached: true,
|
|
55947
55983
|
stdio: fd == null ? "ignore" : ["ignore", fd, fd],
|
|
55948
55984
|
windowsHide: true,
|
|
@@ -55997,7 +56033,7 @@ function enumerateProcesses() {
|
|
|
55997
56033
|
if (!isWin2) return [];
|
|
55998
56034
|
try {
|
|
55999
56035
|
const ps = "Get-CimInstance Win32_Process | Select-Object ProcessId,CommandLine | ConvertTo-Json -Compress";
|
|
56000
|
-
const out = (0,
|
|
56036
|
+
const out = (0, import_node_child_process6.execSync)(`powershell -NoProfile -Command "${ps}"`, {
|
|
56001
56037
|
encoding: "utf8",
|
|
56002
56038
|
windowsHide: true,
|
|
56003
56039
|
maxBuffer: 16 * 1024 * 1024
|
|
@@ -56020,7 +56056,7 @@ function killSupervisor({ tree = true } = {}) {
|
|
|
56020
56056
|
for (const pid of pids) {
|
|
56021
56057
|
log(`stopping supervisor (pid ${pid})\u2026`);
|
|
56022
56058
|
try {
|
|
56023
|
-
(0,
|
|
56059
|
+
(0, import_node_child_process6.execFileSync)("taskkill", taskkillArgs(pid, { tree }), { stdio: "ignore", windowsHide: true });
|
|
56024
56060
|
} catch {
|
|
56025
56061
|
}
|
|
56026
56062
|
}
|
|
@@ -56111,11 +56147,11 @@ async function cmdUpdate() {
|
|
|
56111
56147
|
function removeRegistryFootprint() {
|
|
56112
56148
|
if (!isWin2) return;
|
|
56113
56149
|
try {
|
|
56114
|
-
(0,
|
|
56150
|
+
(0, import_node_child_process6.execFileSync)("reg", ["delete", RUN_KEY, "/v", RUN_VALUE, "/f"], { stdio: "ignore", windowsHide: true });
|
|
56115
56151
|
} catch {
|
|
56116
56152
|
}
|
|
56117
56153
|
try {
|
|
56118
|
-
(0,
|
|
56154
|
+
(0, import_node_child_process6.execFileSync)("reg", ["delete", PROTOCOL_KEY, "/f"], { stdio: "ignore", windowsHide: true });
|
|
56119
56155
|
} catch {
|
|
56120
56156
|
}
|
|
56121
56157
|
}
|
|
@@ -56141,13 +56177,13 @@ async function cmdUninstall(flags = {}) {
|
|
|
56141
56177
|
if (removeAware) {
|
|
56142
56178
|
log("removing the AWARE runtime \u2014 this will affect ANY other tool that uses @aware-aeco/cli.");
|
|
56143
56179
|
try {
|
|
56144
|
-
(0,
|
|
56180
|
+
(0, import_node_child_process6.execSync)("npm uninstall -g @aware-aeco/cli", { stdio: "inherit", shell: isWin2 });
|
|
56145
56181
|
} catch {
|
|
56146
56182
|
log('warning: "npm uninstall -g @aware-aeco/cli" failed \u2014 see the output above.');
|
|
56147
56183
|
}
|
|
56148
56184
|
let lsJson = "";
|
|
56149
56185
|
try {
|
|
56150
|
-
lsJson = (0,
|
|
56186
|
+
lsJson = (0, import_node_child_process6.execSync)("npm ls -g @aware-aeco/cli --depth=0 --json", {
|
|
56151
56187
|
encoding: "utf8",
|
|
56152
56188
|
stdio: ["ignore", "pipe", "ignore"],
|
|
56153
56189
|
shell: isWin2
|
|
@@ -58226,7 +58262,7 @@ async function startServer() {
|
|
|
58226
58262
|
const isWin3 = process.platform === "win32";
|
|
58227
58263
|
const has = (cmd) => {
|
|
58228
58264
|
try {
|
|
58229
|
-
(0,
|
|
58265
|
+
(0, import_node_child_process7.execFileSync)(isWin3 ? "where" : "which", [cmd], { stdio: "ignore", windowsHide: true, timeout: 5e3 });
|
|
58230
58266
|
return true;
|
|
58231
58267
|
} catch {
|
|
58232
58268
|
return false;
|
|
@@ -58235,7 +58271,7 @@ async function startServer() {
|
|
|
58235
58271
|
function installAwareGlobal(spec, onLine = () => {
|
|
58236
58272
|
}) {
|
|
58237
58273
|
return new Promise((resolve6, reject) => {
|
|
58238
|
-
const child = (0,
|
|
58274
|
+
const child = (0, import_node_child_process7.spawn)("npm", ["i", "-g", `@aware-aeco/cli@${spec}`], { shell: isWin3, windowsHide: true });
|
|
58239
58275
|
let stderrTail = "";
|
|
58240
58276
|
let combinedTail = "";
|
|
58241
58277
|
const tail = (buf, s) => (buf + s).slice(-4e3);
|
|
@@ -58625,7 +58661,14 @@ async function startServer() {
|
|
|
58625
58661
|
const first = v.errors[0];
|
|
58626
58662
|
return reply.status(400).send({ ok: false, error: `contract failed schema validation \u2014 ${first ? `${first.path}: ${first.message}` : "invalid"}` });
|
|
58627
58663
|
}
|
|
58628
|
-
const
|
|
58664
|
+
const docTarget = doc2 && typeof doc2 === "object" ? doc2.target_confidence : void 0;
|
|
58665
|
+
let appDefault = 70;
|
|
58666
|
+
try {
|
|
58667
|
+
const inp = readApp(req.params.appId).inputs.find((i) => i.name === "target_confidence");
|
|
58668
|
+
if (typeof inp?.default === "number") appDefault = inp.default;
|
|
58669
|
+
} catch {
|
|
58670
|
+
}
|
|
58671
|
+
const target = typeof req.body?.target === "number" ? req.body.target : typeof docTarget === "number" ? docTarget : appDefault;
|
|
58629
58672
|
const result = scoreContract2(doc2);
|
|
58630
58673
|
const meetsTarget = result.score != null && result.score >= target;
|
|
58631
58674
|
return {
|
|
@@ -58637,6 +58680,19 @@ async function startServer() {
|
|
|
58637
58680
|
};
|
|
58638
58681
|
}
|
|
58639
58682
|
);
|
|
58683
|
+
app.post(
|
|
58684
|
+
"/api/contract/:appId/scene",
|
|
58685
|
+
async (req, reply) => {
|
|
58686
|
+
const doc2 = req.body && "contract" in req.body ? req.body.contract : readContract(req.params.appId);
|
|
58687
|
+
if (doc2 == null) return reply.status(404).send({ ok: false, error: "no contract to render" });
|
|
58688
|
+
const v = validateSteelTakeoff(doc2);
|
|
58689
|
+
if (!v.valid) {
|
|
58690
|
+
const first = v.errors[0];
|
|
58691
|
+
return reply.status(400).send({ ok: false, error: `contract failed schema validation \u2014 ${first ? `${first.path}: ${first.message}` : "invalid"}` });
|
|
58692
|
+
}
|
|
58693
|
+
return { ok: true, ...contractToScene(doc2) };
|
|
58694
|
+
}
|
|
58695
|
+
);
|
|
58640
58696
|
function awareStderr(e) {
|
|
58641
58697
|
if (e instanceof AwareError && e.detail && typeof e.detail === "object") {
|
|
58642
58698
|
const s = e.detail.stderr;
|
|
@@ -58732,9 +58788,10 @@ async function startServer() {
|
|
|
58732
58788
|
return reply.status(422).send({ ok: false, error: "no resolvable members to bake \u2014 every member is an RFI (no AISC size yet)", skipped });
|
|
58733
58789
|
}
|
|
58734
58790
|
const companionId = `${req.params.appId}-tekla`;
|
|
58791
|
+
const teklaVersion = await teklaVersionToBake();
|
|
58735
58792
|
let flo;
|
|
58736
58793
|
try {
|
|
58737
|
-
flo = writeTeklaApp(appPath(companionId), companionId, scene);
|
|
58794
|
+
flo = writeTeklaApp(appPath(companionId), companionId, scene, teklaVersion);
|
|
58738
58795
|
} catch (e) {
|
|
58739
58796
|
app.log.error({ companionId, err: e instanceof Error ? e.message : e }, "export-tekla: companion app write failed");
|
|
58740
58797
|
return reply.status(500).send({ ok: false, error: `could not write the Tekla companion app: ${e instanceof Error ? e.message : "write error"}` });
|
|
@@ -59526,10 +59583,10 @@ async function startServer() {
|
|
|
59526
59583
|
}
|
|
59527
59584
|
|
|
59528
59585
|
// protocol.ts
|
|
59529
|
-
var
|
|
59586
|
+
var import_node_child_process8 = require("node:child_process");
|
|
59530
59587
|
var BASE = PROTOCOL_KEY;
|
|
59531
59588
|
function reg(args) {
|
|
59532
|
-
(0,
|
|
59589
|
+
(0, import_node_child_process8.execFileSync)("reg", args, { stdio: "ignore", windowsHide: true });
|
|
59533
59590
|
}
|
|
59534
59591
|
function registerProtocol(exePath) {
|
|
59535
59592
|
if (process.platform !== "win32") return;
|
|
@@ -59569,13 +59626,13 @@ async function removeAwarePerDecision(removeAwareFlag, prompt) {
|
|
|
59569
59626
|
);
|
|
59570
59627
|
const shell = process.platform === "win32" ? process.env.ComSpec || "cmd.exe" : "/bin/sh";
|
|
59571
59628
|
try {
|
|
59572
|
-
(0,
|
|
59629
|
+
(0, import_node_child_process9.execSync)("npm uninstall -g @aware-aeco/cli", { stdio: "inherit", shell });
|
|
59573
59630
|
} catch {
|
|
59574
59631
|
process.stdout.write('floless: warning: "npm uninstall -g @aware-aeco/cli" failed \u2014 see the output above.\n');
|
|
59575
59632
|
}
|
|
59576
59633
|
let lsJson = "";
|
|
59577
59634
|
try {
|
|
59578
|
-
lsJson = (0,
|
|
59635
|
+
lsJson = (0, import_node_child_process9.execSync)("npm ls -g @aware-aeco/cli --depth=0 --json", {
|
|
59579
59636
|
encoding: "utf8",
|
|
59580
59637
|
stdio: ["ignore", "pipe", "ignore"],
|
|
59581
59638
|
shell
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"additionalProperties": true,
|
|
9
9
|
"properties": {
|
|
10
10
|
"type": { "const": "steel.takeoff/v1", "description": "Renderer-registry discriminator." },
|
|
11
|
+
"target_confidence": { "type": "number", "minimum": 0, "maximum": 100, "description": "User's per-read confidence target (%); overrides the app's target_confidence input default. The editor chip and the /score endpoint use it." },
|
|
11
12
|
"active": { "type": "integer", "description": "Index into plans of the active sheet." },
|
|
12
13
|
"palette": { "type": "array", "items": { "type": "string" }, "description": "Default per-index profile colours." },
|
|
13
14
|
"weights": { "type": "object", "additionalProperties": { "type": ["number", "null"] }, "description": "profile -> plf (pounds per linear foot), from the AISC lookup. null for an unresolved profile (e.g. an unsized moment-frame mark) that has no weight yet." },
|
|
@@ -117,9 +117,12 @@ Follow the methodology condensed here (full detail + every gotcha in
|
|
|
117
117
|
|
|
118
118
|
### 3. Honor `target_confidence` — score, then LOOP to the goal
|
|
119
119
|
|
|
120
|
-
Read the app's `target_confidence` input (default 70). The
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
Read the app's `target_confidence` input (default 70). The user may also set a **per-read override**
|
|
121
|
+
via the editor (stored as a top-level `target_confidence` on the contract); it wins over the input
|
|
122
|
+
default. On a **re-read / rebake**, preserve any `target_confidence` already on the existing contract
|
|
123
|
+
(carry it into the new draft) so the user's chosen target isn't reset. The confidence score is a
|
|
124
|
+
**deterministic function of observable evidence** — not a model self-report — so you measure it with
|
|
125
|
+
the server, never by eyeballing. Per-member bands (full method in
|
|
123
126
|
`docs/superpowers/specs/2026-06-20-steel-takeoff-confidence-report.md`):
|
|
124
127
|
|
|
125
128
|
- **Verified (1.0)** — human-confirmed. **High (0.9)** — resolved from drawing evidence, no contradiction.
|
|
@@ -88,8 +88,10 @@ machine via a `self_test:true` watch fixture — no Tekla, no `--simulate`:
|
|
|
88
88
|
- **The installed SEA server usually already holds :4317** (and it 404s `/api/status` — different
|
|
89
89
|
build). Don't fight it: `index.ts` honors `PORT`, so run the dev server on a free port —
|
|
90
90
|
`PORT=4318 FLOLESS_LICENSE_DIR="$LOCALAPPDATA/FlolessApp-data" npx tsx main.ts --serve` (from
|
|
91
|
-
`server/`, background). Without the license dir it serves the **sign-in gate** (
|
|
92
|
-
— assert `document.getElementById('
|
|
91
|
+
`server/`, background). Without the license dir it serves the **sign-in gate** (the workspace
|
|
92
|
+
chrome is absent) — assert a stable workspace element like `document.getElementById('run-btn')`
|
|
93
|
+
(or `#menu-btn`) exists before driving the UI. (Don't probe `#routines-btn`/`#browse-btn` —
|
|
94
|
+
those header buttons moved into the ≡ menu in #149.)
|
|
93
95
|
- **The Playwright MCP browser is flaky/locked here** ("Browser is already in use…"). A **standalone
|
|
94
96
|
Node script is more reliable**: `playwright-core` resolves at
|
|
95
97
|
`C:/Users/Pawel/AppData/Roaming/npm/node_modules/@playwright/cli/node_modules/playwright-core`;
|
package/dist/web/app.css
CHANGED
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
reachable by scrolling), at viewport widths narrower than the design (#53). */
|
|
116
116
|
min-width: 0;
|
|
117
117
|
}
|
|
118
|
-
/*
|
|
118
|
+
/* The ≡ menu + brand stay full size; .controls is the shrink sink instead. */
|
|
119
119
|
.header-left { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
|
|
120
120
|
.brand { display: flex; align-items: center; gap: 12px; }
|
|
121
121
|
.brand .mark {
|
|
@@ -169,10 +169,8 @@
|
|
|
169
169
|
outline: 2px solid var(--accent);
|
|
170
170
|
outline-offset: 2px;
|
|
171
171
|
}
|
|
172
|
-
/*
|
|
173
|
-
|
|
174
|
-
#browse-btn { background: transparent; border-color: transparent; color: var(--text-dim); }
|
|
175
|
-
#browse-btn:hover { background: var(--surface-2); border-color: var(--border-strong); color: var(--text); }
|
|
172
|
+
/* (The #browse-btn "Agents" toolbar button moved into the ≡ menu in #149; its
|
|
173
|
+
ghost-tier styles were removed with it.) */
|
|
176
174
|
/* Simulate mirrors Compile (outline-accent tier) — a consistent secondary
|
|
177
175
|
sibling; Run stays the only filled (primary) button, so the two outline
|
|
178
176
|
buttons reading alike is the correct hierarchy, not a lost signal. */
|
|
@@ -1490,10 +1488,8 @@
|
|
|
1490
1488
|
#confirm-dont-save:hover { color: var(--err); border-color: var(--err); background: color-mix(in srgb, var(--err) 10%, transparent); }
|
|
1491
1489
|
|
|
1492
1490
|
/* ========== ROUTINES ========== */
|
|
1493
|
-
/*
|
|
1494
|
-
|
|
1495
|
-
#routines-btn { background: transparent; border-color: transparent; color: var(--text-dim); }
|
|
1496
|
-
#routines-btn:hover { background: var(--surface-2); border-color: var(--border-strong); color: var(--text); }
|
|
1491
|
+
/* (The #routines-btn header button moved into the ≡ menu in #149; the panel is
|
|
1492
|
+
opened from there and via Ctrl+R, so its toolbar styles were removed.) */
|
|
1497
1493
|
|
|
1498
1494
|
.modal.routines { width: 600px; max-width: 92vw; max-height: 86vh; display: flex; flex-direction: column; }
|
|
1499
1495
|
.rtn-quota { font-size: 11px; color: var(--text-dim); margin-bottom: 10px; flex: 0 0 auto; }
|
|
@@ -1787,6 +1783,21 @@
|
|
|
1787
1783
|
transition: all 0.15s;
|
|
1788
1784
|
margin-right: 10px;
|
|
1789
1785
|
flex-shrink: 0;
|
|
1786
|
+
position: relative; /* anchors the dashboard-updated badge below */
|
|
1787
|
+
}
|
|
1788
|
+
/* "Dashboard updated" badge — panels.js lights this when dashboard panels change while
|
|
1789
|
+
the user is on Canvas, so the signal stays visible without opening the ≡ menu (#149,
|
|
1790
|
+
the old Dashboard-toggle dot's new home). Cleared when the dashboard is viewed. */
|
|
1791
|
+
.hamburger.has-dash-update::after {
|
|
1792
|
+
content: "";
|
|
1793
|
+
position: absolute;
|
|
1794
|
+
top: 3px;
|
|
1795
|
+
right: 3px;
|
|
1796
|
+
width: 6px;
|
|
1797
|
+
height: 6px;
|
|
1798
|
+
border-radius: 50%;
|
|
1799
|
+
background: var(--accent);
|
|
1800
|
+
box-shadow: 0 0 0 2px var(--bg);
|
|
1790
1801
|
}
|
|
1791
1802
|
.hamburger:hover {
|
|
1792
1803
|
border-color: var(--accent-dim);
|
|
@@ -2881,20 +2892,10 @@ body {
|
|
|
2881
2892
|
shipped UI. No new colors, fonts, or aesthetics here — composition only.
|
|
2882
2893
|
========================================================================== */
|
|
2883
2894
|
|
|
2884
|
-
/*
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
.view-
|
|
2888
|
-
background: var(--surface-2); border: 1px solid var(--border-strong);
|
|
2889
|
-
color: var(--text-dim); font-size: 10.5px; text-transform: uppercase;
|
|
2890
|
-
letter-spacing: 0.1em; padding: 4px 12px; border-radius: 6px; cursor: pointer;
|
|
2891
|
-
transition: color 0.15s, border-color 0.15s, background 0.15s;
|
|
2892
|
-
}
|
|
2893
|
-
.view-btn:hover { color: var(--text); border-color: var(--accent-dim); }
|
|
2894
|
-
.view-btn.active { color: var(--accent); border-color: var(--accent-dim); background: var(--accent-soft); }
|
|
2895
|
-
.view-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
2896
|
-
/* "Dashboard updated" dot — lit when panels changed while the user was on Canvas. */
|
|
2897
|
-
.view-dot { color: var(--accent); font-size: 8px; vertical-align: 2px; margin-left: 5px; }
|
|
2895
|
+
/* The Canvas|Dashboard segmented toggle moved into the ≡ menu in #149; its styles are
|
|
2896
|
+
gone. The "dashboard updated" dot lives on now: in the menu's view-switch item (.view-dot)
|
|
2897
|
+
and as the ≡ hamburger badge (.hamburger.has-dash-update, defined above). */
|
|
2898
|
+
.view-dot { color: var(--accent); font-size: 8px; vertical-align: 2px; margin-left: auto; padding-left: 6px; }
|
|
2898
2899
|
.view-dot[hidden] { display: none; }
|
|
2899
2900
|
|
|
2900
2901
|
/* Customized pill — present only while custom panels exist; opens the ext menu. */
|
package/dist/web/app.js
CHANGED
|
@@ -50,7 +50,6 @@ function saveCollapse() {
|
|
|
50
50
|
const $app = document.getElementById('app');
|
|
51
51
|
const $promptSel = document.getElementById('prompt-select');
|
|
52
52
|
const $runBtn = document.getElementById('run-btn');
|
|
53
|
-
const $browseBtn = document.getElementById('browse-btn');
|
|
54
53
|
const $messages = document.getElementById('messages');
|
|
55
54
|
const $topology = document.getElementById('topology');
|
|
56
55
|
const $tabs = document.getElementById('tabs');
|
|
@@ -952,8 +951,6 @@ $runBtn.onclick = async () => {
|
|
|
952
951
|
appendNarration(`Execution complete · ${nodeIds(p).length} steps fired · check the <strong>Execution</strong> tab on each agent for the full trace.`);
|
|
953
952
|
};
|
|
954
953
|
|
|
955
|
-
$browseBtn.onclick = openLibrary;
|
|
956
|
-
|
|
957
954
|
document.querySelectorAll('.panel-toggle').forEach(btn => {
|
|
958
955
|
btn.onclick = () => toggleCollapse(btn.dataset.side);
|
|
959
956
|
});
|
|
@@ -1050,6 +1047,7 @@ function handleMenuAction(action) {
|
|
|
1050
1047
|
case 'save-as': doSaveAs(); break;
|
|
1051
1048
|
case 'find': openFind(); break;
|
|
1052
1049
|
case 'integrations': openIntegrations(); break;
|
|
1050
|
+
case 'agents': openLibrary(); break;
|
|
1053
1051
|
case 'routines': openRoutines(); break;
|
|
1054
1052
|
case 'report-issue': openReportIssue(); break;
|
|
1055
1053
|
case 'fullscreen': toggleFullscreen(); break;
|
|
@@ -1546,6 +1544,12 @@ document.addEventListener('keydown', (e) => {
|
|
|
1546
1544
|
if (cmd && !e.shiftKey && e.key.toLowerCase() === 's') { e.preventDefault(); doSave(); return; }
|
|
1547
1545
|
if (cmd && e.key.toLowerCase() === 'f') { e.preventDefault(); openFind(); return; }
|
|
1548
1546
|
if (cmd && e.key.toLowerCase() === 'i') { e.preventDefault(); openIntegrations(); return; }
|
|
1547
|
+
// Agents and Routines — the panels reachable from the ≡ menu since #149 moved their
|
|
1548
|
+
// toolbar buttons there. Use the same Ctrl-or-Cmd modifier (`cmd`) as Ctrl+O/S/F/I
|
|
1549
|
+
// above, so on macOS these are Cmd+G/Cmd+R. The !e.shiftKey guard leaves the Shift+
|
|
1550
|
+
// hard-reload (Ctrl/Cmd+Shift+R) untouched. Both override a browser default, as O/S do.
|
|
1551
|
+
if (cmd && !e.shiftKey && e.key.toLowerCase() === 'g') { e.preventDefault(); openLibrary(); return; }
|
|
1552
|
+
if (cmd && !e.shiftKey && e.key.toLowerCase() === 'r') { e.preventDefault(); openRoutines(); return; }
|
|
1549
1553
|
if (cmd && (e.key === '=' || e.key === '+')) { e.preventDefault(); zoomBy(+1); return; }
|
|
1550
1554
|
if (cmd && e.key === '-') { e.preventDefault(); zoomBy(-1); return; }
|
|
1551
1555
|
if (cmd && e.key === '0') { e.preventDefault(); resetView(); autoFit = false; return; }
|
package/dist/web/aware.js
CHANGED
|
@@ -1848,8 +1848,16 @@
|
|
|
1848
1848
|
// `fields`: [{name,label,type,value,placeholder,multiline}]. Resolves with a
|
|
1849
1849
|
// { name: value } object on Save (Enter), or null on Cancel (Esc / backdrop).
|
|
1850
1850
|
const $formModal = document.getElementById('form-modal');
|
|
1851
|
+
// The styled modal is a single shared #form-modal. Opening a second one while one is
|
|
1852
|
+
// already up would clobber the first's button/keydown handlers and strand it (#148).
|
|
1853
|
+
// Refuse re-entry: the second caller resolves null (treated as "cancelled") and aborts,
|
|
1854
|
+
// and warns so the drop is observable rather than a silent no-op. The flag is set at the
|
|
1855
|
+
// END of setup (not here) so a synchronous setup error can't latch it shut for the whole
|
|
1856
|
+
// session; done() clears it on every close.
|
|
1857
|
+
let _formModalOpen = false;
|
|
1851
1858
|
function formModal({ title, sub = '', fields, okLabel = 'Save', danger = false }) {
|
|
1852
1859
|
return new Promise((resolve) => {
|
|
1860
|
+
if (_formModalOpen) { console.warn('formModal: a modal is already open — refusing re-entry'); resolve(null); return; }
|
|
1853
1861
|
const $title = document.getElementById('form-modal-title');
|
|
1854
1862
|
const $sub = document.getElementById('form-modal-sub');
|
|
1855
1863
|
const $body = document.getElementById('form-modal-body');
|
|
@@ -2017,6 +2025,7 @@
|
|
|
2017
2025
|
return out;
|
|
2018
2026
|
};
|
|
2019
2027
|
const done = (result) => {
|
|
2028
|
+
_formModalOpen = false;
|
|
2020
2029
|
hideModal($formModal);
|
|
2021
2030
|
$ok.onclick = null; $cancel.onclick = null; $formModal.onclick = null; $formModal.onkeydown = null; $body.onpaste = null;
|
|
2022
2031
|
$ok.classList.remove('danger'); $ok.classList.add('primary'); $cancel.classList.remove('primary'); // reset chrome for the next caller
|
|
@@ -2032,6 +2041,10 @@
|
|
|
2032
2041
|
if (e.key === 'Escape') { e.preventDefault(); done(null); }
|
|
2033
2042
|
else if (e.key === 'Enter' && !danger && e.target.tagName !== 'TEXTAREA') { e.preventDefault(); done(collect()); }
|
|
2034
2043
|
};
|
|
2044
|
+
// Setup succeeded and the modal is shown — mark it open now. Setting it here (not
|
|
2045
|
+
// before the synchronous setup above) means a setup throw can't latch the guard for
|
|
2046
|
+
// the whole session; done() clears it on close.
|
|
2047
|
+
_formModalOpen = true;
|
|
2035
2048
|
});
|
|
2036
2049
|
}
|
|
2037
2050
|
|
|
@@ -2689,6 +2702,11 @@
|
|
|
2689
2702
|
// installed build exists, show a footer pill → click opens the release-notes
|
|
2690
2703
|
// popover → Update now → POST /api/update/apply, which downloads + relaunches into
|
|
2691
2704
|
// the new version. Hidden in dev/npm (supported:false).
|
|
2705
|
+
// Mutual lock across the two footer update flows (#148). A floless self-update downloads
|
|
2706
|
+
// + relaunches the app; an AWARE upgrade reinstalls @aware-aeco/cli in place. Running both
|
|
2707
|
+
// at once can relaunch mid-install or strand a pill on a phantom "↑ Updating…". Only one
|
|
2708
|
+
// update may be in flight at a time — held from the confirm dialog through to completion.
|
|
2709
|
+
let _updateApplying = null; // 'app' | 'aware' | null
|
|
2692
2710
|
const $appUpdate = document.getElementById('app-update');
|
|
2693
2711
|
let _appUpdateRecoveryTimer = null; // cleared before each new apply attempt
|
|
2694
2712
|
// The apply body, extracted so the popover's "Update now" can drive it with the
|
|
@@ -2704,10 +2722,14 @@
|
|
|
2704
2722
|
showToast('Update in your terminal — ' + cmd, 'info');
|
|
2705
2723
|
return;
|
|
2706
2724
|
}
|
|
2707
|
-
// desktop (Velopack): one-click download + relaunch
|
|
2725
|
+
// desktop (Velopack): one-click download + relaunch. Refuse to start while another
|
|
2726
|
+
// update (or this one) is already in flight, so a back-to-back click can't drive two
|
|
2727
|
+
// updates against the shared confirm modal (#148).
|
|
2728
|
+
if (_updateApplying) { showToast('An update is already in progress — please wait for it to complete.', 'info'); return; }
|
|
2729
|
+
_updateApplying = 'app';
|
|
2708
2730
|
const v = version || '';
|
|
2709
2731
|
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' });
|
|
2710
|
-
if (!go) return;
|
|
2732
|
+
if (!go) { _updateApplying = null; return; }
|
|
2711
2733
|
// Record the pending version so the new build's first paint can reveal the
|
|
2712
2734
|
// what's-new panel (survives the relaunch via localStorage; cleared once shown).
|
|
2713
2735
|
try { localStorage.setItem('floless.whatsNew.pending', v); } catch { /* private mode */ }
|
|
@@ -2727,6 +2749,7 @@
|
|
|
2727
2749
|
_appUpdateRecoveryTimer = null;
|
|
2728
2750
|
if ($appUpdate && $appUpdate.disabled) {
|
|
2729
2751
|
$appUpdate.disabled = false;
|
|
2752
|
+
_updateApplying = null; // relaunch never happened — release the lock so retry works
|
|
2730
2753
|
$appUpdate.textContent = '↑ Relaunch failed — retry';
|
|
2731
2754
|
try { localStorage.removeItem('floless.whatsNew.pending'); } catch { /* private mode */ }
|
|
2732
2755
|
refreshUpdate(); // re-poll so the pill state reflects reality, same as catch path
|
|
@@ -2734,6 +2757,7 @@
|
|
|
2734
2757
|
}, 90_000);
|
|
2735
2758
|
} catch (e) {
|
|
2736
2759
|
$appUpdate.disabled = false;
|
|
2760
|
+
_updateApplying = null;
|
|
2737
2761
|
try { localStorage.removeItem('floless.whatsNew.pending'); } catch { /* private mode */ }
|
|
2738
2762
|
showToast('Update failed — ' + String((e && e.message) || e).slice(0, 80), 'warn');
|
|
2739
2763
|
refreshUpdate();
|
|
@@ -2808,9 +2832,13 @@
|
|
|
2808
2832
|
// affordance and shows a brief "installed" confirmation before the pill hides.
|
|
2809
2833
|
async function applyAwareUpdate(version) {
|
|
2810
2834
|
if (!$awareUpdate) return;
|
|
2835
|
+
// One update at a time — see _updateApplying (#148). AWARE stays open on success, so the
|
|
2836
|
+
// lock is released in every terminal path below (success, cancel, error).
|
|
2837
|
+
if (_updateApplying) { showToast('An update is already in progress — please wait for it to complete.', 'info'); return; }
|
|
2838
|
+
_updateApplying = 'aware';
|
|
2811
2839
|
const v = version || '';
|
|
2812
2840
|
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' });
|
|
2813
|
-
if (!go) return;
|
|
2841
|
+
if (!go) { _updateApplying = null; return; }
|
|
2814
2842
|
$awareUpdate.disabled = true;
|
|
2815
2843
|
$awareUpdate.textContent = '↑ Upgrading…';
|
|
2816
2844
|
try {
|
|
@@ -2820,6 +2848,7 @@
|
|
|
2820
2848
|
if (!r.ok || !d.ok) throw new Error(d.error || 'aware upgrade failed');
|
|
2821
2849
|
// Success is silent: the pill disappears and the AWARE version re-stamps live.
|
|
2822
2850
|
$awareUpdate.disabled = false;
|
|
2851
|
+
_updateApplying = null; // AWARE stays open — release the lock so the app pill can update
|
|
2823
2852
|
const wv = document.getElementById('aware-version');
|
|
2824
2853
|
if (wv && d.version) {
|
|
2825
2854
|
wv.textContent = 'AWARE ' + d.version;
|
|
@@ -2836,6 +2865,7 @@
|
|
|
2836
2865
|
setTimeout(() => { $awareUpdate.hidden = true; refreshAwareUpdate(); }, 2000);
|
|
2837
2866
|
} catch (e) {
|
|
2838
2867
|
$awareUpdate.disabled = false;
|
|
2868
|
+
_updateApplying = null;
|
|
2839
2869
|
const msg = (e && e.name === 'TimeoutError')
|
|
2840
2870
|
? 'AWARE upgrade is taking a while — it may still be installing; the version updates when it finishes'
|
|
2841
2871
|
: 'AWARE upgrade failed — ' + String((e && e.message) || e).slice(0, 80);
|
|
@@ -5253,7 +5283,8 @@
|
|
|
5253
5283
|
};
|
|
5254
5284
|
|
|
5255
5285
|
// Wiring (the modal elements are static in index.html, present when this runs).
|
|
5256
|
-
|
|
5286
|
+
// The header "Routines" button moved into the ≡ menu (#149); it's opened from there
|
|
5287
|
+
// (data-action="routines") and via Ctrl+R, so there's no toolbar button to wire here.
|
|
5257
5288
|
document.getElementById('routines-close').onclick = () => hideModal($routinesModal);
|
|
5258
5289
|
onBackdropDismiss($routinesModal, () => hideModal($routinesModal));
|
|
5259
5290
|
document.getElementById('rtn-add').onclick = () => openRoutineEdit(null);
|
|
@@ -5455,9 +5486,20 @@
|
|
|
5455
5486
|
// app.js already stored the ORIGINAL functions as .onclick/.oninput before we
|
|
5456
5487
|
// reassigned the globals; rebind via arrows so they resolve our versions at
|
|
5457
5488
|
// call-time (otherwise Save/library-search silently run the demo code).
|
|
5458
|
-
|
|
5489
|
+
// The header "⊞ Agents" button moved into the ≡ menu (#149) — openLibrary is now
|
|
5490
|
+
// driven from there (data-action="agents") and via Ctrl+G, so no toolbar button to bind.
|
|
5459
5491
|
$libSearch.oninput = () => renderLibrary();
|
|
5460
5492
|
|
|
5493
|
+
// One-time hint: the Agents/Routines buttons moved into the ≡ menu (#149). Fire once
|
|
5494
|
+
// so existing users (muscle memory) and new users both learn where they are + the
|
|
5495
|
+
// shortcuts. Gated by localStorage like the what's-new panel; private mode → skip.
|
|
5496
|
+
try {
|
|
5497
|
+
if (!localStorage.getItem('floless.hint.menuMoved')) {
|
|
5498
|
+
localStorage.setItem('floless.hint.menuMoved', '1');
|
|
5499
|
+
showToast('Agents and Routines live in the ≡ menu — press Ctrl+G or Ctrl+R to open them.', 'info');
|
|
5500
|
+
}
|
|
5501
|
+
} catch { /* private mode — skip the hint */ }
|
|
5502
|
+
|
|
5461
5503
|
// Agent Library two-tab wiring (Installed | Available). The Available tab is
|
|
5462
5504
|
// lazy-loaded on first switch (see switchLibTab).
|
|
5463
5505
|
const $libTabs = document.getElementById('lib-tabs');
|