@floless/app 0.11.0 → 0.12.1
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 +805 -335
- package/dist/skills/floless-app-ui/SKILL.md +131 -0
- package/dist/web/app.css +183 -0
- package/dist/web/aware.js +85 -7
- package/dist/web/index.html +48 -4
- package/dist/web/panels.js +660 -0
- package/launch.mjs +11 -1
- 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
|
@@ -1935,6 +1935,25 @@ body {
|
|
|
1935
1935
|
.run-state.drift { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 40%, transparent); }
|
|
1936
1936
|
.run-state.uncompiled { color: var(--text-muted); border-color: var(--border-strong); }
|
|
1937
1937
|
|
|
1938
|
+
/* Header Stop — the always-reachable escape hatch during a run, regardless of whether
|
|
1939
|
+
the HTML Viewer modal is open or closed (#39). Shown only while a run is in flight.
|
|
1940
|
+
Mirrors the in-modal .overlay-stop: a quiet destructive action — warn-tinted outline
|
|
1941
|
+
that fills on hover, never a primary. Inherits the header button's size/radius/font;
|
|
1942
|
+
only the colours are overridden here. */
|
|
1943
|
+
.run-stop-btn {
|
|
1944
|
+
background: transparent;
|
|
1945
|
+
color: var(--warn);
|
|
1946
|
+
border: 1px solid color-mix(in srgb, var(--warn) 45%, transparent);
|
|
1947
|
+
font-weight: 600;
|
|
1948
|
+
letter-spacing: 0.04em;
|
|
1949
|
+
}
|
|
1950
|
+
.run-stop-btn:hover {
|
|
1951
|
+
background: color-mix(in srgb, var(--warn) 16%, transparent);
|
|
1952
|
+
border-color: var(--warn);
|
|
1953
|
+
color: var(--text);
|
|
1954
|
+
}
|
|
1955
|
+
.run-stop-btn:disabled { opacity: 0.6; cursor: default; }
|
|
1956
|
+
|
|
1938
1957
|
/* Compile-notes strip over the canvas (CONCERNS §1). These are info-level FYIs
|
|
1939
1958
|
(e.g. AWARE's read-mode-on-exec note), so they read as info — not a warning —
|
|
1940
1959
|
and collapse to a faint one-line pill so they don't eat canvas height. */
|
|
@@ -2483,3 +2502,167 @@ body {
|
|
|
2483
2502
|
.relnotes-skel-line::after { animation: none; }
|
|
2484
2503
|
.whats-new-detail { transition: none; }
|
|
2485
2504
|
}
|
|
2505
|
+
|
|
2506
|
+
/* ============================================================================
|
|
2507
|
+
CUSTOM PANELS — the Dashboard view + Customized badge (web/panels.js).
|
|
2508
|
+
Renders ~/.floless/ui/extensions.json with the EXISTING tokens only (shadcn
|
|
2509
|
+
dark slate-blue baseline); a user panel must be indistinguishable from
|
|
2510
|
+
shipped UI. No new colors, fonts, or aesthetics here — composition only.
|
|
2511
|
+
========================================================================== */
|
|
2512
|
+
|
|
2513
|
+
/* Header view switch (Canvas | Dashboard) — compact segmented control next to
|
|
2514
|
+
the brand; reuses the .rtn-mode pill vocabulary at header scale. */
|
|
2515
|
+
.view-toggle { display: inline-flex; gap: 4px; margin-left: 16px; }
|
|
2516
|
+
.view-btn {
|
|
2517
|
+
background: var(--surface-2); border: 1px solid var(--border-strong);
|
|
2518
|
+
color: var(--text-dim); font-size: 10.5px; text-transform: uppercase;
|
|
2519
|
+
letter-spacing: 0.1em; padding: 4px 12px; border-radius: 6px; cursor: pointer;
|
|
2520
|
+
transition: color 0.15s, border-color 0.15s, background 0.15s;
|
|
2521
|
+
}
|
|
2522
|
+
.view-btn:hover { color: var(--text); border-color: var(--accent-dim); }
|
|
2523
|
+
.view-btn.active { color: var(--accent); border-color: var(--accent-dim); background: var(--accent-soft); }
|
|
2524
|
+
.view-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
2525
|
+
/* "Dashboard updated" dot — lit when panels changed while the user was on Canvas. */
|
|
2526
|
+
.view-dot { color: var(--accent); font-size: 8px; vertical-align: 2px; margin-left: 5px; }
|
|
2527
|
+
.view-dot[hidden] { display: none; }
|
|
2528
|
+
|
|
2529
|
+
/* Customized pill — present only while custom panels exist; opens the ext menu. */
|
|
2530
|
+
.ext-badge {
|
|
2531
|
+
margin-left: 8px; padding: 3px 10px; border-radius: 999px; cursor: pointer;
|
|
2532
|
+
background: var(--accent-soft); border: 1px solid var(--accent-dim);
|
|
2533
|
+
color: var(--accent); font-size: 10.5px; letter-spacing: 0.04em;
|
|
2534
|
+
}
|
|
2535
|
+
.ext-badge:hover { border-color: var(--accent); }
|
|
2536
|
+
.ext-badge[hidden] { display: none; }
|
|
2537
|
+
|
|
2538
|
+
/* Ext menu — inherits .menu chrome; position is set per-open (under the badge). */
|
|
2539
|
+
.ext-menu { width: 290px; }
|
|
2540
|
+
.ext-menu[hidden] { display: none; }
|
|
2541
|
+
.ext-history-head {
|
|
2542
|
+
padding: 6px 12px 2px; font-size: 10px; text-transform: uppercase;
|
|
2543
|
+
letter-spacing: 0.1em; color: var(--text-muted);
|
|
2544
|
+
}
|
|
2545
|
+
.ext-history { max-height: 180px; overflow-y: auto; padding: 2px 4px 4px; }
|
|
2546
|
+
.ext-history-item {
|
|
2547
|
+
display: flex; justify-content: space-between; gap: 10px;
|
|
2548
|
+
padding: 4px 8px; border-radius: 4px; font-size: 12px; color: var(--text-dim);
|
|
2549
|
+
}
|
|
2550
|
+
.ext-history-when { color: var(--text); }
|
|
2551
|
+
.ext-history-count { font-family: var(--mono); font-size: 11px; color: var(--text-muted); }
|
|
2552
|
+
.ext-history-empty { padding: 6px 8px 8px; font-size: 12px; color: var(--text-muted); }
|
|
2553
|
+
/* Reset is destructive-looking (though recoverable) — danger on hover, never accent. */
|
|
2554
|
+
#ext-reset-confirm:hover { color: var(--err); border-color: var(--err); background: color-mix(in srgb, var(--err) 10%, transparent); }
|
|
2555
|
+
|
|
2556
|
+
/* Dashboard view — replaces the topology in the center column; chat + inspect stay. */
|
|
2557
|
+
.canvas.view-dashboard .topology,
|
|
2558
|
+
.canvas.view-dashboard .canvas-toolbar,
|
|
2559
|
+
.canvas.view-dashboard .hint,
|
|
2560
|
+
.canvas.view-dashboard .fav-bar,
|
|
2561
|
+
.canvas.view-dashboard .notes-strip,
|
|
2562
|
+
.canvas.view-dashboard .find-overlay { display: none; }
|
|
2563
|
+
.dashboard { flex: 1; min-height: 0; overflow-y: auto; padding: 18px 24px 24px; }
|
|
2564
|
+
.dashboard[hidden] { display: none; }
|
|
2565
|
+
|
|
2566
|
+
/* Notices above the panels: dim note (degraded validation / warnings) and the
|
|
2567
|
+
invalid-descriptor warning (defaults shown; file never bricks the app). */
|
|
2568
|
+
.ext-note { font-size: 12px; color: var(--text-muted); margin-bottom: 12px; }
|
|
2569
|
+
.ext-invalid {
|
|
2570
|
+
border: 1px solid var(--warn); border-radius: 8px; padding: 12px 14px;
|
|
2571
|
+
background: color-mix(in srgb, var(--warn) 7%, transparent); margin-bottom: 14px;
|
|
2572
|
+
}
|
|
2573
|
+
.ext-invalid-title { font-size: 13px; font-weight: 600; color: var(--warn); }
|
|
2574
|
+
.ext-invalid ul { margin: 8px 0 0 18px; font-size: 12px; color: var(--text-dim); font-family: var(--mono); }
|
|
2575
|
+
.ext-invalid-hint { margin-top: 8px; font-size: 12px; color: var(--text-dim); }
|
|
2576
|
+
|
|
2577
|
+
/* Panel grid + cards — same surface/border/radius family as .modal/.agent-card. */
|
|
2578
|
+
.ext-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(330px, 1fr)); gap: 14px; align-items: start; }
|
|
2579
|
+
.ext-panel {
|
|
2580
|
+
background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
|
|
2581
|
+
padding: 14px 16px 16px; display: flex; flex-direction: column; gap: 10px; min-width: 0;
|
|
2582
|
+
}
|
|
2583
|
+
.ext-panel-title { font-size: 13.5px; font-weight: 600; letter-spacing: -0.01em; }
|
|
2584
|
+
|
|
2585
|
+
/* stat — KPI card; consecutive stats render as one strip. */
|
|
2586
|
+
.ext-stat-row { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
2587
|
+
.ext-stat {
|
|
2588
|
+
flex: 1 1 90px; min-width: 0; background: var(--surface-2);
|
|
2589
|
+
border: 1px solid var(--border); border-radius: 8px; padding: 9px 12px 10px;
|
|
2590
|
+
}
|
|
2591
|
+
.ext-stat-label { font-size: 10px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-dim); }
|
|
2592
|
+
.ext-stat-value { font-size: 19px; font-weight: 600; color: var(--text); margin-top: 3px; overflow-wrap: anywhere; }
|
|
2593
|
+
.ext-stat-hint { font-size: 11px; color: var(--text-muted); margin-top: 2px; }
|
|
2594
|
+
|
|
2595
|
+
/* table — data rows from the resolved payload; columns/sort mirror html-report. */
|
|
2596
|
+
.ext-table-wrap { max-height: 320px; overflow: auto; border: 1px solid var(--border); border-radius: 6px; }
|
|
2597
|
+
.ext-table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
|
2598
|
+
.ext-table th {
|
|
2599
|
+
position: sticky; top: 0; background: var(--surface-2); text-align: left;
|
|
2600
|
+
padding: 6px 10px; font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em;
|
|
2601
|
+
color: var(--text-dim); font-weight: 600; white-space: nowrap;
|
|
2602
|
+
}
|
|
2603
|
+
.ext-table td { padding: 5px 10px; border-top: 1px solid var(--border); color: var(--text); overflow-wrap: anywhere; }
|
|
2604
|
+
.ext-table td.num { font-family: var(--mono); text-align: right; white-space: nowrap; }
|
|
2605
|
+
.ext-table-more { font-size: 11px; color: var(--text-muted); margin-top: 4px; }
|
|
2606
|
+
|
|
2607
|
+
/* text — escaped prose paragraphs. */
|
|
2608
|
+
.ext-text p { font-size: 12.5px; line-height: 1.55; color: var(--text-dim); margin: 0 0 8px; }
|
|
2609
|
+
.ext-text p:last-child { margin-bottom: 0; }
|
|
2610
|
+
|
|
2611
|
+
/* report — opens the run's HTML in the existing sandboxed viewer. */
|
|
2612
|
+
.ext-report-row { display: flex; align-items: center; gap: 10px; }
|
|
2613
|
+
.ext-report-title { font-size: 12.5px; color: var(--text-dim); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2614
|
+
.ext-report-open {
|
|
2615
|
+
flex: none; background: var(--surface-2); border: 1px solid var(--border-strong);
|
|
2616
|
+
color: var(--accent); font-size: 11.5px; padding: 5px 11px; border-radius: 6px; cursor: pointer;
|
|
2617
|
+
}
|
|
2618
|
+
.ext-report-open:hover { border-color: var(--accent-dim); background: var(--accent-soft); }
|
|
2619
|
+
|
|
2620
|
+
/* action — run-app buttons obey the same runnable gate as the header Run. */
|
|
2621
|
+
.ext-action {
|
|
2622
|
+
align-self: flex-start; background: var(--accent); border: 1px solid var(--accent);
|
|
2623
|
+
color: #ffffff; font-size: 12px; font-weight: 600; padding: 6px 14px;
|
|
2624
|
+
border-radius: 6px; cursor: pointer;
|
|
2625
|
+
}
|
|
2626
|
+
.ext-action:hover:not(:disabled) { background: var(--accent-bright); border-color: var(--accent-bright); box-shadow: 0 0 14px var(--accent-glow); }
|
|
2627
|
+
.ext-action:disabled { background: var(--surface-2); border-color: var(--border-strong); color: var(--text-muted); cursor: not-allowed; }
|
|
2628
|
+
|
|
2629
|
+
/* placeholders — missing data + forward-compat unknown blocks (dashed). */
|
|
2630
|
+
.ext-placeholder {
|
|
2631
|
+
border: 1px dashed var(--border-strong); border-radius: 6px; padding: 12px;
|
|
2632
|
+
font-size: 12px; color: var(--text-muted); text-align: center;
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
/* Empty state + the Customize composer (the dashboard's reverse channel). */
|
|
2636
|
+
.ext-empty { font-size: 13px; color: var(--text-dim); margin: 18vh auto 0; max-width: 420px; text-align: center; }
|
|
2637
|
+
.ext-composer-wrap { max-width: 640px; margin: 18px auto 0; }
|
|
2638
|
+
.ext-grid + .ext-composer-wrap { margin-top: 22px; }
|
|
2639
|
+
.ext-pending { font-size: 12px; color: var(--warn); margin-bottom: 8px; }
|
|
2640
|
+
.ext-pending[hidden] { display: none; }
|
|
2641
|
+
.ext-composer { display: flex; gap: 8px; }
|
|
2642
|
+
.ext-composer input {
|
|
2643
|
+
flex: 1; min-width: 0; background: var(--surface-2); border: 1px solid var(--border-strong);
|
|
2644
|
+
border-radius: 6px; color: var(--text); font-size: 13px; padding: 9px 12px;
|
|
2645
|
+
}
|
|
2646
|
+
.ext-composer input:focus { outline: none; border-color: var(--accent-dim); }
|
|
2647
|
+
.ext-composer input::placeholder { color: var(--text-muted); }
|
|
2648
|
+
#ext-composer-send {
|
|
2649
|
+
flex: none; background: var(--accent); border: 1px solid var(--accent); color: #ffffff;
|
|
2650
|
+
font-size: 12.5px; font-weight: 600; padding: 9px 16px; border-radius: 6px; cursor: pointer;
|
|
2651
|
+
}
|
|
2652
|
+
#ext-composer-send:hover:not(:disabled) { background: var(--accent-bright); border-color: var(--accent-bright); }
|
|
2653
|
+
#ext-composer-send:disabled { opacity: 0.6; cursor: default; }
|
|
2654
|
+
.ext-composer-hint { font-size: 11.5px; color: var(--text-muted); margin-top: 7px; }
|
|
2655
|
+
/* Post-reset escape hatch: the empty state's one-line offer of the newest history
|
|
2656
|
+
snapshot — without it, resetting hides the Customized pill and with it the only
|
|
2657
|
+
Undo control, stranding a mistaken reset. Quiet hint scale; accent link. */
|
|
2658
|
+
.ext-restore { max-width: 640px; margin: 10px auto 0; font-size: 12px; color: var(--text-dim); }
|
|
2659
|
+
.ext-restore[hidden] { display: none; }
|
|
2660
|
+
.ext-restore-btn {
|
|
2661
|
+
background: none; border: none; padding: 0; font-size: 12px; cursor: pointer;
|
|
2662
|
+
color: var(--accent); text-decoration: underline; text-underline-offset: 2px;
|
|
2663
|
+
}
|
|
2664
|
+
.ext-restore-btn:hover { color: var(--accent-bright); }
|
|
2665
|
+
.ext-restore-btn:disabled { color: var(--text-muted); cursor: default; text-decoration: none; }
|
|
2666
|
+
|
|
2667
|
+
/* Inspect-tab panels reuse the same block styles inside the inspect body. */
|
|
2668
|
+
.ext-inspect { display: flex; flex-direction: column; gap: 10px; overflow-y: auto; min-height: 0; }
|
package/dist/web/aware.js
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
const $reportOpen = document.getElementById('report-open');
|
|
25
25
|
const $reportShare = document.getElementById('report-share');
|
|
26
26
|
const $reportClose = document.getElementById('report-close');
|
|
27
|
+
const $stopRunBtn = document.getElementById('stop-run-btn');
|
|
27
28
|
|
|
28
29
|
// One persistent array the inspect Execution tab reads; we mutate in place so
|
|
29
30
|
// every AGENTS[node].execution reference stays valid across re-renders.
|
|
@@ -327,7 +328,10 @@
|
|
|
327
328
|
};
|
|
328
329
|
$runState.dataset.tip = stateTips[app.runState] || '';
|
|
329
330
|
|
|
330
|
-
|
|
331
|
+
// Keep Run disabled (and the header Stop shown) if a gate re-paint lands mid-run (#39) —
|
|
332
|
+
// never let a re-render re-arm Run while a run is still in flight.
|
|
333
|
+
$runBtn.disabled = !app.runnable || reportRunning || state.running;
|
|
334
|
+
if ($stopRunBtn) $stopRunBtn.hidden = !(reportRunning || state.running);
|
|
331
335
|
$runBtn.dataset.tip = app.runnable
|
|
332
336
|
? 'Run the approved workflow'
|
|
333
337
|
: app.runState === 'drift'
|
|
@@ -896,6 +900,35 @@
|
|
|
896
900
|
// double-click LOAD the last result instantly instead of re-running.
|
|
897
901
|
const lastReportByApp = new Map();
|
|
898
902
|
|
|
903
|
+
// Reflect run-in-flight state in the ALWAYS-VISIBLE header so a run is stoppable even
|
|
904
|
+
// when the HTML Viewer modal is closed (#39). The header ■ Stop run appears for the whole
|
|
905
|
+
// duration of either run path (the modal run `reportRunning` and the inline run
|
|
906
|
+
// `state.running`) and disappears when neither is active. Run stays disabled while a run
|
|
907
|
+
// is in flight; when idle it falls back to the compile-gate's runnable state. Also tells
|
|
908
|
+
// the user, on the modal's × tooltip, that closing leaves the run going (Stop is in the
|
|
909
|
+
// header) — the canvas keeps showing progress (markCanvasRunning), by design.
|
|
910
|
+
function syncRunControls() {
|
|
911
|
+
const running = reportRunning || state.running;
|
|
912
|
+
if ($stopRunBtn) {
|
|
913
|
+
$stopRunBtn.hidden = !running;
|
|
914
|
+
// Reset to a fresh, clickable Stop each run; stopRun() flips it to "Cancelling…".
|
|
915
|
+
if (running && !cancelRequested) { $stopRunBtn.disabled = false; $stopRunBtn.textContent = '■ Stop run'; }
|
|
916
|
+
}
|
|
917
|
+
if (running) {
|
|
918
|
+
$runBtn.disabled = true;
|
|
919
|
+
} else {
|
|
920
|
+
const app = currentId && apps.get(currentId);
|
|
921
|
+
$runBtn.disabled = !(app && app.runnable);
|
|
922
|
+
}
|
|
923
|
+
// Rely on the native `disabled` attribute for AT state (paintGate also flips it); a
|
|
924
|
+
// separate aria-disabled would be redundant and could go stale on a gate re-paint.
|
|
925
|
+
if ($reportClose) {
|
|
926
|
+
$reportClose.dataset.tip = reportRunning
|
|
927
|
+
? 'Close viewer (the run keeps going — ■ Stop run is in the header)'
|
|
928
|
+
: 'Close';
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
899
932
|
// Stop the in-flight run (the escape hatch for a hung/unattached host). Marks
|
|
900
933
|
// cancelRequested so the catch shows "cancelled", clears the canvas running
|
|
901
934
|
// pulse, and asks the server to kill the `aware app run` child. The in-flight
|
|
@@ -904,8 +937,10 @@
|
|
|
904
937
|
if (!reportRunning && !state.running) return;
|
|
905
938
|
cancelRequested = true;
|
|
906
939
|
clearNodeStatus();
|
|
940
|
+
// Reflect "cancelling" on BOTH Stop surfaces — the in-modal overlay and the header.
|
|
907
941
|
const stopBtn = document.querySelector('.overlay-stop');
|
|
908
942
|
if (stopBtn) { stopBtn.disabled = true; stopBtn.textContent = 'Cancelling…'; }
|
|
943
|
+
if ($stopRunBtn) { $stopRunBtn.disabled = true; $stopRunBtn.textContent = 'Cancelling…'; }
|
|
909
944
|
try { await api('/api/run/stop', { method: 'POST' }); } catch { /* the run's own catch surfaces it */ }
|
|
910
945
|
}
|
|
911
946
|
|
|
@@ -946,6 +981,7 @@
|
|
|
946
981
|
reportRunning = true;
|
|
947
982
|
cancelRequested = false;
|
|
948
983
|
markCanvasRunning(); // paints behind the modal; visible once it's closed
|
|
984
|
+
syncRunControls(); // header ■ Stop run + Run disabled — reachable even if the modal is closed (#39)
|
|
949
985
|
|
|
950
986
|
const inputs = currentInputs();
|
|
951
987
|
const inputBadge = Object.entries(inputs).map(([k, v]) => `${k}=${v}`).join(' · ');
|
|
@@ -1004,6 +1040,7 @@
|
|
|
1004
1040
|
}
|
|
1005
1041
|
} finally {
|
|
1006
1042
|
reportRunning = false;
|
|
1043
|
+
syncRunControls(); // run ended — hide the header Stop, restore Run from the gate
|
|
1007
1044
|
}
|
|
1008
1045
|
}
|
|
1009
1046
|
|
|
@@ -1264,6 +1301,8 @@
|
|
|
1264
1301
|
// The Stop button is rebuilt into the overlay each run — delegate so one
|
|
1265
1302
|
// listener survives every innerHTML swap.
|
|
1266
1303
|
$reportOverlay.addEventListener('click', (e) => { if (e.target.closest('.overlay-stop')) stopRun(); });
|
|
1304
|
+
// The header ■ Stop run — the always-reachable twin of the overlay Stop (#39).
|
|
1305
|
+
if ($stopRunBtn) $stopRunBtn.onclick = () => stopRun();
|
|
1267
1306
|
$reportOpen.onclick = () => {
|
|
1268
1307
|
const html = $reportFrame.srcdoc;
|
|
1269
1308
|
if (!html) return;
|
|
@@ -2064,6 +2103,7 @@
|
|
|
2064
2103
|
$runBtn.disabled = true;
|
|
2065
2104
|
if ($simBtn) $simBtn.disabled = true;
|
|
2066
2105
|
$runBtn.textContent = '◆ Running…';
|
|
2106
|
+
syncRunControls(); // surface the header ■ Stop run for the inline (no-modal) run too (#39)
|
|
2067
2107
|
liveTrace.length = 0;
|
|
2068
2108
|
state.hasRun = true;
|
|
2069
2109
|
try {
|
|
@@ -2102,6 +2142,7 @@
|
|
|
2102
2142
|
$runBtn.disabled = false;
|
|
2103
2143
|
if ($simBtn) $simBtn.disabled = false;
|
|
2104
2144
|
$runBtn.textContent = '▶ Run workflow';
|
|
2145
|
+
syncRunControls(); // hide the header Stop; reconcile Run with the gate's runnable state
|
|
2105
2146
|
}
|
|
2106
2147
|
}
|
|
2107
2148
|
$runBtn.onclick = () => runApp({ simulate: false });
|
|
@@ -2592,7 +2633,7 @@
|
|
|
2592
2633
|
// (popover + what's-new). Empty until /api/health reports it → links omitted.
|
|
2593
2634
|
if (h && h.webBase) webBase = h.webBase;
|
|
2594
2635
|
const av = document.getElementById('app-version');
|
|
2595
|
-
if (av && h && h.appVersion && !shownVersion) { av.textContent = '
|
|
2636
|
+
if (av && h && h.appVersion && !shownVersion) { av.textContent = 'FloLess ' + h.appVersion; shownVersion = true; }
|
|
2596
2637
|
// After the build version is stamped, reveal the relaunch-surviving what's-new
|
|
2597
2638
|
// panel iff this is the build we just self-updated into (guarded to once).
|
|
2598
2639
|
maybeShowWhatsNew();
|
|
@@ -2666,6 +2707,14 @@
|
|
|
2666
2707
|
}).catch(() => {});
|
|
2667
2708
|
} else if (m.type === 'request-added' || m.type === 'requests-changed') {
|
|
2668
2709
|
loadRequests();
|
|
2710
|
+
if (window.flolessPanels) window.flolessPanels.refreshPending(); // composer's "queued" line
|
|
2711
|
+
} else if (m.type === 'extensions-changed') {
|
|
2712
|
+
// The terminal AI (or undo/reset) rewrote ~/.floless/ui/extensions.json —
|
|
2713
|
+
// re-render the Dashboard / inspect-tab panels live. panels.js owns it.
|
|
2714
|
+
if (window.flolessPanels) window.flolessPanels.refresh({ fromChange: true });
|
|
2715
|
+
} else if (m.type === 'run-ended') {
|
|
2716
|
+
// Panel data bindings (last-run-output/-status) go stale after any run.
|
|
2717
|
+
if (window.flolessPanels) window.flolessPanels.refreshData();
|
|
2669
2718
|
} else if (m.type === 'routine-changed') {
|
|
2670
2719
|
loadRoutinesData();
|
|
2671
2720
|
} else if (m.type === 'routine-run-started') {
|
|
@@ -2674,6 +2723,7 @@
|
|
|
2674
2723
|
} else if (m.type === 'routine-run-ended') {
|
|
2675
2724
|
runningRoutines.delete(m.id);
|
|
2676
2725
|
loadRoutinesData();
|
|
2726
|
+
if (window.flolessPanels) window.flolessPanels.refreshData(); // routine runs also refresh bindings
|
|
2677
2727
|
} else if (m.type === 'trigger-session-changed') {
|
|
2678
2728
|
applyTriggerSnapshot(m.id, m.snapshot);
|
|
2679
2729
|
} else if (m.type === 'connect-result') {
|
|
@@ -2829,6 +2879,10 @@
|
|
|
2829
2879
|
? `${base}\nReference snapshots (read these for visual context): ${req.snapshots.join(', ')}`
|
|
2830
2880
|
: base;
|
|
2831
2881
|
}
|
|
2882
|
+
if (req.type === 'ui-customize') {
|
|
2883
|
+
const scope = req.panelId ? ` (panel "${req.panelId}")` : '';
|
|
2884
|
+
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\`.`;
|
|
2885
|
+
}
|
|
2832
2886
|
return '';
|
|
2833
2887
|
}
|
|
2834
2888
|
|
|
@@ -2907,15 +2961,15 @@
|
|
|
2907
2961
|
return;
|
|
2908
2962
|
}
|
|
2909
2963
|
$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';
|
|
2964
|
+
const label = r.type === 'use-template' ? 'template' : r.type === 'ui-customize' ? 'dashboard' : 'tweak';
|
|
2965
|
+
const badgeCls = r.type === 'tweak' || r.type === 'ui-customize' ? 'req-type req-type-tweak' : 'req-type';
|
|
2912
2966
|
const target = r.type === 'tweak' && r.nodeId ? ` · node <code>${escapeHtml(r.nodeId)}</code>` : '';
|
|
2913
2967
|
const when = r.createdAt ? new Date(r.createdAt) : null;
|
|
2914
2968
|
const time = when && !isNaN(when) ? `<span class="req-time">${escapeHtml(nowStamp(when))}</span>` : '';
|
|
2915
2969
|
return `
|
|
2916
2970
|
<div class="req-item">
|
|
2917
2971
|
<div class="req-info">
|
|
2918
|
-
<div class="req-head"><span class="${badgeCls}">${label}</span>
|
|
2972
|
+
<div class="req-head"><span class="${badgeCls}">${label}</span> ${r.appId ? `<code>${escapeHtml(r.appId)}</code>` : ''}${target}${time}</div>
|
|
2919
2973
|
<div class="req-instruction">${escapeHtml(instructionFor(r))}</div>
|
|
2920
2974
|
${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
2975
|
</div>
|
|
@@ -3703,7 +3757,7 @@
|
|
|
3703
3757
|
riAwareVersion = h.awareVersion || '';
|
|
3704
3758
|
const app = currentId && apps.get(currentId);
|
|
3705
3759
|
const wf = app ? ` · workflow "${app.displayName || currentId}"` : '';
|
|
3706
|
-
$riContext.textContent = `Also sent automatically: FloLess
|
|
3760
|
+
$riContext.textContent = `Also sent automatically: FloLess ${h.appVersion || '?'}, AWARE ${h.awareVersion || '?'}${wf}`;
|
|
3707
3761
|
}).catch(() => { /* keep the generic line on a health blip */ });
|
|
3708
3762
|
showModal($riModal);
|
|
3709
3763
|
setTimeout(() => $riTitle.focus(), 0);
|
|
@@ -3859,6 +3913,9 @@
|
|
|
3859
3913
|
loadTemplates().catch(() => {}),
|
|
3860
3914
|
loadRequests().catch(() => {}),
|
|
3861
3915
|
]);
|
|
3916
|
+
// Custom Panels boot is deferred to here so panels.js never fetches while the
|
|
3917
|
+
// subscription gate / AWARE bootstrap would 402/409 it.
|
|
3918
|
+
if (window.flolessPanels) window.flolessPanels.boot();
|
|
3862
3919
|
return loadApps().catch((e) => {
|
|
3863
3920
|
reportErr(e);
|
|
3864
3921
|
setComboTriggerLabel('server unreachable');
|
|
@@ -3955,7 +4012,7 @@
|
|
|
3955
4012
|
// /api/health is un-gated, so this works even while unlicensed.
|
|
3956
4013
|
fetch('/api/health', { cache: 'no-store' })
|
|
3957
4014
|
.then((r) => r.json())
|
|
3958
|
-
.then((h) => { if (h && h.appVersion) document.getElementById('lg-version').textContent = '
|
|
4015
|
+
.then((h) => { if (h && h.appVersion) document.getElementById('lg-version').textContent = 'FloLess ' + h.appVersion; })
|
|
3959
4016
|
.catch(() => { /* version is a nicety; never block the gate on it */ });
|
|
3960
4017
|
document.getElementById('lg-signin').onclick = async () => {
|
|
3961
4018
|
const s = document.getElementById('lg-status');
|
|
@@ -3973,6 +4030,27 @@
|
|
|
3973
4030
|
};
|
|
3974
4031
|
}
|
|
3975
4032
|
|
|
4033
|
+
// ── Custom Panels bridge (web/panels.js) ─────────────────────────────────────
|
|
4034
|
+
// panels.js renders ~/.floless/ui/extensions.json into the Dashboard view. It
|
|
4035
|
+
// loads AFTER this file and reuses these seams instead of duplicating the fetch /
|
|
4036
|
+
// report-viewer plumbing — the renderer composes nothing itself. Kept minimal on
|
|
4037
|
+
// purpose; widen only when a panel block genuinely needs another seam.
|
|
4038
|
+
window.flolessBridge = {
|
|
4039
|
+
api,
|
|
4040
|
+
// Open report HTML in the existing sandboxed HTML Viewer (same modal + srcdoc
|
|
4041
|
+
// iframe the canvas report node uses — no new sandbox surface).
|
|
4042
|
+
showHtmlReport(title, html) {
|
|
4043
|
+
$reportTitle.textContent = title;
|
|
4044
|
+
$reportSub.textContent = "Rendered from the run's output — never composed by the UI.";
|
|
4045
|
+
showModal($reportModal);
|
|
4046
|
+
paintReport(html);
|
|
4047
|
+
},
|
|
4048
|
+
// Refresh the footer requests counter after the Customize box queues a request.
|
|
4049
|
+
loadRequests,
|
|
4050
|
+
copyToClipboard,
|
|
4051
|
+
instructionFor,
|
|
4052
|
+
};
|
|
4053
|
+
|
|
3976
4054
|
// ── boot ──────────────────────────────────────────────────────────────────────
|
|
3977
4055
|
// AWARE setup is orthogonal to the subscription, so surface the bootstrap state and
|
|
3978
4056
|
// 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>
|
|
@@ -51,10 +60,11 @@
|
|
|
51
60
|
<button id="browse-btn" data-tip="Browse all installed agents">⊞ Agents</button>
|
|
52
61
|
<button id="routines-btn" data-tip="Routines — run a workflow on a schedule or a live trigger">⏱ Routines</button>
|
|
53
62
|
<span class="ctl-sep" aria-hidden="true"></span>
|
|
54
|
-
<span class="run-state" id="run-state"></span>
|
|
63
|
+
<span class="run-state" id="run-state" role="status" aria-live="polite"></span>
|
|
55
64
|
<button id="compile-btn" data-tip="Compile + approve → freeze the .lock">⎙ Compile</button>
|
|
56
65
|
<button id="sim-btn" data-tip="Simulate: stub every node from its output schema — no live host is contacted. Validates the workflow's composition end-to-end, even when the real agents aren't connected yet.">Simulate</button>
|
|
57
66
|
<button id="run-btn" data-tip="Run the workflow for real against the live host (uses the inputs; renders the report node in the HTML Viewer)">▶ Run workflow</button>
|
|
67
|
+
<button id="stop-run-btn" class="run-stop-btn" type="button" aria-label="Stop the current run" data-tip="Stop the in-flight run (always reachable, even with the HTML Viewer closed)" hidden>■ Stop run</button>
|
|
58
68
|
</div>
|
|
59
69
|
</header>
|
|
60
70
|
|
|
@@ -78,11 +88,11 @@
|
|
|
78
88
|
<div class="resize-handle resize-handle-right" data-resize="left" data-tip="Drag to resize · double-click to reset"></div>
|
|
79
89
|
</aside>
|
|
80
90
|
|
|
81
|
-
<main class="canvas">
|
|
91
|
+
<main class="canvas" id="canvas-main">
|
|
82
92
|
<div class="panel-label">
|
|
83
|
-
<span>Canvas</span>
|
|
93
|
+
<span id="center-panel-name">Canvas</span>
|
|
84
94
|
<span class="label-end">
|
|
85
|
-
<span class="role">transparency layer · read-mostly</span>
|
|
95
|
+
<span class="role" id="center-panel-role">transparency layer · read-mostly</span>
|
|
86
96
|
</span>
|
|
87
97
|
</div>
|
|
88
98
|
<div class="find-overlay" id="find-overlay">
|
|
@@ -100,6 +110,10 @@
|
|
|
100
110
|
<button class="tb-btn" id="zoom-fit" data-tip="Fit the whole workflow to the screen (Home)">⤢</button>
|
|
101
111
|
</div>
|
|
102
112
|
</div>
|
|
113
|
+
<!-- Dashboard view — the user's custom panels (web/panels.js renders
|
|
114
|
+
~/.floless/ui/extensions.json here; the canvas children hide via
|
|
115
|
+
.canvas.view-dashboard). Composed by the terminal AI, rendered by us. -->
|
|
116
|
+
<div class="dashboard" id="dashboard" hidden></div>
|
|
103
117
|
<div class="hint" id="canvas-hint">Click any node to inspect. Star ★ a node to save it as a reusable Template.</div>
|
|
104
118
|
<div class="fav-bar" id="fav-bar">
|
|
105
119
|
<div class="fav-bar-label"><span class="star">★</span><span>Templates</span></div>
|
|
@@ -175,6 +189,35 @@
|
|
|
175
189
|
#aware-update; its body is rendered per-open in aware.js. -->
|
|
176
190
|
<div id="notes-popover" class="relnotes-popover" role="dialog" aria-modal="true" aria-label="Release notes" tabindex="-1" hidden></div>
|
|
177
191
|
|
|
192
|
+
<!-- Customized-badge menu — Undo · Reset · a read-only history list. Anchored
|
|
193
|
+
under the header badge; populated per-open in panels.js. -->
|
|
194
|
+
<div class="menu ext-menu" id="ext-menu" role="menu" hidden>
|
|
195
|
+
<button class="menu-item" id="ext-undo" role="menuitem">
|
|
196
|
+
<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>
|
|
197
|
+
<span class="menu-label">Undo last change</span>
|
|
198
|
+
</button>
|
|
199
|
+
<button class="menu-item" id="ext-reset" role="menuitem">
|
|
200
|
+
<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>
|
|
201
|
+
<span class="menu-label">Reset to default</span>
|
|
202
|
+
</button>
|
|
203
|
+
<div class="menu-divider"></div>
|
|
204
|
+
<div class="ext-history-head">History · restored by Undo, newest first</div>
|
|
205
|
+
<div class="ext-history" id="ext-history"></div>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<!-- Reset-to-default confirmation — destructive-looking but recoverable; Cancel
|
|
209
|
+
holds focus (Enter/Esc/backdrop all cancel), mirroring the routine delete. -->
|
|
210
|
+
<div class="modal-backdrop" id="ext-reset-modal">
|
|
211
|
+
<div class="modal">
|
|
212
|
+
<div class="modal-title">Reset dashboard to default</div>
|
|
213
|
+
<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>
|
|
214
|
+
<div class="modal-actions">
|
|
215
|
+
<button id="ext-reset-cancel">Cancel</button>
|
|
216
|
+
<button id="ext-reset-confirm">Reset</button>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
|
|
178
221
|
<div class="menu" id="menu" role="menu">
|
|
179
222
|
<button class="menu-item" data-action="open" role="menuitem">
|
|
180
223
|
<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 +589,6 @@
|
|
|
546
589
|
</div>
|
|
547
590
|
<script src="app.js"></script>
|
|
548
591
|
<script src="aware.js"></script>
|
|
592
|
+
<script src="panels.js"></script>
|
|
549
593
|
</body>
|
|
550
594
|
</html>
|