@floless/app 0.16.2 → 0.18.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 +512 -310
- package/dist/skills/floless-app-bridge/SKILL.md +27 -1
- package/dist/skills/floless-app-rebake/SKILL.md +77 -0
- package/dist/skills/floless-app-routines/SKILL.md +1 -1
- package/dist/skills/floless-app-ui/SKILL.md +1 -1
- package/dist/skills/floless-app-workflows/SKILL.md +8 -2
- package/dist/skills/floless-app-workflows/references/exec-contract.md +10 -1
- package/dist/skills/floless-app-workflows/references/visual-inputs.md +94 -0
- package/dist/web/app.css +90 -5
- package/dist/web/app.js +51 -5
- package/dist/web/aware.js +330 -54
- package/dist/web/index.html +11 -1
- package/dist/web/panels.js +5 -3
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: floless-app-bridge
|
|
3
|
-
description: This skill should be used when the user is driving the floless.app web UI (the local prompt-native front door at http://127.0.0.1:4317) and wants the terminal AI to act on what they did there — picking up queued Tweak requests, "use this template" requests, or the context of a node they selected/edited on the canvas. Use it when the user says things like "apply the tweak I just queued", "I clicked Tweak on the bom node — make that change", "pick up my floless request", "what's pending in floless", or after they double-click/Tweak a node in the UI. It reads the floless.app local API, applies the change to the app's .flo, recompiles, and clears the request.
|
|
3
|
+
description: This skill should be used when the user is driving the floless.app web UI (the local prompt-native front door at http://127.0.0.1:4317) and wants the terminal AI to act on what they did there — picking up queued Tweak requests, "use this template" requests, or the context of a node they selected/edited on the canvas. Use it when the user says things like "apply the tweak I just queued", "I clicked Tweak on the bom node — make that change", "pick up my floless request", "what's pending in floless", or after they double-click/Tweak a node in the UI. ALSO use it the moment the user pastes a message that begins with a `[floless-request type=… id=…]` marker — that is a request copied from the FloLess Dashboard, NOT a literal instruction to run; resolve it via the API. It reads the floless.app local API, applies the change to the app's .flo, recompiles, and clears the request.
|
|
4
4
|
metadata:
|
|
5
5
|
version: 0.1.0
|
|
6
6
|
---
|
|
@@ -13,6 +13,32 @@ and you **pull** them here, do the real work (edit the `.flo`, recompile), and c
|
|
|
13
13
|
skill is that pull side. Pair it with the **`floless-app-workflows`** skill, which owns
|
|
14
14
|
the actual `.flo` authoring / install → compile → run loop.
|
|
15
15
|
|
|
16
|
+
## Receiving a *pasted* request (the `[floless-request]` marker)
|
|
17
|
+
|
|
18
|
+
The user can either ask you to **pull** pending requests (below) **or paste one straight into
|
|
19
|
+
this chat**. Every request the Dashboard copies to the clipboard is prefixed with a
|
|
20
|
+
self-identifying marker line:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
[floless-request type=ui-customize id=4a1f…] — queued from the FloLess Dashboard. Apply it with your floless-app-ui skill: fetch the authoritative spec from GET http://127.0.0.1:<port>/api/requests, apply that request, then DELETE …/api/requests/<id>. Don't run the line below verbatim.
|
|
24
|
+
<a human-readable summary of the change>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
When a message starts with `[floless-request …]`, treat it as a **queued Dashboard request, not
|
|
28
|
+
a literal instruction**:
|
|
29
|
+
|
|
30
|
+
1. **Resolve it from the API, not the pasted text.** `GET /api/requests` and find the entry whose
|
|
31
|
+
`id` matches the marker — that JSON is authoritative; the pasted line is only a summary.
|
|
32
|
+
2. **Apply it with the skill the marker names** (`ui-customize` → `floless-app-ui`;
|
|
33
|
+
`tweak` / `use-template` → `floless-app-workflows`; `rebake` → `floless-app-rebake`). Use the
|
|
34
|
+
**port from the marker** if it isn't 4317.
|
|
35
|
+
3. **`DELETE /api/requests/<id>`** when done.
|
|
36
|
+
|
|
37
|
+
Do **not** run the pasted line as a shell command, and do **not** report a "missing file/path" as
|
|
38
|
+
a bug — a path like `~/.floless/ui/extensions.json` is *created by the skill's own flow*, so its
|
|
39
|
+
absence is expected, not an error. (This is the #73 fix: a pasted request was being misread as a
|
|
40
|
+
direct instruction and a path-not-found bug was filed.)
|
|
41
|
+
|
|
16
42
|
## The local API (http://127.0.0.1:4317)
|
|
17
43
|
|
|
18
44
|
The floless.app server runs locally on a **fixed port 4317** (override: `$PORT`). Confirm it's up
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: floless-app-rebake
|
|
3
|
+
description: This skill should be used when the user swaps the source drawing of a BAKED floless.app workflow and wants it re-read — the "Re-read & re-bake" (B3) loop for Visual Inputs. Triggers on a pending floless `rebake` request (queued from the app's "Re-read & re-bake ▸" button), or asks like "re-bake screenshot-to-saved-settings with this new schedule", "I swapped the drawing, re-read it", "re-extract and re-bake the config". It teaches the host AI to read the new image, re-extract its values, rewrite ONLY the app's config.yaml baked literals, recompile, and ask the user to approve — all at COMPOSE time (AWARE forbids reading a drawing during a run).
|
|
4
|
+
metadata:
|
|
5
|
+
version: 0.1.0
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Re-read & re-bake a baked Visual Input (B3)
|
|
9
|
+
|
|
10
|
+
## What this is
|
|
11
|
+
|
|
12
|
+
Some floless.app workflows **bake** an image/PDF at compose time: the terminal AI reads a
|
|
13
|
+
drawing once and writes the extracted values into `config.yaml` as literals, so the run is
|
|
14
|
+
fully deterministic and reads no drawing (the **decalog-#9 / "bake"** strategy — AWARE's
|
|
15
|
+
`aware app validate` *rejects* a node that reads a drawing during a run). When the user wants
|
|
16
|
+
to process a **different** drawing, they can't just "swap and run" — the literals must be
|
|
17
|
+
re-extracted and re-baked. That re-extraction is an LLM act, so it happens **here, at compose
|
|
18
|
+
time**, then the user approves the new lock. This is strategy **B3** of the Visual Inputs design
|
|
19
|
+
(`docs/superpowers/specs/2026-06-12-visual-inputs-design.md`).
|
|
20
|
+
|
|
21
|
+
The floless.app UI surfaces a **"Re-read & re-bake ▸"** button on a baked-visual app's entry
|
|
22
|
+
node. Clicking it (after attaching a new drawing) records a **`rebake` request** that you pick
|
|
23
|
+
up here.
|
|
24
|
+
|
|
25
|
+
> **Pasted a request?** If the user pastes a message beginning with a
|
|
26
|
+
> `[floless-request type=rebake id=…]` marker, that's a request **copied from the FloLess
|
|
27
|
+
> Dashboard**, not a literal instruction. Resolve it via `GET /api/requests` (match the id) —
|
|
28
|
+
> don't run the pasted line verbatim — then follow the loop below and `DELETE` it when done.
|
|
29
|
+
|
|
30
|
+
## The loop
|
|
31
|
+
|
|
32
|
+
1. **Find the request.** `GET http://localhost:<port>/api/requests` → find the entry with
|
|
33
|
+
`type: "rebake"`. It carries `appId`, the `inputName` (the baked input, e.g. `drawing`),
|
|
34
|
+
an `instruction`, and `snapshots: ["<abs path to the new image>"]`. (The user may also just
|
|
35
|
+
ask in prose with an image attached — same procedure, skip the request lookup.)
|
|
36
|
+
2. **Read the new drawing.** Open the image at `snapshots[0]` (a real PNG/JPEG/WebP on disk).
|
|
37
|
+
Read it with your own vision — this is the compose-time extraction.
|
|
38
|
+
3. **Re-extract the same schema the app already bakes.** Open the app source +
|
|
39
|
+
`config.yaml` under `~/.aware/apps/<appId>/` (and the editable copy under `demos/<appId>/`
|
|
40
|
+
if present). Identify the baked literals (e.g. `config.schedule-rows`). Re-extract **the same
|
|
41
|
+
shape** from the new drawing — same fields, same types — so the deterministic nodes keep
|
|
42
|
+
working. Do NOT invent values the drawing doesn't state; leave the app's documented
|
|
43
|
+
sentinel/default for anything absent (mirror how it was originally baked).
|
|
44
|
+
4. **Rewrite ONLY the baked config values.** Edit `~/.aware/apps/<appId>/config.yaml`
|
|
45
|
+
(and the `demos/<appId>/config.yaml` editable copy) — replace the baked literals with the
|
|
46
|
+
re-extracted ones. **Do not change node logic, the `.flo`, or the schema** — only the values.
|
|
47
|
+
5. **Recompile.** `aware app compile ~/.aware/apps/<appId>` → writes a fresh `<appId>.lock`.
|
|
48
|
+
The source-hash changes, so floless's Run gate disarms until approval. Copy the lock back to
|
|
49
|
+
`demos/<appId>/` if you keep an editable copy.
|
|
50
|
+
6. **Hand the approval to the user.** Tell them what changed (e.g. "re-baked 7 schedule rows
|
|
51
|
+
from the new drawing — review the diff in Inspect → Code, then Compile/Approve, then ▶ Run").
|
|
52
|
+
The human eyeballing the re-extraction before any run **is the safety gate** (it replaces the
|
|
53
|
+
`approve:` block AWARE's bake pattern recommends). Never auto-run.
|
|
54
|
+
7. **Clear the request.** `DELETE http://localhost:<port>/api/requests/<id>` once done.
|
|
55
|
+
|
|
56
|
+
## Guardrails
|
|
57
|
+
|
|
58
|
+
- **Compose-time only.** Never add a runtime node that reads the drawing — `aware app validate`
|
|
59
|
+
rejects it, and it would break determinism. The whole point of B3 is to keep the run
|
|
60
|
+
deterministic by re-baking up front.
|
|
61
|
+
- **Values, not logic.** Re-bake only `config.yaml` literals. If the new drawing needs a
|
|
62
|
+
*different shape* (new columns, a different schema), that's a workflow change, not a re-bake —
|
|
63
|
+
tell the user and use `floless-app-workflows` instead.
|
|
64
|
+
- **Thin-UI contract holds.** The browser only recorded intent + the image; the brain is you,
|
|
65
|
+
in the terminal. The UI never re-bakes and never reads the drawing.
|
|
66
|
+
- **Resolve the port** from the running floless.app (the `/api` base the user's workspace is on)
|
|
67
|
+
the same way the other floless-app skills do.
|
|
68
|
+
|
|
69
|
+
## Relationship to the other strategies
|
|
70
|
+
|
|
71
|
+
- **A (bake)** is the first-time version of this (read at compose, bake). B3 is "do it again for a
|
|
72
|
+
new drawing." Same act, re-triggered by a swap.
|
|
73
|
+
- **B1 (parse)** is for deterministically-parseable inputs that DON'T need re-baking — there the
|
|
74
|
+
image is a true runtime input read by an `exec` node; the user swaps and runs with no re-bake.
|
|
75
|
+
- **B2 (vision)** — a curated runtime `vision.extract` agent — is the future zero-friction
|
|
76
|
+
replacement for B3 once AWARE ships it (RFC `aware-aeco/aware#223`). Until then, B3 is the
|
|
77
|
+
swap-and-(almost)-run answer for vision inputs.
|
|
@@ -7,7 +7,7 @@ metadata:
|
|
|
7
7
|
|
|
8
8
|
# Setting up floless.app routines (automatic .flo runs)
|
|
9
9
|
|
|
10
|
-
A **routine** runs an installed
|
|
10
|
+
A **routine** runs an installed FloLess workflow (`.flo` app) automatically — the hands-off
|
|
11
11
|
equivalent of clicking ▶ Run workflow in floless.app. There are two **kinds**, both authored
|
|
12
12
|
through the same **`/api/routines`** REST surface:
|
|
13
13
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: floless-app-ui
|
|
3
|
-
description: This skill should be used when the user wants to customize the floless.app Dashboard — add, change, rearrange, or remove custom panels (stat cards, data tables, report buttons, run buttons) — or when picking up queued 'ui-customize' requests from the floless.app Customize box. Use it when the user says things like "add a panel showing last night's BOM run", "pin a run button to my dashboard", "apply my dashboard change", "customize my floless dashboard", "pick up my floless UI request",
|
|
3
|
+
description: This skill should be used when the user wants to customize the floless.app Dashboard — add, change, rearrange, or remove custom panels (stat cards, data tables, report buttons, run buttons) — or when picking up queued 'ui-customize' requests from the floless.app Customize box. Use it when the user says things like "add a panel showing last night's BOM run", "pin a run button to my dashboard", "apply my dashboard change", "customize my floless dashboard", "pick up my floless UI request", after they type into the Dashboard's Customize box, OR when they paste a message beginning with a `[floless-request type=ui-customize id=…]` marker (a request copied from the Dashboard — resolve it via /api/requests, don't run it verbatim). It edits ~/.floless/ui/extensions.json (the declarative UI descriptor), validates with the AWARE ui agent, and the app re-renders live.
|
|
4
4
|
metadata:
|
|
5
5
|
version: 0.1.0
|
|
6
6
|
---
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: floless-app-workflows
|
|
3
|
-
description: This skill should be used when building, modifying, or reasoning about
|
|
3
|
+
description: This skill should be used when building, modifying, or reasoning about FloLess .flo workflows for floless.app — especially reusable apps that take inputs (e.g. a phase number) and run across different Tekla models, exec nodes that run Roslyn C# against the live model, the in-app HTML Viewer, the Code tab, or "Debug in VS". Covers the exec contract, the install→validate→compile→run loop, app inputs templating, and the server seams (aware-adapter, app-reader, index).
|
|
4
4
|
metadata:
|
|
5
5
|
version: 0.1.0
|
|
6
6
|
---
|
|
@@ -14,10 +14,16 @@ Every capability is either `aware <verb>` (shelled through one adapter) or readi
|
|
|
14
14
|
state off disk. The browser is just the window. Never make the UI compose workflows or call
|
|
15
15
|
an LLM — it renders AWARE state, triggers `aware` verbs, and relays user intent.
|
|
16
16
|
|
|
17
|
-
The deliverable floless.app produces is a **reusable
|
|
17
|
+
The deliverable floless.app produces is a **reusable FloLess `.flo` app** (which runs on AWARE): one that declares
|
|
18
18
|
`inputs:` with defaults and runs unchanged across different models. The canonical example
|
|
19
19
|
lives at `demos/tekla-bom-by-phase/` — study it before building a new one.
|
|
20
20
|
|
|
21
|
+
> **Pasted a request?** If the user pastes a message beginning with a
|
|
22
|
+
> `[floless-request type=tweak|use-template id=…]` marker, that's a request **copied from the
|
|
23
|
+
> FloLess Dashboard**, not a literal instruction. Resolve it via the API (`GET /api/requests`,
|
|
24
|
+
> match the id) and apply it per the **`floless-app-bridge`** skill's "Receiving a pasted
|
|
25
|
+
> request" section — then `DELETE /api/requests/<id>`. Don't run the pasted line verbatim.
|
|
26
|
+
|
|
21
27
|
## The reusable-app shape
|
|
22
28
|
|
|
23
29
|
An app becomes reusable by declaring a top-level `inputs:` block and templating those inputs
|
|
@@ -44,7 +44,16 @@ floless extracts `data.result.html` for the HTML Viewer.
|
|
|
44
44
|
`System.IO`, `System.Diagnostics.Debugger`, and the Tekla Open API
|
|
45
45
|
(`Tekla.Structures.Model`, etc. — discovered/probed from the running install).
|
|
46
46
|
- **NOT referenced:** `System.Net` (so **no** `WebUtility.HtmlEncode` — escape manually),
|
|
47
|
-
`System.Diagnostics.Process
|
|
47
|
+
`System.Diagnostics.Process`, and **`System.Text.RegularExpressions`** (no `Regex` —
|
|
48
|
+
use `IndexOf`/`Substring` scans instead). Don't rely on them.
|
|
49
|
+
|
|
50
|
+
> **FOOTGUN — the bridge swallows the compiler diagnostic.** An exec script that fails
|
|
51
|
+
> to compile (e.g. references an unreferenced assembly like `Regex`) does NOT surface a
|
|
52
|
+
> `CS####` error — the run aborts with an opaque `agent tekla/exec failed (exit Some(2))`
|
|
53
|
+
> and an empty detail (verified on aware 0.65.0). It reads like a host-attach failure but
|
|
54
|
+
> is a compile error in your C#. Isolate by running a known-good Tekla-free exec
|
|
55
|
+
> (`dummy-report`): if that runs and yours exits 2, the fault is your script, not the
|
|
56
|
+
> bridge/host. (The opacity is an AWARE DX gap — candidate to file on aware-aeco.)
|
|
48
57
|
|
|
49
58
|
Manual HTML escape (since `System.Net` is unavailable):
|
|
50
59
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Visual Inputs (image / PDF): one input, four read-strategies
|
|
2
|
+
|
|
3
|
+
A workflow takes an image/PDF via an input that declares **`widget: file`** (and
|
|
4
|
+
typically `type: image`). The user swaps it on the **input node** ("Set inputs ▸" →
|
|
5
|
+
a file picker); floless stores the bytes under `~/.floless/inputs/<app>/<sha>.<ext>`
|
|
6
|
+
(content-addressed, PDF-aware) and sets `{{ inputs.<name> }}` to that **path**. The
|
|
7
|
+
bytes never pass through a model in the UI — the web layer moves a file, AWARE (or
|
|
8
|
+
the terminal AI at compose) interprets it.
|
|
9
|
+
|
|
10
|
+
```yaml
|
|
11
|
+
inputs:
|
|
12
|
+
drawing:
|
|
13
|
+
type: image
|
|
14
|
+
widget: file # ← load-bearing: renders the picker, not a text box
|
|
15
|
+
accept: [pdf, png, jpg] # narrows the picker + the chip glyph
|
|
16
|
+
read-strategy: parse # parse | bake | rebake | vision (see below)
|
|
17
|
+
description: The framing PDF/image to take off (swappable per run).
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Pick the strategy from two questions — needs vision? swappable per run?
|
|
21
|
+
|
|
22
|
+
| | deterministic parser exists | needs vision |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| **fixed / rare** | parse-once, or **bake** (A) | **bake** (A) |
|
|
25
|
+
| **swap per run** | **parse** (B1) | **rebake** (B3) today · **vision** (B2) later |
|
|
26
|
+
|
|
27
|
+
- **parse (B1)** — a read-only `exec` node opens `{{ inputs.<name> }}` with `System.IO`
|
|
28
|
+
and a deterministic library (PdfPig geometry, OCR, barcode). No LLM, legal at
|
|
29
|
+
runtime. The file is a true swappable input; swap + Run, no recompile. Reference:
|
|
30
|
+
`demos/tekla-pdf-takeoff/` (page-count today; geometry takeoff is the same shape).
|
|
31
|
+
- **bake (A)** — the terminal AI reads the artifact at **compose** time and bakes
|
|
32
|
+
literals into `config.yaml`; the run never reads it. Reference:
|
|
33
|
+
`demos/screenshot-to-saved-settings/`. **AWARE REQUIRES this for any vision read:**
|
|
34
|
+
`aware app validate` rejects a runtime think-node that reads a drawing
|
|
35
|
+
(`app-spec.md` §"Reading a drawing / PDF / image", and `validate.rs`).
|
|
36
|
+
- **rebake (B3)** — swapping the artifact re-triggers the compose-time bake via the
|
|
37
|
+
request relay; the **`floless-app-rebake` skill** teaches the host AI to re-read +
|
|
38
|
+
re-bake + recompile + approve. The baked input declares `read-strategy: bake`; the
|
|
39
|
+
entry node shows a **"Re-read & re-bake ▸"** affordance. Feels like swap-and-run,
|
|
40
|
+
keeps determinism. (Shipped — Sub-project 2.)
|
|
41
|
+
- **vision (B2)** — a curated runtime `vision.extract` agent (image → schema-bound
|
|
42
|
+
JSON, content-cached, approve-gated). Needs an AWARE substrate change — RFC
|
|
43
|
+
`aware-aeco/aware#223` (+ reference impl PR). **Substrate-gated:** until `aware`
|
|
44
|
+
ships `vision.extract`, authoring a B2 app fails `validate`; use **rebake (B3)** for
|
|
45
|
+
swap + vision in the meantime (a B2 input is a drop-in upgrade of a B3 input).
|
|
46
|
+
|
|
47
|
+
**Rule:** never put an LLM in the run path except B2's curated, lock-pinned,
|
|
48
|
+
approve-gated agent. The UI never interprets the file — it only stores it and passes
|
|
49
|
+
the path.
|
|
50
|
+
|
|
51
|
+
## Authoring a B2 (vision) app — gated on AWARE `vision.extract`
|
|
52
|
+
|
|
53
|
+
A B2 app is a normal reusable `.flo` whose **source drawing is a runtime input** (the SP1
|
|
54
|
+
picker — `read-strategy: vision`, `widget: file`) feeding a **`vision.extract` node**, whose
|
|
55
|
+
schema-bound JSON then drives deterministic downstream nodes behind an `approve:` gate:
|
|
56
|
+
|
|
57
|
+
```yaml
|
|
58
|
+
inputs:
|
|
59
|
+
drawing: { type: image, widget: file, read-strategy: vision, accept: [png, jpg, pdf],
|
|
60
|
+
description: The schedule drawing to extract (swappable per run). }
|
|
61
|
+
nodes:
|
|
62
|
+
- id: extract
|
|
63
|
+
agent: vision
|
|
64
|
+
command: extract # the curated carve-out — runs a model at RUN time
|
|
65
|
+
config:
|
|
66
|
+
file: "{{ inputs.drawing }}" # bytes on the edge
|
|
67
|
+
model: "claude-..." # pinned in the lock; a swap re-invalidates approval
|
|
68
|
+
schema: { type: object, properties: { rows: { type: array, ... } } } # FIXED output schema
|
|
69
|
+
prompt: "Extract each schedule row as {member, boltCount, ...}."
|
|
70
|
+
- id: write
|
|
71
|
+
# ...deterministic for-each over {{ extract.result.rows }}...
|
|
72
|
+
approve: { ... } # human eyeballs the extraction before any write
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**floless seams for B2 (mostly already in place from SP1):**
|
|
76
|
+
- The picker is the **SP1 file field** — a `read-strategy: vision` input already gets "Set
|
|
77
|
+
inputs ▸" and stores the path as `{{ inputs.drawing }}` (no new UI). Swap + Run; the
|
|
78
|
+
`vision.extract` node re-reads at runtime (content-cached, so a repeat input is replayed
|
|
79
|
+
from cache with no model call).
|
|
80
|
+
- When AWARE stamps a `runtime-model` marker on the compiled node (RFC §5.3), `app-reader`
|
|
81
|
+
surfaces it and the canvas badges **"calls a model at run time"** so the approver sees it
|
|
82
|
+
honestly — wire this against the real lock field once `vision.extract` lands.
|
|
83
|
+
- Gate the affordance/authoring on the installed `aware` version exposing `vision`
|
|
84
|
+
(`aware agent has vision extract`); degrade to B3 below the floor.
|
|
85
|
+
|
|
86
|
+
## Server / UI seams (where this lives)
|
|
87
|
+
|
|
88
|
+
- `server/visual-input-store.ts` — content-addressed store (`storeVisualInput`),
|
|
89
|
+
PDF-aware magic-byte sniff, path-safety.
|
|
90
|
+
- `server/app-reader.ts` — `AppInput.widget/accept/readStrategy` + `isVisualInput`.
|
|
91
|
+
- `POST /api/visual-input` (`server/index.ts`) — `{appId, name, dataUrl}` → `{path}`.
|
|
92
|
+
- `web/aware.js` — `openInputsDialog` renders a `type:'file'` field (reuses `.fm-drop`);
|
|
93
|
+
on a fresh pick it uploads the data URL and stores the returned path as the input
|
|
94
|
+
value; `addNodeInputs` shows a `.ni-pair-file` chip (file glyph + basename).
|
package/dist/web/app.css
CHANGED
|
@@ -347,6 +347,21 @@
|
|
|
347
347
|
}
|
|
348
348
|
/* While middle-mouse panning, force the grab cursor over the whole canvas. */
|
|
349
349
|
.canvas.panning, .canvas.panning * { cursor: grabbing !important; }
|
|
350
|
+
/* OS-file drop target for importing a shared .flo (#66). Shown ONLY while a real file
|
|
351
|
+
is dragged over the canvas — distinguished from in-app node-card drags (which carry
|
|
352
|
+
no "Files" type) by the JS that toggles .active. Existing tokens only. */
|
|
353
|
+
.canvas-drop-target {
|
|
354
|
+
position: absolute; inset: 8px; z-index: 20;
|
|
355
|
+
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px;
|
|
356
|
+
background: color-mix(in srgb, var(--bg) 80%, transparent);
|
|
357
|
+
border: 2px dashed var(--accent-dim); border-radius: 6px;
|
|
358
|
+
pointer-events: none; opacity: 0; transition: opacity 0.15s ease;
|
|
359
|
+
}
|
|
360
|
+
.canvas-drop-target.active { opacity: 1; pointer-events: auto; }
|
|
361
|
+
.canvas-drop-icon { font-size: 30px; color: var(--accent); line-height: 1; }
|
|
362
|
+
.canvas-drop-label { font-size: 14px; font-weight: 600; color: var(--text); letter-spacing: 0.01em; }
|
|
363
|
+
.canvas-drop-sub { font-size: 11px; color: var(--text-muted); }
|
|
364
|
+
@media (prefers-reduced-motion: reduce) { .canvas-drop-target { transition: none; } }
|
|
350
365
|
.topology {
|
|
351
366
|
flex: 1;
|
|
352
367
|
display: flex;
|
|
@@ -574,6 +589,16 @@
|
|
|
574
589
|
word-break: break-word;
|
|
575
590
|
}
|
|
576
591
|
.agent-card .blurb { font-size: 11.5px; color: var(--text-muted); line-height: 1.5; margin-bottom: 10px; }
|
|
592
|
+
/* B2: the one node that calls a model at run time (vision.extract). A subtle accent
|
|
593
|
+
pill so the approver sees the non-deterministic-on-first-input node honestly. */
|
|
594
|
+
.agent-card .rt-model-badge {
|
|
595
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
596
|
+
margin: 0 0 8px; padding: 2px 8px;
|
|
597
|
+
font: 600 10px/1.4 var(--ui); letter-spacing: 0.01em;
|
|
598
|
+
color: var(--accent); background: var(--accent-soft);
|
|
599
|
+
border: 1px solid var(--accent-dim); border-radius: 999px;
|
|
600
|
+
cursor: help;
|
|
601
|
+
}
|
|
577
602
|
.agent-card .footer-row {
|
|
578
603
|
display: flex;
|
|
579
604
|
justify-content: space-between;
|
|
@@ -702,6 +727,7 @@
|
|
|
702
727
|
|
|
703
728
|
/* ========== FAVORITES BAR ========== */
|
|
704
729
|
.fav-bar {
|
|
730
|
+
position: relative; /* anchors the .fav-bar-drop-label overlay (#71) */
|
|
705
731
|
border-top: 1px solid var(--border);
|
|
706
732
|
background: var(--surface);
|
|
707
733
|
padding: 9px 16px;
|
|
@@ -710,7 +736,7 @@
|
|
|
710
736
|
gap: 12px;
|
|
711
737
|
min-height: 46px;
|
|
712
738
|
overflow-x: auto;
|
|
713
|
-
transition: background 0.15s, border-top-color 0.15s;
|
|
739
|
+
transition: background 0.15s, border-top-color 0.15s, border-top-style 0.15s;
|
|
714
740
|
}
|
|
715
741
|
.fav-bar-label {
|
|
716
742
|
font-size: 10px;
|
|
@@ -725,11 +751,38 @@
|
|
|
725
751
|
.fav-bar-label .star { color: var(--star); font-size: 12px; }
|
|
726
752
|
.fav-bar-empty { font-style: italic; font-size: 11px; color: var(--text-dim); }
|
|
727
753
|
.fav-chip-row { display: flex; gap: 6px; align-items: center; flex-wrap: nowrap; }
|
|
754
|
+
/* Two-level drop affordance (#71): the bar ADVERTISES itself as a drop target the
|
|
755
|
+
moment a node-card drag starts (.drop-armed — a quiet dashed accent edge), then
|
|
756
|
+
INTENSIFIES to the full accent fill when the node is dragged over it (.drag-over).
|
|
757
|
+
Both use existing tokens only; cleared on drop/cancel. */
|
|
758
|
+
.fav-bar.drop-armed {
|
|
759
|
+
background: var(--surface-2);
|
|
760
|
+
border-top: 1px dashed var(--accent-dim);
|
|
761
|
+
}
|
|
728
762
|
.fav-bar.drag-over {
|
|
729
763
|
background: var(--accent-soft);
|
|
730
|
-
border-top
|
|
764
|
+
border-top: 1px solid var(--accent);
|
|
731
765
|
box-shadow: inset 0 1px 0 var(--accent);
|
|
732
766
|
}
|
|
767
|
+
/* "Drop to save as Template" hint, shown while armed (and whether or not chips already
|
|
768
|
+
exist — it overlays flush-right, never reflows the chip row). The bar's own fill is
|
|
769
|
+
confirmation enough once the node is over it, so it fades out on .drag-over. */
|
|
770
|
+
.fav-bar-drop-label {
|
|
771
|
+
position: absolute;
|
|
772
|
+
right: 16px;
|
|
773
|
+
top: 50%;
|
|
774
|
+
transform: translateY(-50%);
|
|
775
|
+
font-size: 10px;
|
|
776
|
+
font-style: italic;
|
|
777
|
+
color: var(--accent-dim);
|
|
778
|
+
white-space: nowrap;
|
|
779
|
+
pointer-events: none;
|
|
780
|
+
transition: opacity 0.12s;
|
|
781
|
+
}
|
|
782
|
+
.fav-bar.drag-over .fav-bar-drop-label { opacity: 0; }
|
|
783
|
+
@media (prefers-reduced-motion: reduce) {
|
|
784
|
+
.fav-bar, .fav-bar.drop-armed, .fav-bar.drag-over, .fav-bar-drop-label { transition: none; }
|
|
785
|
+
}
|
|
733
786
|
.fav-chip {
|
|
734
787
|
display: inline-flex;
|
|
735
788
|
align-items: center;
|
|
@@ -769,6 +822,19 @@
|
|
|
769
822
|
}
|
|
770
823
|
.fav-chip:hover .del { opacity: 1; }
|
|
771
824
|
.fav-chip .del:hover { color: var(--err); background: rgba(248, 113, 113, 0.1); }
|
|
825
|
+
/* Edit (rename / recategorize) — mirrors .del, revealed on chip hover, but goes
|
|
826
|
+
accent-blue rather than err-red so the two actions read distinctly (#68). */
|
|
827
|
+
.fav-chip .edit {
|
|
828
|
+
color: var(--text-dim);
|
|
829
|
+
opacity: 0.4;
|
|
830
|
+
cursor: pointer;
|
|
831
|
+
padding: 4px 4px;
|
|
832
|
+
line-height: 1;
|
|
833
|
+
transition: all 0.15s;
|
|
834
|
+
border-radius: 2px;
|
|
835
|
+
}
|
|
836
|
+
.fav-chip:hover .edit { opacity: 1; }
|
|
837
|
+
.fav-chip .edit:hover { color: var(--accent); background: var(--accent-soft); }
|
|
772
838
|
|
|
773
839
|
/* ========== INSPECT ========== */
|
|
774
840
|
.inspect {
|
|
@@ -2211,9 +2277,11 @@ body {
|
|
|
2211
2277
|
}
|
|
2212
2278
|
|
|
2213
2279
|
/* Report + input nodes on the canvas carry a first-class action button
|
|
2214
|
-
("View report ▸" / "Set inputs ▸"). Double-click still works as a shortcut.
|
|
2215
|
-
|
|
2216
|
-
.
|
|
2280
|
+
("View report ▸" / "Set inputs ▸"). Double-click still works as a shortcut.
|
|
2281
|
+
The card BODY keeps the shared `grab` cursor (every card is draggable) — only the
|
|
2282
|
+
actionable bits (.node-action, .inspect-hint, .fav-btn) read as `pointer`. Earlier
|
|
2283
|
+
these cards forced `cursor: pointer` on the whole body, which clashed with the grab
|
|
2284
|
+
cursor on every other card (#70). */
|
|
2217
2285
|
.agent-card .node-action {
|
|
2218
2286
|
display: block; width: 100%; margin-top: 8px;
|
|
2219
2287
|
padding: 5px 8px; border-radius: 6px;
|
|
@@ -2366,6 +2434,23 @@ body {
|
|
|
2366
2434
|
.fm-thumb-del { position: absolute; top: 2px; right: 2px; width: 16px; height: 16px; padding: 0; display: inline-flex; align-items: center; justify-content: center; font-size: 11px; line-height: 1; background: var(--surface); border: 1px solid var(--border-strong); border-radius: 2px; color: var(--text-muted); cursor: pointer; opacity: 0; transition: opacity 0.12s, color 0.12s, border-color 0.12s; }
|
|
2367
2435
|
.fm-thumb:hover .fm-thumb-del, .fm-thumb-del:focus-visible { opacity: 1; }
|
|
2368
2436
|
.fm-thumb-del:hover { color: var(--err); border-color: var(--err); }
|
|
2437
|
+
/* ===== Visual Input: single image/PDF file field (inputs dialog) ===== */
|
|
2438
|
+
.fm-file { position: relative; }
|
|
2439
|
+
.fm-file .fm-file-input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
|
|
2440
|
+
.fm-drop .fm-drop-hint { display: block; margin-top: 3px; font-size: 10px; color: var(--text-dim); }
|
|
2441
|
+
.fm-drop.has-file { min-height: auto; padding: 6px 10px; flex-direction: row; justify-content: space-between; gap: 8px; border-style: solid; cursor: default; text-align: left; }
|
|
2442
|
+
.fm-drop.has-file:hover, .fm-drop.has-file:focus-visible { border-color: var(--border-strong); background: var(--surface-2); color: var(--text-muted); outline: none; }
|
|
2443
|
+
.fm-file-glyph { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 18px; border: 1px solid var(--accent-dim); border-radius: 3px; background: var(--accent-soft); color: var(--accent); font-family: var(--mono); font-size: 8px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; flex-shrink: 0; }
|
|
2444
|
+
.fm-file-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--mono); font-size: 11px; color: var(--text); text-align: left; }
|
|
2445
|
+
.fm-file-actions { display: flex; gap: 5px; flex-shrink: 0; }
|
|
2446
|
+
.fm-file-replace, .fm-file-clear { font-size: 10px; padding: 3px 8px; background: var(--surface); border: 1px solid var(--border-strong); color: var(--text-muted); border-radius: 3px; cursor: pointer; font-family: var(--ui); }
|
|
2447
|
+
.fm-file-replace:hover { color: var(--text); border-color: var(--accent-dim); }
|
|
2448
|
+
.fm-file-clear:hover { color: var(--err); border-color: var(--err); }
|
|
2449
|
+
/* Visual Input chip on the canvas input node */
|
|
2450
|
+
.agent-card .node-inputs .ni-pair-file { align-items: center; }
|
|
2451
|
+
.agent-card .node-inputs .ni-file-glyph { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 15px; border: 1px solid var(--accent-dim); border-radius: 3px; background: var(--accent-soft); color: var(--accent); font: 700 8px/1 var(--mono); letter-spacing: 0.04em; text-transform: uppercase; flex-shrink: 0; }
|
|
2452
|
+
.agent-card .node-inputs .ni-file-name { max-width: 92px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: inline-block; vertical-align: bottom; }
|
|
2453
|
+
.agent-card .node-inputs .ni-not-set { color: var(--text-dim); font-style: italic; font-weight: 400; }
|
|
2369
2454
|
/* ===== Requests: snapshot thumbnails ===== */
|
|
2370
2455
|
.req-thumbs { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 6px; }
|
|
2371
2456
|
.req-thumb { width: 46px; height: 46px; padding: 0; border: 1px solid var(--border-strong); border-radius: 4px; background: var(--bg); overflow: hidden; cursor: pointer; flex-shrink: 0; transition: border-color 0.15s; }
|
package/dist/web/app.js
CHANGED
|
@@ -19,6 +19,7 @@ const state = {
|
|
|
19
19
|
favorites: [],
|
|
20
20
|
collapse: { left: false, right: false },
|
|
21
21
|
pendingFavAgentId: null,
|
|
22
|
+
editingTemplateId: null, // set while the add/edit Template modal is in EDIT mode (#68)
|
|
22
23
|
traceFilter: null, // Execution tab: node id to filter rows to, or null = all
|
|
23
24
|
};
|
|
24
25
|
|
|
@@ -346,6 +347,7 @@ function cardEl(id, ports) {
|
|
|
346
347
|
</div>
|
|
347
348
|
<div class="title">${a.title}</div>
|
|
348
349
|
<div class="subtitle">${a.subtitle}</div>
|
|
350
|
+
${a._runtimeModel ? '<div class="rt-model-badge" title="This node calls a model at run time (vision.extract). Output is content-cached and sits behind an approve gate — review the extraction before any write.">⚡ calls a model at run time</div>' : ''}
|
|
349
351
|
<div class="blurb">${a.blurb}</div>
|
|
350
352
|
<div class="footer-row">
|
|
351
353
|
<span class="ports"><span class="ports-num">${ports.in}</span> in <span class="ports-arrow">·</span> <span class="ports-num">${ports.out}</span> out</span>
|
|
@@ -613,6 +615,7 @@ function renderCategorySuggestions() {
|
|
|
613
615
|
function closeAddFavModal() {
|
|
614
616
|
$addFavModal.classList.remove('show');
|
|
615
617
|
state.pendingFavAgentId = null;
|
|
618
|
+
state.editingTemplateId = null; // leave EDIT mode so the next open is a fresh create (#68)
|
|
616
619
|
}
|
|
617
620
|
|
|
618
621
|
function commitFav() {
|
|
@@ -674,17 +677,37 @@ function refreshFavMarkers() {
|
|
|
674
677
|
}
|
|
675
678
|
|
|
676
679
|
function setupFavDropZone() {
|
|
680
|
+
// Advertise the Templates bar as a drop target the moment ANY node-card drag starts,
|
|
681
|
+
// so drag-to-save is discoverable instead of invisible (#71). dragend fires for both a
|
|
682
|
+
// completed drop AND a cancelled (Esc / dropped-nowhere) drag, so it's the disarm hook.
|
|
683
|
+
document.addEventListener('dragstart', (e) => {
|
|
684
|
+
if (!(e.target instanceof Element) || !e.target.closest('.agent-card')) return;
|
|
685
|
+
$favBar.classList.add('drop-armed');
|
|
686
|
+
// The teaching label rides in the empty bar's free space. Once templates exist the
|
|
687
|
+
// chip row can fill (and horizontally scroll) the bar, so an overlaid label would
|
|
688
|
+
// collide with chips — there the dashed armed edge + hover-intensify is the affordance.
|
|
689
|
+
if (state.favorites.length === 0 && !$favBar.querySelector('.fav-bar-drop-label')) {
|
|
690
|
+
const lbl = document.createElement('span');
|
|
691
|
+
lbl.className = 'fav-bar-drop-label';
|
|
692
|
+
lbl.textContent = '▾ Drop to save as Template';
|
|
693
|
+
$favBar.appendChild(lbl);
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
document.addEventListener('dragend', disarmFavBar);
|
|
697
|
+
|
|
677
698
|
$favBar.addEventListener('dragover', (e) => {
|
|
678
699
|
e.preventDefault();
|
|
679
700
|
e.dataTransfer.dropEffect = 'copy';
|
|
680
701
|
$favBar.classList.add('drag-over');
|
|
681
702
|
});
|
|
703
|
+
// relatedTarget containment, not `e.target === $favBar`: the latter cleared the
|
|
704
|
+
// highlight prematurely when the cursor crossed from the bar's padding onto a chip.
|
|
682
705
|
$favBar.addEventListener('dragleave', (e) => {
|
|
683
|
-
if (e.
|
|
706
|
+
if (!$favBar.contains(e.relatedTarget)) $favBar.classList.remove('drag-over');
|
|
684
707
|
});
|
|
685
708
|
$favBar.addEventListener('drop', (e) => {
|
|
686
709
|
e.preventDefault();
|
|
687
|
-
|
|
710
|
+
disarmFavBar();
|
|
688
711
|
const id = e.dataTransfer.getData('text/plain');
|
|
689
712
|
if (!id || !AGENTS[id]) return;
|
|
690
713
|
if (state.favorites.some(f => f.id === id)) {
|
|
@@ -696,6 +719,13 @@ function setupFavDropZone() {
|
|
|
696
719
|
});
|
|
697
720
|
}
|
|
698
721
|
|
|
722
|
+
// Clear every drag-affordance state on the Templates bar (drop, cancel, or dragend).
|
|
723
|
+
function disarmFavBar() {
|
|
724
|
+
$favBar.classList.remove('drop-armed', 'drag-over');
|
|
725
|
+
const lbl = $favBar.querySelector('.fav-bar-drop-label');
|
|
726
|
+
if (lbl) lbl.remove();
|
|
727
|
+
}
|
|
728
|
+
|
|
699
729
|
/* ============= LIBRARY ============= */
|
|
700
730
|
|
|
701
731
|
function openLibrary() {
|
|
@@ -921,8 +951,8 @@ document.querySelectorAll('.panel-toggle').forEach(btn => {
|
|
|
921
951
|
document.getElementById('fav-cancel').onclick = closeAddFavModal;
|
|
922
952
|
document.getElementById('fav-save').onclick = commitFav;
|
|
923
953
|
document.getElementById('lib-close').onclick = () => $libModal.classList.remove('show');
|
|
924
|
-
|
|
925
|
-
$libModal
|
|
954
|
+
onBackdropDismiss($addFavModal, closeAddFavModal);
|
|
955
|
+
onBackdropDismiss($libModal, () => $libModal.classList.remove('show'));
|
|
926
956
|
|
|
927
957
|
document.addEventListener('keydown', (e) => {
|
|
928
958
|
if (e.key === 'Escape') {
|
|
@@ -951,6 +981,22 @@ function escapeHtml(s) {
|
|
|
951
981
|
}
|
|
952
982
|
function escapeAttr(s) { return escapeHtml(s).replace(/"/g, '"'); }
|
|
953
983
|
|
|
984
|
+
// Backdrop-dismiss that ignores text-selection drags. A modal closes only when BOTH
|
|
985
|
+
// the mousedown AND the click land on the backdrop element itself — so selecting text
|
|
986
|
+
// inside a modal input and releasing the cursor outside the modal no longer dismisses
|
|
987
|
+
// it on mouseup (#69). `guard` (optional) can veto dismissal, e.g. while a run is busy.
|
|
988
|
+
// Defined here (app.js loads first) and reused by aware.js for every modal.
|
|
989
|
+
function onBackdropDismiss(modal, dismiss, guard) {
|
|
990
|
+
if (!modal) return;
|
|
991
|
+
let downOnBackdrop = false;
|
|
992
|
+
modal.onmousedown = (e) => { downOnBackdrop = (e.target === modal); };
|
|
993
|
+
modal.onclick = (e) => {
|
|
994
|
+
const hit = e.target === modal && downOnBackdrop;
|
|
995
|
+
downOnBackdrop = false;
|
|
996
|
+
if (hit && (!guard || guard())) dismiss();
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
|
|
954
1000
|
/* ============= MENU ============= */
|
|
955
1001
|
|
|
956
1002
|
const $menuBtn = document.getElementById('menu-btn');
|
|
@@ -1328,7 +1374,7 @@ const $integrationsModal = document.getElementById('integrations-modal');
|
|
|
1328
1374
|
const $integrationsList = document.getElementById('integrations-list');
|
|
1329
1375
|
|
|
1330
1376
|
document.getElementById('integrations-close').onclick = () => $integrationsModal.classList.remove('show');
|
|
1331
|
-
$integrationsModal
|
|
1377
|
+
onBackdropDismiss($integrationsModal, () => $integrationsModal.classList.remove('show'));
|
|
1332
1378
|
|
|
1333
1379
|
/* ============= KEYBOARD SHORTCUTS ============= */
|
|
1334
1380
|
|