@floless/app 0.11.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 +757 -328
- package/dist/skills/floless-app-ui/SKILL.md +131 -0
- package/dist/web/app.css +164 -0
- package/dist/web/aware.js +40 -3
- package/dist/web/index.html +46 -3
- 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
|
@@ -2666,6 +2666,14 @@
|
|
|
2666
2666
|
}).catch(() => {});
|
|
2667
2667
|
} else if (m.type === 'request-added' || m.type === 'requests-changed') {
|
|
2668
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();
|
|
2669
2677
|
} else if (m.type === 'routine-changed') {
|
|
2670
2678
|
loadRoutinesData();
|
|
2671
2679
|
} else if (m.type === 'routine-run-started') {
|
|
@@ -2674,6 +2682,7 @@
|
|
|
2674
2682
|
} else if (m.type === 'routine-run-ended') {
|
|
2675
2683
|
runningRoutines.delete(m.id);
|
|
2676
2684
|
loadRoutinesData();
|
|
2685
|
+
if (window.flolessPanels) window.flolessPanels.refreshData(); // routine runs also refresh bindings
|
|
2677
2686
|
} else if (m.type === 'trigger-session-changed') {
|
|
2678
2687
|
applyTriggerSnapshot(m.id, m.snapshot);
|
|
2679
2688
|
} else if (m.type === 'connect-result') {
|
|
@@ -2829,6 +2838,10 @@
|
|
|
2829
2838
|
? `${base}\nReference snapshots (read these for visual context): ${req.snapshots.join(', ')}`
|
|
2830
2839
|
: base;
|
|
2831
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
|
+
}
|
|
2832
2845
|
return '';
|
|
2833
2846
|
}
|
|
2834
2847
|
|
|
@@ -2907,15 +2920,15 @@
|
|
|
2907
2920
|
return;
|
|
2908
2921
|
}
|
|
2909
2922
|
$list.innerHTML = pendingRequests.map((r) => {
|
|
2910
|
-
const label = r.type === 'use-template' ? 'template' : 'tweak';
|
|
2911
|
-
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';
|
|
2912
2925
|
const target = r.type === 'tweak' && r.nodeId ? ` · node <code>${escapeHtml(r.nodeId)}</code>` : '';
|
|
2913
2926
|
const when = r.createdAt ? new Date(r.createdAt) : null;
|
|
2914
2927
|
const time = when && !isNaN(when) ? `<span class="req-time">${escapeHtml(nowStamp(when))}</span>` : '';
|
|
2915
2928
|
return `
|
|
2916
2929
|
<div class="req-item">
|
|
2917
2930
|
<div class="req-info">
|
|
2918
|
-
<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>
|
|
2919
2932
|
<div class="req-instruction">${escapeHtml(instructionFor(r))}</div>
|
|
2920
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>` : ''}
|
|
2921
2934
|
</div>
|
|
@@ -3859,6 +3872,9 @@
|
|
|
3859
3872
|
loadTemplates().catch(() => {}),
|
|
3860
3873
|
loadRequests().catch(() => {}),
|
|
3861
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();
|
|
3862
3878
|
return loadApps().catch((e) => {
|
|
3863
3879
|
reportErr(e);
|
|
3864
3880
|
setComboTriggerLabel('server unreachable');
|
|
@@ -3973,6 +3989,27 @@
|
|
|
3973
3989
|
};
|
|
3974
3990
|
}
|
|
3975
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
|
+
|
|
3976
4013
|
// ── boot ──────────────────────────────────────────────────────────────────────
|
|
3977
4014
|
// AWARE setup is orthogonal to the subscription, so surface the bootstrap state and
|
|
3978
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>
|
|
@@ -546,5 +588,6 @@
|
|
|
546
588
|
</div>
|
|
547
589
|
<script src="app.js"></script>
|
|
548
590
|
<script src="aware.js"></script>
|
|
591
|
+
<script src="panels.js"></script>
|
|
549
592
|
</body>
|
|
550
593
|
</html>
|