@floless/app 0.23.2 → 0.24.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 +671 -544
- package/dist/skills/floless-app-routines/SKILL.md +3 -1
- package/dist/skills/floless-app-steel-from-drawings/SKILL.md +5 -3
- package/dist/skills/floless-app-ui/SKILL.md +4 -4
- package/dist/web/app.css +6 -0
- package/dist/web/aware.js +12 -2
- package/dist/web/index.html +18 -2
- package/dist/web/panels.js +56 -12
- package/package.json +1 -1
|
@@ -97,10 +97,12 @@ curl -s http://127.0.0.1:4317/api/health
|
|
|
97
97
|
## Schedule presets (structured, NOT cron)
|
|
98
98
|
|
|
99
99
|
Pick exactly one shape. Times are **local machine time**, `HH:MM` 24-hour. The finest granularity
|
|
100
|
-
is **
|
|
100
|
+
is **every minute** (`minutes`), for near-real-time monitoring (e.g. polling a log folder); the
|
|
101
|
+
1-minute floor + the serialized run queue keep a routine from flooding.
|
|
101
102
|
|
|
102
103
|
| User intent | ScheduleSpec |
|
|
103
104
|
|---|---|
|
|
105
|
+
| "every 5 minutes" / "every 15 min" | `{"kind":"minutes","everyMinutes":5}` (must divide 60 and stay sub-hourly: 1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30) |
|
|
104
106
|
| "every 2 hours" | `{"kind":"hourly","everyHours":2}` (must divide 24: 1, 2, 3, 4, 6, 8, 12, 24) |
|
|
105
107
|
| "every day at 7am" | `{"kind":"daily","time":"07:00"}` |
|
|
106
108
|
| "every weekday at 7am" | `{"kind":"weekdays","time":"07:00"}` (Mon–Fri) |
|
|
@@ -48,13 +48,15 @@ in 3D (`steel-from-drawings`) → bake it out (an IFC file and/or native Tekla p
|
|
|
48
48
|
to `demos/steel-from-drawings/` if you keep the editable copy in sync.
|
|
49
49
|
6. **Bake the SAME scene into `steel-to-ifc` (the IFC bake-out), if it's installed.** Check
|
|
50
50
|
`~/.aware/apps/steel-to-ifc/`. If present, replace the `scene:` mapping under that app's `bake-ifc`
|
|
51
|
-
node `config
|
|
52
|
-
|
|
51
|
+
node `config` (the `ifc` agent's `scene` input — NOT `config.args`; the app now calls the host-free
|
|
52
|
+
`ifc` AWARE agent, not an exec) with the IDENTICAL scene (the installed copy and `demos/steel-to-ifc/`
|
|
53
|
+
if present), then `aware app compile ~/.aware/apps/steel-to-ifc`. This keeps the exported IFC matching
|
|
53
54
|
the 3D the user confirmed. (`steel-to-ifc` writes a real IFC file — IfcColumn/IfcBeam on the grid —
|
|
54
55
|
openable in Tekla, SDS2, Revit or Navisworks; it never touches a Tekla model, so no Tekla needed.)
|
|
55
56
|
7. **Bake the SAME scene into `steel-to-tekla` (native Tekla parts), if it's installed.** Check
|
|
56
57
|
`~/.aware/apps/steel-to-tekla/`. If present, replace the `scene:` under that app's `bake-tekla` node
|
|
57
|
-
`config
|
|
58
|
+
`config` (the `tekla` `bake-scene` verb's `scene` input — NOT `config.args`; the app now calls the
|
|
59
|
+
first-class `bake-scene` verb, not an exec) with the IDENTICAL scene (installed + `demos/steel-to-tekla/`), set `config.version`
|
|
58
60
|
to the user's RUNNING Tekla version (e.g. "2026.0" — the Open API connects to a version-specific
|
|
59
61
|
channel, so a wrong version silently finds no model), then `aware app compile
|
|
60
62
|
~/.aware/apps/steel-to-tekla`. It creates a real Tekla member per element via the Open API (mode:
|
|
@@ -123,9 +123,9 @@ app that hasn't run yet is fine.
|
|
|
123
123
|
|
|
124
124
|
- **Never write raw HTML/markup into descriptor strings** — it renders as literal text
|
|
125
125
|
by design. Describe; don't mark up.
|
|
126
|
-
- Every write is
|
|
127
|
-
|
|
128
|
-
never destructive: don't delete panels the user
|
|
129
|
-
editing the existing document over regenerating it.
|
|
126
|
+
- Every write is recorded in the app's undo timeline (`~/.floless/ui/timeline.json`,
|
|
127
|
+
newest 20 states), and the user has multi-level **Undo / Redo** and **Reset to default**
|
|
128
|
+
in the app — so be bold composing, but never destructive: don't delete panels the user
|
|
129
|
+
didn't ask to remove, and prefer editing the existing document over regenerating it.
|
|
130
130
|
- The UI renders; AWARE runs; you compose. Don't put computed values in `stat` blocks
|
|
131
131
|
that a `source`-bound block could keep live — a stale hand-written number misleads.
|
package/dist/web/app.css
CHANGED
|
@@ -2813,8 +2813,14 @@ body {
|
|
|
2813
2813
|
display: flex; justify-content: space-between; gap: 10px;
|
|
2814
2814
|
padding: 4px 8px; border-radius: 4px; font-size: 12px; color: var(--text-dim);
|
|
2815
2815
|
}
|
|
2816
|
+
.ext-history-item.is-current .ext-history-when { color: var(--accent); }
|
|
2816
2817
|
.ext-history-when { color: var(--text); }
|
|
2817
2818
|
.ext-history-count { font-family: var(--mono); font-size: 11px; color: var(--text-muted); }
|
|
2819
|
+
/* "you are here" marker on the cursor's history entry (multi-level undo/redo, #102). */
|
|
2820
|
+
.ext-history-current {
|
|
2821
|
+
font-size: 9px; text-transform: uppercase; letter-spacing: 0.08em;
|
|
2822
|
+
color: var(--accent); margin-right: 6px; vertical-align: 1px;
|
|
2823
|
+
}
|
|
2818
2824
|
.ext-history-empty { padding: 6px 8px 8px; font-size: 12px; color: var(--text-muted); }
|
|
2819
2825
|
/* Reset is destructive-looking (though recoverable) — danger on hover, never accent. */
|
|
2820
2826
|
#ext-reset-confirm:hover { color: var(--err); border-color: var(--err); background: color-mix(in srgb, var(--err) 10%, transparent); }
|
package/dist/web/aware.js
CHANGED
|
@@ -2778,7 +2778,12 @@
|
|
|
2778
2778
|
if (cancelRequested) return;
|
|
2779
2779
|
if (ev.kind === 'run-start') clearNodeStatus();
|
|
2780
2780
|
else if (ev.node && ev.kind === 'node-start') setNodeStatus(ev.node, 'running');
|
|
2781
|
-
|
|
2781
|
+
// `would-write` is the simulate-mode counterpart of `write`: a write-mode node,
|
|
2782
|
+
// stubbed during Simulate, emits would-write (with proposed_inputs) instead of
|
|
2783
|
+
// actually writing. Treat it as a completed node so it leaves "running" like its
|
|
2784
|
+
// read-mode peers' node-output does — otherwise write nodes hang on "running"
|
|
2785
|
+
// after a Simulate completes (#104).
|
|
2786
|
+
else if (ev.node && /^(node-output|output|write|would-write|node-end|node-ok)$/.test(ev.kind)) setNodeStatus(ev.node, 'done');
|
|
2782
2787
|
else if (ev.node && /error/i.test(ev.kind)) setNodeStatus(ev.node, 'error');
|
|
2783
2788
|
else if (ev.kind === 'run-end' && ev.status && ev.status !== 'ok') {
|
|
2784
2789
|
// run failed — any node still "running" (started, never produced output) failed
|
|
@@ -3902,6 +3907,7 @@
|
|
|
3902
3907
|
|
|
3903
3908
|
function scheduleText(s) {
|
|
3904
3909
|
if (!s || !s.kind) return '—';
|
|
3910
|
+
if (s.kind === 'minutes') return s.everyMinutes === 1 ? 'Every minute' : `Every ${s.everyMinutes} minutes`;
|
|
3905
3911
|
if (s.kind === 'hourly') return s.everyHours === 1 ? 'Every hour' : `Every ${s.everyHours} hours`;
|
|
3906
3912
|
if (s.kind === 'daily') return `Daily · ${s.time}`;
|
|
3907
3913
|
if (s.kind === 'weekdays') return `Weekdays · ${s.time}`;
|
|
@@ -4481,6 +4487,7 @@
|
|
|
4481
4487
|
const sched = (r && r.schedule) ? r.schedule : { kind: 'daily', time: '07:00' };
|
|
4482
4488
|
fillTimeSelects();
|
|
4483
4489
|
document.getElementById('rtn-kind').value = sched.kind;
|
|
4490
|
+
document.getElementById('rtn-everyminutes').value = sched.kind === 'minutes' ? String(sched.everyMinutes) : '5';
|
|
4484
4491
|
document.getElementById('rtn-everyhours').value = sched.kind === 'hourly' ? String(sched.everyHours) : '2';
|
|
4485
4492
|
setTimeSelects(sched.time || '07:00');
|
|
4486
4493
|
document.getElementById('rtn-cron').value = sched.kind === 'cron' ? sched.expr : '';
|
|
@@ -4503,7 +4510,10 @@
|
|
|
4503
4510
|
let schedule;
|
|
4504
4511
|
if (!asTrigger) {
|
|
4505
4512
|
const kind = document.getElementById('rtn-kind').value;
|
|
4506
|
-
if (kind === '
|
|
4513
|
+
if (kind === 'minutes') {
|
|
4514
|
+
const n = Math.max(1, Math.min(30, parseInt(document.getElementById('rtn-everyminutes').value, 10) || 5));
|
|
4515
|
+
schedule = { kind: 'minutes', everyMinutes: n };
|
|
4516
|
+
} else if (kind === 'hourly') {
|
|
4507
4517
|
const n = Math.max(1, Math.min(24, parseInt(document.getElementById('rtn-everyhours').value, 10) || 1));
|
|
4508
4518
|
schedule = { kind: 'hourly', everyHours: n };
|
|
4509
4519
|
} else if (kind === 'weekly') {
|
package/dist/web/index.html
CHANGED
|
@@ -216,14 +216,18 @@
|
|
|
216
216
|
<div class="menu ext-menu" id="ext-menu" role="menu" hidden>
|
|
217
217
|
<button class="menu-item" id="ext-undo" role="menuitem">
|
|
218
218
|
<span class="menu-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11"/></svg></span>
|
|
219
|
-
<span class="menu-label">Undo
|
|
219
|
+
<span class="menu-label">Undo</span>
|
|
220
|
+
</button>
|
|
221
|
+
<button class="menu-item" id="ext-redo" role="menuitem">
|
|
222
|
+
<span class="menu-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M15 14l5-5-5-5"/><path d="M20 9H9.5a5.5 5.5 0 0 0 0 11H13"/></svg></span>
|
|
223
|
+
<span class="menu-label">Redo</span>
|
|
220
224
|
</button>
|
|
221
225
|
<button class="menu-item" id="ext-reset" role="menuitem">
|
|
222
226
|
<span class="menu-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg></span>
|
|
223
227
|
<span class="menu-label">Reset to default</span>
|
|
224
228
|
</button>
|
|
225
229
|
<div class="menu-divider"></div>
|
|
226
|
-
<div class="ext-history-head">History
|
|
230
|
+
<div class="ext-history-head">History — newest first</div>
|
|
227
231
|
<div class="ext-history" id="ext-history"></div>
|
|
228
232
|
</div>
|
|
229
233
|
|
|
@@ -491,11 +495,23 @@
|
|
|
491
495
|
<option value="daily">Every day</option>
|
|
492
496
|
<option value="weekdays">Every weekday (Mon–Fri)</option>
|
|
493
497
|
<option value="weekly">Specific days</option>
|
|
498
|
+
<option value="minutes">Every N minutes</option>
|
|
494
499
|
<option value="hourly">Every N hours</option>
|
|
495
500
|
<option value="cron">Custom (cron)</option>
|
|
496
501
|
</select>
|
|
497
502
|
</div>
|
|
498
503
|
<div id="rtn-sched-detail">
|
|
504
|
+
<div class="modal-field" data-sched="minutes" hidden>
|
|
505
|
+
<label for="rtn-everyminutes">Run every</label>
|
|
506
|
+
<select id="rtn-everyminutes" class="rtn-kind-select" aria-describedby="rtn-minutes-hint">
|
|
507
|
+
<option value="5" selected>Every 5 minutes</option>
|
|
508
|
+
<option value="10">Every 10 minutes</option>
|
|
509
|
+
<option value="15">Every 15 minutes</option>
|
|
510
|
+
<option value="30">Every 30 minutes</option>
|
|
511
|
+
<option value="1">Every minute</option>
|
|
512
|
+
</select>
|
|
513
|
+
<div class="rtn-cron-hint" id="rtn-minutes-hint">Clock-aligned to the top of the hour (e.g. :00, :15, :30, :45) · actual precision is the ~1-minute scheduler tick.</div>
|
|
514
|
+
</div>
|
|
499
515
|
<div class="modal-field" data-sched="hourly" hidden>
|
|
500
516
|
<label for="rtn-everyhours">Run every</label>
|
|
501
517
|
<select id="rtn-everyhours" class="rtn-kind-select">
|
package/dist/web/panels.js
CHANGED
|
@@ -32,6 +32,8 @@
|
|
|
32
32
|
const $dashDot = document.getElementById('dash-dot');
|
|
33
33
|
const $extBadge = document.getElementById('ext-badge');
|
|
34
34
|
const $extMenu = document.getElementById('ext-menu');
|
|
35
|
+
const $extUndo = document.getElementById('ext-undo');
|
|
36
|
+
const $extRedo = document.getElementById('ext-redo');
|
|
35
37
|
const $extHistory = document.getElementById('ext-history');
|
|
36
38
|
const $centerName = document.getElementById('center-panel-name');
|
|
37
39
|
const $centerRole = document.getElementById('center-panel-role');
|
|
@@ -499,42 +501,82 @@
|
|
|
499
501
|
if (h < 48) return `${h} h ago`;
|
|
500
502
|
return `${Math.round(h / 24)} d ago`;
|
|
501
503
|
}
|
|
504
|
+
// Reflect undo/redo availability on the menu buttons (disabled, not hidden — they're
|
|
505
|
+
// persistent navigation controls; the native :disabled state communicates "you're at
|
|
506
|
+
// the end of the stack"). Called on menu open and after every undo/redo/reset.
|
|
507
|
+
function setUndoRedoEnabled(canUndo, canRedo) {
|
|
508
|
+
if ($extUndo) $extUndo.disabled = !canUndo;
|
|
509
|
+
if ($extRedo) $extRedo.disabled = !canRedo;
|
|
510
|
+
}
|
|
502
511
|
async function renderHistoryList() {
|
|
503
512
|
if (!$extHistory) return;
|
|
504
513
|
$extHistory.innerHTML = '<div class="ext-history-empty">loading…</div>';
|
|
505
514
|
try {
|
|
506
|
-
const { history } = await api('/api/extensions/history');
|
|
515
|
+
const { history, canUndo, canRedo } = await api('/api/extensions/history');
|
|
516
|
+
setUndoRedoEnabled(canUndo, canRedo);
|
|
507
517
|
if (!history || !history.length) {
|
|
508
518
|
$extHistory.innerHTML = '<div class="ext-history-empty">No snapshots yet — every change lands here automatically.</div>';
|
|
509
519
|
return;
|
|
510
520
|
}
|
|
511
521
|
$extHistory.innerHTML = history.slice(0, 8).map((h) => {
|
|
512
522
|
const when = relAgo(h.timestamp) || (h.timestamp ? new Date(h.timestamp).toLocaleString() : '—');
|
|
513
|
-
|
|
514
|
-
|
|
523
|
+
const now = h.current ? '<span class="ext-history-current">now</span>' : '';
|
|
524
|
+
return `<div class="ext-history-item${h.current ? ' is-current' : ''}"><span class="ext-history-when" data-tip="${escapeAttr(h.timestamp)}">${escapeHtml(when)}</span>` +
|
|
525
|
+
`<span class="ext-history-count">${now}${Number(h.panelsCount) || 0} panel${h.panelsCount === 1 ? '' : 's'}</span></div>`;
|
|
515
526
|
}).join('');
|
|
516
527
|
} catch {
|
|
517
528
|
$extHistory.innerHTML = '<div class="ext-history-empty">history unavailable</div>';
|
|
518
529
|
}
|
|
519
530
|
}
|
|
520
531
|
|
|
521
|
-
// The one undo body — shared by the Customized menu's "Undo
|
|
522
|
-
//
|
|
532
|
+
// The one undo body — shared by the Customized menu's "Undo" and the empty state's
|
|
533
|
+
// "Restore it". Walks one state back per call (multi-level). Returns whether it moved.
|
|
523
534
|
async function undoNow() {
|
|
524
535
|
try {
|
|
525
|
-
await api('/api/extensions/undo', { method: 'POST' });
|
|
536
|
+
const res = await api('/api/extensions/undo', { method: 'POST' });
|
|
537
|
+
setUndoRedoEnabled(res.canUndo, res.canRedo);
|
|
526
538
|
showToast('Restored the previous layout', 'ok');
|
|
527
539
|
fetchExtensions();
|
|
528
540
|
return true;
|
|
529
541
|
} catch (err) {
|
|
530
|
-
const
|
|
531
|
-
if (
|
|
542
|
+
const b = err && err.body;
|
|
543
|
+
if (b) setUndoRedoEnabled(b.canUndo, b.canRedo);
|
|
544
|
+
if (b && b.error === 'nothing to undo') showToast('Nothing to undo', 'info');
|
|
532
545
|
else showToast('Undo failed: ' + (err && err.message ? err.message : err), 'warn');
|
|
533
546
|
return false;
|
|
534
547
|
}
|
|
535
548
|
}
|
|
536
|
-
|
|
549
|
+
// Redo — walks one state forward through the redo branch (a new edit truncates it).
|
|
550
|
+
async function redoNow() {
|
|
551
|
+
try {
|
|
552
|
+
const res = await api('/api/extensions/redo', { method: 'POST' });
|
|
553
|
+
setUndoRedoEnabled(res.canUndo, res.canRedo);
|
|
554
|
+
showToast('Restored the next layout', 'ok');
|
|
555
|
+
fetchExtensions();
|
|
556
|
+
return true;
|
|
557
|
+
} catch (err) {
|
|
558
|
+
const b = err && err.body;
|
|
559
|
+
if (b) setUndoRedoEnabled(b.canUndo, b.canRedo);
|
|
560
|
+
if (b && b.error === 'nothing to redo') showToast('Nothing to redo', 'info');
|
|
561
|
+
else showToast('Redo failed: ' + (err && err.message ? err.message : err), 'warn');
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
537
565
|
if ($extUndo) $extUndo.onclick = () => { closeExtMenu(); undoNow(); };
|
|
566
|
+
if ($extRedo) $extRedo.onclick = () => { closeExtMenu(); redoNow(); };
|
|
567
|
+
|
|
568
|
+
// Ctrl/Cmd+Z = undo, Ctrl/Cmd+Y or Ctrl/Cmd+Shift+Z = redo — multi-level, matching the
|
|
569
|
+
// menu. Only while the Dashboard view is active and focus isn't in a text field, so it
|
|
570
|
+
// never steals a real input's (or the terminal's) own undo.
|
|
571
|
+
document.addEventListener('keydown', (e) => {
|
|
572
|
+
if (view !== 'dashboard') return;
|
|
573
|
+
if (!(e.ctrlKey || e.metaKey) || e.altKey) return;
|
|
574
|
+
const t = e.target;
|
|
575
|
+
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return;
|
|
576
|
+
const k = e.key.toLowerCase();
|
|
577
|
+
if (k === 'z' && !e.shiftKey) { e.preventDefault(); undoNow(); }
|
|
578
|
+
else if (k === 'y' || (k === 'z' && e.shiftKey)) { e.preventDefault(); redoNow(); }
|
|
579
|
+
});
|
|
538
580
|
|
|
539
581
|
// Reset is recoverable (archived to history first) but still asks — mirrors the
|
|
540
582
|
// routine-delete confirm: Cancel holds focus; Esc/backdrop cancel.
|
|
@@ -562,12 +604,14 @@
|
|
|
562
604
|
closeExtMenu();
|
|
563
605
|
if (!(await confirmReset())) return;
|
|
564
606
|
try {
|
|
565
|
-
await api('/api/extensions/reset', { method: 'POST' });
|
|
607
|
+
const res = await api('/api/extensions/reset', { method: 'POST' });
|
|
608
|
+
setUndoRedoEnabled(res.canUndo, res.canRedo); // symmetric with undo/redo (Undo recovers it)
|
|
566
609
|
showToast('Dashboard reset — your panels were saved to history', 'ok');
|
|
567
610
|
fetchExtensions();
|
|
568
611
|
} catch (err) {
|
|
569
|
-
const
|
|
570
|
-
if (
|
|
612
|
+
const b = err && err.body;
|
|
613
|
+
if (b) setUndoRedoEnabled(b.canUndo, b.canRedo);
|
|
614
|
+
if (b && b.error && /nothing to reset/i.test(b.error)) showToast('Already the default dashboard', 'info');
|
|
571
615
|
else showToast('Reset failed: ' + (err && err.message ? err.message : err), 'warn');
|
|
572
616
|
}
|
|
573
617
|
};
|