@floless/app 0.10.0 → 0.12.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 +824 -340
- package/dist/skills/floless-app-ui/SKILL.md +131 -0
- package/dist/web/app.css +164 -0
- package/dist/web/aware.js +92 -4
- package/dist/web/index.html +52 -4
- package/dist/web/panels.js +660 -0
- package/package.json +1 -1
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
---
|
|
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", or after they type into the Dashboard's Customize box. It edits ~/.floless/ui/extensions.json (the declarative UI descriptor), validates with the AWARE ui agent, and the app re-renders live.
|
|
4
|
+
metadata:
|
|
5
|
+
version: 0.1.0
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# floless.app Custom Panels — the dashboard descriptor
|
|
9
|
+
|
|
10
|
+
floless.app's Dashboard view renders **`~/.floless/ui/extensions.json`** — one declarative
|
|
11
|
+
JSON document describing the user's custom panels. **You (the terminal AI) compose the
|
|
12
|
+
description; floless.app renders it** with its own design system. You never write HTML,
|
|
13
|
+
CSS, or JS — and never need to: every string in the descriptor is escaped at render time,
|
|
14
|
+
so markup would show up as literal text.
|
|
15
|
+
|
|
16
|
+
The file is watched: the moment you write it, the open browser re-renders the Dashboard.
|
|
17
|
+
No compile, no approve, no restart.
|
|
18
|
+
|
|
19
|
+
## Picking up a queued request
|
|
20
|
+
|
|
21
|
+
When the user types into the Dashboard's **Customize box**, floless.app queues a request:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
curl -s http://127.0.0.1:4317/api/requests # → { ok, requests: [...] }
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```jsonc
|
|
28
|
+
// type "ui-customize" — a dashboard change in plain English. No appId (it's
|
|
29
|
+
// workspace-level); panelId optionally scopes it to one existing panel.
|
|
30
|
+
{ "id": "uuid", "type": "ui-customize", "status": "pending",
|
|
31
|
+
"instruction": "show last night's BOM run as a table with a re-run button",
|
|
32
|
+
"panelId": null }
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Workflow: read the request → edit `extensions.json` (below) → validate → **delete the
|
|
36
|
+
processed request** (`curl -s -X DELETE http://127.0.0.1:4317/api/requests/<id>`).
|
|
37
|
+
The request files also live at `~/.floless/requests/*.json` if the server is down.
|
|
38
|
+
|
|
39
|
+
## The descriptor schema (v1)
|
|
40
|
+
|
|
41
|
+
Authoritative sources — read them, don't guess field names:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
aware agent describe ui # the agent's commands
|
|
45
|
+
aware agent skill ui descriptor-authoring.md # the full authoring guide
|
|
46
|
+
aware agent invoke ui catalog --json # machine-readable block contracts
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Shape: `{ "version": 1, "panels": [ { "id", "slot", "title", "blocks": [...] } ] }`
|
|
50
|
+
|
|
51
|
+
- `id` — unique, `[a-z0-9-]+` (floless.app keys panels by it).
|
|
52
|
+
- `slot` — floless.app honors **`"dashboard"`** (the Dashboard grid) and
|
|
53
|
+
**`"inspect-tab"`** (an extra tab in the right Inspect column, after Execution).
|
|
54
|
+
Anything else falls back to the Dashboard.
|
|
55
|
+
- `blocks` — ordered; order is the layout. Types floless.app renders:
|
|
56
|
+
- `stat` `{label, value, hint?}` — a KPI card. Consecutive stats become one strip;
|
|
57
|
+
lead a panel with 2–4. Values are LITERALS — write the number/text yourself.
|
|
58
|
+
- `table` `{source, columns?, sort?, "sort-desc"?}` — a data table fed by a
|
|
59
|
+
floless-resolved `source` (below). Columns/sort mirror html-report (#210).
|
|
60
|
+
- `text` `{content}` — escaped prose; blank lines separate paragraphs.
|
|
61
|
+
- `report` `{source, title?}` — a "View report" button opening the run's HTML
|
|
62
|
+
report in the app's sandboxed viewer.
|
|
63
|
+
- `action` `{label, "action-id", inputs?}` — a button. floless.app wires ONLY
|
|
64
|
+
`"action-id": "run-app:<appId>"` (runs that installed workflow with `inputs`,
|
|
65
|
+
respecting the compile/lock gate). Other action-ids render inert.
|
|
66
|
+
- An unknown `type` renders as a friendly placeholder — never an error — so
|
|
67
|
+
composing for a newer FloLess degrades safely.
|
|
68
|
+
|
|
69
|
+
## Source strings the floless server resolves
|
|
70
|
+
|
|
71
|
+
`table.source` / `report.source` bind data BY NAME; floless.app resolves these:
|
|
72
|
+
|
|
73
|
+
| source | payload |
|
|
74
|
+
|---|---|
|
|
75
|
+
| `last-run-output:<appId>` | the app's latest run trace → `{appId, runId, status, rows, html}` — `table` uses the rows, `report` uses the html |
|
|
76
|
+
| `last-run-status:<appId>` | `{appId, status, finishedAt, runId}` of the latest run |
|
|
77
|
+
| `routine-status:<routineId-or-appId>` | `{id, name, workflow, kind, enabled, nextFireAt, lastRun, broken}` from the user's routines |
|
|
78
|
+
|
|
79
|
+
`<appId>` is an installed app id (`aware app list`). An unresolvable source renders a
|
|
80
|
+
quiet "no data yet" placeholder — the descriptor outlives any one run, so binding to an
|
|
81
|
+
app that hasn't run yet is fine.
|
|
82
|
+
|
|
83
|
+
## Worked example
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"version": 1,
|
|
88
|
+
"panels": [
|
|
89
|
+
{
|
|
90
|
+
"id": "morning-bom",
|
|
91
|
+
"slot": "dashboard",
|
|
92
|
+
"title": "Morning BOM",
|
|
93
|
+
"blocks": [
|
|
94
|
+
{ "type": "stat", "label": "Scope", "value": "Phase 2", "hint": "live Tekla model" },
|
|
95
|
+
{ "type": "table", "source": "last-run-output:tekla-bom-by-phase",
|
|
96
|
+
"columns": ["profile", "qty"], "sort": "qty", "sort-desc": true },
|
|
97
|
+
{ "type": "report", "source": "last-run-output:tekla-bom-by-phase", "title": "Full BOM report" },
|
|
98
|
+
{ "type": "action", "label": "Re-run BOM", "action-id": "run-app:tekla-bom-by-phase",
|
|
99
|
+
"inputs": { "phase": 2 } }
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Write → validate → done
|
|
107
|
+
|
|
108
|
+
1. **Edit atomically**: write to `~/.floless/ui/extensions.json.tmp`, then rename over
|
|
109
|
+
`~/.floless/ui/extensions.json` (the watcher fires once, on a complete file). Create
|
|
110
|
+
`~/.floless/ui/` if missing. Edit the EXISTING file when the user is changing/adding —
|
|
111
|
+
read it first; replace it wholesale only when they ask to start over.
|
|
112
|
+
2. **Validate** (the check, before or right after writing):
|
|
113
|
+
```sh
|
|
114
|
+
aware agent invoke ui validate --inputs '{"descriptor": { ...the json... }}' --json
|
|
115
|
+
# or, for a large descriptor: --inputs @args.json where args.json = {"descriptor": …}
|
|
116
|
+
```
|
|
117
|
+
Fix `errors` (the UI falls back to the default view on an invalid file and shows a
|
|
118
|
+
warning); review `warnings` (typos surface as unknown-field warnings).
|
|
119
|
+
3. **Delete the processed request** if you picked one up from `/api/requests`.
|
|
120
|
+
4. Nothing else — the app re-renders live and shows a "Customized" badge.
|
|
121
|
+
|
|
122
|
+
## Guardrails
|
|
123
|
+
|
|
124
|
+
- **Never write raw HTML/markup into descriptor strings** — it renders as literal text
|
|
125
|
+
by design. Describe; don't mark up.
|
|
126
|
+
- Every write is snapshotted to `~/.floless/ui/history/` (newest 20), and the user has
|
|
127
|
+
one-click **Undo** and **Reset to default** in the app — so be bold composing, but
|
|
128
|
+
never destructive: don't delete panels the user didn't ask to remove, and prefer
|
|
129
|
+
editing the existing document over regenerating it.
|
|
130
|
+
- The UI renders; AWARE runs; you compose. Don't put computed values in `stat` blocks
|
|
131
|
+
that a `source`-bound block could keep live — a stale hand-written number misleads.
|
package/dist/web/app.css
CHANGED
|
@@ -2483,3 +2483,167 @@ body {
|
|
|
2483
2483
|
.relnotes-skel-line::after { animation: none; }
|
|
2484
2484
|
.whats-new-detail { transition: none; }
|
|
2485
2485
|
}
|
|
2486
|
+
|
|
2487
|
+
/* ============================================================================
|
|
2488
|
+
CUSTOM PANELS — the Dashboard view + Customized badge (web/panels.js).
|
|
2489
|
+
Renders ~/.floless/ui/extensions.json with the EXISTING tokens only (shadcn
|
|
2490
|
+
dark slate-blue baseline); a user panel must be indistinguishable from
|
|
2491
|
+
shipped UI. No new colors, fonts, or aesthetics here — composition only.
|
|
2492
|
+
========================================================================== */
|
|
2493
|
+
|
|
2494
|
+
/* Header view switch (Canvas | Dashboard) — compact segmented control next to
|
|
2495
|
+
the brand; reuses the .rtn-mode pill vocabulary at header scale. */
|
|
2496
|
+
.view-toggle { display: inline-flex; gap: 4px; margin-left: 16px; }
|
|
2497
|
+
.view-btn {
|
|
2498
|
+
background: var(--surface-2); border: 1px solid var(--border-strong);
|
|
2499
|
+
color: var(--text-dim); font-size: 10.5px; text-transform: uppercase;
|
|
2500
|
+
letter-spacing: 0.1em; padding: 4px 12px; border-radius: 6px; cursor: pointer;
|
|
2501
|
+
transition: color 0.15s, border-color 0.15s, background 0.15s;
|
|
2502
|
+
}
|
|
2503
|
+
.view-btn:hover { color: var(--text); border-color: var(--accent-dim); }
|
|
2504
|
+
.view-btn.active { color: var(--accent); border-color: var(--accent-dim); background: var(--accent-soft); }
|
|
2505
|
+
.view-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
2506
|
+
/* "Dashboard updated" dot — lit when panels changed while the user was on Canvas. */
|
|
2507
|
+
.view-dot { color: var(--accent); font-size: 8px; vertical-align: 2px; margin-left: 5px; }
|
|
2508
|
+
.view-dot[hidden] { display: none; }
|
|
2509
|
+
|
|
2510
|
+
/* Customized pill — present only while custom panels exist; opens the ext menu. */
|
|
2511
|
+
.ext-badge {
|
|
2512
|
+
margin-left: 8px; padding: 3px 10px; border-radius: 999px; cursor: pointer;
|
|
2513
|
+
background: var(--accent-soft); border: 1px solid var(--accent-dim);
|
|
2514
|
+
color: var(--accent); font-size: 10.5px; letter-spacing: 0.04em;
|
|
2515
|
+
}
|
|
2516
|
+
.ext-badge:hover { border-color: var(--accent); }
|
|
2517
|
+
.ext-badge[hidden] { display: none; }
|
|
2518
|
+
|
|
2519
|
+
/* Ext menu — inherits .menu chrome; position is set per-open (under the badge). */
|
|
2520
|
+
.ext-menu { width: 290px; }
|
|
2521
|
+
.ext-menu[hidden] { display: none; }
|
|
2522
|
+
.ext-history-head {
|
|
2523
|
+
padding: 6px 12px 2px; font-size: 10px; text-transform: uppercase;
|
|
2524
|
+
letter-spacing: 0.1em; color: var(--text-muted);
|
|
2525
|
+
}
|
|
2526
|
+
.ext-history { max-height: 180px; overflow-y: auto; padding: 2px 4px 4px; }
|
|
2527
|
+
.ext-history-item {
|
|
2528
|
+
display: flex; justify-content: space-between; gap: 10px;
|
|
2529
|
+
padding: 4px 8px; border-radius: 4px; font-size: 12px; color: var(--text-dim);
|
|
2530
|
+
}
|
|
2531
|
+
.ext-history-when { color: var(--text); }
|
|
2532
|
+
.ext-history-count { font-family: var(--mono); font-size: 11px; color: var(--text-muted); }
|
|
2533
|
+
.ext-history-empty { padding: 6px 8px 8px; font-size: 12px; color: var(--text-muted); }
|
|
2534
|
+
/* Reset is destructive-looking (though recoverable) — danger on hover, never accent. */
|
|
2535
|
+
#ext-reset-confirm:hover { color: var(--err); border-color: var(--err); background: color-mix(in srgb, var(--err) 10%, transparent); }
|
|
2536
|
+
|
|
2537
|
+
/* Dashboard view — replaces the topology in the center column; chat + inspect stay. */
|
|
2538
|
+
.canvas.view-dashboard .topology,
|
|
2539
|
+
.canvas.view-dashboard .canvas-toolbar,
|
|
2540
|
+
.canvas.view-dashboard .hint,
|
|
2541
|
+
.canvas.view-dashboard .fav-bar,
|
|
2542
|
+
.canvas.view-dashboard .notes-strip,
|
|
2543
|
+
.canvas.view-dashboard .find-overlay { display: none; }
|
|
2544
|
+
.dashboard { flex: 1; min-height: 0; overflow-y: auto; padding: 18px 24px 24px; }
|
|
2545
|
+
.dashboard[hidden] { display: none; }
|
|
2546
|
+
|
|
2547
|
+
/* Notices above the panels: dim note (degraded validation / warnings) and the
|
|
2548
|
+
invalid-descriptor warning (defaults shown; file never bricks the app). */
|
|
2549
|
+
.ext-note { font-size: 12px; color: var(--text-muted); margin-bottom: 12px; }
|
|
2550
|
+
.ext-invalid {
|
|
2551
|
+
border: 1px solid var(--warn); border-radius: 8px; padding: 12px 14px;
|
|
2552
|
+
background: color-mix(in srgb, var(--warn) 7%, transparent); margin-bottom: 14px;
|
|
2553
|
+
}
|
|
2554
|
+
.ext-invalid-title { font-size: 13px; font-weight: 600; color: var(--warn); }
|
|
2555
|
+
.ext-invalid ul { margin: 8px 0 0 18px; font-size: 12px; color: var(--text-dim); font-family: var(--mono); }
|
|
2556
|
+
.ext-invalid-hint { margin-top: 8px; font-size: 12px; color: var(--text-dim); }
|
|
2557
|
+
|
|
2558
|
+
/* Panel grid + cards — same surface/border/radius family as .modal/.agent-card. */
|
|
2559
|
+
.ext-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(330px, 1fr)); gap: 14px; align-items: start; }
|
|
2560
|
+
.ext-panel {
|
|
2561
|
+
background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
|
|
2562
|
+
padding: 14px 16px 16px; display: flex; flex-direction: column; gap: 10px; min-width: 0;
|
|
2563
|
+
}
|
|
2564
|
+
.ext-panel-title { font-size: 13.5px; font-weight: 600; letter-spacing: -0.01em; }
|
|
2565
|
+
|
|
2566
|
+
/* stat — KPI card; consecutive stats render as one strip. */
|
|
2567
|
+
.ext-stat-row { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
2568
|
+
.ext-stat {
|
|
2569
|
+
flex: 1 1 90px; min-width: 0; background: var(--surface-2);
|
|
2570
|
+
border: 1px solid var(--border); border-radius: 8px; padding: 9px 12px 10px;
|
|
2571
|
+
}
|
|
2572
|
+
.ext-stat-label { font-size: 10px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-dim); }
|
|
2573
|
+
.ext-stat-value { font-size: 19px; font-weight: 600; color: var(--text); margin-top: 3px; overflow-wrap: anywhere; }
|
|
2574
|
+
.ext-stat-hint { font-size: 11px; color: var(--text-muted); margin-top: 2px; }
|
|
2575
|
+
|
|
2576
|
+
/* table — data rows from the resolved payload; columns/sort mirror html-report. */
|
|
2577
|
+
.ext-table-wrap { max-height: 320px; overflow: auto; border: 1px solid var(--border); border-radius: 6px; }
|
|
2578
|
+
.ext-table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
|
2579
|
+
.ext-table th {
|
|
2580
|
+
position: sticky; top: 0; background: var(--surface-2); text-align: left;
|
|
2581
|
+
padding: 6px 10px; font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em;
|
|
2582
|
+
color: var(--text-dim); font-weight: 600; white-space: nowrap;
|
|
2583
|
+
}
|
|
2584
|
+
.ext-table td { padding: 5px 10px; border-top: 1px solid var(--border); color: var(--text); overflow-wrap: anywhere; }
|
|
2585
|
+
.ext-table td.num { font-family: var(--mono); text-align: right; white-space: nowrap; }
|
|
2586
|
+
.ext-table-more { font-size: 11px; color: var(--text-muted); margin-top: 4px; }
|
|
2587
|
+
|
|
2588
|
+
/* text — escaped prose paragraphs. */
|
|
2589
|
+
.ext-text p { font-size: 12.5px; line-height: 1.55; color: var(--text-dim); margin: 0 0 8px; }
|
|
2590
|
+
.ext-text p:last-child { margin-bottom: 0; }
|
|
2591
|
+
|
|
2592
|
+
/* report — opens the run's HTML in the existing sandboxed viewer. */
|
|
2593
|
+
.ext-report-row { display: flex; align-items: center; gap: 10px; }
|
|
2594
|
+
.ext-report-title { font-size: 12.5px; color: var(--text-dim); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2595
|
+
.ext-report-open {
|
|
2596
|
+
flex: none; background: var(--surface-2); border: 1px solid var(--border-strong);
|
|
2597
|
+
color: var(--accent); font-size: 11.5px; padding: 5px 11px; border-radius: 6px; cursor: pointer;
|
|
2598
|
+
}
|
|
2599
|
+
.ext-report-open:hover { border-color: var(--accent-dim); background: var(--accent-soft); }
|
|
2600
|
+
|
|
2601
|
+
/* action — run-app buttons obey the same runnable gate as the header Run. */
|
|
2602
|
+
.ext-action {
|
|
2603
|
+
align-self: flex-start; background: var(--accent); border: 1px solid var(--accent);
|
|
2604
|
+
color: #ffffff; font-size: 12px; font-weight: 600; padding: 6px 14px;
|
|
2605
|
+
border-radius: 6px; cursor: pointer;
|
|
2606
|
+
}
|
|
2607
|
+
.ext-action:hover:not(:disabled) { background: var(--accent-bright); border-color: var(--accent-bright); box-shadow: 0 0 14px var(--accent-glow); }
|
|
2608
|
+
.ext-action:disabled { background: var(--surface-2); border-color: var(--border-strong); color: var(--text-muted); cursor: not-allowed; }
|
|
2609
|
+
|
|
2610
|
+
/* placeholders — missing data + forward-compat unknown blocks (dashed). */
|
|
2611
|
+
.ext-placeholder {
|
|
2612
|
+
border: 1px dashed var(--border-strong); border-radius: 6px; padding: 12px;
|
|
2613
|
+
font-size: 12px; color: var(--text-muted); text-align: center;
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
/* Empty state + the Customize composer (the dashboard's reverse channel). */
|
|
2617
|
+
.ext-empty { font-size: 13px; color: var(--text-dim); margin: 18vh auto 0; max-width: 420px; text-align: center; }
|
|
2618
|
+
.ext-composer-wrap { max-width: 640px; margin: 18px auto 0; }
|
|
2619
|
+
.ext-grid + .ext-composer-wrap { margin-top: 22px; }
|
|
2620
|
+
.ext-pending { font-size: 12px; color: var(--warn); margin-bottom: 8px; }
|
|
2621
|
+
.ext-pending[hidden] { display: none; }
|
|
2622
|
+
.ext-composer { display: flex; gap: 8px; }
|
|
2623
|
+
.ext-composer input {
|
|
2624
|
+
flex: 1; min-width: 0; background: var(--surface-2); border: 1px solid var(--border-strong);
|
|
2625
|
+
border-radius: 6px; color: var(--text); font-size: 13px; padding: 9px 12px;
|
|
2626
|
+
}
|
|
2627
|
+
.ext-composer input:focus { outline: none; border-color: var(--accent-dim); }
|
|
2628
|
+
.ext-composer input::placeholder { color: var(--text-muted); }
|
|
2629
|
+
#ext-composer-send {
|
|
2630
|
+
flex: none; background: var(--accent); border: 1px solid var(--accent); color: #ffffff;
|
|
2631
|
+
font-size: 12.5px; font-weight: 600; padding: 9px 16px; border-radius: 6px; cursor: pointer;
|
|
2632
|
+
}
|
|
2633
|
+
#ext-composer-send:hover:not(:disabled) { background: var(--accent-bright); border-color: var(--accent-bright); }
|
|
2634
|
+
#ext-composer-send:disabled { opacity: 0.6; cursor: default; }
|
|
2635
|
+
.ext-composer-hint { font-size: 11.5px; color: var(--text-muted); margin-top: 7px; }
|
|
2636
|
+
/* Post-reset escape hatch: the empty state's one-line offer of the newest history
|
|
2637
|
+
snapshot — without it, resetting hides the Customized pill and with it the only
|
|
2638
|
+
Undo control, stranding a mistaken reset. Quiet hint scale; accent link. */
|
|
2639
|
+
.ext-restore { max-width: 640px; margin: 10px auto 0; font-size: 12px; color: var(--text-dim); }
|
|
2640
|
+
.ext-restore[hidden] { display: none; }
|
|
2641
|
+
.ext-restore-btn {
|
|
2642
|
+
background: none; border: none; padding: 0; font-size: 12px; cursor: pointer;
|
|
2643
|
+
color: var(--accent); text-decoration: underline; text-underline-offset: 2px;
|
|
2644
|
+
}
|
|
2645
|
+
.ext-restore-btn:hover { color: var(--accent-bright); }
|
|
2646
|
+
.ext-restore-btn:disabled { color: var(--text-muted); cursor: default; text-decoration: none; }
|
|
2647
|
+
|
|
2648
|
+
/* Inspect-tab panels reuse the same block styles inside the inspect body. */
|
|
2649
|
+
.ext-inspect { display: flex; flex-direction: column; gap: 10px; overflow-y: auto; min-height: 0; }
|
package/dist/web/aware.js
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
const $reportTitle = document.getElementById('report-title');
|
|
23
23
|
const $reportSub = document.getElementById('report-sub');
|
|
24
24
|
const $reportOpen = document.getElementById('report-open');
|
|
25
|
+
const $reportShare = document.getElementById('report-share');
|
|
25
26
|
const $reportClose = document.getElementById('report-close');
|
|
26
27
|
|
|
27
28
|
// One persistent array the inspect Execution tab reads; we mutate in place so
|
|
@@ -908,7 +909,9 @@
|
|
|
908
909
|
try { await api('/api/run/stop', { method: 'POST' }); } catch { /* the run's own catch surfaces it */ }
|
|
909
910
|
}
|
|
910
911
|
|
|
911
|
-
|
|
912
|
+
// Share is visible exactly when the frame holds a report (the empty state HIDES it —
|
|
913
|
+
// never a disabled affordance with no path forward).
|
|
914
|
+
function paintReport(html) { $reportFrame.srcdoc = html; $reportOverlay.hidden = true; $reportOverlay.replaceChildren(); $reportShare.hidden = false; }
|
|
912
915
|
|
|
913
916
|
// Double-click the HTML Viewer node → LOAD the last report (no run; running is
|
|
914
917
|
// the header "▶ Run workflow" button's job). Shows a prompt if nothing has run yet.
|
|
@@ -925,6 +928,7 @@
|
|
|
925
928
|
paintReport(cached.html);
|
|
926
929
|
} else {
|
|
927
930
|
$reportFrame.srcdoc = '';
|
|
931
|
+
$reportShare.hidden = true; // nothing to share — hide, don't disable
|
|
928
932
|
$reportOverlay.hidden = false;
|
|
929
933
|
$reportOverlay.innerHTML = `<div>No report yet. Click <strong>▶ Run workflow</strong> (top right) to run it against the live model, then double-click to view it any time.</div>`;
|
|
930
934
|
$reportSub.textContent = 'Double-click loads the last report; ▶ Run workflow generates a fresh one.';
|
|
@@ -948,6 +952,7 @@
|
|
|
948
952
|
$reportTitle.textContent = `HTML Viewer · ${app.displayName}`;
|
|
949
953
|
$reportSub.innerHTML = `Live run of <code>${escapeHtml(nodeId)}</code>${inputBadge ? ' · ' + escapeHtml(inputBadge) : ''} — rendered from the node's output, never composed by the UI.`;
|
|
950
954
|
$reportFrame.srcdoc = ''; // clear any previously rendered report while this run is in flight
|
|
955
|
+
$reportShare.hidden = true; // the frame is blank — only a painted report is shareable
|
|
951
956
|
$reportOverlay.hidden = false;
|
|
952
957
|
$reportOverlay.innerHTML = opts.debug
|
|
953
958
|
? `<div class="spinner"></div><div>Launching the .NET debugger for <code>${escapeHtml(nodeId)}</code> — a Windows picker will pop; choose your <strong>Visual Studio</strong> to attach to <code>aware-tekla.exe</code>, then step through. The report renders when you let it finish.</div>`
|
|
@@ -1022,6 +1027,7 @@
|
|
|
1022
1027
|
|
|
1023
1028
|
function showCredentialReconnect(cred) {
|
|
1024
1029
|
$reportFrame.srcdoc = '';
|
|
1030
|
+
$reportShare.hidden = true;
|
|
1025
1031
|
$reportOverlay.hidden = false;
|
|
1026
1032
|
$reportOverlay.replaceChildren();
|
|
1027
1033
|
|
|
@@ -1267,6 +1273,51 @@
|
|
|
1267
1273
|
window.open(url, '_blank');
|
|
1268
1274
|
setTimeout(() => URL.revokeObjectURL(url), 30000);
|
|
1269
1275
|
};
|
|
1276
|
+
|
|
1277
|
+
// Share the on-screen report → a hosted floless.io link. The local server does the
|
|
1278
|
+
// upload (the bearer never reaches this page); we relay exactly the HTML the run
|
|
1279
|
+
// produced and surface the returned link. Disabled while in flight (double-click =
|
|
1280
|
+
// double upload), and the link lands in $reportSub as a real DOM anchor (mkEl, not
|
|
1281
|
+
// markup) so a server-returned URL can never inject HTML.
|
|
1282
|
+
$reportShare.onclick = async () => {
|
|
1283
|
+
const cached = currentId && lastReportByApp.get(currentId);
|
|
1284
|
+
// Share exactly what's on screen — the frame's srcdoc — so the link can never carry
|
|
1285
|
+
// a different report than the one the user is looking at (e.g. another app's cache).
|
|
1286
|
+
const html = $reportFrame.srcdoc;
|
|
1287
|
+
if (!html || !cached) return; // button is hidden in this state; belt-and-braces
|
|
1288
|
+
const app = apps.get(currentId);
|
|
1289
|
+
const title = `${app ? app.displayName : currentId}${cached.label ? ' · ' + cached.label : ''}`;
|
|
1290
|
+
$reportShare.disabled = true;
|
|
1291
|
+
const prev = $reportShare.textContent;
|
|
1292
|
+
$reportShare.textContent = 'Sharing…';
|
|
1293
|
+
try {
|
|
1294
|
+
const res = await api('/api/share-report', { method: 'POST', body: JSON.stringify({ title, html }) });
|
|
1295
|
+
await navigator.clipboard.writeText(res.url).catch(() => {});
|
|
1296
|
+
const link = mkEl('a', null, res.url.replace(/^https:\/\//, ''));
|
|
1297
|
+
link.href = res.url;
|
|
1298
|
+
link.target = '_blank';
|
|
1299
|
+
link.rel = 'noopener';
|
|
1300
|
+
$reportSub.replaceChildren(
|
|
1301
|
+
document.createTextNode('Shared — '),
|
|
1302
|
+
link,
|
|
1303
|
+
document.createTextNode(' — copied to clipboard. Anyone with the link can view this frozen report.'),
|
|
1304
|
+
);
|
|
1305
|
+
showToast('Link copied to clipboard', 'ok');
|
|
1306
|
+
} catch (e) {
|
|
1307
|
+
const err = e && e.body && e.body.error;
|
|
1308
|
+
const msg =
|
|
1309
|
+
err === 'signed_out' ? 'Your FloLess sign-in has expired — sign in again to share reports.' :
|
|
1310
|
+
err === 'no_subscription' || err === 'subscription required'
|
|
1311
|
+
? 'A FloLess subscription is required to share reports — sign in to continue.' :
|
|
1312
|
+
err === 'rate_limited' ? 'Share limit reached (30 per hour) — try again in a few minutes.' :
|
|
1313
|
+
err === 'too_large' ? 'Report is too large to share (over 5 MB).' :
|
|
1314
|
+
'Couldn’t reach FloLess — check your connection and try sharing again.';
|
|
1315
|
+
showToast(msg, 'warn');
|
|
1316
|
+
} finally {
|
|
1317
|
+
$reportShare.disabled = false;
|
|
1318
|
+
$reportShare.textContent = prev;
|
|
1319
|
+
}
|
|
1320
|
+
};
|
|
1270
1321
|
$promptSel.onchange = () => { loadApp($promptSel.value).catch(reportErr); };
|
|
1271
1322
|
|
|
1272
1323
|
$compileBtn.onclick = async () => {
|
|
@@ -2615,6 +2666,14 @@
|
|
|
2615
2666
|
}).catch(() => {});
|
|
2616
2667
|
} else if (m.type === 'request-added' || m.type === 'requests-changed') {
|
|
2617
2668
|
loadRequests();
|
|
2669
|
+
if (window.flolessPanels) window.flolessPanels.refreshPending(); // composer's "queued" line
|
|
2670
|
+
} else if (m.type === 'extensions-changed') {
|
|
2671
|
+
// The terminal AI (or undo/reset) rewrote ~/.floless/ui/extensions.json —
|
|
2672
|
+
// re-render the Dashboard / inspect-tab panels live. panels.js owns it.
|
|
2673
|
+
if (window.flolessPanels) window.flolessPanels.refresh({ fromChange: true });
|
|
2674
|
+
} else if (m.type === 'run-ended') {
|
|
2675
|
+
// Panel data bindings (last-run-output/-status) go stale after any run.
|
|
2676
|
+
if (window.flolessPanels) window.flolessPanels.refreshData();
|
|
2618
2677
|
} else if (m.type === 'routine-changed') {
|
|
2619
2678
|
loadRoutinesData();
|
|
2620
2679
|
} else if (m.type === 'routine-run-started') {
|
|
@@ -2623,6 +2682,7 @@
|
|
|
2623
2682
|
} else if (m.type === 'routine-run-ended') {
|
|
2624
2683
|
runningRoutines.delete(m.id);
|
|
2625
2684
|
loadRoutinesData();
|
|
2685
|
+
if (window.flolessPanels) window.flolessPanels.refreshData(); // routine runs also refresh bindings
|
|
2626
2686
|
} else if (m.type === 'trigger-session-changed') {
|
|
2627
2687
|
applyTriggerSnapshot(m.id, m.snapshot);
|
|
2628
2688
|
} else if (m.type === 'connect-result') {
|
|
@@ -2778,6 +2838,10 @@
|
|
|
2778
2838
|
? `${base}\nReference snapshots (read these for visual context): ${req.snapshots.join(', ')}`
|
|
2779
2839
|
: base;
|
|
2780
2840
|
}
|
|
2841
|
+
if (req.type === 'ui-customize') {
|
|
2842
|
+
const scope = req.panelId ? ` (panel "${req.panelId}")` : '';
|
|
2843
|
+
return `Customize my floless.app dashboard${scope}: ${req.instruction}. Edit ~/.floless/ui/extensions.json per the floless-app-ui skill, then check it with \`aware agent invoke ui validate\`.`;
|
|
2844
|
+
}
|
|
2781
2845
|
return '';
|
|
2782
2846
|
}
|
|
2783
2847
|
|
|
@@ -2856,15 +2920,15 @@
|
|
|
2856
2920
|
return;
|
|
2857
2921
|
}
|
|
2858
2922
|
$list.innerHTML = pendingRequests.map((r) => {
|
|
2859
|
-
const label = r.type === 'use-template' ? 'template' : 'tweak';
|
|
2860
|
-
const badgeCls = r.type === 'tweak' ? 'req-type req-type-tweak' : 'req-type';
|
|
2923
|
+
const label = r.type === 'use-template' ? 'template' : r.type === 'ui-customize' ? 'dashboard' : 'tweak';
|
|
2924
|
+
const badgeCls = r.type === 'tweak' || r.type === 'ui-customize' ? 'req-type req-type-tweak' : 'req-type';
|
|
2861
2925
|
const target = r.type === 'tweak' && r.nodeId ? ` · node <code>${escapeHtml(r.nodeId)}</code>` : '';
|
|
2862
2926
|
const when = r.createdAt ? new Date(r.createdAt) : null;
|
|
2863
2927
|
const time = when && !isNaN(when) ? `<span class="req-time">${escapeHtml(nowStamp(when))}</span>` : '';
|
|
2864
2928
|
return `
|
|
2865
2929
|
<div class="req-item">
|
|
2866
2930
|
<div class="req-info">
|
|
2867
|
-
<div class="req-head"><span class="${badgeCls}">${label}</span>
|
|
2931
|
+
<div class="req-head"><span class="${badgeCls}">${label}</span> ${r.appId ? `<code>${escapeHtml(r.appId)}</code>` : ''}${target}${time}</div>
|
|
2868
2932
|
<div class="req-instruction">${escapeHtml(instructionFor(r))}</div>
|
|
2869
2933
|
${r.snapshots && r.snapshots.length ? `<div class="req-thumbs">${r.snapshots.map((_, i) => `<button type="button" class="req-thumb" data-id="${escapeAttr(r.id)}" data-n="${i}" aria-label="View screenshot ${i + 1}"><img src="/api/requests/${encodeURIComponent(r.id)}/snapshot/${i}" alt=""></button>`).join('')}</div>` : ''}
|
|
2870
2934
|
</div>
|
|
@@ -3808,6 +3872,9 @@
|
|
|
3808
3872
|
loadTemplates().catch(() => {}),
|
|
3809
3873
|
loadRequests().catch(() => {}),
|
|
3810
3874
|
]);
|
|
3875
|
+
// Custom Panels boot is deferred to here so panels.js never fetches while the
|
|
3876
|
+
// subscription gate / AWARE bootstrap would 402/409 it.
|
|
3877
|
+
if (window.flolessPanels) window.flolessPanels.boot();
|
|
3811
3878
|
return loadApps().catch((e) => {
|
|
3812
3879
|
reportErr(e);
|
|
3813
3880
|
setComboTriggerLabel('server unreachable');
|
|
@@ -3922,6 +3989,27 @@
|
|
|
3922
3989
|
};
|
|
3923
3990
|
}
|
|
3924
3991
|
|
|
3992
|
+
// ── Custom Panels bridge (web/panels.js) ─────────────────────────────────────
|
|
3993
|
+
// panels.js renders ~/.floless/ui/extensions.json into the Dashboard view. It
|
|
3994
|
+
// loads AFTER this file and reuses these seams instead of duplicating the fetch /
|
|
3995
|
+
// report-viewer plumbing — the renderer composes nothing itself. Kept minimal on
|
|
3996
|
+
// purpose; widen only when a panel block genuinely needs another seam.
|
|
3997
|
+
window.flolessBridge = {
|
|
3998
|
+
api,
|
|
3999
|
+
// Open report HTML in the existing sandboxed HTML Viewer (same modal + srcdoc
|
|
4000
|
+
// iframe the canvas report node uses — no new sandbox surface).
|
|
4001
|
+
showHtmlReport(title, html) {
|
|
4002
|
+
$reportTitle.textContent = title;
|
|
4003
|
+
$reportSub.textContent = "Rendered from the run's output — never composed by the UI.";
|
|
4004
|
+
showModal($reportModal);
|
|
4005
|
+
paintReport(html);
|
|
4006
|
+
},
|
|
4007
|
+
// Refresh the footer requests counter after the Customize box queues a request.
|
|
4008
|
+
loadRequests,
|
|
4009
|
+
copyToClipboard,
|
|
4010
|
+
instructionFor,
|
|
4011
|
+
};
|
|
4012
|
+
|
|
3925
4013
|
// ── boot ──────────────────────────────────────────────────────────────────────
|
|
3926
4014
|
// AWARE setup is orthogonal to the subscription, so surface the bootstrap state and
|
|
3927
4015
|
// run the health poll REGARDLESS of license — and show "Setting up AWARE…" FIRST.
|
package/dist/web/index.html
CHANGED
|
@@ -33,6 +33,15 @@
|
|
|
33
33
|
</span>
|
|
34
34
|
<span class="name">FloLess</span>
|
|
35
35
|
</div>
|
|
36
|
+
<!-- Workspace view switch (Canvas = the workflow topology; Dashboard = the
|
|
37
|
+
user's custom panels from ~/.floless/ui/extensions.json). The dot on
|
|
38
|
+
Dashboard lights when panels changed while the user was on Canvas. -->
|
|
39
|
+
<div class="view-toggle" id="view-toggle" role="group" aria-label="Workspace view">
|
|
40
|
+
<button type="button" class="view-btn active" data-view="canvas" aria-pressed="true">Canvas</button>
|
|
41
|
+
<button type="button" class="view-btn" data-view="dashboard" aria-pressed="false">Dashboard<span class="view-dot" id="dash-dot" hidden aria-label="Dashboard updated">●</span></button>
|
|
42
|
+
</div>
|
|
43
|
+
<!-- Shown only while custom panels exist; opens Undo · History · Reset. -->
|
|
44
|
+
<button type="button" id="ext-badge" class="ext-badge" hidden aria-haspopup="menu" aria-expanded="false" aria-controls="ext-menu" data-tip="Your dashboard is customized — undo, history, reset to default">Customized</button>
|
|
36
45
|
</div>
|
|
37
46
|
<div class="controls">
|
|
38
47
|
<label id="wf-label">workflow</label>
|
|
@@ -78,11 +87,11 @@
|
|
|
78
87
|
<div class="resize-handle resize-handle-right" data-resize="left" data-tip="Drag to resize · double-click to reset"></div>
|
|
79
88
|
</aside>
|
|
80
89
|
|
|
81
|
-
<main class="canvas">
|
|
90
|
+
<main class="canvas" id="canvas-main">
|
|
82
91
|
<div class="panel-label">
|
|
83
|
-
<span>Canvas</span>
|
|
92
|
+
<span id="center-panel-name">Canvas</span>
|
|
84
93
|
<span class="label-end">
|
|
85
|
-
<span class="role">transparency layer · read-mostly</span>
|
|
94
|
+
<span class="role" id="center-panel-role">transparency layer · read-mostly</span>
|
|
86
95
|
</span>
|
|
87
96
|
</div>
|
|
88
97
|
<div class="find-overlay" id="find-overlay">
|
|
@@ -100,6 +109,10 @@
|
|
|
100
109
|
<button class="tb-btn" id="zoom-fit" data-tip="Fit the whole workflow to the screen (Home)">⤢</button>
|
|
101
110
|
</div>
|
|
102
111
|
</div>
|
|
112
|
+
<!-- Dashboard view — the user's custom panels (web/panels.js renders
|
|
113
|
+
~/.floless/ui/extensions.json here; the canvas children hide via
|
|
114
|
+
.canvas.view-dashboard). Composed by the terminal AI, rendered by us. -->
|
|
115
|
+
<div class="dashboard" id="dashboard" hidden></div>
|
|
103
116
|
<div class="hint" id="canvas-hint">Click any node to inspect. Star ★ a node to save it as a reusable Template.</div>
|
|
104
117
|
<div class="fav-bar" id="fav-bar">
|
|
105
118
|
<div class="fav-bar-label"><span class="star">★</span><span>Templates</span></div>
|
|
@@ -175,6 +188,35 @@
|
|
|
175
188
|
#aware-update; its body is rendered per-open in aware.js. -->
|
|
176
189
|
<div id="notes-popover" class="relnotes-popover" role="dialog" aria-modal="true" aria-label="Release notes" tabindex="-1" hidden></div>
|
|
177
190
|
|
|
191
|
+
<!-- Customized-badge menu — Undo · Reset · a read-only history list. Anchored
|
|
192
|
+
under the header badge; populated per-open in panels.js. -->
|
|
193
|
+
<div class="menu ext-menu" id="ext-menu" role="menu" hidden>
|
|
194
|
+
<button class="menu-item" id="ext-undo" role="menuitem">
|
|
195
|
+
<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>
|
|
196
|
+
<span class="menu-label">Undo last change</span>
|
|
197
|
+
</button>
|
|
198
|
+
<button class="menu-item" id="ext-reset" role="menuitem">
|
|
199
|
+
<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>
|
|
200
|
+
<span class="menu-label">Reset to default</span>
|
|
201
|
+
</button>
|
|
202
|
+
<div class="menu-divider"></div>
|
|
203
|
+
<div class="ext-history-head">History · restored by Undo, newest first</div>
|
|
204
|
+
<div class="ext-history" id="ext-history"></div>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<!-- Reset-to-default confirmation — destructive-looking but recoverable; Cancel
|
|
208
|
+
holds focus (Enter/Esc/backdrop all cancel), mirroring the routine delete. -->
|
|
209
|
+
<div class="modal-backdrop" id="ext-reset-modal">
|
|
210
|
+
<div class="modal">
|
|
211
|
+
<div class="modal-title">Reset dashboard to default</div>
|
|
212
|
+
<div class="modal-sub">Removes all custom panels and restores the FloLess default view. Your current layout is saved to history first — Undo (or your terminal AI) can bring it back.</div>
|
|
213
|
+
<div class="modal-actions">
|
|
214
|
+
<button id="ext-reset-cancel">Cancel</button>
|
|
215
|
+
<button id="ext-reset-confirm">Reset</button>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
|
|
178
220
|
<div class="menu" id="menu" role="menu">
|
|
179
221
|
<button class="menu-item" data-action="open" role="menuitem">
|
|
180
222
|
<span class="menu-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2"/></svg></span>
|
|
@@ -246,12 +288,17 @@
|
|
|
246
288
|
<div class="modal-sub" id="report-sub">Rendered from the live run — never composed by the UI.</div>
|
|
247
289
|
</div>
|
|
248
290
|
<div class="report-actions">
|
|
291
|
+
<button id="report-share" class="ghost" hidden data-tip="Share this report as a link anyone can view — hosted on floless.io">↗ Share</button>
|
|
249
292
|
<button id="report-open" class="ghost" data-tip="Open the report in a new browser tab">↗ Open</button>
|
|
250
293
|
<button id="report-close" data-tip="Close">×</button>
|
|
251
294
|
</div>
|
|
252
295
|
</div>
|
|
253
296
|
<div class="report-stage" id="report-stage">
|
|
254
|
-
|
|
297
|
+
<!-- allow-popups (+ escape): the report's own links — the "Made with FloLess"
|
|
298
|
+
badge above all — must open in a real, unsandboxed tab. Scripts stay
|
|
299
|
+
blocked and there is NO allow-same-origin: the report remains an opaque
|
|
300
|
+
origin with zero access to the app page. -->
|
|
301
|
+
<iframe id="report-frame" sandbox="allow-popups allow-popups-to-escape-sandbox" title="Report"></iframe>
|
|
255
302
|
<div class="report-overlay" id="report-overlay" hidden></div>
|
|
256
303
|
</div>
|
|
257
304
|
</div>
|
|
@@ -541,5 +588,6 @@
|
|
|
541
588
|
</div>
|
|
542
589
|
<script src="app.js"></script>
|
|
543
590
|
<script src="aware.js"></script>
|
|
591
|
+
<script src="panels.js"></script>
|
|
544
592
|
</body>
|
|
545
593
|
</html>
|