@floless/app 0.34.0 → 0.34.2
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
CHANGED
|
@@ -52454,6 +52454,24 @@ function isTrustedFlolessHost(url) {
|
|
|
52454
52454
|
var OFFLINE_GRACE_MS = 24 * 60 * 60 * 1e3;
|
|
52455
52455
|
var MEM_CACHE_MS = 60 * 1e3;
|
|
52456
52456
|
var REFRESH_SKEW_MS = 60 * 1e3;
|
|
52457
|
+
function normalizeSeatReason(raw) {
|
|
52458
|
+
switch ((raw ?? "").toLowerCase().replace(/-/g, "_")) {
|
|
52459
|
+
case "self_takeover":
|
|
52460
|
+
case "takeover":
|
|
52461
|
+
return "takeover";
|
|
52462
|
+
case "inactivity":
|
|
52463
|
+
case "inactive":
|
|
52464
|
+
case "timeout":
|
|
52465
|
+
return "inactivity";
|
|
52466
|
+
case "admin_revoke":
|
|
52467
|
+
case "admin_revoked":
|
|
52468
|
+
case "revoked":
|
|
52469
|
+
case "revoke":
|
|
52470
|
+
return "revoked";
|
|
52471
|
+
default:
|
|
52472
|
+
return "unknown";
|
|
52473
|
+
}
|
|
52474
|
+
}
|
|
52457
52475
|
function isSea() {
|
|
52458
52476
|
try {
|
|
52459
52477
|
return (0, import_node_module2.createRequire)(__import_meta_url)("node:sea").isSea();
|
|
@@ -52584,11 +52602,12 @@ async function accessToken() {
|
|
|
52584
52602
|
}
|
|
52585
52603
|
var mem = null;
|
|
52586
52604
|
var seatLost = false;
|
|
52605
|
+
var seatLostReason;
|
|
52587
52606
|
async function getLicenseStatus(opts = {}) {
|
|
52588
52607
|
const url = signInUrl();
|
|
52589
52608
|
if (seatLost) {
|
|
52590
52609
|
mem = null;
|
|
52591
|
-
return { state: "unlicensed", reason: "seat-taken", signInUrl: url };
|
|
52610
|
+
return { state: "unlicensed", reason: "seat-taken", seatReason: seatLostReason, signInUrl: url };
|
|
52592
52611
|
}
|
|
52593
52612
|
if (!opts.force && mem && Date.now() - mem.at < MEM_CACHE_MS) return mem.status;
|
|
52594
52613
|
const finish = (s) => {
|
|
@@ -52671,7 +52690,7 @@ async function seatHeartbeat() {
|
|
|
52671
52690
|
return;
|
|
52672
52691
|
}
|
|
52673
52692
|
if (res.status === 401) {
|
|
52674
|
-
let reason = "
|
|
52693
|
+
let reason = "unknown";
|
|
52675
52694
|
try {
|
|
52676
52695
|
const b = await res.json();
|
|
52677
52696
|
reason = b.details?.reason ?? b.reason ?? reason;
|
|
@@ -52701,16 +52720,18 @@ async function seatHeartbeat() {
|
|
|
52701
52720
|
}
|
|
52702
52721
|
function loseSeat(reason) {
|
|
52703
52722
|
seatLost = true;
|
|
52723
|
+
seatLostReason = normalizeSeatReason(reason);
|
|
52704
52724
|
seat = null;
|
|
52705
52725
|
mem = null;
|
|
52706
52726
|
if (hbTimer) {
|
|
52707
52727
|
clearInterval(hbTimer);
|
|
52708
52728
|
hbTimer = null;
|
|
52709
52729
|
}
|
|
52710
|
-
onSeatLostCb?.(
|
|
52730
|
+
onSeatLostCb?.(seatLostReason);
|
|
52711
52731
|
}
|
|
52712
52732
|
function resetSeat() {
|
|
52713
52733
|
seatLost = false;
|
|
52734
|
+
seatLostReason = void 0;
|
|
52714
52735
|
seat = null;
|
|
52715
52736
|
if (hbTimer) {
|
|
52716
52737
|
clearInterval(hbTimer);
|
|
@@ -52835,7 +52856,7 @@ function appVersion() {
|
|
|
52835
52856
|
return resolveVersion({
|
|
52836
52857
|
isSea: isSea2(),
|
|
52837
52858
|
sqVersionXml: readSqVersionXml(),
|
|
52838
|
-
define: true ? "0.34.
|
|
52859
|
+
define: true ? "0.34.2" : void 0,
|
|
52839
52860
|
pkgVersion: readPkgVersion()
|
|
52840
52861
|
});
|
|
52841
52862
|
}
|
|
@@ -52845,7 +52866,7 @@ function resolveChannel(s) {
|
|
|
52845
52866
|
return "dev";
|
|
52846
52867
|
}
|
|
52847
52868
|
function appChannel() {
|
|
52848
|
-
return resolveChannel({ isSea: isSea2(), define: true ? "0.34.
|
|
52869
|
+
return resolveChannel({ isSea: isSea2(), define: true ? "0.34.2" : void 0 });
|
|
52849
52870
|
}
|
|
52850
52871
|
|
|
52851
52872
|
// oauth-presets.ts
|
|
@@ -56243,6 +56264,8 @@ var PRODUCT_SKILLS = [
|
|
|
56243
56264
|
// drive the floless.app CLI / desktop bridge from the user's AI
|
|
56244
56265
|
"floless-app-onboarding",
|
|
56245
56266
|
// guided, re-runnable tour of AWARE + floless.app for new users
|
|
56267
|
+
"floless-app-queue",
|
|
56268
|
+
// list the queued Dashboard requests in plain English + let the user pick which to process
|
|
56246
56269
|
"floless-app-rebake",
|
|
56247
56270
|
// re-read & re-bake a baked Visual Input (B3) after the user swaps the drawing
|
|
56248
56271
|
"floless-app-report-issue",
|
|
@@ -58425,7 +58448,10 @@ async function startServer() {
|
|
|
58425
58448
|
}
|
|
58426
58449
|
return { ok: true, bootstrap: getBootstrapState().status };
|
|
58427
58450
|
});
|
|
58428
|
-
onSeatLost((reason) =>
|
|
58451
|
+
onSeatLost((reason) => {
|
|
58452
|
+
app.log.warn({ reason }, "license seat lost \u2014 gating this session until re-login");
|
|
58453
|
+
broadcast({ type: "seat-taken", reason });
|
|
58454
|
+
});
|
|
58429
58455
|
app.get("/api/apps", async () => {
|
|
58430
58456
|
const apps = await aware.list();
|
|
58431
58457
|
return { ok: true, apps: apps.map((a) => ({ ...a, provider: appProvider(a.id), baked: appBaked(a.id) })) };
|
|
@@ -78,7 +78,10 @@ digraph { "GET /api/requests" -> "for each pending" -> "read app .flo + node" ->
|
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
1. **Pull**: `curl -s http://127.0.0.1:4317/api/requests` → take the `pending` ones (oldest first).
|
|
81
|
-
Summarize them for the user before acting on anything destructive.
|
|
81
|
+
Summarize them for the user before acting on anything destructive. **When several are queued and
|
|
82
|
+
the user wants to choose which to apply (not just the oldest), use the `floless-app-queue` skill** —
|
|
83
|
+
it lists the queue in plain English and asks which to process, then routes each pick to the owning
|
|
84
|
+
authoring skill (`floless-app-workflows` / `-tweak-contract` / `-ui` / `-rebake`).
|
|
82
85
|
2. **Locate the app source.** The editable `.flo` is the repo copy `demos/<appId>/<appId>.flo`
|
|
83
86
|
(preferred — it's version-controlled); the installed copy is `~/.aware/apps/<appId>/<appId>.flo`.
|
|
84
87
|
Edit the **repo** copy. `GET /api/app/<appId>` returns the parsed nodes + each node's
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: floless-app-queue
|
|
3
|
+
description: This skill should be used when the user has MULTIPLE floless.app requests queued from the Dashboard and wants to see them and choose which to process, rather than the AI just picking up the oldest one. Use it when the user says things like "what's in my floless queue", "list my queued floless requests", "show me the pending requests and let me pick", "I queued a few tweaks — which should I apply?", "let me choose what to process", or "pick from the floless queue". It lists the pending requests from GET /api/requests in plain language (type, which app/node, the instruction), asks the user which one(s) to apply, then hands each chosen request to the owning skill (floless-app-workflows / -tweak-contract / -ui / -rebake) which applies it and clears it. It is the interactive picker that sits in front of floless-app-bridge (which is the actual pull-apply-clear engine).
|
|
4
|
+
metadata:
|
|
5
|
+
version: 0.1.0
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# floless.app queue picker
|
|
9
|
+
|
|
10
|
+
floless.app is the **thin web UI**; the **terminal AI (you) is the brain**. The UI cannot edit
|
|
11
|
+
`.flo` files — it **queues requests** (Tweak / use-template / customize-UI / rebake / edit-contract)
|
|
12
|
+
to a local API. The **`floless-app-bridge`** skill pulls those and applies them, but it processes
|
|
13
|
+
the queue **oldest-first with no choice step**. This skill adds the missing affordance: **list the
|
|
14
|
+
whole queue in plain English and let the user pick which one(s) to process** — then route each pick
|
|
15
|
+
through the normal bridge flow.
|
|
16
|
+
|
|
17
|
+
Use this when there are **several** pending requests and the user wants to triage. For a single
|
|
18
|
+
"apply the tweak I just queued" or a pasted `[floless-request …]` marker, `floless-app-bridge` is
|
|
19
|
+
enough.
|
|
20
|
+
|
|
21
|
+
## 1. Fetch the queue
|
|
22
|
+
|
|
23
|
+
The server runs locally on **port 4317** (override: `$PORT`). Confirm it's up, then read the queue:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
curl -s http://127.0.0.1:4317/api/health # → {"ok":true,...}; if down, user runs `cd server && npm run dev`
|
|
27
|
+
curl -s http://127.0.0.1:4317/api/requests # → { "ok": true, "requests": [ ... ] }
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`requests` is every **pending** request, oldest first. If it's empty, tell the user the queue is
|
|
31
|
+
empty and stop — there is nothing to pick.
|
|
32
|
+
|
|
33
|
+
## 2. Present the queue in plain language
|
|
34
|
+
|
|
35
|
+
Render a **numbered list** the user can choose from. For each request show the index, a short id
|
|
36
|
+
(first 8 chars), and a one-line plain-English summary by `type` — never raw JSON. The fields are
|
|
37
|
+
documented in `floless-app-bridge`; the five types map to:
|
|
38
|
+
|
|
39
|
+
| `type` | Plain-English line | Owning skill |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| `tweak` | "Tweak node **`<nodeId>`** in **`<appId>`** — *<instruction>*" | `floless-app-workflows` |
|
|
42
|
+
| `use-template` | "Add template **<template.name>** to **`<appId>`**" | `floless-app-workflows` |
|
|
43
|
+
| `tweak-contract` | "Edit the steel-takeoff contract for **`<appId>`** — *<instruction>* (+N screenshot(s))" | `floless-app-tweak-contract` |
|
|
44
|
+
| `ui-customize` | "Customize the Dashboard (optionally `panel <panelId>`) — *<instruction>*" | `floless-app-ui` |
|
|
45
|
+
| `rebake` | "Re-read the **<inputName>** drawing (<sourceName>) in **`<appId>`** — *<instruction>*" | `floless-app-rebake` |
|
|
46
|
+
|
|
47
|
+
Note the count of screenshots for requests that carry `snapshots`. If a request has an unfamiliar
|
|
48
|
+
`type`, show it verbatim and flag it rather than guessing what it does.
|
|
49
|
+
|
|
50
|
+
## 3. Ask which to process
|
|
51
|
+
|
|
52
|
+
Ask the user to choose: **one, several, all, or none** (e.g. "1 and 3", "all", "skip 2"). This skill
|
|
53
|
+
is read-and-choose: **never apply a request the user did not pick.** Wait for the answer before
|
|
54
|
+
touching any `.flo`, contract, or panel.
|
|
55
|
+
|
|
56
|
+
## 4. Process each chosen request — route to the owning skill
|
|
57
|
+
|
|
58
|
+
For each pick, hand it to the skill in the table above (the type→skill routing
|
|
59
|
+
`floless-app-bridge` uses for pasted markers, extended here to also cover `tweak-contract`).
|
|
60
|
+
That skill does the real work — read the app's
|
|
61
|
+
current `.flo`/contract/panel, apply the change, recompile if needed — **and clears the request**:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
curl -s -X DELETE http://127.0.0.1:4317/api/requests/<id> # the UI's "requests" badge updates live over SSE
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Process the picks in the order the user gave (or oldest-first if they said "all"). Re-run the
|
|
68
|
+
owning skill's normal verification before reporting each one done.
|
|
69
|
+
|
|
70
|
+
## 5. The un-picked rest
|
|
71
|
+
|
|
72
|
+
Leave any request the user did **not** pick in the queue — do nothing to it. If the user wants to
|
|
73
|
+
**discard** one without processing, `DELETE /api/requests/<id>`; to drop the whole queue at once
|
|
74
|
+
the UI's "Clear all" maps to `DELETE /api/requests`. Only delete on an explicit ask.
|
|
75
|
+
|
|
76
|
+
When you're done, tell the user what was processed and **what is still queued**, so the picker can
|
|
77
|
+
be re-run for the remainder.
|
|
78
|
+
|
|
79
|
+
## Guardrails
|
|
80
|
+
|
|
81
|
+
- **You are the brain; the UI only queues intent.** This skill never composes a `.flo` itself — it
|
|
82
|
+
triages the queue and delegates each pick to the owning authoring skill.
|
|
83
|
+
- **Choose, don't bulk-apply.** Listing is non-destructive; only act on the user's explicit picks.
|
|
84
|
+
Never silently process the whole queue without an explicit "all".
|
|
85
|
+
- **Always clear a processed request** (the owning skill does the `DELETE`) so the UI's count
|
|
86
|
+
reflects reality; only `DELETE` an *un*-processed request when the user asks to discard it.
|
|
87
|
+
- **Don't invent request types** — only the five real ones exist; surface anything else rather than
|
|
88
|
+
guessing. This is the picker; **`floless-app-bridge`** is the puller/applier it sits in front of.
|
package/dist/web/aware.js
CHANGED
|
@@ -3742,8 +3742,14 @@
|
|
|
3742
3742
|
loadApp(currentId).catch(reportErr);
|
|
3743
3743
|
}, 250);
|
|
3744
3744
|
} else if (m.type === 'seat-taken') {
|
|
3745
|
-
// This session's seat was
|
|
3746
|
-
|
|
3745
|
+
// This session's seat was lost. m.reason (normalized server-side) says why — only a
|
|
3746
|
+
// genuine takeover should claim "another device"; an inactivity/revoke reads neutrally (#142).
|
|
3747
|
+
const SEAT_TOAST = {
|
|
3748
|
+
takeover: 'Signed in on another device — sign in here to continue.',
|
|
3749
|
+
inactivity: 'Your session timed out — sign in here to continue.',
|
|
3750
|
+
revoked: 'Your session was ended by an administrator — sign in here to continue.',
|
|
3751
|
+
};
|
|
3752
|
+
showToast(SEAT_TOAST[m.reason] || 'Your session ended — sign in here to continue.', 'err');
|
|
3747
3753
|
recheckLicenseAndGate();
|
|
3748
3754
|
}
|
|
3749
3755
|
};
|
|
@@ -5573,7 +5579,21 @@
|
|
|
5573
5579
|
body: 'Sign in with your FloLess account to use floless.app.',
|
|
5574
5580
|
secondary: { label: 'No subscription yet? Subscribe →', href: subUrl } },
|
|
5575
5581
|
};
|
|
5576
|
-
|
|
5582
|
+
let copy = COPY[reason] || COPY['signed-out']; // neutral fallback for an unknown/absent reason
|
|
5583
|
+
// A lost seat carries a sub-reason; only a genuine takeover should claim "another device".
|
|
5584
|
+
// Inactivity/revoke/unknown read neutrally so the gate stops misattributing the cause (#142).
|
|
5585
|
+
if (reason === 'seat-taken') {
|
|
5586
|
+
const SEAT_COPY = {
|
|
5587
|
+
inactivity: { headline: 'Session timed out',
|
|
5588
|
+
body: 'Your session ended after a period of inactivity. Sign in to continue.' },
|
|
5589
|
+
revoked: { headline: 'Session ended by an administrator',
|
|
5590
|
+
body: 'Your session was ended by an administrator. Sign in again to continue.' },
|
|
5591
|
+
unknown: { headline: 'Session ended',
|
|
5592
|
+
body: 'Your session has ended. Sign in to continue using floless.app.' },
|
|
5593
|
+
};
|
|
5594
|
+
const sc = SEAT_COPY[status.seatReason]; // 'takeover'/absent → keep the default takeover copy
|
|
5595
|
+
if (sc) copy = { ...copy, ...sc };
|
|
5596
|
+
}
|
|
5577
5597
|
const style = document.createElement('style');
|
|
5578
5598
|
style.textContent = `
|
|
5579
5599
|
#license-gate{position:fixed;inset:0;z-index:99999;display:flex;align-items:center;justify-content:center;
|