@extenshi/mcp 0.24.0 → 0.25.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/README.md +3 -0
- package/dist/build-map-app.d.ts +135 -0
- package/dist/build-map-app.d.ts.map +1 -0
- package/dist/build-map-app.js +238 -0
- package/dist/build-map-app.js.map +1 -0
- package/dist/build-map-page.d.ts +58 -0
- package/dist/build-map-page.d.ts.map +1 -0
- package/dist/build-map-page.js +1327 -0
- package/dist/build-map-page.js.map +1 -0
- package/dist/build-map-tools.d.ts +5 -1
- package/dist/build-map-tools.d.ts.map +1 -1
- package/dist/build-map-tools.js +26 -1
- package/dist/build-map-tools.js.map +1 -1
- package/dist/build-map-ui.d.ts +16 -0
- package/dist/build-map-ui.d.ts.map +1 -0
- package/dist/build-map-ui.js +20 -0
- package/dist/build-map-ui.js.map +1 -0
- package/dist/development-guide.d.ts +19 -19
- package/dist/development-guide.d.ts.map +1 -1
- package/dist/development-guide.js +3 -2
- package/dist/development-guide.js.map +1 -1
- package/dist/feedback-form-tools.d.ts +4 -4
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +19 -0
- package/dist/tools.js.map +1 -1
- package/dist/uninstall-form-tools.d.ts +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,1327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The build map page `extenshi map` serves (plan 2026-11-03-extension-build-map
|
|
3
|
+
* §4, §6.6). One self-contained HTML document — no network, no fonts to fetch —
|
|
4
|
+
* drawn from the static graph inlined here and the live snapshot the local
|
|
5
|
+
* server pushes over SSE (`/events`). Phase 1 is read-only, with three
|
|
6
|
+
* exceptions, all posted with the page token (build-map-server.ts):
|
|
7
|
+
* - a task only the author can do outside extenshi (`confirm` in the graph,
|
|
8
|
+
* such as a store account) gets a "Mark as done" button (/api/confirm);
|
|
9
|
+
* - a step the author can do themselves or skip (`manual` / `skip` in the
|
|
10
|
+
* graph) gets a form with what to give, and a skip button (/api/route).
|
|
11
|
+
* The store submits show what an upload by hand needs: the package on
|
|
12
|
+
* disk, the dashboard, the values to paste, the steps left to do there;
|
|
13
|
+
* - a step that spends credits (`credits` in the graph) shows what is left
|
|
14
|
+
* of its pool, and to an author with no account a sign-in for the free
|
|
15
|
+
* credits (/api/login) with the pairing code to check (phase 1g). Its own
|
|
16
|
+
* mark opens only once the free credits of that pool are spent.
|
|
17
|
+
* The header also offers an Update button while a newer @extenshi/cli is out
|
|
18
|
+
* (snap.update): it restarts the map on that release (/api/update), and the
|
|
19
|
+
* page reloads when the new server answers on the same port.
|
|
20
|
+
*
|
|
21
|
+
* The same page is the map in a chat (plan phase 4): the local MCP server
|
|
22
|
+
* (@extenshi/mcp, which keeps a byte-identical copy of this file) serves it as
|
|
23
|
+
* an MCP Apps resource with `app: true`. There it has no server to fetch from:
|
|
24
|
+
* it talks to the host over postMessage (JSON-RPC, MCP Apps 2026-01-26), reads
|
|
25
|
+
* the snapshot through the app-only tool `build_map_view` every few seconds,
|
|
26
|
+
* writes the author's answers through `build_map_answer` and loads variant
|
|
27
|
+
* thumbnails through `build_map_file`. Links open through the host. Sign-in
|
|
28
|
+
* stays on the browser map, which runs the pairing.
|
|
29
|
+
*
|
|
30
|
+
* Look and interaction follow the prototype in
|
|
31
|
+
* internal-docs/prototypes/extension-progress-map/index.html (catalog design
|
|
32
|
+
* tokens, both themes, pan/zoom, inspector). Everything the project or an agent
|
|
33
|
+
* wrote (extension name, fact sentences, log lines) reaches the DOM through
|
|
34
|
+
* textContent or `esc()`, never as markup.
|
|
35
|
+
*
|
|
36
|
+
* The client script is plain ES2017 inside a template literal: it must not
|
|
37
|
+
* contain backticks or `${`, so it concatenates strings.
|
|
38
|
+
*/
|
|
39
|
+
import { BUILD_CLUSTERS, BUILD_MAP_REVIEWS, BUILD_NODES, BUILD_WORLD } from '@extenshi/contracts';
|
|
40
|
+
import { MAP_PROTOCOL } from './build-map-store.js';
|
|
41
|
+
/** Line icons (24×24 path data) the nodes and the inspector draw. */
|
|
42
|
+
const ICONS = {
|
|
43
|
+
idea: 'M9 18h6M10 22h4M12 2a7 7 0 0 0-4 12.7V17h8v-2.3A7 7 0 0 0 12 2z',
|
|
44
|
+
layers: 'M12 2 2 7l10 5 10-5-10-5zM2 12l10 5 10-5M2 17l10 5 10-5',
|
|
45
|
+
globe: 'M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zM2 12h20M12 2a15 15 0 0 1 0 20M12 2a15 15 0 0 0 0 20',
|
|
46
|
+
target: 'M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zM12 7a5 5 0 1 0 0 10 5 5 0 0 0 0-10zM12 11a1 1 0 1 0 0 2 1 1 0 0 0 0-2z',
|
|
47
|
+
tag: 'M20.6 13.4 13.4 20.6a2 2 0 0 1-2.8 0L2 12V2h10l8.6 8.6a2 2 0 0 1 0 2.8zM7 7h.01',
|
|
48
|
+
search: 'M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14zM21 21l-4.3-4.3',
|
|
49
|
+
sliders: 'M4 21v-7M4 10V3M12 21v-9M12 8V3M20 21v-5M20 12V3M1 14h6M9 8h6M17 16h6',
|
|
50
|
+
folder: 'M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z',
|
|
51
|
+
file: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8zM14 2v6h6M9 13h6M9 17h6',
|
|
52
|
+
key: 'M7.5 10a5.5 5.5 0 1 0 0 11 5.5 5.5 0 0 0 0-11zM21 2l-9.6 9.6M15.5 7.5l3 3L22 7l-3-3',
|
|
53
|
+
cpu: 'M4 4h16v16H4zM9 9h6v6H9zM9 1v3M15 1v3M9 20v3M15 20v3M20 9h3M20 14h3M1 9h3M1 14h3',
|
|
54
|
+
code: 'm16 18 6-6-6-6M8 6l-6 6 6 6',
|
|
55
|
+
window: 'M3 5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2zM3 9h18M7 6h.01',
|
|
56
|
+
settings: 'M12 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6zM12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4',
|
|
57
|
+
sidebar: 'M3 5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2zM15 3v18',
|
|
58
|
+
lang: 'M5 8l6 6M4 14l6-6 2-3M2 5h12M7 2h1M22 22l-5-10-5 10M14 18h6',
|
|
59
|
+
box: 'M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16zM3.3 7 12 12l8.7-5M12 22V12',
|
|
60
|
+
shuffle: 'M16 3h5v5M4 20 21 3M21 16v5h-5M15 15l6 6M4 4l5 5',
|
|
61
|
+
image: 'M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2zM8.5 7a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zM21 15l-5-5L5 21',
|
|
62
|
+
grid: 'M3 3h7v7H3zM14 3h7v7h-7zM14 14h7v7h-7zM3 14h7v7H3z',
|
|
63
|
+
link: 'M10 13a5 5 0 0 0 7.5.5l3-3a5 5 0 0 0-7-7l-1.7 1.7M14 11a5 5 0 0 0-7.5-.5l-3 3a5 5 0 0 0 7 7l1.7-1.7',
|
|
64
|
+
camera: 'M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2zM12 9a4 4 0 1 0 0 8 4 4 0 0 0 0-8z',
|
|
65
|
+
megaphone: 'm3 11 18-5v12L3 14v-3zM11.6 16.8a3 3 0 1 1-5.8-1.6',
|
|
66
|
+
text: 'M17 6.1H3M21 12.1H3M15.1 18H3',
|
|
67
|
+
db: 'M12 2c5 0 9 1.3 9 3s-4 3-9 3-9-1.3-9-3 4-3 9-3zM21 12c0 1.7-4 3-9 3s-9-1.3-9-3M3 5v14c0 1.7 4 3 9 3s9-1.3 9-3V5',
|
|
68
|
+
shield: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z',
|
|
69
|
+
clipboard: 'M9 2h6a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1zM16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2M9 14l2 2 4-4',
|
|
70
|
+
scale: 'M16 16.5 19 7l3 9.5a4 4 0 0 1-6 0zM2 16.5 5 7l3 9.5a4 4 0 0 1-6 0zM7 21h10M12 3v18M3 7h18',
|
|
71
|
+
filecheck: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8zM14 2v6h6M9 15l2 2 4-4',
|
|
72
|
+
activity: 'M22 12h-4l-3 9L9 3l-3 9H2',
|
|
73
|
+
bug: 'M8 2l1.9 1.9M16 2l-1.9 1.9M9 7.1V6a3 3 0 0 1 6 0v1.1M12 20v-9M6.5 9A6 6 0 0 0 6 11.5v3A6 6 0 0 0 18 14.5v-3A6 6 0 0 0 17.5 9zM3 13h3M18 13h3M4 20l2.5-2M20 20l-2.5-2M4 7l2.5 2M20 7l-2.5 2',
|
|
74
|
+
message: 'M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z',
|
|
75
|
+
logout: 'M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9',
|
|
76
|
+
spark: 'M12 3l1.8 5.2L19 10l-5.2 1.8L12 17l-1.8-5.2L5 10l5.2-1.8zM19 17l.7 2 2 .7-2 .7-.7 2-.7-2-2-.7 2-.7z',
|
|
77
|
+
coins: 'M8 2a6 6 0 1 0 0 12A6 6 0 0 0 8 2zM18.1 8.7a6 6 0 1 1-7.4 7.4M7 6h1v4M16.7 13.1l.7.7-2.8 2.8',
|
|
78
|
+
card: 'M3 4h18a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2zM1 10h22M6 15h4',
|
|
79
|
+
lock: 'M5 11h14a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2zM7 11V7a5 5 0 0 1 10 0v4',
|
|
80
|
+
flask: 'M9 3h6M10 3v6L4 20a1 1 0 0 0 1 1.5h14a1 1 0 0 0 1-1.5L14 9V3M7 15h10',
|
|
81
|
+
monitor: 'M4 3h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2zM8 21h8M12 17v4',
|
|
82
|
+
scan: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10zM12 8v4M12 16h.01',
|
|
83
|
+
wrench: 'M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.8-3.8a6 6 0 0 1-7.9 7.9l-6.9 6.9a2.1 2.1 0 0 1-3-3l6.9-6.9a6 6 0 0 1 7.9-7.9l-3.8 3.8z',
|
|
84
|
+
git: 'M6 3v12M18 6a3 3 0 1 0 0 6 3 3 0 0 0 0-6zM6 15a3 3 0 1 0 0 6 3 3 0 0 0 0-6zM18 12a9 9 0 0 1-9 9',
|
|
85
|
+
user: 'M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2M12 3a4 4 0 1 0 0 8 4 4 0 0 0 0-8z',
|
|
86
|
+
checks: 'm3 17 2 2 4-4M3 7l2 2 4-4M13 6h8M13 12h8M13 18h8',
|
|
87
|
+
zip: 'M21 8v13H3V8M1 3h22v5H1zM10 12h4',
|
|
88
|
+
upload: 'M12 19V5M5 12l7-7 7 7',
|
|
89
|
+
eye: 'M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8zM12 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6z',
|
|
90
|
+
live: 'M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zM9 12l2 2 4-4',
|
|
91
|
+
check: 'm5 12 4 4L19 7',
|
|
92
|
+
lockS: 'M7 11V8a5 5 0 0 1 10 0v3M6 11h12v9H6z',
|
|
93
|
+
redo: 'M21 12a9 9 0 1 1-3-6.7M21 3v6h-6',
|
|
94
|
+
};
|
|
95
|
+
/** JSON safe to embed in a <script> element: no `</script>`, no HTML comment openers. */
|
|
96
|
+
function scriptJson(value) {
|
|
97
|
+
return JSON.stringify(value).replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/&/g, '\\u0026');
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* `token` authorises the page's writes (/api/confirm); without it, as in a
|
|
101
|
+
* saved copy, the page shows no write buttons. The token belongs to one server
|
|
102
|
+
* run, so `instance` names that run: when the page reconnects to a server
|
|
103
|
+
* restarted on the same port, the stream's hello carries another instance and
|
|
104
|
+
* the page reloads for the new token. `protocol` does the same when a newer
|
|
105
|
+
* server takes over the port.
|
|
106
|
+
*
|
|
107
|
+
* `app` makes it the map in a chat (see the top of this file): no token, no
|
|
108
|
+
* stream; the host and the MCP server's app-only tools stand in for both.
|
|
109
|
+
*/
|
|
110
|
+
export function buildMapPage(options) {
|
|
111
|
+
const graph = scriptJson({
|
|
112
|
+
clusters: BUILD_CLUSTERS,
|
|
113
|
+
nodes: BUILD_NODES,
|
|
114
|
+
world: BUILD_WORLD,
|
|
115
|
+
icons: ICONS,
|
|
116
|
+
mapReviews: BUILD_MAP_REVIEWS,
|
|
117
|
+
});
|
|
118
|
+
const session = scriptJson({
|
|
119
|
+
token: options.token ?? null,
|
|
120
|
+
instance: options.instance ?? null,
|
|
121
|
+
protocol: MAP_PROTOCOL,
|
|
122
|
+
...(options.app ? { app: options.app } : {}),
|
|
123
|
+
});
|
|
124
|
+
return `<!doctype html>
|
|
125
|
+
<html lang="en">
|
|
126
|
+
<head>
|
|
127
|
+
<meta charset="utf-8">
|
|
128
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
129
|
+
<title>Extension Build Map</title>
|
|
130
|
+
<style>${STYLE}</style>
|
|
131
|
+
</head>
|
|
132
|
+
<body>
|
|
133
|
+
${BODY}
|
|
134
|
+
<script type="application/json" id="graph">${graph}</script>
|
|
135
|
+
<script type="application/json" id="session">${session}</script>
|
|
136
|
+
<script nonce="${options.nonce}">${CLIENT}</script>
|
|
137
|
+
</body>
|
|
138
|
+
</html>
|
|
139
|
+
`;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Dark tokens: the browser map follows the system; the map in a chat (MCP Apps) is
|
|
143
|
+
* told the host's theme and sets `data-theme`, which wins over the system.
|
|
144
|
+
*/
|
|
145
|
+
const DARK_TOKENS = `
|
|
146
|
+
--bg: oklch(0.1377 0.0167 150.77); --bg-2: oklch(0.17 0.018 150); --panel: oklch(0.165 0.017 150);
|
|
147
|
+
--fg: oklch(0.9423 0.0097 72.6); --fg-2: oklch(0.74 0.015 90); --fg-3: oklch(0.55 0.015 120);
|
|
148
|
+
--line: oklch(0.27 0.02 150); --line-2: oklch(0.34 0.025 150);
|
|
149
|
+
--primary: oklch(0.6731 0.1624 144.2); --primary-soft: oklch(0.3 0.06 145); --primary-fg: oklch(0.12 0.02 150);
|
|
150
|
+
--ok: oklch(0.72 0.15 145); --ok-soft: oklch(0.28 0.06 145); --warn: oklch(0.8 0.15 75); --warn-soft: oklch(0.32 0.07 75);
|
|
151
|
+
--run: oklch(0.74 0.11 240); --run-soft: oklch(0.28 0.05 245);
|
|
152
|
+
--node-fill: oklch(0.21 0.02 150); --node-locked: oklch(0.18 0.015 150);
|
|
153
|
+
--c-found: oklch(0.78 0.12 60); --c-scaf: oklch(0.74 0.11 240); --c-vis: oklch(0.76 0.13 340); --c-comp: oklch(0.74 0.14 145);
|
|
154
|
+
--c-tele: oklch(0.72 0.14 300); --c-money: oklch(0.8 0.14 90); --c-qa: oklch(0.74 0.11 200); --c-rel: oklch(0.6731 0.1624 144.2);
|
|
155
|
+
--cluster-alpha: 10%; --shadow: 0 10px 30px oklch(0 0 0 / 0.45);
|
|
156
|
+
color-scheme: dark;
|
|
157
|
+
`;
|
|
158
|
+
const STYLE = `
|
|
159
|
+
:root {
|
|
160
|
+
--bg: oklch(0.9711 0.0074 80.72); --bg-2: oklch(0.945 0.012 78); --panel: oklch(0.99 0.004 80);
|
|
161
|
+
--fg: oklch(0.3 0.0358 30.2); --fg-2: oklch(0.52 0.03 35); --fg-3: oklch(0.68 0.02 40);
|
|
162
|
+
--line: oklch(0.87 0.014 75); --line-2: oklch(0.8 0.018 70);
|
|
163
|
+
--primary: oklch(0.7234 0.1783 41.15); --primary-soft: oklch(0.9 0.06 50); --primary-fg: oklch(0.99 0.01 80);
|
|
164
|
+
--ok: oklch(0.6 0.14 150); --ok-soft: oklch(0.92 0.06 150); --warn: oklch(0.72 0.16 65); --warn-soft: oklch(0.94 0.07 75);
|
|
165
|
+
--run: oklch(0.6 0.13 250); --run-soft: oklch(0.92 0.045 250);
|
|
166
|
+
--node-fill: oklch(0.995 0.004 80); --node-locked: oklch(0.93 0.01 78);
|
|
167
|
+
--c-found: oklch(0.72 0.14 45); --c-scaf: oklch(0.62 0.12 250); --c-vis: oklch(0.68 0.15 340); --c-comp: oklch(0.6 0.13 150);
|
|
168
|
+
--c-tele: oklch(0.58 0.16 300); --c-money: oklch(0.72 0.15 85); --c-qa: oklch(0.6 0.12 200); --c-rel: oklch(0.7234 0.1783 41.15);
|
|
169
|
+
--cluster-alpha: 7%; --shadow: 0 10px 30px oklch(0.3 0.03 40 / 0.14);
|
|
170
|
+
--font-sans: 'Space Grotesk', system-ui, -apple-system, 'Segoe UI', sans-serif;
|
|
171
|
+
--font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
172
|
+
color-scheme: light;
|
|
173
|
+
}
|
|
174
|
+
@media (prefers-color-scheme: dark) {
|
|
175
|
+
:root:not([data-theme="light"]) {${DARK_TOKENS} }
|
|
176
|
+
}
|
|
177
|
+
:root[data-theme="dark"] {${DARK_TOKENS}}
|
|
178
|
+
html, body { height: 100%; }
|
|
179
|
+
body { margin: 0; background: var(--bg); color: var(--fg); font-family: var(--font-sans); font-size: 14px; line-height: 1.45; overflow: hidden; -webkit-font-smoothing: antialiased; }
|
|
180
|
+
* { box-sizing: border-box; }
|
|
181
|
+
[hidden] { display: none !important; }
|
|
182
|
+
button { font: inherit; color: inherit; }
|
|
183
|
+
.mono { font-family: var(--font-mono); font-size: 12px; }
|
|
184
|
+
.app { display: grid; grid-template-rows: auto 1fr; height: 100%; }
|
|
185
|
+
.bar { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; padding-block: 10px; padding-left: max(16px, env(safe-area-inset-left, 0px)); padding-right: max(16px, env(safe-area-inset-right, 0px)); border-bottom: 1px solid var(--line); background: var(--panel); position: relative; z-index: 5; }
|
|
186
|
+
.brand { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
187
|
+
.brand .mark { width: 26px; height: 26px; border-radius: 7px; background: var(--primary); display: grid; place-items: center; color: var(--primary-fg); flex: none; }
|
|
188
|
+
.brand b { font-weight: 600; letter-spacing: -0.01em; white-space: nowrap; }
|
|
189
|
+
.brand .proj { color: var(--fg-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 40vw; }
|
|
190
|
+
.stats { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
|
191
|
+
.ring { position: relative; width: 36px; height: 36px; flex: none; }
|
|
192
|
+
.ring svg { width: 36px; height: 36px; transform: rotate(-90deg); }
|
|
193
|
+
.ring .track { stroke: var(--line); }
|
|
194
|
+
.ring .val { stroke: var(--primary); transition: stroke-dashoffset .5s; }
|
|
195
|
+
.ring span { position: absolute; inset: 0; display: grid; place-items: center; font-family: var(--font-mono); font-size: 10px; font-weight: 500; }
|
|
196
|
+
.chip { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: 999px; border: 1px solid var(--line); background: var(--bg); font-size: 12.5px; font-variant-numeric: tabular-nums; cursor: default; }
|
|
197
|
+
.chip i { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
|
|
198
|
+
.chip.done i { background: var(--ok); }
|
|
199
|
+
.chip.run i { background: var(--run); }
|
|
200
|
+
.chip.you { border-color: var(--primary); background: var(--primary-soft); cursor: pointer; }
|
|
201
|
+
.chip.you i { background: var(--primary); animation: blink 1.4s ease-in-out infinite; }
|
|
202
|
+
.chip.you[disabled] { opacity: .5; cursor: default; }
|
|
203
|
+
.chip.you[disabled] i { animation: none; }
|
|
204
|
+
.chip.conn i { background: var(--fg-3); }
|
|
205
|
+
.chip.conn.live i { background: var(--ok); }
|
|
206
|
+
.chip.conn.wait i { background: var(--warn); animation: blink 1.4s ease-in-out infinite; }
|
|
207
|
+
.controls { margin-left: auto; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
|
208
|
+
.btn { display: inline-flex; align-items: center; gap: 6px; height: 30px; padding: 0 10px; border-radius: 7px; border: 1px solid var(--line-2); background: var(--bg); cursor: pointer; font-size: 12.5px; font-weight: 500; white-space: nowrap; }
|
|
209
|
+
.btn:hover { border-color: var(--fg-3); }
|
|
210
|
+
.btn:focus-visible, .chip:focus-visible, .dep:focus-visible, .node:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
|
|
211
|
+
.btn.icon { width: 30px; padding: 0; justify-content: center; }
|
|
212
|
+
.btn.on { background: var(--primary-soft); border-color: var(--primary); }
|
|
213
|
+
.btn[hidden] { display: none; }
|
|
214
|
+
.btn.upd { background: var(--primary); border-color: var(--primary); color: var(--primary-fg); }
|
|
215
|
+
.btn.upd[disabled] { opacity: .6; cursor: default; }
|
|
216
|
+
.sep { width: 1px; height: 22px; background: var(--line); margin: 0 4px; }
|
|
217
|
+
.btn svg { width: 15px; height: 15px; }
|
|
218
|
+
.stage { position: relative; overflow: hidden; background: radial-gradient(circle at 1px 1px, var(--line) 1px, transparent 1.2px) 0 0 / 28px 28px; }
|
|
219
|
+
.stage svg.map { width: 100%; height: 100%; display: block; touch-action: none; cursor: grab; }
|
|
220
|
+
.stage svg.map.dragging { cursor: grabbing; }
|
|
221
|
+
.world { transform-origin: 0 0; }
|
|
222
|
+
.world.glide { transition: transform .45s cubic-bezier(.3,.7,.2,1); }
|
|
223
|
+
.cluster rect { fill: color-mix(in oklch, var(--cc) var(--cluster-alpha), transparent); stroke: color-mix(in oklch, var(--cc) 35%, transparent); stroke-width: 1; rx: 22; }
|
|
224
|
+
.cluster text { font-family: var(--font-sans); font-size: 13px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; fill: var(--cc); }
|
|
225
|
+
.cluster .hint { font-size: 11.5px; font-weight: 400; letter-spacing: 0; text-transform: none; fill: var(--fg-2); }
|
|
226
|
+
.edge { fill: none; stroke: var(--line-2); stroke-width: 1.6; transition: opacity .25s, stroke .25s; }
|
|
227
|
+
.edge.sat { stroke: color-mix(in oklch, var(--cc) 70%, var(--fg-3)); stroke-width: 2; }
|
|
228
|
+
.edge.flow { stroke: var(--run); stroke-width: 2.2; stroke-dasharray: 6 8; animation: flow 1.1s linear infinite; }
|
|
229
|
+
.edge.off { stroke-dasharray: 3 6; opacity: .4; }
|
|
230
|
+
.edge.dim { opacity: .18; }
|
|
231
|
+
.edge.hi { stroke: var(--primary); stroke-width: 2.6; opacity: 1; }
|
|
232
|
+
.edge.hi.flow { stroke: var(--run); }
|
|
233
|
+
.node { cursor: pointer; transition: opacity .25s; outline: none; }
|
|
234
|
+
.node .body { fill: var(--node-fill); stroke: var(--cc); stroke-width: 2; transition: stroke .25s, fill .25s; }
|
|
235
|
+
.node .halo { fill: var(--primary); opacity: 0; }
|
|
236
|
+
.node .pr { fill: none; stroke: var(--cc); stroke-width: 3.5; stroke-linecap: round; opacity: .9; transition: stroke-dashoffset .5s; transform: rotate(-90deg); }
|
|
237
|
+
.node .pr-track { fill: none; stroke: var(--line); stroke-width: 3.5; }
|
|
238
|
+
.node .ic { stroke: var(--cc); fill: none; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
|
|
239
|
+
.node .lbl { font-family: var(--font-sans); font-size: 12.5px; font-weight: 500; fill: var(--fg); text-anchor: middle; paint-order: stroke; stroke: var(--bg); stroke-width: 4px; stroke-linejoin: round; }
|
|
240
|
+
.node .badge circle { fill: var(--bg); stroke: var(--line-2); stroke-width: 1.5; }
|
|
241
|
+
.node .badge path, .node .badge circle.spin { stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; fill: none; }
|
|
242
|
+
.node .badge { opacity: 0; }
|
|
243
|
+
.node.st-done .body { fill: color-mix(in oklch, var(--cc) 14%, var(--node-fill)); }
|
|
244
|
+
.node.st-done .badge { opacity: 1; }
|
|
245
|
+
.node.st-done .badge circle { fill: var(--ok); stroke: var(--ok); }
|
|
246
|
+
.node.st-done .badge path { stroke: var(--primary-fg); }
|
|
247
|
+
.node.st-done .pr { stroke: var(--ok); }
|
|
248
|
+
/* Done on the author's word only: a hollow check, distinct from a proven one in both themes. */
|
|
249
|
+
.node.st-done.by-you .badge circle { fill: var(--bg); stroke: var(--ok); }
|
|
250
|
+
.node.st-done.by-you .badge path { stroke: var(--ok); }
|
|
251
|
+
.node.st-running .badge { opacity: 1; }
|
|
252
|
+
.node.st-running .badge circle.bg { fill: var(--bg); stroke: var(--run); }
|
|
253
|
+
.node.st-running .badge .spin { stroke: var(--run); stroke-dasharray: 20 30; animation: spin 1s linear infinite; transform-origin: 0 0; }
|
|
254
|
+
.node.st-running .body, .node.st-running .pr, .node.st-running .ic { stroke: var(--run); }
|
|
255
|
+
.node.st-needs_you .halo { animation: pulse 1.8s ease-out infinite; }
|
|
256
|
+
.node.st-needs_you .body { stroke: var(--primary); stroke-width: 2.5; fill: color-mix(in oklch, var(--primary) 12%, var(--node-fill)); }
|
|
257
|
+
.node.st-needs_you .ic { stroke: var(--primary); }
|
|
258
|
+
.node.st-needs_you .badge { opacity: 1; }
|
|
259
|
+
.node.st-needs_you .badge circle { fill: var(--primary); stroke: var(--primary); }
|
|
260
|
+
.node.st-needs_you .badge path { stroke: var(--primary-fg); }
|
|
261
|
+
.node.st-needs_you .lbl { font-weight: 600; }
|
|
262
|
+
.node.st-locked .body { fill: var(--node-locked); stroke: var(--line-2); }
|
|
263
|
+
.node.st-locked .ic { stroke: var(--fg-3); }
|
|
264
|
+
.node.st-locked .lbl { fill: var(--fg-2); }
|
|
265
|
+
.node.st-locked .badge { opacity: 1; }
|
|
266
|
+
.node.st-locked .badge path { stroke: var(--fg-3); }
|
|
267
|
+
.node.redo .badge, .node.stalled .badge { opacity: 1; }
|
|
268
|
+
.node.redo .badge circle, .node.stalled .badge circle { fill: var(--warn); stroke: var(--warn); }
|
|
269
|
+
.node.redo .badge path, .node.stalled .badge path { stroke: var(--fg); }
|
|
270
|
+
.node.st-inactive { opacity: .32; }
|
|
271
|
+
.node.st-inactive .body { stroke-dasharray: 4 4; fill: transparent; }
|
|
272
|
+
.node.st-inactive .ic { stroke: var(--fg-3); }
|
|
273
|
+
.node.st-inactive .lbl { fill: var(--fg-2); }
|
|
274
|
+
.hide-inactive .node.st-inactive, .hide-inactive .edge.off { display: none; }
|
|
275
|
+
.node.dim { opacity: .28; }
|
|
276
|
+
.node.st-inactive.dim { opacity: .12; }
|
|
277
|
+
.node.sel .body { stroke-width: 3.5; }
|
|
278
|
+
.node.sel .sel-ring { opacity: 1; }
|
|
279
|
+
.node .sel-ring { fill: none; stroke: var(--primary); stroke-width: 1.5; stroke-dasharray: 3 4; opacity: 0; }
|
|
280
|
+
.node .you-tag { font-family: var(--font-mono); font-size: 9px; fill: var(--primary); text-anchor: middle; letter-spacing: .06em; opacity: 0; }
|
|
281
|
+
.node.st-needs_you .you-tag { opacity: 1; }
|
|
282
|
+
.legend { position: absolute; left: max(16px, env(safe-area-inset-left, 0px)); bottom: 14px; display: flex; gap: 12px; flex-wrap: wrap; align-items: center; padding: 8px 12px; border-radius: 10px; background: color-mix(in oklch, var(--panel) 85%, transparent); border: 1px solid var(--line); font-size: 12px; color: var(--fg-2); backdrop-filter: blur(6px); max-width: calc(100% - 32px); }
|
|
283
|
+
.legend span { display: inline-flex; align-items: center; gap: 6px; }
|
|
284
|
+
.legend i { width: 12px; height: 12px; border-radius: 50%; border: 2px solid var(--fg-3); display: inline-block; }
|
|
285
|
+
.legend i.l-done { background: var(--ok); border-color: var(--ok); }
|
|
286
|
+
.legend i.l-run { border-color: var(--run); border-top-color: transparent; animation: spin 1s linear infinite; }
|
|
287
|
+
.legend i.l-you { border-color: var(--primary); background: var(--primary-soft); }
|
|
288
|
+
.legend i.l-word { background: var(--bg); border-color: var(--ok); }
|
|
289
|
+
.legend i.l-lock { background: var(--node-locked); border-color: var(--line-2); }
|
|
290
|
+
.legend i.l-off { border-style: dashed; opacity: .5; }
|
|
291
|
+
.notice { position: absolute; top: 12px; left: 50%; transform: translateX(-50%); max-width: min(560px, calc(100% - 32px)); padding: 8px 14px; border-radius: 10px; background: var(--warn-soft); border: 1px solid var(--warn); font-size: 13px; z-index: 3; display: none; }
|
|
292
|
+
.notice.show { display: block; }
|
|
293
|
+
.notice code { font-family: var(--font-mono); font-size: 12px; }
|
|
294
|
+
.panel { position: absolute; top: 0; right: 0; bottom: 0; width: min(380px, 100%); background: var(--panel); border-left: 1px solid var(--line); transform: translateX(100%); transition: transform .3s cubic-bezier(.3,.7,.2,1); display: flex; flex-direction: column; box-shadow: var(--shadow); z-index: 4; padding-right: env(safe-area-inset-right, 0px); }
|
|
295
|
+
.panel.open { transform: none; }
|
|
296
|
+
/* With a step open, a notice centres on the part of the map left of the panel (a chat's map is narrow). */
|
|
297
|
+
.stage:has(.panel.open) .notice { left: calc((100% - min(380px, 100%)) / 2); max-width: min(560px, calc(100% - min(380px, 100%) - 32px)); }
|
|
298
|
+
.panel header { display: flex; align-items: flex-start; gap: 10px; padding: 16px 16px 12px; border-bottom: 1px solid var(--line); }
|
|
299
|
+
.panel header .pico { width: 40px; height: 40px; border-radius: 11px; display: grid; place-items: center; flex: none; background: color-mix(in oklch, var(--cc) 14%, transparent); color: var(--cc); }
|
|
300
|
+
.panel header .pico svg { width: 22px; height: 22px; stroke: currentColor; fill: none; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
|
|
301
|
+
.panel header h2 { margin: 0; font-size: 16px; font-weight: 600; line-height: 1.25; }
|
|
302
|
+
.panel header .sub { color: var(--fg-2); font-size: 12px; margin-top: 2px; }
|
|
303
|
+
.panel header .x { margin-left: auto; }
|
|
304
|
+
.pbody { overflow: auto; padding: 14px 16px 24px; display: flex; flex-direction: column; gap: 16px; min-width: 0; }
|
|
305
|
+
.status { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
306
|
+
.pill { display: inline-flex; align-items: center; gap: 6px; padding: 3px 9px; border-radius: 999px; font-size: 12px; font-weight: 500; border: 1px solid transparent; }
|
|
307
|
+
.pill.st-done { background: var(--ok-soft); color: var(--ok); }
|
|
308
|
+
.pill.st-running { background: var(--run-soft); color: var(--run); }
|
|
309
|
+
.pill.st-needs_you { background: var(--primary-soft); color: var(--fg); border-color: var(--primary); }
|
|
310
|
+
.pill.st-locked, .pill.st-ready { background: var(--bg-2); color: var(--fg-2); }
|
|
311
|
+
.pill.st-inactive { background: var(--bg-2); color: var(--fg-3); }
|
|
312
|
+
.pill.warn { background: var(--warn-soft); color: var(--fg); }
|
|
313
|
+
.pill .sp { width: 10px; height: 10px; border-radius: 50%; border: 2px solid currentColor; border-top-color: transparent; animation: spin 1s linear infinite; }
|
|
314
|
+
.pill .dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; }
|
|
315
|
+
.actor { color: var(--fg-2); font-size: 12px; }
|
|
316
|
+
.actor b { color: var(--fg); font-weight: 500; }
|
|
317
|
+
.pbar { height: 6px; border-radius: 3px; background: var(--line); overflow: hidden; }
|
|
318
|
+
.pbar i { display: block; height: 100%; background: var(--cc); width: 0; transition: width .5s; border-radius: 3px; }
|
|
319
|
+
.pct { font-family: var(--font-mono); font-size: 12px; color: var(--fg-2); display: flex; justify-content: space-between; }
|
|
320
|
+
.desc { color: var(--fg-2); margin: 0; }
|
|
321
|
+
.fact { margin: 0; font-size: 13px; padding: 8px 10px; border-radius: 8px; background: var(--ok-soft); }
|
|
322
|
+
.why { display: flex; flex-direction: column; gap: 6px; padding: 8px 10px; border-radius: 8px; background: var(--ok-soft); font-size: 13px; }
|
|
323
|
+
.why.b-author { background: var(--bg-2); border: 1px solid var(--line-2); }
|
|
324
|
+
.why.unknown { background: var(--bg-2); color: var(--fg-2); }
|
|
325
|
+
.why p { margin: 0; }
|
|
326
|
+
.basis { align-self: flex-start; font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .04em; text-transform: uppercase; color: var(--fg-2); }
|
|
327
|
+
.where { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; font-size: 12px; color: var(--fg-2); }
|
|
328
|
+
.where code { font-family: var(--font-mono); font-size: 11.5px; color: var(--fg); word-break: break-all; }
|
|
329
|
+
.where a { color: var(--fg); word-break: break-all; }
|
|
330
|
+
.where .sha { font-family: var(--font-mono); font-size: 10.5px; color: var(--fg-3); }
|
|
331
|
+
.ask .note { height: 32px; border-radius: 8px; border: 1px solid var(--line-2); background: var(--panel); color: var(--fg); font: inherit; font-size: 13px; padding: 0 10px; }
|
|
332
|
+
.ask .note:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
|
|
333
|
+
.ask .hint { font-size: 11.5px; color: var(--fg-3); }
|
|
334
|
+
.hist { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; font-size: 12.5px; }
|
|
335
|
+
.hist li { display: flex; gap: 8px; color: var(--fg-2); }
|
|
336
|
+
.hist li .t { font-family: var(--font-mono); font-size: 11px; color: var(--fg-3); flex: none; padding-top: 2px; min-width: 52px; }
|
|
337
|
+
.hist li > div { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
|
338
|
+
.hist li.revoked > div > span:first-child { text-decoration: line-through; text-decoration-color: var(--fg-3); }
|
|
339
|
+
.hist li b { color: var(--fg); font-weight: 500; }
|
|
340
|
+
.hist .note { color: var(--fg); font-style: italic; }
|
|
341
|
+
.tool { display: inline-flex; align-items: center; gap: 6px; font-family: var(--font-mono); font-size: 11.5px; padding: 4px 8px; border-radius: 6px; background: var(--bg-2); border: 1px solid var(--line); color: var(--fg-2); max-width: 100%; }
|
|
342
|
+
.tool code { color: var(--fg); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
343
|
+
h3 { margin: 0; font-size: 11px; font-weight: 600; letter-spacing: .09em; text-transform: uppercase; color: var(--fg-3); }
|
|
344
|
+
.sec { display: flex; flex-direction: column; gap: 8px; }
|
|
345
|
+
.todos { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
|
|
346
|
+
.todos li { display: flex; align-items: flex-start; gap: 9px; padding: 7px 9px; border-radius: 7px; background: var(--bg-2); }
|
|
347
|
+
.todos li .m { width: 16px; height: 16px; flex: none; margin-top: 2px; border-radius: 50%; border: 1.5px solid var(--line-2); display: grid; place-items: center; }
|
|
348
|
+
.todos li.done .m { background: var(--ok); border-color: var(--ok); }
|
|
349
|
+
.todos li.done .m svg { width: 10px; height: 10px; stroke: var(--primary-fg); fill: none; stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round; }
|
|
350
|
+
.todos li.run .m { border-color: var(--run); border-top-color: transparent; animation: spin .9s linear infinite; }
|
|
351
|
+
.todos li.wait .m { border-color: var(--primary); background: var(--primary-soft); }
|
|
352
|
+
.todos li.wait { background: var(--primary-soft); }
|
|
353
|
+
.todos li .who { margin-left: auto; font-family: var(--font-mono); font-size: 10px; color: var(--fg-3); white-space: nowrap; padding-top: 3px; }
|
|
354
|
+
.todos li.wait .who { color: var(--primary); }
|
|
355
|
+
.todos li.done span, .todos li.done .who { color: var(--fg-2); }
|
|
356
|
+
.deps { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
357
|
+
.dep { display: inline-flex; align-items: center; gap: 6px; padding: 3px 9px 3px 6px; border-radius: 999px; border: 1px solid var(--line); background: var(--bg); font-size: 12px; cursor: pointer; }
|
|
358
|
+
.dep:hover { border-color: var(--fg-3); }
|
|
359
|
+
.dep i { width: 8px; height: 8px; border-radius: 50%; display: inline-block; background: var(--line-2); }
|
|
360
|
+
.dep.st-done i { background: var(--ok); }
|
|
361
|
+
.dep.st-running i { background: var(--run); }
|
|
362
|
+
.dep.st-needs_you i { background: var(--primary); }
|
|
363
|
+
.dep.st-inactive { opacity: .5; text-decoration: line-through; }
|
|
364
|
+
.ask { border: 1px solid var(--primary); background: color-mix(in oklch, var(--primary-soft) 60%, var(--panel)); border-radius: 10px; padding: 12px; display: flex; flex-direction: column; gap: 6px; }
|
|
365
|
+
.ask.muted { border-color: var(--line); background: var(--bg-2); }
|
|
366
|
+
.ask h4 { margin: 0; font-size: 13.5px; font-weight: 600; }
|
|
367
|
+
.ask p { margin: 0; color: var(--fg-2); font-size: 13px; }
|
|
368
|
+
.ask .act { align-self: flex-start; margin-top: 4px; height: 32px; padding: 0 14px; border-radius: 8px; border: 1px solid var(--primary); background: var(--primary); color: var(--primary-fg); font: inherit; font-size: 13px; font-weight: 600; cursor: pointer; }
|
|
369
|
+
.ask .act:hover { filter: brightness(1.06); }
|
|
370
|
+
.ask .act:disabled { opacity: .6; cursor: default; filter: none; }
|
|
371
|
+
.ask .act:focus-visible, .todos .undo:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
|
|
372
|
+
.ask p.err { color: var(--fg); background: var(--warn-soft); border-radius: 6px; padding: 6px 8px; }
|
|
373
|
+
.ask .act.ghost { background: var(--panel); color: var(--fg); border-color: var(--line-2); }
|
|
374
|
+
.ask .row { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
|
|
375
|
+
.ask textarea.note { height: auto; min-height: 96px; padding: 8px 10px; resize: vertical; line-height: 1.4; }
|
|
376
|
+
.ask .code { align-self: flex-start; font-family: var(--font-mono); font-size: 18px; font-weight: 600; letter-spacing: .12em; padding: 4px 10px; border-radius: 8px; border: 1px solid var(--line-2); background: var(--panel); }
|
|
377
|
+
.ask a { color: var(--primary); }
|
|
378
|
+
.ask .req { margin: 0; padding-left: 18px; color: var(--fg-2); font-size: 12.5px; display: flex; flex-direction: column; gap: 2px; }
|
|
379
|
+
.ask .picks { display: flex; gap: 14px; flex-wrap: wrap; font-size: 13px; }
|
|
380
|
+
.ask .picks label { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; }
|
|
381
|
+
.ask .kv { display: flex; flex-direction: column; gap: 4px; }
|
|
382
|
+
.ask .kv > span { font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--fg-3); }
|
|
383
|
+
.copyrow { display: flex; align-items: flex-start; gap: 8px; min-width: 0; }
|
|
384
|
+
.copyrow code { flex: 1; min-width: 0; font-family: var(--font-mono); font-size: 11.5px; padding: 5px 8px; border-radius: 6px; background: var(--panel); border: 1px solid var(--line); overflow-wrap: anywhere; white-space: pre-wrap; }
|
|
385
|
+
.copy { flex: none; height: 26px; padding: 0 9px; border-radius: 6px; border: 1px solid var(--line-2); background: var(--panel); font-size: 12px; cursor: pointer; }
|
|
386
|
+
.copy:hover { border-color: var(--fg-3); }
|
|
387
|
+
.copy:focus-visible, .ask .picks input:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
|
|
388
|
+
.ask a { color: var(--fg); }
|
|
389
|
+
.ask .tolist { margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: 4px; font-size: 12.5px; }
|
|
390
|
+
.ask .tolist b { font-weight: 600; }
|
|
391
|
+
.variants { display: grid; grid-template-columns: repeat(auto-fill, minmax(124px, 1fr)); gap: 8px; }
|
|
392
|
+
.variant { border: 1px solid var(--line-2); border-radius: 10px; background: var(--panel); padding: 8px; display: flex; flex-direction: column; gap: 6px; min-width: 0; }
|
|
393
|
+
.variant.picked { border-color: var(--ok); box-shadow: inset 0 0 0 1px var(--ok); }
|
|
394
|
+
.variant .thumb { aspect-ratio: 1; border-radius: 8px; background: var(--bg-2); display: grid; place-items: center; overflow: hidden; color: var(--fg-3); font-family: var(--font-mono); font-size: 20px; }
|
|
395
|
+
.variant .thumb img { width: 100%; height: 100%; object-fit: contain; }
|
|
396
|
+
.variant .vl { font-size: 13px; font-weight: 600; overflow-wrap: anywhere; }
|
|
397
|
+
.variant .num { font-family: var(--font-mono); font-weight: 500; color: var(--fg-3); margin-right: 4px; }
|
|
398
|
+
.variant a { font-size: 12.5px; }
|
|
399
|
+
.ask .variant .act { margin-top: 0; align-self: stretch; height: 30px; padding: 0 8px; }
|
|
400
|
+
.variant .tag { font-size: 12px; font-weight: 600; color: var(--ok); }
|
|
401
|
+
.ask .quote { color: var(--fg); font-style: italic; overflow-wrap: anywhere; }
|
|
402
|
+
.rounds { font-size: 12.5px; color: var(--fg-2); }
|
|
403
|
+
.rounds summary { cursor: pointer; }
|
|
404
|
+
.rounds ul { margin: 6px 0 0; padding-left: 18px; display: flex; flex-direction: column; gap: 4px; }
|
|
405
|
+
.todos .undo { margin-left: auto; flex: none; border: 0; background: none; padding: 2px 0 0; font: inherit; font-family: var(--font-mono); font-size: 10px; color: var(--fg-2); text-decoration: underline; cursor: pointer; }
|
|
406
|
+
.todos .undo:hover { color: var(--fg); }
|
|
407
|
+
.todos .undo:disabled { cursor: default; text-decoration: none; }
|
|
408
|
+
.opts { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
|
|
409
|
+
.opts li { display: flex; align-items: center; gap: 8px; font-size: 13px; }
|
|
410
|
+
.opts li i { width: 10px; height: 10px; border-radius: 50%; border: 2px solid var(--line-2); }
|
|
411
|
+
.opts li.on i { background: var(--primary); border-color: var(--primary); }
|
|
412
|
+
.opts li:not(.on) { color: var(--fg-3); }
|
|
413
|
+
.log { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; font-size: 12.5px; }
|
|
414
|
+
.log li { display: flex; gap: 8px; color: var(--fg-2); }
|
|
415
|
+
.log li .t { font-family: var(--font-mono); font-size: 11px; color: var(--fg-3); flex: none; padding-top: 2px; }
|
|
416
|
+
.log li b { color: var(--fg); font-weight: 500; }
|
|
417
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
418
|
+
@keyframes flow { to { stroke-dashoffset: -28; } }
|
|
419
|
+
@keyframes pulse { 0% { transform: scale(1); opacity: .45; } 100% { transform: scale(2.1); opacity: 0; } }
|
|
420
|
+
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }
|
|
421
|
+
@media (prefers-reduced-motion: reduce) {
|
|
422
|
+
.edge.flow, .node .halo, .chip i, .legend i.l-run, .pill .sp, .todos li.run .m, .node.st-running .badge .spin { animation: none !important; }
|
|
423
|
+
.node.st-needs_you .halo { opacity: .25; transform: scale(1.5); }
|
|
424
|
+
.world.glide, .panel { transition: none; }
|
|
425
|
+
}
|
|
426
|
+
@media (max-width: 720px) {
|
|
427
|
+
.bar { gap: 10px; }
|
|
428
|
+
.brand .proj { display: none; }
|
|
429
|
+
.panel { top: auto; width: 100%; max-height: 62%; border-left: 0; border-top: 1px solid var(--line); border-radius: 16px 16px 0 0; transform: translateY(100%); padding-right: 0; padding-bottom: env(safe-area-inset-bottom, 0px); }
|
|
430
|
+
.panel.open { transform: none; }
|
|
431
|
+
.stage:has(.panel.open) .notice { left: 50%; max-width: min(560px, calc(100% - 32px)); }
|
|
432
|
+
.legend { bottom: 10px; font-size: 11px; gap: 8px; }
|
|
433
|
+
}
|
|
434
|
+
`;
|
|
435
|
+
const BODY = `<div class="app">
|
|
436
|
+
<header class="bar">
|
|
437
|
+
<div class="brand">
|
|
438
|
+
<span class="mark" aria-hidden="true"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 12h-2a2 2 0 0 0-2 2v2a2 2 0 0 1-2 2h-2v-2a2 2 0 0 0-4 0v2H6a2 2 0 0 1-2-2v-2h2a2 2 0 0 0 0-4H4V8a2 2 0 0 1 2-2h2V4a2 2 0 0 1 4 0v2h2a2 2 0 0 1 2 2v2h2a2 2 0 0 1 2 2z"/></svg></span>
|
|
439
|
+
<b>Extension Build Map</b>
|
|
440
|
+
<span class="proj mono" id="projName"></span>
|
|
441
|
+
</div>
|
|
442
|
+
<div class="stats">
|
|
443
|
+
<div class="ring" title="Active steps done"><svg viewBox="0 0 36 36"><circle class="track" cx="18" cy="18" r="15" fill="none" stroke-width="3.5"/><circle class="val" id="ringVal" cx="18" cy="18" r="15" fill="none" stroke-width="3.5" stroke-linecap="round" stroke-dasharray="94.25" stroke-dashoffset="94.25"/></svg><span id="ringPct">0%</span></div>
|
|
444
|
+
<span class="chip done"><i></i><span id="cDone">0 / 0 done</span></span>
|
|
445
|
+
<span class="chip run"><i></i><span id="cRun">0 running</span></span>
|
|
446
|
+
<button class="chip you" id="cYou" title="Jump to the next step waiting on you" disabled><i></i><span id="cYouT">0 need you</span></button>
|
|
447
|
+
<span class="chip conn wait" id="cConn" title="Connection to the local map server"><i></i><span id="cConnT">connecting</span></span>
|
|
448
|
+
</div>
|
|
449
|
+
<div class="controls">
|
|
450
|
+
<button class="btn upd" id="bUpdate" hidden></button>
|
|
451
|
+
<button class="btn on" id="bInactive" title="Show steps this extension does not need">Show inactive</button>
|
|
452
|
+
<span class="sep"></span>
|
|
453
|
+
<button class="btn icon" id="bZoomOut" title="Zoom out" aria-label="Zoom out"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M5 12h14"/></svg></button>
|
|
454
|
+
<button class="btn" id="bFit" title="Fit the whole map">Fit</button>
|
|
455
|
+
<button class="btn icon" id="bZoomIn" title="Zoom in" aria-label="Zoom in"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg></button>
|
|
456
|
+
<button class="btn" id="bFull" title="Show the map full screen" hidden>Full screen</button>
|
|
457
|
+
</div>
|
|
458
|
+
</header>
|
|
459
|
+
<main class="stage" id="stage">
|
|
460
|
+
<svg class="map" id="map" xmlns="http://www.w3.org/2000/svg">
|
|
461
|
+
<g class="world" id="world"><g id="gClusters"></g><g id="gEdges"></g><g id="gNodes"></g></g>
|
|
462
|
+
</svg>
|
|
463
|
+
<div class="notice" id="notice" role="status"></div>
|
|
464
|
+
<div class="legend" aria-label="Legend">
|
|
465
|
+
<span><i class="l-done"></i>done</span>
|
|
466
|
+
<span><i class="l-run"></i>agent working</span>
|
|
467
|
+
<span><i class="l-you"></i>waiting on you</span>
|
|
468
|
+
<span><i class="l-word"></i>done on your word</span>
|
|
469
|
+
<span><i class="l-lock"></i>locked by dependencies</span>
|
|
470
|
+
<span><i class="l-off"></i>not needed, or skipped</span>
|
|
471
|
+
</div>
|
|
472
|
+
<aside class="panel" id="panel" aria-live="polite">
|
|
473
|
+
<header>
|
|
474
|
+
<div class="pico" id="pIco"></div>
|
|
475
|
+
<div style="min-width:0;flex:1"><h2 id="pTitle">Select a step</h2><div class="sub" id="pSub"></div></div>
|
|
476
|
+
<button class="btn icon x" id="pClose" title="Close" aria-label="Close"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
|
|
477
|
+
</header>
|
|
478
|
+
<div class="pbody" id="pBody"></div>
|
|
479
|
+
</aside>
|
|
480
|
+
</main>
|
|
481
|
+
</div>`;
|
|
482
|
+
const CLIENT = `
|
|
483
|
+
(function () {
|
|
484
|
+
'use strict';
|
|
485
|
+
var G = JSON.parse(document.getElementById('graph').textContent);
|
|
486
|
+
var ICONS = G.icons, WORLD = G.world, NODES = G.nodes, CLUSTERS = G.clusters;
|
|
487
|
+
var COLORS = { found: 'var(--c-found)', scaf: 'var(--c-scaf)', vis: 'var(--c-vis)', comp: 'var(--c-comp)', tele: 'var(--c-tele)', money: 'var(--c-money)', qa: 'var(--c-qa)', rel: 'var(--c-rel)' };
|
|
488
|
+
var byId = {}, children = {};
|
|
489
|
+
NODES.forEach(function (n) { byId[n.id] = n; });
|
|
490
|
+
NODES.forEach(function (n) { n.deps.forEach(function (d) { (children[d] = children[d] || []).push(n.id); }); });
|
|
491
|
+
var SV = 'http://www.w3.org/2000/svg', NODE_R = 27, C = 2 * Math.PI * (NODE_R + 5.5);
|
|
492
|
+
var $ = function (id) { return document.getElementById(id); };
|
|
493
|
+
var esc = function (s) { return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; }); };
|
|
494
|
+
var el = function (tag, attrs, parent) { var e = document.createElementNS(SV, tag); for (var k in attrs) e.setAttribute(k, attrs[k]); if (parent) parent.appendChild(e); return e; };
|
|
495
|
+
var SESSION = JSON.parse(document.getElementById('session').textContent);
|
|
496
|
+
// The map in a chat (MCP Apps), with the variant thumbnails the MCP server has sent so far.
|
|
497
|
+
var APP = SESSION.app || null, thumbs = {}, thumbAsked = {};
|
|
498
|
+
var snap = null, selected = null, showInactive = true, edgeEls = {}, nodeEls = {}, firstSnapshot = true;
|
|
499
|
+
// The author's write (a mark, or a route): in flight (or saved, until the snapshot shows it), and the last failure.
|
|
500
|
+
// What the author typed belongs to the step (and task) it was typed for: another step never sends it.
|
|
501
|
+
var pending = null, writeError = null, drafts = {};
|
|
502
|
+
// A sign-in the author started (until the snapshot shows its new state), and why it did not start.
|
|
503
|
+
var loginWait = null, loginError = '';
|
|
504
|
+
var NOUNS = { scan: 'scans', read: 'catalog reads', icon: 'icon generations' };
|
|
505
|
+
var MAP_REVIEWS = G.mapReviews || [];
|
|
506
|
+
function draft(key) { return Object.prototype.hasOwnProperty.call(drafts, key) ? drafts[key] : null; }
|
|
507
|
+
function draftFor(id, task) { return draft(id + '|note|' + task) || ''; }
|
|
508
|
+
function routeOf(id) { return (snap && snap.routes && snap.routes[id]) || null; }
|
|
509
|
+
function routeAt(id) { var r = routeOf(id); return r ? r.at : null; }
|
|
510
|
+
function reviewOf(id) { return (snap && snap.reviews && snap.reviews[id]) || null; }
|
|
511
|
+
// Which round the step shows and how it stands: an error about a review lasts until this changes.
|
|
512
|
+
function reviewKey(id) { var r = reviewOf(id); return r ? r.round + '|' + (r.revision || 1) + '|' + r.status + '|' + (r.choice || '') : ''; }
|
|
513
|
+
// A write is in the snapshot once the step shows its result.
|
|
514
|
+
function landed(p) {
|
|
515
|
+
if (p.review) { var r = reviewOf(p.node); return !r || r.round !== p.round || (r.revision || 1) !== p.revision || r.status !== p.status || (r.choice || null) !== p.before; }
|
|
516
|
+
return p.route ? routeAt(p.node) !== p.since : nv(p.node).taskIndex === p.expect;
|
|
517
|
+
}
|
|
518
|
+
try { showInactive = localStorage.getItem('build-map.inactive') !== 'hidden'; } catch (e) {}
|
|
519
|
+
|
|
520
|
+
function nv(id) { return snap && snap.view.nodes[id] || { status: 'locked', progress: 0, taskIndex: 0 }; }
|
|
521
|
+
function agentLabel(n, v) { return v && v.agent ? v.agent : ({ found: 'orchestrator', scaf: 'code agent', vis: 'design agent', comp: 'compliance agent', tele: 'lifecycle agent', money: 'billing agent', qa: 'qa agent', rel: 'release agent' })[n.cluster]; }
|
|
522
|
+
function ago(iso) {
|
|
523
|
+
var s = Math.max(0, Math.round((Date.now() - Date.parse(iso)) / 1000));
|
|
524
|
+
if (!isFinite(s)) return '';
|
|
525
|
+
if (s < 60) return s + 's ago'; if (s < 3600) return Math.round(s / 60) + 'm ago';
|
|
526
|
+
if (s < 86400) return Math.round(s / 3600) + 'h ago'; return Math.round(s / 86400) + 'd ago';
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function edgePath(s, t) {
|
|
530
|
+
var sx = s.x + NODE_R + 6, tx = t.x - NODE_R - 6, sy = s.y, ty = t.y, k;
|
|
531
|
+
if (t.x - s.x > 80) { k = Math.max(40, (tx - sx) * 0.5); return 'M' + sx + ',' + sy + ' C' + (sx + k) + ',' + sy + ' ' + (tx - k) + ',' + ty + ' ' + tx + ',' + ty; }
|
|
532
|
+
var dir = ty > sy ? 1 : -1, s2y = s.y + dir * (NODE_R + 6), t2y = t.y - dir * (NODE_R + 6);
|
|
533
|
+
k = Math.max(40, Math.abs(t2y - s2y) * 0.45);
|
|
534
|
+
return 'M' + s.x + ',' + s2y + ' C' + s.x + ',' + (s2y + dir * k) + ' ' + t.x + ',' + (t2y - dir * k) + ' ' + t.x + ',' + t2y;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function buildWorld() {
|
|
538
|
+
CLUSTERS.forEach(function (c) {
|
|
539
|
+
var g = el('g', { 'class': 'cluster', style: '--cc:' + COLORS[c.id] }, $('gClusters'));
|
|
540
|
+
el('rect', { x: c.x, y: c.y, width: c.w, height: c.h }, g);
|
|
541
|
+
el('text', { x: c.x + 18, y: c.y + 26 }, g).textContent = c.name;
|
|
542
|
+
el('text', { x: c.x + 18, y: c.y + 44, 'class': 'hint' }, g).textContent = c.hint;
|
|
543
|
+
});
|
|
544
|
+
NODES.forEach(function (n) { n.deps.forEach(function (d) {
|
|
545
|
+
edgeEls[d + '>' + n.id] = el('path', { 'class': 'edge', d: edgePath(byId[d], n), style: '--cc:' + COLORS[byId[d].cluster] }, $('gEdges'));
|
|
546
|
+
}); });
|
|
547
|
+
NODES.forEach(function (n) {
|
|
548
|
+
var g = el('g', { 'class': 'node', transform: 'translate(' + n.x + ',' + n.y + ')', style: '--cc:' + COLORS[n.cluster], tabindex: '0', role: 'button', 'aria-label': n.label }, $('gNodes'));
|
|
549
|
+
el('circle', { 'class': 'halo', r: NODE_R }, g);
|
|
550
|
+
el('circle', { 'class': 'sel-ring', r: NODE_R + 11 }, g);
|
|
551
|
+
el('circle', { 'class': 'pr-track', r: NODE_R + 5.5 }, g);
|
|
552
|
+
el('circle', { 'class': 'pr', r: NODE_R + 5.5, 'stroke-dasharray': C, 'stroke-dashoffset': C }, g);
|
|
553
|
+
el('circle', { 'class': 'body', r: NODE_R }, g);
|
|
554
|
+
el('path', { 'class': 'ic', d: ICONS[n.icon] || '' }, el('g', { transform: 'translate(-12,-12)' }, g));
|
|
555
|
+
var b = el('g', { 'class': 'badge', transform: 'translate(19,-19)' }, g);
|
|
556
|
+
el('circle', { 'class': 'bg', r: 8.5 }, b);
|
|
557
|
+
el('circle', { 'class': 'spin', r: 5.5 }, el('g', { 'class': 'spin' }, b));
|
|
558
|
+
el('path', { 'class': 'bp', d: '', transform: 'translate(-5,-5) scale(0.42)' }, b);
|
|
559
|
+
el('text', { 'class': 'lbl', y: NODE_R + 18 }, g).textContent = n.label;
|
|
560
|
+
el('text', { 'class': 'you-tag', y: -NODE_R - 10 }, g).textContent = 'YOUR TURN';
|
|
561
|
+
g.addEventListener('click', function (ev) { if (dragMoved) return; ev.stopPropagation(); select(n.id); });
|
|
562
|
+
g.addEventListener('keydown', function (ev) { if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); select(n.id); } });
|
|
563
|
+
nodeEls[n.id] = g;
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function related(a, b) { return a === b || byId[b].deps.indexOf(a) >= 0 || byId[a].deps.indexOf(b) >= 0; }
|
|
568
|
+
|
|
569
|
+
function render() {
|
|
570
|
+
if (!snap) return;
|
|
571
|
+
var view = snap.view;
|
|
572
|
+
NODES.forEach(function (n) {
|
|
573
|
+
var v = nv(n.id), st = v.status, g = nodeEls[n.id];
|
|
574
|
+
var cls = 'node st-' + st + (st === 'done' && v.basis === 'author' ? ' by-you' : '') + (v.skippedBy ? ' skipped' : '') + (v.redo && st !== 'done' ? ' redo' : '') + (v.stalled ? ' stalled' : '') + (selected === n.id ? ' sel' : '') + (selected && !related(selected, n.id) ? ' dim' : '');
|
|
575
|
+
g.setAttribute('class', cls);
|
|
576
|
+
g.querySelector('.pr').setAttribute('stroke-dashoffset', C * (1 - v.progress / 100));
|
|
577
|
+
g.querySelector('.bp').setAttribute('d', st === 'done' ? ICONS.check : st === 'needs_you' ? 'M12 5v9M12 18h.01' : (v.redo || v.stalled) ? ICONS.redo : st === 'locked' ? ICONS.lockS : '');
|
|
578
|
+
});
|
|
579
|
+
NODES.forEach(function (n) { n.deps.forEach(function (d) {
|
|
580
|
+
var ts = nv(n.id).status, ss = nv(d).status, cls = 'edge';
|
|
581
|
+
if (ts === 'inactive' || ss === 'inactive') cls += ' off';
|
|
582
|
+
else if (ss === 'done' && ts === 'running') cls += ' flow';
|
|
583
|
+
else if (ss === 'done') cls += ' sat';
|
|
584
|
+
if (selected) cls += (d === selected || n.id === selected) ? ' hi' : ' dim';
|
|
585
|
+
edgeEls[d + '>' + n.id].setAttribute('class', cls);
|
|
586
|
+
}); });
|
|
587
|
+
$('ringVal').setAttribute('stroke-dashoffset', 94.25 * (1 - view.percent / 100));
|
|
588
|
+
$('ringPct').textContent = view.percent + '%';
|
|
589
|
+
$('cDone').textContent = view.done + ' / ' + view.active + ' done';
|
|
590
|
+
$('cRun').textContent = view.running.length + ' running';
|
|
591
|
+
$('cYouT').textContent = view.needsYou.length + ' need' + (view.needsYou.length === 1 ? 's' : '') + ' you';
|
|
592
|
+
$('cYou').disabled = view.needsYou.length === 0;
|
|
593
|
+
var p = snap.project || {};
|
|
594
|
+
var name = [p.name || 'Unnamed extension', p.version].filter(Boolean).join(' · ');
|
|
595
|
+
$('projName').textContent = name;
|
|
596
|
+
$('projName').title = snap.home;
|
|
597
|
+
document.title = (view.needsYou.length ? '(' + view.needsYou.length + ') ' : '') + (p.name ? p.name + ' · ' : '') + 'Build Map';
|
|
598
|
+
if (selected) renderPanel(selected);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
var STATUS_LABEL = { done: 'Done', running: 'Agent working', needs_you: 'Waiting on you', ready: 'Ready to start', locked: 'Locked', inactive: 'Not needed' };
|
|
602
|
+
var OPTIONS = {
|
|
603
|
+
surfaces: [['popup', 'Popup'], ['background', 'Service worker'], ['content', 'Content script'], ['options', 'Options page'], ['sidepanel', 'Side panel']],
|
|
604
|
+
browsers: [['chrome', 'Chrome'], ['firefox', 'Firefox'], ['edge', 'Edge']],
|
|
605
|
+
features: [['analytics', 'Analytics'], ['uninstall', 'Uninstall survey'], ['feedback', 'Feedback form'], ['i18n', 'Localization'], ['monetization', 'Monetization'], ['onboarding', 'Welcome page'], ['promo', 'Promo tiles'], ['research', 'Niche research'], ['ci', 'CI evidence']]
|
|
606
|
+
};
|
|
607
|
+
function inactiveReason(n) {
|
|
608
|
+
var r = n.requires || {};
|
|
609
|
+
if (r.surface) return 'This extension has no ' + r.surface + ' surface.';
|
|
610
|
+
if (r.browser) return r.browser.charAt(0).toUpperCase() + r.browser.slice(1) + ' is not a target browser.';
|
|
611
|
+
if (r.multiBrowser) return 'Only one target browser, so there is nothing to reconcile.';
|
|
612
|
+
if (r.feature) return 'The ' + r.feature + ' module is off for this extension.';
|
|
613
|
+
return 'A module or surface this step needs is off for this extension.';
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function select(id) { selected = id; $('panel').classList.add('open'); render(); if (!snap) renderPanel(id); }
|
|
617
|
+
function closePanel() { selected = null; $('panel').classList.remove('open'); if (location.hash) history.replaceState(null, '', location.pathname); render(); }
|
|
618
|
+
|
|
619
|
+
// The evidence journal (plan 6.2.1): why a step counts as done, and its history.
|
|
620
|
+
var BASIS_LABEL = { author: 'Your mark', agent: 'Agent report', project: 'Found in the project files', dojo: 'From Dojo' };
|
|
621
|
+
function journal(id) { return (snap && snap.evidence && snap.evidence[id]) || []; }
|
|
622
|
+
function whereList(where) {
|
|
623
|
+
if (!where || !where.length) return '';
|
|
624
|
+
return '<ul class="where">' + where.map(function (w) {
|
|
625
|
+
if (w.file) return '<li><code>' + esc(w.file) + '</code>' + (w.sha256 ? ' <span class="sha" title="sha256 ' + esc(w.sha256) + '">sha256 ' + esc(w.sha256.slice(0, 10)) + '</span>' : '') + '</li>';
|
|
626
|
+
if (w.url) return '<li>' + (/^https?:[/][/]/i.test(w.url) ? '<a href="' + esc(w.url) + '" target="_blank" rel="noopener noreferrer">' + esc(w.url) + '</a>' : esc(w.url)) + '</li>';
|
|
627
|
+
if (w.dojo) return '<li>Dojo · ' + esc(w.dojo) + '</li>';
|
|
628
|
+
if (w.command) return '<li><code>' + esc(w.command) + '</code></li>';
|
|
629
|
+
return '';
|
|
630
|
+
}).join('') + '</ul>';
|
|
631
|
+
}
|
|
632
|
+
function whyDone(id, v) {
|
|
633
|
+
if (v.status !== 'done') return '';
|
|
634
|
+
if (v.fact) return '<div class="why b-' + esc(v.basis || 'project') + '"><span class="basis">' + esc(v.local ? 'Checked by the map' : BASIS_LABEL[v.basis] || BASIS_LABEL.project) + '</span><p>' + esc(v.fact) + '</p>' + whereList(v.where) + '</div>';
|
|
635
|
+
var entry = journal(id).filter(function (e) { return !e.revoked && (v.route === 'self' ? e.route === 'self' : !e.route) && (e.basis === 'author' || e.basis === 'agent'); })[0];
|
|
636
|
+
if (entry) return '<div class="why b-' + esc(entry.basis) + '"><span class="basis">' + esc(BASIS_LABEL[entry.basis]) + (entry.basis === 'agent' && entry.who ? ' · ' + esc(entry.who) : '') + (entry.checked ? ' · checked' : '') + '</span><p>' + esc(entry.claim) + '</p>' + (entry.note ? '<p class="note">' + esc(entry.note) + '</p>' : '') + whereList(entry.where) + '</div>';
|
|
637
|
+
if (v.basis === 'agent') return '<div class="why unknown"><span class="basis">Agent report</span><p>' + esc(v.agent || 'An agent') + ' reported the step done without saying how it checked.</p></div>';
|
|
638
|
+
return '<div class="why unknown"><p>Done before the map kept a record of why.</p></div>';
|
|
639
|
+
}
|
|
640
|
+
function historyLine(e) {
|
|
641
|
+
if (e.basis === 'author' && e.route === 'skip') return '<li class="' + (e.revoked ? 'revoked' : '') + '"><span class="t">' + esc(ago(e.t)) + '</span><div><span><b>' + (e.revoked ? 'you took the skip back' : 'you skipped this step') + '</b></span></div></li>';
|
|
642
|
+
var what = e.basis === 'author' ? (e.route === 'self' ? (e.revoked ? 'you took it back' : 'you did it yourself') : e.revoked ? 'you took the mark back' : 'you marked done')
|
|
643
|
+
: e.basis === 'agent' ? esc(e.who) + (e.revoked ? ' withdrew' : ' reported')
|
|
644
|
+
: e.basis === 'dojo' ? (e.revoked ? 'no longer in Dojo' : 'Dojo shows')
|
|
645
|
+
: (e.revoked ? 'no longer in the files' : 'found in the files');
|
|
646
|
+
return '<li class="' + (e.revoked ? 'revoked' : '') + '"><span class="t">' + esc(ago(e.t)) + '</span><div><span><b>' + what + '</b> ' + esc(e.claim) + '</span>' + (e.note ? '<span class="note">' + esc(e.note) + '</span>' : '') + (e.revoked ? '' : whereList(e.where)) + '</div></li>';
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function renderPanel(id) {
|
|
650
|
+
var n = byId[id], v = nv(id), st = v.status, color = COLORS[n.cluster];
|
|
651
|
+
var cluster = CLUSTERS.filter(function (c) { return c.id === n.cluster; })[0];
|
|
652
|
+
$('pIco').style.setProperty('--cc', color);
|
|
653
|
+
$('pIco').innerHTML = '<svg viewBox="0 0 24 24"><path d="' + esc(ICONS[n.icon]) + '"/></svg>';
|
|
654
|
+
$('pTitle').textContent = n.label;
|
|
655
|
+
$('pSub').textContent = cluster.name + ' · ' + (n.actor === 'you' ? 'needs your decision' : 'agent step');
|
|
656
|
+
var h = '';
|
|
657
|
+
h += '<div class="status"><span class="pill st-' + st + '">' + (st === 'running' ? '<span class="sp"></span>' : '<span class="dot"></span>') + (v.route === 'skip' ? 'Skipped' : v.skippedBy ? 'Off' : STATUS_LABEL[st]) + '</span>';
|
|
658
|
+
if (v.redo && st !== 'done') h += '<span class="pill warn">re-run queued</span>';
|
|
659
|
+
if (v.stalled) h += '<span class="pill warn">agent went quiet</span>';
|
|
660
|
+
h += '<span class="actor">' + (n.actor === 'you' ? '<b>you</b> + ' : '') + '<b>' + esc(agentLabel(n, v)) + '</b></span></div>';
|
|
661
|
+
h += whyDone(id, v);
|
|
662
|
+
if (st !== 'inactive' && !v.fact) h += '<div class="sec"><div class="pbar" style="--cc:' + color + '"><i style="width:' + v.progress + '%"></i></div><div class="pct"><span>' + v.progress + '%</span><span>' + Math.min(v.taskIndex, n.todos.length) + ' / ' + n.todos.length + ' tasks' + (v.updatedAt ? ' · ' + esc(ago(v.updatedAt)) : '') + '</span></div></div>';
|
|
663
|
+
if (v.stalled) h += '<div class="ask muted"><h4>No report for a while</h4><p>' + esc(v.agent) + ' last reported ' + esc(ago(v.updatedAt)) + '. Another agent can pick the step up.</p></div>';
|
|
664
|
+
h += '<p class="desc">' + esc(n.desc) + '</p>';
|
|
665
|
+
if (n.tool) h += '<div class="tool"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="' + ICONS.wrench + '"/></svg><code>' + esc(n.tool) + '</code></div>';
|
|
666
|
+
var writable = Boolean(snap && (APP ? snap.app && snap.app.writable : SESSION.token));
|
|
667
|
+
var busy = pending && pending.node === id;
|
|
668
|
+
var routeFailed = writeError && writeError.node === id && writeError.kind === 'route' ? writeError.message : '';
|
|
669
|
+
if (routeFailed) h += '<div class="ask muted"><p class="err">' + esc(routeFailed) + '</p></div>';
|
|
670
|
+
if (v.route === 'skip') h += '<div class="ask muted"><h4>You skipped this step</h4><p>' + esc(n.skip ? n.skip.consequence : '') + '</p>' + (n.skip && n.skip.store ? '<p>It is on the list of what to do in the store dashboards.</p>' : '') + (writable ? '<button class="act ghost" data-route="null"' + (busy ? ' disabled' : '') + '>' + (busy ? 'Saving…' : 'Take the skip back') + '</button>' : '') + '</div>';
|
|
671
|
+
else if (v.skippedBy) h += '<div class="ask muted"><h4>Off with a skipped step</h4><p>It only uses the result of <a href="#node=' + esc(v.skippedBy) + '">' + esc(byId[v.skippedBy] ? byId[v.skippedBy].label : v.skippedBy) + '</a>, which you skipped.</p></div>';
|
|
672
|
+
else if (st === 'inactive') h += '<div class="ask muted"><h4>Not needed for this extension</h4><p>' + esc(inactiveReason(n)) + '</p></div>';
|
|
673
|
+
// A stored route stays editable even once the project files prove the step (the agent wired the link).
|
|
674
|
+
var stored = routeOf(id);
|
|
675
|
+
if (n.credits && snap && snap.account && st !== 'inactive' && st !== 'done' && v.route !== 'skip' && !v.skippedBy) h += creditBox(n, writable);
|
|
676
|
+
var ownRoute = v.route === 'self' || Boolean(stored && stored.how === 'self');
|
|
677
|
+
if (writable && n.manual && st !== 'inactive' && (st !== 'done' || ownRoute) && (ownRoute || selfOpen(n))) h += manualBox(id, n, v, busy);
|
|
678
|
+
if (writable && n.skip && st !== 'inactive' && st !== 'done') h += skipBox(n, busy);
|
|
679
|
+
if (writable && st === 'done' && stored && stored.how === 'skip') h += '<div class="ask muted"><h4>You skipped this step</h4><p>The project files show it done anyway, so the skip has no effect.</p><button class="act ghost" data-route="null"' + (busy ? ' disabled' : '') + '>' + (busy ? 'Saving…' : 'Take the skip back') + '</button></div>';
|
|
680
|
+
if (id === 'ready' && snap && snap.view.byHand.length) h += '<div class="ask muted"><h4>To do in the store dashboards</h4>' + byHandList(snap.view.byHand) + '</div>';
|
|
681
|
+
var review = reviewOf(id), onMap = MAP_REVIEWS.indexOf(n.review) >= 0;
|
|
682
|
+
if (onMap && review && st !== 'inactive') h += reviewBox(id, n, review, writable, busy);
|
|
683
|
+
var cur = n.todos[v.taskIndex], last = v.taskIndex - 1;
|
|
684
|
+
var canMark = writable && st !== 'inactive' && st !== 'done' && cur && cur.confirm;
|
|
685
|
+
var canUndo = writable && st !== 'inactive' && !v.fact && !v.route && last >= 0 && n.todos[last] && n.todos[last].confirm;
|
|
686
|
+
var failed = writeError && writeError.node === id && writeError.kind === 'confirm' ? writeError.message : '';
|
|
687
|
+
if (canMark) {
|
|
688
|
+
h += '<div class="ask"><h4>' + esc(cur.t) + '</h4><p>Only you can do this. When it is done, mark it here: every agent working on this project sees it.</p>';
|
|
689
|
+
if (failed) h += '<p class="err">' + esc(failed) + '</p>';
|
|
690
|
+
h += '<input class="note" data-draft="' + id + '|note|' + v.taskIndex + '" maxlength="280" placeholder="Note for the history (optional), e.g. the publisher name" value="' + esc(draftFor(id, v.taskIndex)) + '"' + (busy ? ' disabled' : '') + '>';
|
|
691
|
+
h += '<span class="hint">Kept in the project's map file. Never put passwords or keys here.</span>';
|
|
692
|
+
h += '<button class="act" data-confirm="1" data-task="' + v.taskIndex + '"' + (busy ? ' disabled' : '') + '>' + (busy ? 'Saving…' : 'Mark as done') + '</button></div>';
|
|
693
|
+
} else if (st === 'needs_you' && cur && onMap && !review) h += '<div class="ask"><h4>' + esc(cur.t) + '</h4><p>The variants to choose from show up here when the agent offers them.</p></div>';
|
|
694
|
+
else if (st === 'needs_you' && cur && !(onMap && review)) h += '<div class="ask"><h4>' + esc(cur.t) + '</h4><p>The agent is waiting for your answer in the chat.</p></div>';
|
|
695
|
+
else if (canUndo && failed) h += '<div class="ask muted"><p class="err">' + esc(failed) + '</p></div>';
|
|
696
|
+
if (snap && OPTIONS[n.review]) {
|
|
697
|
+
var on = snap.view.settings[n.review];
|
|
698
|
+
h += '<div class="sec"><h3>Current choice</h3><ul class="opts">' + OPTIONS[n.review].map(function (o) { return '<li class="' + (on[o[0]] ? 'on' : '') + '"><i></i>' + o[1] + '</li>'; }).join('') + '</ul></div>';
|
|
699
|
+
}
|
|
700
|
+
if (n.todos.length) {
|
|
701
|
+
h += '<div class="sec"><h3>Tasks</h3><ul class="todos">';
|
|
702
|
+
n.todos.forEach(function (t, i) {
|
|
703
|
+
var cls = st === 'done' || i < v.taskIndex ? 'done' : i === v.taskIndex && st === 'running' ? 'run' : i === v.taskIndex && st === 'needs_you' ? 'wait' : '';
|
|
704
|
+
var who = canUndo && i === last
|
|
705
|
+
? '<button class="undo" data-confirm="0" data-task="' + i + '" title="Mark this task as not done"' + (busy ? ' disabled' : '') + '>' + (busy ? 'saving…' : 'undo') + '</button>'
|
|
706
|
+
: '<span class="who">' + (t.a === 'you' ? 'you' : 'agent') + '</span>';
|
|
707
|
+
h += '<li class="' + cls + '"><span class="m">' + (cls === 'done' ? '<svg viewBox="0 0 24 24"><path d="' + ICONS.check + '"/></svg>' : '') + '</span><span>' + esc(t.t) + '</span>' + who + '</li>';
|
|
708
|
+
});
|
|
709
|
+
h += '</ul></div>';
|
|
710
|
+
}
|
|
711
|
+
var chips = function (ids) { return ids.map(function (d) { return '<button class="dep st-' + nv(d).status + '" data-go="' + d + '"><i></i>' + esc(byId[d].label) + '</button>'; }).join(''); };
|
|
712
|
+
if (n.deps.length) h += '<div class="sec"><h3>Depends on</h3><div class="deps">' + chips(n.deps) + '</div></div>';
|
|
713
|
+
if (children[id]) h += '<div class="sec"><h3>Unlocks</h3><div class="deps">' + chips(children[id]) + '</div></div>';
|
|
714
|
+
var history = journal(id);
|
|
715
|
+
if (history.length) h += '<div class="sec"><h3>Evidence history</h3><ul class="hist">' + history.map(historyLine).join('') + '</ul></div>';
|
|
716
|
+
var logs = snap ? snap.log.filter(function (l) { return l.node === id; }).slice(0, 8) : [];
|
|
717
|
+
if (logs.length) h += '<div class="sec"><h3>Activity</h3><ul class="log">' + logs.map(function (l) { return '<li><span class="t">' + esc(ago(l.t)) + '</span><span><b>' + esc(l.who) + '</b> ' + esc(l.what) + '</span></li>'; }).join('') + '</ul></div>';
|
|
718
|
+
var body = $('pBody');
|
|
719
|
+
if (body.dataset.html === h) return;
|
|
720
|
+
// Someone is typing in a field of this step: keep it; the panel catches up when it loses focus.
|
|
721
|
+
var typing = document.activeElement;
|
|
722
|
+
if (typing && typing.getAttribute && typing.getAttribute('data-draft') && body.contains(typing) && body.dataset.node === id && !busy) return;
|
|
723
|
+
body.dataset.node = id;
|
|
724
|
+
body.dataset.html = h;
|
|
725
|
+
body.innerHTML = h;
|
|
726
|
+
if (APP) loadThumbs();
|
|
727
|
+
body.querySelectorAll('[data-go]').forEach(function (b) { b.addEventListener('click', function () { focusNode(b.getAttribute('data-go')); select(b.getAttribute('data-go')); }); });
|
|
728
|
+
body.querySelectorAll('[data-confirm]').forEach(function (b) { b.addEventListener('click', function () { sendConfirm(id, Number(b.getAttribute('data-task')), b.getAttribute('data-confirm') === '1'); }); });
|
|
729
|
+
body.querySelectorAll('[data-route]').forEach(function (b) { b.addEventListener('click', function () { var how = b.getAttribute('data-route'); sendRoute(id, how === 'null' ? null : how); }); });
|
|
730
|
+
body.querySelectorAll('[data-copy]').forEach(function (b) { b.addEventListener('click', function () { copyText(b); }); });
|
|
731
|
+
body.querySelectorAll('[data-login]').forEach(function (b) { b.addEventListener('click', function () { sendLogin(id); }); });
|
|
732
|
+
body.querySelectorAll('[data-pick]').forEach(function (b) { b.addEventListener('click', function () { sendReview(id, { choice: b.getAttribute('data-pick') }); }); });
|
|
733
|
+
body.querySelectorAll('[data-redo]').forEach(function (b) { b.addEventListener('click', function () {
|
|
734
|
+
var r = reviewOf(id), text = r ? (draft(id + '|redo|' + r.round) || '').trim() : '';
|
|
735
|
+
if (!text) { writeError = { node: id, kind: 'review', key: reviewKey(id), message: 'Say what to change first.' }; renderPanel(id); return; }
|
|
736
|
+
sendReview(id, { redo: text });
|
|
737
|
+
}); });
|
|
738
|
+
body.querySelectorAll('[data-draft]').forEach(function (f) {
|
|
739
|
+
var key = f.getAttribute('data-draft');
|
|
740
|
+
if (f.type === 'checkbox') f.addEventListener('change', function () {
|
|
741
|
+
drafts[key] = Array.prototype.filter.call(body.querySelectorAll('[data-draft="' + key + '"]'), function (c) { return c.checked; }).map(function (c) { return c.value; }).join(',');
|
|
742
|
+
});
|
|
743
|
+
else f.addEventListener('input', function () { drafts[key] = f.value; });
|
|
744
|
+
f.addEventListener('blur', function () { setTimeout(function () { if (selected === id) renderPanel(id); }, 0); });
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Doing a step by hand (plan phase 1f): what the author gives, and for a store submit what an upload by hand needs.
|
|
749
|
+
var BS = String.fromCharCode(92);
|
|
750
|
+
function absPath(file) {
|
|
751
|
+
var home = snap.home || '', sep = home.indexOf(BS) >= 0 && home.indexOf('/') < 0 ? BS : '/';
|
|
752
|
+
while (home.length > 1 && (home.charAt(home.length - 1) === '/' || home.charAt(home.length - 1) === BS)) home = home.slice(0, -1);
|
|
753
|
+
return home + sep + file;
|
|
754
|
+
}
|
|
755
|
+
function copyRow(text, label) {
|
|
756
|
+
var shown = label || (text.length > 160 ? text.slice(0, 157) + '…' : text);
|
|
757
|
+
return '<div class="copyrow"><code>' + esc(shown) + '</code><button class="copy" data-copy="' + esc(text) + '">Copy</button></div>';
|
|
758
|
+
}
|
|
759
|
+
function copyText(b) {
|
|
760
|
+
var text = b.getAttribute('data-copy');
|
|
761
|
+
var done = function (label) { b.textContent = label; setTimeout(function () { b.textContent = 'Copy'; }, 1500); };
|
|
762
|
+
if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(text).then(function () { done('Copied'); }, function () { done('Copy failed'); });
|
|
763
|
+
else done('Copy failed');
|
|
764
|
+
}
|
|
765
|
+
function byHandList(ids) {
|
|
766
|
+
return '<ul class="tolist">' + ids.map(function (d) { return '<li><b>' + esc(byId[d].label) + '.</b> ' + esc(byId[d].skip ? byId[d].skip.consequence : '') + '</li>'; }).join('') + '</ul>';
|
|
767
|
+
}
|
|
768
|
+
// The packages on disk for one store: named for its browser (and the Firefox source), else every package.
|
|
769
|
+
function packagesFor(browser) {
|
|
770
|
+
var files = (nv('zip').where || []).filter(function (w) { return w.file; }).map(function (w) { return w.file; });
|
|
771
|
+
var named = files.filter(function (f) { return new RegExp(browser, 'i').test(f) || (browser === 'firefox' && /source/i.test(f)); });
|
|
772
|
+
if (named.length) return named;
|
|
773
|
+
var plain = files.filter(function (f) { return !/chrome|firefox|edge/i.test(f); });
|
|
774
|
+
return plain.length ? plain : files;
|
|
775
|
+
}
|
|
776
|
+
function uploadParts(n) {
|
|
777
|
+
var browser = n.requires && n.requires.browser, h = '';
|
|
778
|
+
var files = browser ? packagesFor(browser) : [];
|
|
779
|
+
h += '<div class="kv"><span>Package</span>' + (files.length ? files.map(function (f) { return copyRow(absPath(f), f); }).join('') : '<p>No package on disk yet: the Package per browser step makes it.</p>') + '</div>';
|
|
780
|
+
// What the author gave, even once the project files prove the step (the agent wired the link).
|
|
781
|
+
var policy = routeOf('policy'), reasons = routeOf('justifications'), listing = nv('listing');
|
|
782
|
+
if (policy && policy.how === 'self' && policy.value) h += '<div class="kv"><span>Privacy policy URL</span>' + copyRow(policy.value) + '</div>';
|
|
783
|
+
if (browser === 'chrome' && reasons && reasons.how === 'self' && reasons.value) h += '<div class="kv"><span>Permission reasons</span>' + copyRow(reasons.value) + '</div>';
|
|
784
|
+
var listingFile = (listing.where || []).filter(function (w) { return w.file; })[0];
|
|
785
|
+
if (listingFile) h += '<div class="kv"><span>Listing copy</span>' + copyRow(absPath(listingFile.file), listingFile.file) + '</div>';
|
|
786
|
+
var left = snap.view.byHand.filter(function (d) { var r = byId[d].requires; return !r || !r.browser || r.browser === browser; });
|
|
787
|
+
if (left.length) h += '<div class="kv"><span>Do there, skipped on the map</span>' + byHandList(left) + '</div>';
|
|
788
|
+
h += '<p><a href="' + esc(n.manual.dashboard) + '" target="_blank" rel="noopener noreferrer">Open the dashboard</a></p>';
|
|
789
|
+
return h;
|
|
790
|
+
}
|
|
791
|
+
function storesNow(id, v) {
|
|
792
|
+
var chosen = draft(id + '|stores');
|
|
793
|
+
if (chosen !== null) return chosen;
|
|
794
|
+
var given = givenValue(id, v);
|
|
795
|
+
if (given) return given;
|
|
796
|
+
var on = snap.view.settings.browsers;
|
|
797
|
+
return OPTIONS.browsers.filter(function (o) { return on[o[0]]; }).map(function (o) { return o[0]; }).join(',');
|
|
798
|
+
}
|
|
799
|
+
// What the author gave: in the view, or in the stored route once the project files prove the step instead.
|
|
800
|
+
function givenValue(id, v) {
|
|
801
|
+
if (v.route === 'self' && v.value) return v.value;
|
|
802
|
+
var r = routeOf(id);
|
|
803
|
+
return r && r.how === 'self' && r.value ? r.value : '';
|
|
804
|
+
}
|
|
805
|
+
function manualBox(id, n, v, busy) {
|
|
806
|
+
var r = routeOf(id), m = n.manual, mine = v.route === 'self' || Boolean(r && r.how === 'self'), given = givenValue(id, v);
|
|
807
|
+
var h = '<div class="ask' + (mine ? ' muted' : '') + '">';
|
|
808
|
+
h += '<h4>' + esc(mine ? 'You did this yourself' : m.label) + '</h4>';
|
|
809
|
+
if (mine && v.fact) h += '<p>The project files show it too.</p>';
|
|
810
|
+
if (!mine && m.dashboard) h += '<p>Upload it yourself, then mark it here.</p>' + uploadParts(n);
|
|
811
|
+
if (!mine && m.requirements && m.requirements.length) h += '<ul class="req">' + m.requirements.map(function (r) { return '<li>' + esc(r) + '</li>'; }).join('') + '</ul>';
|
|
812
|
+
if (mine && given && m.input !== 'browsers') h += copyRow(given);
|
|
813
|
+
if (m.input === 'url' || m.input === 'text') {
|
|
814
|
+
var val = draft(id + '|value');
|
|
815
|
+
if (val === null) val = mine ? given : '';
|
|
816
|
+
h += m.input === 'url'
|
|
817
|
+
? '<input class="note" type="url" data-draft="' + id + '|value" maxlength="2048" placeholder="https://" value="' + esc(val) + '"' + (busy ? ' disabled' : '') + '>'
|
|
818
|
+
: '<textarea class="note" data-draft="' + id + '|value" maxlength="4000"' + (busy ? ' disabled' : '') + '>' + esc(val) + '</textarea>';
|
|
819
|
+
if (m.key) h += '<span class="hint">An agent writes it into src/extenshi.config.js as ' + esc(m.key) + '.</span>';
|
|
820
|
+
} else if (m.input === 'browsers') {
|
|
821
|
+
var stores = ',' + storesNow(id, v) + ',';
|
|
822
|
+
h += '<div class="picks">' + OPTIONS.browsers.map(function (o) { return '<label><input type="checkbox" data-draft="' + id + '|stores" value="' + o[0] + '"' + (stores.indexOf(',' + o[0] + ',') >= 0 ? ' checked' : '') + (busy ? ' disabled' : '') + '> ' + o[1] + '</label>'; }).join('') + '</div>';
|
|
823
|
+
if (snap.project && snap.project.projectId) h += '<span class="hint">This project is bound to Dojo: a decision there wins over the choice here.</span>';
|
|
824
|
+
}
|
|
825
|
+
if (!mine || m.input !== 'mark') {
|
|
826
|
+
var rnote = draft(id + '|rnote') || '';
|
|
827
|
+
h += '<input class="note" data-draft="' + id + '|rnote" maxlength="280" placeholder="Note for the history (optional)" value="' + esc(rnote) + '"' + (busy ? ' disabled' : '') + '>';
|
|
828
|
+
h += '<span class="hint">Kept in the project's map file. Never put passwords or keys here.</span>';
|
|
829
|
+
}
|
|
830
|
+
h += '<div class="row">';
|
|
831
|
+
if (!mine || m.input !== 'mark') h += '<button class="act" data-route="self"' + (busy ? ' disabled' : '') + '>' + (busy ? 'Saving…' : m.input === 'mark' ? esc(m.label) : mine ? 'Save the change' : 'Save') + '</button>';
|
|
832
|
+
if (mine) h += '<button class="act ghost" data-route="null"' + (busy ? ' disabled' : '') + '>Take it back</button>';
|
|
833
|
+
return h + '</div></div>';
|
|
834
|
+
}
|
|
835
|
+
// A credit step's own mark opens once the free credits of its pool are spent (the server checks it too).
|
|
836
|
+
function selfOpen(n) {
|
|
837
|
+
if (!n.credits) return true;
|
|
838
|
+
var a = snap && snap.account, b = a && a.signedIn && a.credits && a.credits[n.credits];
|
|
839
|
+
return Boolean(b) && b.freeRemaining <= 0;
|
|
840
|
+
}
|
|
841
|
+
function timeLeft(iso) {
|
|
842
|
+
var m = Math.round((new Date(iso).getTime() - Date.now()) / 60000);
|
|
843
|
+
return m > 1 ? 'in ' + m + ' minutes' : 'in a minute';
|
|
844
|
+
}
|
|
845
|
+
function creditBox(n, writable) {
|
|
846
|
+
var a = snap.account, noun = NOUNS[n.credits], lg = a.login, h;
|
|
847
|
+
if (a.checking) return '<div class="ask muted"><h4>Credits</h4><p>Checking your Extenshi account…</p></div>';
|
|
848
|
+
if (!a.signedIn) {
|
|
849
|
+
h = '<div class="ask"><h4>Sign in for free credits</h4>';
|
|
850
|
+
h += '<p>This step spends ' + noun + '. A new Extenshi account gets free scans, catalog reads and icon generations, once. Signing in costs nothing.</p>';
|
|
851
|
+
if (a.problem) h += '<p class="err">' + esc(a.problem) + '</p>';
|
|
852
|
+
if (lg && lg.status === 'waiting') {
|
|
853
|
+
h += '<p>Check that the sign-in page shows this code, then approve it there:</p><div class="code">' + esc(lg.code) + '</div>';
|
|
854
|
+
h += '<p>' + (lg.opened ? 'The sign-in page opened in a new tab. ' : '') + '<a href="' + esc(lg.url) + '" target="_blank" rel="noopener noreferrer">' + (lg.opened ? 'Open it again' : 'Open the sign-in page') + '</a></p>';
|
|
855
|
+
h += '<span class="hint">Waiting for your approval. The code expires ' + esc(timeLeft(lg.expiresAt)) + '.</span>';
|
|
856
|
+
return h + '</div>';
|
|
857
|
+
}
|
|
858
|
+
var failed = loginError || (lg && lg.status === 'failed' ? lg.message : '');
|
|
859
|
+
if (failed) h += '<p class="err">' + esc(failed) + '</p>';
|
|
860
|
+
if (writable && !APP) {
|
|
861
|
+
var starting = Boolean(loginWait);
|
|
862
|
+
h += '<button class="act" data-login="1"' + (starting ? ' disabled' : '') + '>' + (starting ? 'Starting…' : 'Sign in for free credits') + '</button>';
|
|
863
|
+
h += '<span class="hint">Opens extenshi.io to sign in or create an account. The key is saved in ~/.extenshi/config.json on this computer, where every extenshi command finds it.</span>';
|
|
864
|
+
} else h += '<p>Run <code>npx @extenshi/cli login</code> in a terminal' + (APP ? ', or sign in on the map in your browser' : '') + '.</p>';
|
|
865
|
+
if (n.skip) h += '<span class="hint">Or skip the step below.</span>';
|
|
866
|
+
return h + '</div>';
|
|
867
|
+
}
|
|
868
|
+
var b = a.credits && a.credits[n.credits];
|
|
869
|
+
if (!b) return '<div class="ask muted"><h4>Credits</h4><p>' + esc(a.problem || 'You are signed in, but the map does not know your credits right now.') + '</p></div>';
|
|
870
|
+
h = '<div class="ask muted"><h4>' + esc(noun.charAt(0).toUpperCase() + noun.slice(1)) + ': ' + b.remaining + ' left</h4>';
|
|
871
|
+
if (lg && lg.status === 'done') h += '<p>Signed in' + (lg.email ? ' as ' + esc(lg.email) : '') + '.' + (b.remaining > 0 ? ' An agent can run this step now.' : '') + '</p>';
|
|
872
|
+
if (b.freeRemaining > 0) {
|
|
873
|
+
h += '<p>' + b.freeRemaining + ' of them free, of the ' + b.freeGranted + ' every account gets once.</p>';
|
|
874
|
+
if (n.manual) h += '<p>“' + esc(n.manual.label) + '” opens once the free ones are spent.</p>';
|
|
875
|
+
} else if (b.remaining > 0) h += '<p>The free ones are spent; the rest are credits you bought.</p>';
|
|
876
|
+
else {
|
|
877
|
+
h += '<p>None left. Buy more, ' + (n.manual ? 'mark the step done yourself, ' : '') + 'or skip it.</p>';
|
|
878
|
+
if (a.billingUrl) h += '<p><a href="' + esc(a.billingUrl) + '" target="_blank" rel="noopener noreferrer">Buy credits in Dojo</a></p>';
|
|
879
|
+
}
|
|
880
|
+
return h + '</div>';
|
|
881
|
+
}
|
|
882
|
+
// A write: to the map server with the page token, or, in a chat, through the MCP server's app-only tool.
|
|
883
|
+
// Either way it resolves once the write is saved and fails with the reason to show.
|
|
884
|
+
function post(path, body) {
|
|
885
|
+
if (APP) return appWrite(path.replace('/api/', ''), body);
|
|
886
|
+
return fetch(path, {
|
|
887
|
+
method: 'POST',
|
|
888
|
+
headers: { 'content-type': 'application/json', 'x-extenshi-map-page': SESSION.token },
|
|
889
|
+
body: JSON.stringify(body)
|
|
890
|
+
}).then(function (res) {
|
|
891
|
+
// 403: the server restarted since this page loaded, so its token is gone. The new page has the new one.
|
|
892
|
+
if (res.status === 403) { location.reload(); return new Promise(function () {}); }
|
|
893
|
+
return res.json().catch(function () { return {}; }).then(function (answer) {
|
|
894
|
+
if (!res.ok) throw new Error(answer.message || ('The map server answered ' + res.status + '.'));
|
|
895
|
+
});
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
// The author signs in from the map: the server starts the browser pairing; the code comes back over SSE.
|
|
899
|
+
function sendLogin(id) {
|
|
900
|
+
if (loginWait || !snap || !snap.account) return;
|
|
901
|
+
loginWait = { before: JSON.stringify(snap.account.login || null) };
|
|
902
|
+
loginError = '';
|
|
903
|
+
renderPanel(id);
|
|
904
|
+
post('/api/login', {}).then(function () {
|
|
905
|
+
var mine = loginWait;
|
|
906
|
+
setTimeout(function () { if (loginWait === mine) { loginWait = null; if (selected) renderPanel(selected); } }, 15000);
|
|
907
|
+
}, function (error) {
|
|
908
|
+
loginWait = null;
|
|
909
|
+
loginError = (error && error.message) || 'The map server did not answer.';
|
|
910
|
+
if (selected === id) renderPanel(id);
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
function skipBox(n, busy) {
|
|
914
|
+
var h = '<div class="ask muted"><h4>Skip this step</h4><p>' + esc(n.skip.consequence) + '</p>';
|
|
915
|
+
if (n.skip.store) h += '<p>It goes on the list of what to do in the store dashboards.</p>';
|
|
916
|
+
if (n.skip.with && n.skip.with.length) h += '<p>Also switches off: ' + n.skip.with.map(function (d) { return esc(byId[d].label); }).join(', ') + '.</p>';
|
|
917
|
+
return h + '<button class="act ghost" data-route="skip"' + (busy ? ' disabled' : '') + '>' + (busy ? 'Saving…' : 'Skip') + '</button></div>';
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// A variant's file goes through the map server (/review/…), which serves it in a sandbox; an https address opens as it is.
|
|
921
|
+
// In a chat the page has no server of its own: a preview opens on the browser map's server while it runs, else not at all.
|
|
922
|
+
// revision: variants an agent replaced keep their ids, so the address names the revision, or the browser keeps
|
|
923
|
+
// showing the image it loaded for the old one (a document reuses an image at the same address, no-store or not).
|
|
924
|
+
function variantSrc(id, round, x, which, revision) {
|
|
925
|
+
var loc = which === 'image' ? x.image : x.preview;
|
|
926
|
+
if (!loc) return '';
|
|
927
|
+
// No backslashes in this script: it lives in a template literal, which would eat them.
|
|
928
|
+
if (/^https:/i.test(loc)) return loc;
|
|
929
|
+
var rel = 'review/' + encodeURIComponent(id) + '/' + round + '/' + encodeURIComponent(x.id) + '/' + which + (revision ? '?v=' + revision : '');
|
|
930
|
+
if (APP) return snap && snap.app && snap.app.url ? snap.app.url + rel : '';
|
|
931
|
+
return '/' + rel;
|
|
932
|
+
}
|
|
933
|
+
function thumbOf(id, r, x) {
|
|
934
|
+
var round = r.round, revision = r.revision || 1;
|
|
935
|
+
var image = function (loc) { return loc && !/^https:/i.test(loc) && /[.](svg|png|jpe?g|webp|gif)$/i.test(loc); };
|
|
936
|
+
if (APP) {
|
|
937
|
+
// A chat lets the page show data: images only: the MCP server reads the file and sends it (loadThumbs).
|
|
938
|
+
var which = image(x.image) ? 'image' : image(x.preview) ? 'preview' : '';
|
|
939
|
+
// The revision too: replaced variants keep their ids, and the old thumbnail must not stand for the new one.
|
|
940
|
+
var key = [id, round, x.id, which, revision].join('|');
|
|
941
|
+
if (which && thumbs[key]) return '<span class="thumb"><img src="' + esc(thumbs[key]) + '" alt=""></span>';
|
|
942
|
+
return '<span class="thumb"' + (which ? ' data-thumb="' + esc(key) + '"' : '') + '>' + esc(x.id) + '</span>';
|
|
943
|
+
}
|
|
944
|
+
// The page loads images from this server only, so a thumbnail at an https address shows as its number.
|
|
945
|
+
var src = image(x.image) ? variantSrc(id, round, x, 'image', revision) : image(x.preview) ? variantSrc(id, round, x, 'preview', revision) : '';
|
|
946
|
+
return src ? '<span class="thumb"><img src="' + esc(src) + '" alt=""></span>' : '<span class="thumb">' + esc(x.id) + '</span>';
|
|
947
|
+
}
|
|
948
|
+
function variantLabel(r, vid) { var x = (r.variants || []).filter(function (c) { return c.id === vid; })[0]; return x ? x.label : vid; }
|
|
949
|
+
|
|
950
|
+
// Variants an agent offered (plan phase 2): pick one, or say what to change for another round.
|
|
951
|
+
function reviewBox(id, n, r, writable, busy) {
|
|
952
|
+
var failed = writeError && writeError.node === id && writeError.kind === 'review' ? writeError.message : '';
|
|
953
|
+
var task = n.todos.filter(function (t) { return t.a === 'you' && !t.confirm; })[0];
|
|
954
|
+
var picked = r.status === 'picked' ? r.choice : null;
|
|
955
|
+
var h = '<div class="ask' + (r.status === 'waiting' ? '' : ' muted') + '">';
|
|
956
|
+
if (r.status === 'waiting') h += '<h4>' + esc(task ? task.t : 'Pick a variant') + (r.round > 1 ? ' · round ' + r.round : '') + '</h4><p>' + (writable ? 'Open each preview, then pick one. Or say what to change, and the agent makes another round.' : 'The agent waits for your pick.') + '</p>';
|
|
957
|
+
else if (r.status === 'picked') h += '<h4>' + (r.via === 'chat' ? 'Picked in the chat: ' : 'You picked ') + '“' + esc(variantLabel(r, picked)) + '”</h4>' + (writable ? '<p>Changed your mind? Pick another one: the steps built from this pick run again.</p>' : '');
|
|
958
|
+
else h += '<h4>You asked for another round</h4><p class="quote">' + esc(r.feedback) + '</p><p>' + esc(r.agent || 'The agent') + ' is making new variants.</p>';
|
|
959
|
+
if (failed) h += '<p class="err">' + esc(failed) + '</p>';
|
|
960
|
+
if (r.status !== 'redo') {
|
|
961
|
+
h += '<div class="variants">' + r.variants.map(function (x) {
|
|
962
|
+
var mine = picked === x.id, saving = busy && pending.choice === x.id;
|
|
963
|
+
var act = mine ? '<span class="tag">Your pick</span>'
|
|
964
|
+
: writable ? '<button class="act' + (picked ? ' ghost' : '') + '" data-pick="' + esc(x.id) + '"' + (busy ? ' disabled' : '') + '>' + (saving ? 'Saving…' : picked ? 'Pick this instead' : 'Pick this') + '</button>' : '';
|
|
965
|
+
var open = variantSrc(id, r.round, x, 'preview', r.revision || 1);
|
|
966
|
+
return '<div class="variant' + (mine ? ' picked' : '') + '">' + thumbOf(id, r, x) + '<span class="vl"><span class="num">' + esc(x.id) + '</span>' + esc(x.label) + '</span>' + (open ? '<a href="' + esc(open) + '" target="_blank" rel="noopener noreferrer">Open preview</a>' : '') + act + '</div>';
|
|
967
|
+
}).join('') + '</div>';
|
|
968
|
+
if (writable) {
|
|
969
|
+
h += '<textarea class="note" data-draft="' + id + '|redo|' + r.round + '" maxlength="2000" placeholder="Or say what to change, e.g. a simpler shape, no letters, warmer colours"' + (busy ? ' disabled' : '') + '>' + esc(draft(id + '|redo|' + r.round) || '') + '</textarea>';
|
|
970
|
+
h += '<button class="act ghost" data-redo="1"' + (busy ? ' disabled' : '') + '>' + (busy && pending.redo ? 'Saving…' : 'Ask for another round') + '</button>';
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
if (r.history && r.history.length) {
|
|
974
|
+
h += '<details class="rounds"><summary>Earlier rounds (' + r.history.length + ')</summary><ul>' + r.history.map(function (p) {
|
|
975
|
+
var links = p.variants.map(function (x) { var open = variantSrc(id, p.round, x, 'preview'); return open ? '<a href="' + esc(open) + '" target="_blank" rel="noopener noreferrer">' + esc(x.label) + '</a>' : esc(x.label); }).join(', ');
|
|
976
|
+
return '<li>Round ' + p.round + ': ' + (links || 'variants not kept') + (p.choice ? ' · picked “' + esc(variantLabel(p, p.choice)) + '”' : '') + (p.feedback ? ' · you asked: <span class="quote">' + esc(p.feedback) + '</span>' : '') + '</li>';
|
|
977
|
+
}).join('') + '</ul></details>';
|
|
978
|
+
}
|
|
979
|
+
return h + '</div>';
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// The author answers a review on the map. round and revision: the variants the page showed, so an
|
|
983
|
+
// answer to an older round, or to variants since replaced, gets 409 instead of landing on ones the author did not see.
|
|
984
|
+
function sendReview(id, body) {
|
|
985
|
+
var r = reviewOf(id);
|
|
986
|
+
if (pending || !r) return;
|
|
987
|
+
body.node = id;
|
|
988
|
+
body.round = r.round;
|
|
989
|
+
body.revision = r.revision || 1;
|
|
990
|
+
pending = { node: id, review: true, round: r.round, revision: r.revision || 1, status: r.status, before: r.choice || null, choice: body.choice || null, redo: Boolean(body.redo), saved: false };
|
|
991
|
+
writeError = null;
|
|
992
|
+
renderPanel(id);
|
|
993
|
+
post('/api/review', body).then(function () {
|
|
994
|
+
delete drafts[id + '|redo|' + r.round];
|
|
995
|
+
var mine = pending;
|
|
996
|
+
if (landed(mine)) { pending = null; if (selected === id) renderPanel(id); return; }
|
|
997
|
+
mine.saved = true;
|
|
998
|
+
setTimeout(function () { if (pending === mine) { pending = null; render(); } }, 3000);
|
|
999
|
+
}, function (error) {
|
|
1000
|
+
pending = null;
|
|
1001
|
+
writeError = { node: id, kind: 'review', key: reviewKey(id), message: (error && error.message) || 'The map server did not answer.' };
|
|
1002
|
+
if (selected === id) renderPanel(id);
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// The author marks their own task done, or takes the mark back. The server
|
|
1007
|
+
// writes the shared state file; the change comes back like any other, over SSE.
|
|
1008
|
+
function sendConfirm(id, task, done) {
|
|
1009
|
+
if (pending) return;
|
|
1010
|
+
// expect: the task index the step shows once the write is in a snapshot.
|
|
1011
|
+
pending = { node: id, expect: done ? task + 1 : task, saved: false };
|
|
1012
|
+
writeError = null;
|
|
1013
|
+
renderPanel(id);
|
|
1014
|
+
var note = done ? draftFor(id, task).trim() : '';
|
|
1015
|
+
post('/api/confirm', note ? { node: id, task: task, done: done, note: note } : { node: id, task: task, done: done }).then(function () {
|
|
1016
|
+
if (done) delete drafts[id + '|note|' + task];
|
|
1017
|
+
var mine = pending;
|
|
1018
|
+
// The snapshot with the write may arrive before this answer, or after it.
|
|
1019
|
+
if (landed(mine)) { pending = null; if (selected === id) renderPanel(id); return; }
|
|
1020
|
+
mine.saved = true;
|
|
1021
|
+
setTimeout(function () { if (pending === mine) { pending = null; render(); } }, 3000);
|
|
1022
|
+
}, function (error) {
|
|
1023
|
+
pending = null;
|
|
1024
|
+
writeError = { node: id, kind: 'confirm', ti: nv(id).taskIndex, message: (error && error.message) || 'The map server did not answer.' };
|
|
1025
|
+
if (selected === id) renderPanel(id);
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// The author does a step themselves, skips it, or takes that back. since: the route the page showed,
|
|
1030
|
+
// so a page behind another write gets 409 instead of overwriting it.
|
|
1031
|
+
function sendRoute(id, how) {
|
|
1032
|
+
if (pending || !snap) return;
|
|
1033
|
+
var n = byId[id], v = nv(id), since = routeAt(id);
|
|
1034
|
+
var body = { node: id, how: how, since: since };
|
|
1035
|
+
if (how === 'self' && n.manual.input === 'browsers') body.value = storesNow(id, v);
|
|
1036
|
+
else if (how === 'self' && n.manual.input !== 'mark') { var val = draft(id + '|value'); body.value = val === null ? givenValue(id, v) : val; }
|
|
1037
|
+
var note = how === 'self' ? (draft(id + '|rnote') || '').trim() : '';
|
|
1038
|
+
if (note) body.note = note;
|
|
1039
|
+
pending = { node: id, route: true, since: since, saved: false };
|
|
1040
|
+
writeError = null;
|
|
1041
|
+
renderPanel(id);
|
|
1042
|
+
post('/api/route', body).then(function () {
|
|
1043
|
+
delete drafts[id + '|value']; delete drafts[id + '|rnote']; delete drafts[id + '|stores'];
|
|
1044
|
+
var mine = pending;
|
|
1045
|
+
if (landed(mine)) { pending = null; if (selected === id) renderPanel(id); return; }
|
|
1046
|
+
mine.saved = true;
|
|
1047
|
+
setTimeout(function () { if (pending === mine) { pending = null; render(); } }, 3000);
|
|
1048
|
+
}, function (error) {
|
|
1049
|
+
pending = null;
|
|
1050
|
+
writeError = { node: id, kind: 'route', at: since, message: (error && error.message) || 'The map server did not answer.' };
|
|
1051
|
+
if (selected === id) renderPanel(id);
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
var view = { x: 0, y: 0, k: 1 }, world = $('world'), map = $('map'), stage = $('stage');
|
|
1056
|
+
var dragging = false, dragMoved = false, dragStart = null;
|
|
1057
|
+
function apply() { world.style.transform = 'translate(' + view.x + 'px,' + view.y + 'px) scale(' + view.k + ')'; }
|
|
1058
|
+
function fit() {
|
|
1059
|
+
var W = stage.clientWidth, H = stage.clientHeight, panelW = (selected && W > 720) ? 380 : 0;
|
|
1060
|
+
view.k = Math.max(0.2, Math.min((W - panelW - 40) / WORLD.w, (H - 70) / WORLD.h, 1.4));
|
|
1061
|
+
view.x = (W - panelW - WORLD.w * view.k) / 2; view.y = (H - 30 - WORLD.h * view.k) / 2; apply();
|
|
1062
|
+
}
|
|
1063
|
+
function zoomAt(px, py, f) { var nk = Math.max(0.2, Math.min(2.6, view.k * f)); view.x = px - (px - view.x) * (nk / view.k); view.y = py - (py - view.y) * (nk / view.k); view.k = nk; apply(); }
|
|
1064
|
+
function focusNode(id) {
|
|
1065
|
+
var n = byId[id], W = stage.clientWidth, H = stage.clientHeight, panelW = W > 720 ? 380 : 0, bottom = W > 720 ? 0 : H * 0.55;
|
|
1066
|
+
view.k = Math.max(view.k, 1.1); view.x = (W - panelW) / 2 - n.x * view.k; view.y = (H - bottom) / 2 - n.y * view.k;
|
|
1067
|
+
world.classList.add('glide'); apply(); setTimeout(function () { world.classList.remove('glide'); }, 500);
|
|
1068
|
+
}
|
|
1069
|
+
// Capture the pointer only once the press turns into a drag: captured early, the
|
|
1070
|
+
// click would go to the SVG instead of the node under it and never select it.
|
|
1071
|
+
map.addEventListener('pointerdown', function (e) { dragging = true; dragMoved = false; dragStart = { x: e.clientX - view.x, y: e.clientY - view.y, sx: e.clientX, sy: e.clientY }; });
|
|
1072
|
+
map.addEventListener('pointermove', function (e) {
|
|
1073
|
+
if (!dragging) return;
|
|
1074
|
+
if (!dragMoved && Math.hypot(e.clientX - dragStart.sx, e.clientY - dragStart.sy) > 4) {
|
|
1075
|
+
dragMoved = true;
|
|
1076
|
+
try { map.setPointerCapture(e.pointerId); } catch (err) {}
|
|
1077
|
+
map.classList.add('dragging');
|
|
1078
|
+
}
|
|
1079
|
+
if (dragMoved) { view.x = e.clientX - dragStart.x; view.y = e.clientY - dragStart.y; apply(); }
|
|
1080
|
+
});
|
|
1081
|
+
map.addEventListener('pointerup', function (e) { var dragged = dragMoved; dragging = false; map.classList.remove('dragging'); if (!dragged && !(e.target.closest && e.target.closest('.node'))) closePanel(); setTimeout(function () { dragMoved = false; }, 0); });
|
|
1082
|
+
map.addEventListener('pointercancel', function () { dragging = false; map.classList.remove('dragging'); });
|
|
1083
|
+
map.addEventListener('wheel', function (e) { if (APP && hostMode === 'inline' && !e.ctrlKey) return; e.preventDefault(); var r = map.getBoundingClientRect(); zoomAt(e.clientX - r.left, e.clientY - r.top, Math.exp(-e.deltaY * 0.0016)); }, { passive: false });
|
|
1084
|
+
$('bZoomIn').addEventListener('click', function () { zoomAt(stage.clientWidth / 2, stage.clientHeight / 2, 1.25); });
|
|
1085
|
+
$('bZoomOut').addEventListener('click', function () { zoomAt(stage.clientWidth / 2, stage.clientHeight / 2, 0.8); });
|
|
1086
|
+
$('bFit').addEventListener('click', fit);
|
|
1087
|
+
$('pClose').addEventListener('click', closePanel);
|
|
1088
|
+
// In a chat the host resizes the frame (to its height, to full screen and back): keep the open step in view.
|
|
1089
|
+
window.addEventListener('resize', function () { if (APP && selected) focusNode(selected); else fit(); });
|
|
1090
|
+
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closePanel(); if (e.key === '0' && e.target === document.body) fit(); });
|
|
1091
|
+
var youCursor = 0;
|
|
1092
|
+
$('cYou').addEventListener('click', function () { if (!snap || !snap.view.needsYou.length) return; var id = snap.view.needsYou[youCursor++ % snap.view.needsYou.length]; focusNode(id); select(id); });
|
|
1093
|
+
function applyInactive() { $('bInactive').classList.toggle('on', showInactive); $('bInactive').textContent = showInactive ? 'Show inactive' : 'Inactive hidden'; map.classList.toggle('hide-inactive', !showInactive); }
|
|
1094
|
+
$('bInactive').addEventListener('click', function () { showInactive = !showInactive; try { localStorage.setItem('build-map.inactive', showInactive ? 'shown' : 'hidden'); } catch (e) {} applyInactive(); });
|
|
1095
|
+
|
|
1096
|
+
function fromHash() { var m = /node=([A-Za-z]+)/.exec(location.hash); if (m && byId[m[1]]) { focusNode(m[1]); select(m[1]); } }
|
|
1097
|
+
window.addEventListener('hashchange', fromHash);
|
|
1098
|
+
|
|
1099
|
+
function setConn(state, text) { $('cConn').className = 'chip conn ' + state; $('cConnT').textContent = text; }
|
|
1100
|
+
function notice(html) { $('notice').innerHTML = html; $('notice').classList.toggle('show', Boolean(html)); }
|
|
1101
|
+
// A new snapshot, from the stream or, in a chat, from build_map_view. note: what the page says (trusted
|
|
1102
|
+
// markup) when the snapshot reports no problem of its own.
|
|
1103
|
+
function showSnapshot(next, note) {
|
|
1104
|
+
snap = next;
|
|
1105
|
+
if (pending && pending.saved && landed(pending)) pending = null;
|
|
1106
|
+
if (loginWait && JSON.stringify((snap.account && snap.account.login) || null) !== loginWait.before) loginWait = null;
|
|
1107
|
+
// An error about a step that has moved on since describes a state the page no longer shows.
|
|
1108
|
+
if (writeError && (writeError.kind === 'route' ? routeAt(writeError.node) !== writeError.at : writeError.kind === 'review' ? reviewKey(writeError.node) !== writeError.key : nv(writeError.node).taskIndex !== writeError.ti)) writeError = null;
|
|
1109
|
+
// A restart that failed says why; the button offers it again.
|
|
1110
|
+
if (updateWait && snap.update && !snap.update.updating && snap.update.problem) updateWait = null;
|
|
1111
|
+
var issue = snap.problem || (snap.live && snap.live.problem) || (snap.update && snap.update.problem);
|
|
1112
|
+
notice(issue ? esc(issue) : note);
|
|
1113
|
+
renderUpdate();
|
|
1114
|
+
render();
|
|
1115
|
+
if (firstSnapshot) { firstSnapshot = false; if (APP) fromArgs(); else fromHash(); }
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// The release this map runs, and the newer one on npm it can restart on (snap.update).
|
|
1119
|
+
var updateWait = null;
|
|
1120
|
+
function renderUpdate() {
|
|
1121
|
+
var u = snap && snap.update, b = $('bUpdate');
|
|
1122
|
+
if (u) $('cConn').title = 'Connection to the local map server: @extenshi/cli ' + u.version + ', steps revision ' + u.graph;
|
|
1123
|
+
if (!u || !SESSION.token || !(u.available || u.updating)) { b.hidden = true; return; }
|
|
1124
|
+
b.hidden = false;
|
|
1125
|
+
if (u.updating || updateWait) {
|
|
1126
|
+
b.disabled = true;
|
|
1127
|
+
b.textContent = 'Updating…';
|
|
1128
|
+
b.title = 'Starting @extenshi/cli ' + (u.updating ? u.updating.to : updateWait) + ' for this map. The page reloads by itself once it runs.';
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
b.disabled = false;
|
|
1132
|
+
b.textContent = u.agent ? 'Update: new steps' : 'Update to ' + u.available;
|
|
1133
|
+
b.title = 'Restart this map on @extenshi/cli ' + u.available + '. It runs ' + u.version + ' now' + (u.agent ? '; an agent already works with the newer steps' : '') + '. Your progress stays: it is in .extenshi/build-map.json.';
|
|
1134
|
+
}
|
|
1135
|
+
function sendUpdate() {
|
|
1136
|
+
var u = snap && snap.update;
|
|
1137
|
+
if (!u || !u.available || updateWait) return;
|
|
1138
|
+
updateWait = u.available;
|
|
1139
|
+
renderUpdate();
|
|
1140
|
+
fetch('/api/update', {
|
|
1141
|
+
method: 'POST',
|
|
1142
|
+
headers: { 'content-type': 'application/json', 'x-extenshi-map-page': SESSION.token },
|
|
1143
|
+
body: JSON.stringify({ to: u.available })
|
|
1144
|
+
}).then(function (res) {
|
|
1145
|
+
if (res.status === 403) { location.reload(); return new Promise(function () {}); }
|
|
1146
|
+
return res.json().catch(function () { return {}; }).then(function (body) {
|
|
1147
|
+
if (!res.ok) throw new Error(body.message || ('The map server answered ' + res.status + '.'));
|
|
1148
|
+
});
|
|
1149
|
+
}).catch(function (err) {
|
|
1150
|
+
updateWait = null;
|
|
1151
|
+
renderUpdate();
|
|
1152
|
+
notice(esc(err.message));
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
$('bUpdate').addEventListener('click', sendUpdate);
|
|
1156
|
+
function connect() {
|
|
1157
|
+
if (APP) { appConnect(); return; }
|
|
1158
|
+
if (!window.EventSource || location.protocol === 'file:') { setConn('', 'snapshot'); return; }
|
|
1159
|
+
var es = new EventSource('/events');
|
|
1160
|
+
// Every connection opens with the server's instance. Another one means the
|
|
1161
|
+
// server restarted on this port: reload for its page token.
|
|
1162
|
+
es.addEventListener('hello', function (ev) {
|
|
1163
|
+
var hello = JSON.parse(ev.data);
|
|
1164
|
+
if (SESSION.instance && hello.instance !== SESSION.instance) location.reload();
|
|
1165
|
+
});
|
|
1166
|
+
es.addEventListener('map', function (ev) {
|
|
1167
|
+
var next = JSON.parse(ev.data);
|
|
1168
|
+
// A newer server took over this port: its page knows things this one does not.
|
|
1169
|
+
if (next.protocol !== SESSION.protocol) { location.reload(); return; }
|
|
1170
|
+
setConn('live', 'live');
|
|
1171
|
+
showSnapshot(next, '');
|
|
1172
|
+
});
|
|
1173
|
+
es.addEventListener('bye', function (ev) {
|
|
1174
|
+
// A newer release takes the port over: the stream reconnects to it and its hello reloads the page.
|
|
1175
|
+
if (ev.data === 'updated') {
|
|
1176
|
+
setConn('wait', 'updating');
|
|
1177
|
+
notice('The map restarts on the newer release. This page reloads by itself.');
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
setConn('', 'stopped');
|
|
1181
|
+
notice('The map server stopped' + (ev.data ? ' (' + esc(ev.data) + ')' : '') + '. Run <code>npx @extenshi/cli map</code> in the project to bring it back; this page reconnects by itself.');
|
|
1182
|
+
});
|
|
1183
|
+
es.onerror = function () { setConn('wait', 'reconnecting'); };
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// ── The map in a chat (MCP Apps 2026-01-26) ──────────────────────────────────
|
|
1187
|
+
// The host renders this page in a sandboxed frame and runs the MCP server's tools for it. JSON-RPC over
|
|
1188
|
+
// postMessage: ui/initialize, then the tool's arguments (projectDir, step); the snapshot comes from the
|
|
1189
|
+
// app-only tool build_map_view every few seconds (the answer is "unchanged" while it is).
|
|
1190
|
+
var rpcSeq = 0, rpcWaiting = {}, hostCtx = {}, hostMode = 'inline', appArgs = {}, argsSeen = false, gotArgs = null;
|
|
1191
|
+
var appEtag = '', appTimer = null, appStopped = false;
|
|
1192
|
+
var NO_SERVER = 'The map server is not running, so the steps only the project files prove look not done here. It starts with <code>npx @extenshi/cli map</code> in the project, and this view fills in by itself.';
|
|
1193
|
+
var SKEW = 'The map server runs another version of <code>@extenshi/cli</code> than this view. If something looks off, restart it with <code>npx @extenshi/cli@latest map</code>.';
|
|
1194
|
+
function appSend(message) { window.parent.postMessage(message, '*'); }
|
|
1195
|
+
function appRequest(method, params) {
|
|
1196
|
+
return new Promise(function (resolve, reject) {
|
|
1197
|
+
var id = ++rpcSeq;
|
|
1198
|
+
rpcWaiting[id] = { resolve: resolve, reject: reject };
|
|
1199
|
+
appSend({ jsonrpc: '2.0', id: id, method: method, params: params || {} });
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
function appNotify(method, params) { appSend({ jsonrpc: '2.0', method: method, params: params || {} }); }
|
|
1203
|
+
// The MCP server answers with JSON text; a refusal carries its reason there too.
|
|
1204
|
+
function appTool(name, args) {
|
|
1205
|
+
return appRequest('tools/call', { name: name, arguments: args }).then(function (result) {
|
|
1206
|
+
var text = result && result.content && result.content[0] && result.content[0].text;
|
|
1207
|
+
if (!result || result.isError) throw new Error(text || 'The map did not answer.');
|
|
1208
|
+
return text ? JSON.parse(text) : {};
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
function projectArgs(extra) {
|
|
1212
|
+
var args = {};
|
|
1213
|
+
if (appArgs.projectDir) args.projectDir = appArgs.projectDir;
|
|
1214
|
+
for (var k in extra) args[k] = extra[k];
|
|
1215
|
+
return args;
|
|
1216
|
+
}
|
|
1217
|
+
function appWrite(kind, body) {
|
|
1218
|
+
return appTool('build_map_answer', projectArgs({ kind: kind, body: body })).then(function (answer) {
|
|
1219
|
+
if (!answer.ok) throw new Error(answer.message || 'The map did not take the answer.');
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
function loadThumbs() {
|
|
1223
|
+
document.querySelectorAll('[data-thumb]').forEach(function (t) {
|
|
1224
|
+
var key = t.getAttribute('data-thumb');
|
|
1225
|
+
if (thumbAsked[key]) return;
|
|
1226
|
+
thumbAsked[key] = true;
|
|
1227
|
+
var p = key.split('|');
|
|
1228
|
+
appTool('build_map_file', projectArgs({ step: p[0], round: Number(p[1]), variant: p[2], which: p[3] })).then(function (file) {
|
|
1229
|
+
if (!file.src) return;
|
|
1230
|
+
thumbs[key] = file.src;
|
|
1231
|
+
if (selected) renderPanel(selected);
|
|
1232
|
+
}, function () {});
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
function fromArgs() { if (appArgs.step && byId[appArgs.step]) { focusNode(appArgs.step); select(appArgs.step); } }
|
|
1236
|
+
// Fixed by the host, or full screen: fill it. Otherwise ask for a height that fits a chat.
|
|
1237
|
+
function appSize() {
|
|
1238
|
+
var d = hostCtx.containerDimensions || {}, root = document.documentElement;
|
|
1239
|
+
root.style.height = hostMode === 'fullscreen' || d.height ? '100vh' : Math.min(d.maxHeight || 640, 640) + 'px';
|
|
1240
|
+
var box = root.getBoundingClientRect();
|
|
1241
|
+
appNotify('ui/notifications/size-changed', { width: Math.ceil(box.width), height: Math.ceil(box.height) });
|
|
1242
|
+
}
|
|
1243
|
+
function applyHost(ctx) {
|
|
1244
|
+
for (var k in ctx) hostCtx[k] = ctx[k];
|
|
1245
|
+
if (hostCtx.theme === 'dark' || hostCtx.theme === 'light') document.documentElement.setAttribute('data-theme', hostCtx.theme);
|
|
1246
|
+
if (hostCtx.displayMode) hostMode = hostCtx.displayMode;
|
|
1247
|
+
$('bFull').hidden = (hostCtx.availableDisplayModes || []).indexOf('fullscreen') < 0;
|
|
1248
|
+
$('bFull').textContent = hostMode === 'fullscreen' ? 'Exit full screen' : 'Full screen';
|
|
1249
|
+
appSize();
|
|
1250
|
+
}
|
|
1251
|
+
function appPoll() {
|
|
1252
|
+
clearTimeout(appTimer);
|
|
1253
|
+
if (appStopped) return;
|
|
1254
|
+
appTool('build_map_view', projectArgs(appEtag ? { since: appEtag } : {})).then(function (answer) {
|
|
1255
|
+
if (answer.unchanged) return;
|
|
1256
|
+
appEtag = answer.etag || '';
|
|
1257
|
+
var next = answer.snapshot, facts = Boolean(next.app && next.app.facts);
|
|
1258
|
+
setConn(facts ? 'live' : 'wait', facts ? 'live' : 'files not read');
|
|
1259
|
+
showSnapshot(next, !facts ? NO_SERVER : next.protocol !== SESSION.protocol ? SKEW : '');
|
|
1260
|
+
}, function () {
|
|
1261
|
+
setConn('wait', 'reconnecting');
|
|
1262
|
+
}).then(function () {
|
|
1263
|
+
if (!appStopped) appTimer = setTimeout(appPoll, document.hidden ? 10000 : 2500);
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
function appConnect() {
|
|
1267
|
+
setConn('wait', 'connecting');
|
|
1268
|
+
window.addEventListener('message', function (ev) {
|
|
1269
|
+
var m = ev.data;
|
|
1270
|
+
if (ev.source !== window.parent || !m || m.jsonrpc !== '2.0') return;
|
|
1271
|
+
if (m.method === undefined && m.id != null && rpcWaiting[m.id]) {
|
|
1272
|
+
var w = rpcWaiting[m.id];
|
|
1273
|
+
delete rpcWaiting[m.id];
|
|
1274
|
+
if (m.error) w.reject(new Error(m.error.message || 'The host refused.'));
|
|
1275
|
+
else w.resolve(m.result);
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
if (m.method === 'ui/notifications/tool-input') {
|
|
1279
|
+
appArgs = (m.params && m.params.arguments) || {};
|
|
1280
|
+
argsSeen = true;
|
|
1281
|
+
if (gotArgs) gotArgs();
|
|
1282
|
+
} else if (m.method === 'ui/notifications/host-context-changed') applyHost(m.params || {});
|
|
1283
|
+
else if (m.method === 'ui/resource-teardown') {
|
|
1284
|
+
appStopped = true;
|
|
1285
|
+
clearTimeout(appTimer);
|
|
1286
|
+
if (m.id != null) appSend({ jsonrpc: '2.0', id: m.id, result: {} });
|
|
1287
|
+
} else if (m.method === 'ping' && m.id != null) appSend({ jsonrpc: '2.0', id: m.id, result: {} });
|
|
1288
|
+
});
|
|
1289
|
+
// Links (variant previews, Dojo, the stores) open through the host; #node= links stay on the map.
|
|
1290
|
+
document.addEventListener('click', function (e) {
|
|
1291
|
+
var a = e.target.closest && e.target.closest('a[href]');
|
|
1292
|
+
if (!a || a.getAttribute('href').charAt(0) === '#') return;
|
|
1293
|
+
e.preventDefault();
|
|
1294
|
+
appRequest('ui/open-link', { url: a.href }).catch(function () {});
|
|
1295
|
+
});
|
|
1296
|
+
$('bFull').addEventListener('click', function () {
|
|
1297
|
+
appRequest('ui/request-display-mode', { mode: hostMode === 'fullscreen' ? 'inline' : 'fullscreen' }).then(function (r) {
|
|
1298
|
+
if (r && r.mode) applyHost({ displayMode: r.mode });
|
|
1299
|
+
}, function () {});
|
|
1300
|
+
});
|
|
1301
|
+
document.addEventListener('visibilitychange', function () { if (!document.hidden && snap) appPoll(); });
|
|
1302
|
+
appRequest('ui/initialize', {
|
|
1303
|
+
protocolVersion: '2026-01-26',
|
|
1304
|
+
appInfo: { name: 'extenshi-build-map', version: APP.version },
|
|
1305
|
+
appCapabilities: { availableDisplayModes: ['inline', 'fullscreen'] }
|
|
1306
|
+
}).then(function (result) {
|
|
1307
|
+
applyHost((result && result.hostContext) || {});
|
|
1308
|
+
// The tool's arguments come right after this; a host that sends none gets the map of the server's working directory.
|
|
1309
|
+
var args = new Promise(function (resolve) { gotArgs = resolve; if (argsSeen) resolve(); setTimeout(resolve, 1500); });
|
|
1310
|
+
appNotify('ui/notifications/initialized', {});
|
|
1311
|
+
return args;
|
|
1312
|
+
}).then(function () {
|
|
1313
|
+
gotArgs = null;
|
|
1314
|
+
appPoll();
|
|
1315
|
+
}, function () {
|
|
1316
|
+
setConn('', 'no host');
|
|
1317
|
+
notice('This page is the build map for a chat that shows MCP Apps, and no chat answered. <code>npx @extenshi/cli map</code> opens it in the browser.');
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
buildWorld();
|
|
1322
|
+
applyInactive();
|
|
1323
|
+
fit();
|
|
1324
|
+
connect();
|
|
1325
|
+
})();
|
|
1326
|
+
`;
|
|
1327
|
+
//# sourceMappingURL=build-map-page.js.map
|