@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.
@@ -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> <code>${escapeHtml(r.appId || '')}</code>${target}${time}</div>
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.
@@ -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>