@floless/app 0.56.0 → 0.57.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.
- package/dist/floless-server.cjs +764 -569
- package/dist/skills/floless-app-workflows/SKILL.md +8 -0
- package/dist/web/app.css +27 -0
- package/dist/web/aware.js +102 -6
- package/dist/web/index.html +9 -0
- package/package.json +1 -1
|
@@ -430,6 +430,14 @@ past a few hard facts, split it into a `references/` file and link it.
|
|
|
430
430
|
Enforced by `npm run verify:flo` in CI. See "Every node's Description must be plain English".
|
|
431
431
|
- Determinism is AWARE's: `<app>.lock` is the approved artifact; Run is gated on a fresh
|
|
432
432
|
source-hash. Never let the UI run on drift.
|
|
433
|
+
- **Never edit a MAINTAINED app's `.flo` in place.** A maintained app is FloLess-published
|
|
434
|
+
(`publisher: floless`) and not forked — it tracks our update channel and its output is our
|
|
435
|
+
responsibility. To run it across projects with different data, **Duplicate** it (stays maintained,
|
|
436
|
+
keeps updates) and change only its inputs/contract. To change the *workflow logic* (add/edit a
|
|
437
|
+
node, the 2D/3D, an export), **Fork first** (`POST /api/app/:id/fork {accept:true}` — the user
|
|
438
|
+
accepts the liability terms), which takes it off the update channel and makes it theirs; only then
|
|
439
|
+
edit the `.flo`. An out-of-band logic edit to a maintained app trips the drift gate, which forces
|
|
440
|
+
the user to Restore the official version or Customize (fork) — shipped apps stay form-only.
|
|
433
441
|
- Any change under `web/` requires a Playwright pass (visual + interaction), not just a check of
|
|
434
442
|
the request body — see the project's standing rule.
|
|
435
443
|
- When a real `aware`/bridge bug surfaces, file it on `aware-aeco/aware` (don't file your own
|
package/dist/web/app.css
CHANGED
|
@@ -1237,6 +1237,25 @@
|
|
|
1237
1237
|
.modal-field textarea { resize: vertical; min-height: 76px; line-height: 1.5; }
|
|
1238
1238
|
.modal-field input:focus,
|
|
1239
1239
|
.modal-field textarea:focus { border-color: var(--accent); }
|
|
1240
|
+
/* Consent checkbox (fork liability terms) — inline label + fully themed box (no native
|
|
1241
|
+
default leaks the dark baseline). Custom checkmark via clip-path, accent on check. */
|
|
1242
|
+
.modal-check { display: flex; gap: 9px; align-items: flex-start; cursor: pointer; font-size: 12.5px; line-height: 1.5; color: var(--text-muted); margin: 2px 0 4px; }
|
|
1243
|
+
.modal-check input[type="checkbox"] {
|
|
1244
|
+
appearance: none; -webkit-appearance: none; flex: none;
|
|
1245
|
+
width: 16px; height: 16px; margin: 1px 0 0;
|
|
1246
|
+
border: 1px solid var(--border-strong); border-radius: 4px; background: var(--surface-2);
|
|
1247
|
+
cursor: pointer; display: grid; place-content: center;
|
|
1248
|
+
transition: border-color 0.12s, background 0.12s;
|
|
1249
|
+
}
|
|
1250
|
+
.modal-check input[type="checkbox"]::before {
|
|
1251
|
+
content: ""; width: 10px; height: 10px; transform: scale(0); transition: transform 0.12s ease;
|
|
1252
|
+
background: var(--accent-bright);
|
|
1253
|
+
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
|
|
1254
|
+
}
|
|
1255
|
+
.modal-check input[type="checkbox"]:checked { border-color: var(--accent); background: var(--accent-soft); }
|
|
1256
|
+
.modal-check input[type="checkbox"]:checked::before { transform: scale(1); }
|
|
1257
|
+
.modal-check input[type="checkbox"]:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
1258
|
+
.modal-check span { user-select: none; }
|
|
1240
1259
|
.modal-categories { display: flex; gap: 5px; margin-top: 7px; flex-wrap: wrap; }
|
|
1241
1260
|
.modal-categories button {
|
|
1242
1261
|
background: var(--surface-2);
|
|
@@ -1759,6 +1778,14 @@
|
|
|
1759
1778
|
.wf-update-pill:not([hidden]) { display: inline-flex; align-items: center; gap: 4px; }
|
|
1760
1779
|
.wf-update-pill:hover { border-color: var(--accent); color: var(--accent); }
|
|
1761
1780
|
.wf-update-pill:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
1781
|
+
/* "yours" — a FORKED (user-owned, off-channel) workflow. Amber (--warn), NOT red: a forked app
|
|
1782
|
+
is an intentional customization, not a fault. Header pill (informational) + compact row badge. */
|
|
1783
|
+
.wf-forked-pill { display: none; border: 1px solid var(--warn); background: rgba(251, 191, 36, 0.08); color: var(--warn); font-size: 10px; padding: 1px 8px; border-radius: 999px; margin-left: 8px; line-height: 1.5; white-space: nowrap; cursor: default; }
|
|
1784
|
+
.wf-forked-pill:not([hidden]) { display: inline-flex; align-items: center; }
|
|
1785
|
+
.wf-fork-badge { font-size: 9px; font-weight: 600; letter-spacing: 0.06em; color: var(--warn); border: 1px solid color-mix(in srgb, var(--warn) 55%, transparent); border-radius: 999px; padding: 1px 6px; line-height: 1.4; white-space: nowrap; flex-shrink: 0; align-self: center; }
|
|
1786
|
+
/* Restore (primary, accent) — re-installs the official version. Customize uses the default outline. */
|
|
1787
|
+
#restore-btn { color: var(--accent-bright); border-color: var(--accent-dim); }
|
|
1788
|
+
#restore-btn:hover { color: var(--text); border-color: var(--accent); background: var(--accent-soft); }
|
|
1762
1789
|
/* Changelog list inside the update modal — themed scroll container (no native bar leaks). */
|
|
1763
1790
|
.wf-update-changelog { max-height: 280px; overflow-y: auto; margin-bottom: 14px; display: flex; flex-direction: column; gap: 12px; scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; }
|
|
1764
1791
|
.wf-update-changelog::-webkit-scrollbar { width: 6px; }
|
package/dist/web/aware.js
CHANGED
|
@@ -27,6 +27,9 @@
|
|
|
27
27
|
const $reportClose = document.getElementById('report-close');
|
|
28
28
|
const $reportStop = document.getElementById('report-stop');
|
|
29
29
|
const $stopRunBtn = document.getElementById('stop-run-btn');
|
|
30
|
+
const $restoreBtn = document.getElementById('restore-btn');
|
|
31
|
+
const $customizeBtn = document.getElementById('customize-btn');
|
|
32
|
+
const $wfForked = document.getElementById('wf-forked');
|
|
30
33
|
|
|
31
34
|
// Contract editor — a dedicated full-window surface for an editable typed
|
|
32
35
|
// contract (steel takeoff, …). Unlike the opaque report viewer, this is a
|
|
@@ -426,11 +429,14 @@
|
|
|
426
429
|
...app.nodes.map((n) => `**${escapeHtml(n.id)}** → \`${escapeHtml(n.agent || n.kind)}${n.command ? '/' + escapeHtml(n.command) : ''}\` · ${escapeHtml(n.mode)}`),
|
|
427
430
|
];
|
|
428
431
|
if (app.notes.length) narration.push(`ℹ ${app.notes.length} compiler note${app.notes.length === 1 ? '' : 's'} — see the strip over the canvas.`);
|
|
432
|
+
const maintainedDrift = app.publisher === 'floless' && !app.forked && app.runState === 'drift';
|
|
429
433
|
narration.push(app.runnable
|
|
430
434
|
? 'Lock is fresh (source-hash matches). **Run** is armed.'
|
|
431
|
-
:
|
|
432
|
-
? 'The
|
|
433
|
-
:
|
|
435
|
+
: maintainedDrift
|
|
436
|
+
? 'The workflow logic changed since it was last approved. **Restore** the official version, or **Customize** it to make it your own.'
|
|
437
|
+
: app.runState === 'drift'
|
|
438
|
+
? 'The `.flo` changed since the last approve — **Compile** to refresh the lock before Run.'
|
|
439
|
+
: 'No lock yet — **Compile** to freeze the approved contract.');
|
|
434
440
|
|
|
435
441
|
if (app.layout === 'dag') {
|
|
436
442
|
PROMPTS[app.id] = { name: app.name, layout: 'dag', nodes: layoutDag(app.nodes, app.connections), connections: app.connections, userText, narration };
|
|
@@ -475,6 +481,21 @@
|
|
|
475
481
|
$compileBtn.disabled = false;
|
|
476
482
|
$compileBtn.textContent = needsCompile ? '⎙ Compile *' : '⎙ Compile';
|
|
477
483
|
|
|
484
|
+
// Maintained = a FloLess-published app the user hasn't forked (so it has an official version to
|
|
485
|
+
// restore + an update channel). Customize (fork) is offered for any maintained app; Restore +
|
|
486
|
+
// suppressing Compile fire only when a maintained app's LOGIC drifted — approving that via Compile
|
|
487
|
+
// would silently accept an edit to a shipped workflow, defeating "shipped apps are form-only".
|
|
488
|
+
const maintained = app.publisher === 'floless' && !app.forked;
|
|
489
|
+
const drifted = app.runState === 'drift';
|
|
490
|
+
if ($wfForked) $wfForked.hidden = !app.forked;
|
|
491
|
+
if ($customizeBtn) $customizeBtn.hidden = !maintained;
|
|
492
|
+
if ($restoreBtn) $restoreBtn.hidden = !(maintained && drifted);
|
|
493
|
+
// Suppress Compile for a maintained, drifted app: approving an out-of-band logic edit via
|
|
494
|
+
// Compile would defeat "shipped apps are form-only". Disable as well as hide, so no programmatic
|
|
495
|
+
// click / queued event / shortcut can fire it through the hidden-but-enabled element.
|
|
496
|
+
if (maintained && drifted) { $compileBtn.hidden = true; $compileBtn.disabled = true; }
|
|
497
|
+
else $compileBtn.hidden = false;
|
|
498
|
+
|
|
478
499
|
// Bake: package the workflow as a reusable agent. Gated on the same runnable
|
|
479
500
|
// state as Run (baking a stale/uncompiled app makes no sense — the lock is
|
|
480
501
|
// what the server reads to mirror inputs). Disabled-with-tooltip (teach the
|
|
@@ -867,6 +888,7 @@
|
|
|
867
888
|
const aid = escapeAttr(a.id);
|
|
868
889
|
html += `<div class="wf-option" role="option" id="wf-opt-${aid}" data-id="${aid}" data-baked="${a.baked ? '1' : ''}" data-search="${escapeAttr((a.id + ' ' + p).toLowerCase())}" tabindex="-1" aria-selected="false">`
|
|
869
890
|
+ `<span class="wf-option-name">${escapeHtml(a.id)}</span><span class="wf-option-meta">${meta}</span>`
|
|
891
|
+
+ (a.forked ? `<span class="wf-fork-badge" data-tip="You forked this workflow — it no longer receives FloLess updates. You own its output.">yours</span>` : '')
|
|
870
892
|
+ `<span class="wf-option-actions">`
|
|
871
893
|
+ `<button type="button" class="wf-act act-edit" data-wf-act="rename" data-id="${aid}" tabindex="-1" data-tip="Rename" aria-label="Rename ${aid}">✎</button>`
|
|
872
894
|
+ `<button type="button" class="wf-act act-dup" data-wf-act="duplicate" data-id="${aid}" tabindex="-1" data-tip="Duplicate" aria-label="Duplicate ${aid}">⧉</button>`
|
|
@@ -1127,6 +1149,64 @@
|
|
|
1127
1149
|
}
|
|
1128
1150
|
}
|
|
1129
1151
|
|
|
1152
|
+
// ── Fork / Restore: the maintained ⇄ owned boundary ──────────────────────────
|
|
1153
|
+
// FORK crosses a maintained app to user-owned: it requires explicit liability acceptance (a ticked
|
|
1154
|
+
// checkbox — informed consent for structural-steel output) and takes the app off the update channel.
|
|
1155
|
+
// The .flo is NOT edited here; the user changes the workflow afterwards (via the terminal AI). RESTORE
|
|
1156
|
+
// re-installs the official version over a forked/edited app and returns it to maintained.
|
|
1157
|
+
async function forkWorkflow(id) {
|
|
1158
|
+
closeCombo(false);
|
|
1159
|
+
const res = await formModal({
|
|
1160
|
+
title: 'Customize this workflow',
|
|
1161
|
+
sub: 'Forking lets you change the workflow logic — but it takes it off the FloLess update channel. If you only need a different project’s inputs, use Duplicate (⧉) instead — duplicates stay maintained.',
|
|
1162
|
+
fields: [{
|
|
1163
|
+
type: 'checkbox',
|
|
1164
|
+
name: 'accept',
|
|
1165
|
+
label: 'I understand this is structural-steel software. I am responsible for verifying that any changes I make produce correct output before using the results on a real project.',
|
|
1166
|
+
required: true,
|
|
1167
|
+
value: false,
|
|
1168
|
+
}],
|
|
1169
|
+
okLabel: 'Fork & customize',
|
|
1170
|
+
});
|
|
1171
|
+
if (!res || res.accept !== true) return; // cancelled, or (defensive) terms not ticked
|
|
1172
|
+
showToast('Forking…', 'ok');
|
|
1173
|
+
try {
|
|
1174
|
+
await api(`/api/app/${encodeURIComponent(id)}/fork`, { method: 'POST', body: JSON.stringify({ accept: true }) });
|
|
1175
|
+
await loadApps();
|
|
1176
|
+
if (currentId === id) await loadApp(id);
|
|
1177
|
+
await refreshWorkflowUpdates(); // it just left the channel — drop any pending update pill
|
|
1178
|
+
showToast('Forked — this workflow is now yours to edit. Run ⎙ Compile when you’re ready.', 'ok');
|
|
1179
|
+
} catch (e) {
|
|
1180
|
+
console.error(e);
|
|
1181
|
+
showToast(e.message || 'Fork failed', 'warn');
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
async function restoreWorkflow(id) {
|
|
1186
|
+
closeCombo(false);
|
|
1187
|
+
const ok = await formModal({
|
|
1188
|
+
title: 'Restore the official version?',
|
|
1189
|
+
sub: 'Re-installs the official workflow and discards any changes made to its logic. Your saved inputs are kept.',
|
|
1190
|
+
fields: [],
|
|
1191
|
+
okLabel: 'Restore',
|
|
1192
|
+
});
|
|
1193
|
+
if (!ok) return;
|
|
1194
|
+
showToast('Restoring…', 'ok');
|
|
1195
|
+
try {
|
|
1196
|
+
await api(`/api/app/${encodeURIComponent(id)}/restore`, { method: 'POST', body: JSON.stringify({}) });
|
|
1197
|
+
await loadApps();
|
|
1198
|
+
if (currentId === id) await loadApp(id);
|
|
1199
|
+
await refreshWorkflowUpdates(); // back on the channel — re-offer any pending update
|
|
1200
|
+
showToast('Restored to the official version.', 'ok');
|
|
1201
|
+
} catch (e) {
|
|
1202
|
+
console.error(e);
|
|
1203
|
+
showToast(e.message || 'Restore failed', 'warn');
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
if ($customizeBtn) $customizeBtn.onclick = () => { if (currentId) forkWorkflow(currentId); };
|
|
1208
|
+
if ($restoreBtn) $restoreBtn.onclick = () => { if (currentId) restoreWorkflow(currentId); };
|
|
1209
|
+
|
|
1130
1210
|
// ── app inputs + HTML Viewer ────────────────────────────────────────────────
|
|
1131
1211
|
|
|
1132
1212
|
// The id of the report-viewer node — the terminal node a user double-clicks to
|
|
@@ -2165,6 +2245,11 @@
|
|
|
2165
2245
|
const id = `fm-${f.name}`;
|
|
2166
2246
|
const val = f.value != null ? String(f.value) : '';
|
|
2167
2247
|
const ph = escapeHtml(f.placeholder || '');
|
|
2248
|
+
// A consent checkbox renders inline (label beside the box, not above) — used for the
|
|
2249
|
+
// fork liability terms. `required: true` gates the OK button until it's ticked.
|
|
2250
|
+
if (f.type === 'checkbox') {
|
|
2251
|
+
return `<label class="modal-check" for="${id}"><input type="checkbox" id="${id}" data-fm-check="${escapeHtml(f.name)}"${f.value ? ' checked' : ''}><span>${escapeHtml(f.label || f.name)}</span></label>`;
|
|
2252
|
+
}
|
|
2168
2253
|
let ctl;
|
|
2169
2254
|
if (f.type === 'images') {
|
|
2170
2255
|
ctl = `<div class="fm-images" data-fm-images="${escapeHtml(f.name)}">
|
|
@@ -2308,9 +2393,20 @@
|
|
|
2308
2393
|
const file = fileItems[0].getAsFile();
|
|
2309
2394
|
if (entry && file) entry.setFile(file);
|
|
2310
2395
|
};
|
|
2396
|
+
// Required consent checkboxes (e.g. the fork liability terms) disable OK until ticked.
|
|
2397
|
+
const requiredChecks = Array.from($body.querySelectorAll('[data-fm-check]')).filter((el) => {
|
|
2398
|
+
const f = fields.find((x) => x.name === el.dataset.fmCheck);
|
|
2399
|
+
return f && f.required;
|
|
2400
|
+
});
|
|
2401
|
+
if (requiredChecks.length) {
|
|
2402
|
+
const syncOkGate = () => { $ok.disabled = requiredChecks.some((el) => !el.checked); };
|
|
2403
|
+
requiredChecks.forEach((el) => { el.onchange = syncOkGate; });
|
|
2404
|
+
syncOkGate();
|
|
2405
|
+
}
|
|
2311
2406
|
const collect = () => {
|
|
2312
2407
|
const out = {};
|
|
2313
2408
|
$body.querySelectorAll('[data-fm]').forEach((el) => { out[el.dataset.fm] = el.value; });
|
|
2409
|
+
$body.querySelectorAll('[data-fm-check]').forEach((el) => { out[el.dataset.fmCheck] = el.checked; });
|
|
2314
2410
|
imageStates.forEach((entry, name) => { out[name] = entry.state; });
|
|
2315
2411
|
fileStates.forEach((entry, name) => {
|
|
2316
2412
|
out[name] = entry.st.mode === 'empty' ? '' : entry.st.value;
|
|
@@ -2323,7 +2419,7 @@
|
|
|
2323
2419
|
_formModalOpen = false;
|
|
2324
2420
|
hideModal($formModal);
|
|
2325
2421
|
$ok.onclick = null; $cancel.onclick = null; $formModal.onclick = null; $formModal.onkeydown = null; $body.onpaste = null;
|
|
2326
|
-
$ok.classList.remove('danger'); $ok.classList.add('primary'); $cancel.classList.remove('primary'); // reset chrome for the next caller
|
|
2422
|
+
$ok.classList.remove('danger'); $ok.classList.add('primary'); $cancel.classList.remove('primary'); $ok.disabled = false; // reset chrome for the next caller
|
|
2327
2423
|
resolve(result);
|
|
2328
2424
|
};
|
|
2329
2425
|
$ok.onclick = () => done(collect());
|
|
@@ -2334,7 +2430,7 @@
|
|
|
2334
2430
|
// commits only for a normal form (never a danger confirm; never inside a textarea).
|
|
2335
2431
|
$formModal.onkeydown = (e) => {
|
|
2336
2432
|
if (e.key === 'Escape') { e.preventDefault(); done(null); }
|
|
2337
|
-
else if (e.key === 'Enter' && !danger && e.target.tagName !== 'TEXTAREA') { e.preventDefault(); done(collect()); }
|
|
2433
|
+
else if (e.key === 'Enter' && !danger && e.target.tagName !== 'TEXTAREA' && !$ok.disabled) { e.preventDefault(); done(collect()); }
|
|
2338
2434
|
};
|
|
2339
2435
|
// Setup succeeded and the modal is shown — mark it open now. Setting it here (not
|
|
2340
2436
|
// before the synchronous setup above) means a setup throw can't latch the guard for
|
|
@@ -2441,7 +2537,7 @@
|
|
|
2441
2537
|
$promptSel.onchange = () => { loadApp($promptSel.value).catch(reportErr); };
|
|
2442
2538
|
|
|
2443
2539
|
$compileBtn.onclick = async () => {
|
|
2444
|
-
if (!currentId || $compileBtn.disabled) return;
|
|
2540
|
+
if (!currentId || $compileBtn.disabled || $compileBtn.hidden) return; // hidden = suppressed for a maintained drift
|
|
2445
2541
|
const id = currentId;
|
|
2446
2542
|
$compileBtn.disabled = true;
|
|
2447
2543
|
const prev = $compileBtn.textContent;
|
package/dist/web/index.html
CHANGED
|
@@ -55,6 +55,9 @@
|
|
|
55
55
|
</div>
|
|
56
56
|
<select id="prompt-select" hidden></select>
|
|
57
57
|
<button id="wf-update" class="wf-update-pill" type="button" hidden aria-haspopup="dialog"></button>
|
|
58
|
+
<!-- Shown only when the OPEN workflow is forked (user-owned, off the update channel). Purely
|
|
59
|
+
informational (the badge of ownership); the picker row carries the same "yours" cue. -->
|
|
60
|
+
<span id="wf-forked" class="wf-forked-pill" hidden role="status" data-tip="You forked this workflow — it no longer receives FloLess updates. You are responsible for verifying its output.">yours · off updates</span>
|
|
58
61
|
<!-- Agents (⊞) and Routines (⏱) buttons moved into the ≡ menu in #149
|
|
59
62
|
(Ctrl+G / Ctrl+R); the toolbar keeps only the run-critical controls. -->
|
|
60
63
|
<span class="ctl-sep" aria-hidden="true"></span>
|
|
@@ -66,6 +69,12 @@
|
|
|
66
69
|
<span class="guide-beacon-label">Start here</span>
|
|
67
70
|
</button>
|
|
68
71
|
<span class="run-state" id="run-state" role="status" aria-live="polite"></span>
|
|
72
|
+
<!-- Restore + Customize: shown for a MAINTAINED (publisher:floless, non-forked) app. Restore
|
|
73
|
+
appears only when its logic drifted (re-installs the official version); Customize (fork) is
|
|
74
|
+
always available for a maintained app. When shown on drift they replace Compile, so a
|
|
75
|
+
shipped app's logic edit can't be silently approved (form-only). -->
|
|
76
|
+
<button id="restore-btn" type="button" hidden data-tip="Restore the official workflow — undoes any out-of-band edits and re-arms Run">↺ Restore</button>
|
|
77
|
+
<button id="customize-btn" type="button" hidden data-tip="Fork this workflow to edit it yourself — takes it off the FloLess update channel">Customize…</button>
|
|
69
78
|
<button id="compile-btn" data-tip="Compile + approve → freeze the .lock">⎙ Compile</button>
|
|
70
79
|
<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>
|
|
71
80
|
<button id="run-btn" data-tip="Run the workflow for real against the live host (uses the inputs; renders the report node in the HTML Viewer)">▶ Run workflow</button>
|