@fingerskier/augment 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp/apps.js CHANGED
@@ -36,57 +36,57 @@ export function escapeInlineJson(value) {
36
36
  export function escapeInlineScript(js) {
37
37
  return js.replace(/<\/script/gi, "<\\/script");
38
38
  }
39
- const SNIPPET_CSS = `
40
- :root {
41
- --bg: #0f1419; --panel: #161b22; --line: #2a313c; --fg: #e6edf3; --muted: #8b949e;
42
- --accent: #58a6ff; --danger: #f85149; --ok: #3fb950;
43
- --spec: #58a6ff; --arch: #bc8cff; --issue: #f85149; --todo: #d29922; --work: #3fb950;
44
- }
45
- :root[data-theme="light"] {
46
- --bg: #ffffff; --panel: #f6f8fa; --line: #d0d7de; --fg: #1f2328; --muted: #59636e;
47
- --accent: #0969da; --danger: #d1242f; --ok: #1a7f37;
48
- }
49
- * { box-sizing: border-box; }
50
- html, body { margin: 0; }
51
- body {
52
- background: var(--color-background-primary, var(--bg));
53
- color: var(--color-text-primary, var(--fg));
54
- font: 13px/1.5 var(--font-sans, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);
55
- padding: 12px 14px;
56
- }
57
- h1 { font-size: 15px; margin: 0; }
58
- h2 { font-size: 12px; margin: 14px 0 6px; color: var(--color-text-secondary, var(--muted));
59
- text-transform: uppercase; letter-spacing: .4px; }
60
- .head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
61
- .head .brand { color: var(--accent); font-weight: 700; letter-spacing: .5px; font-size: 12px; }
62
- .head .totals { color: var(--color-text-secondary, var(--muted)); font-size: 12px; margin-left: auto; }
63
- .muted { color: var(--color-text-secondary, var(--muted)); }
64
- .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 4px 18px; }
65
- .bar { display: flex; height: 12px; border-radius: 6px; overflow: hidden;
66
- border: 1px solid var(--color-border-primary, var(--line)); background: var(--color-background-secondary, var(--panel)); }
67
- .bar > span { display: block; height: 100%; }
68
- .legend { display: flex; flex-wrap: wrap; gap: 4px 14px; margin-top: 6px; font-size: 12px; }
69
- .dot { display: inline-block; width: 8px; height: 8px; border-radius: 4px; margin-right: 5px; }
70
- .chips { display: flex; flex-wrap: wrap; gap: 6px; }
71
- .chip { border: 1px solid var(--color-border-primary, var(--line)); border-radius: 10px;
72
- padding: 1px 9px; font-size: 12px; background: var(--color-background-secondary, var(--panel)); }
73
- .chip b { color: var(--accent); font-weight: 600; margin-left: 4px; }
74
- .rows .row { display: flex; gap: 8px; align-items: center; padding: 3px 0; font-size: 12px; }
75
- .rows .row .grow { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
76
- .kind-pill { display: inline-block; padding: 0 7px; border-radius: 10px; font-size: 11px;
77
- font-weight: 600; color: #04121f; }
78
- .spark { width: 100%; height: 56px; display: block; }
79
- .footer { margin-top: 14px; display: flex; align-items: center; gap: 10px; }
80
- .footer .stamp { color: var(--color-text-secondary, var(--muted)); font-size: 11px; }
81
- button {
82
- font: inherit; color: var(--color-text-primary, var(--fg));
83
- background: var(--color-background-secondary, var(--panel));
84
- border: 1px solid var(--color-border-primary, var(--line)); border-radius: 6px;
85
- padding: 3px 10px; cursor: pointer;
86
- }
87
- button:hover { border-color: var(--accent); }
88
- button[hidden] { display: none; }
89
- #status { color: var(--color-text-secondary, var(--muted)); }
39
+ const SNIPPET_CSS = `
40
+ :root {
41
+ --bg: #0f1419; --panel: #161b22; --line: #2a313c; --fg: #e6edf3; --muted: #8b949e;
42
+ --accent: #58a6ff; --danger: #f85149; --ok: #3fb950;
43
+ --spec: #58a6ff; --arch: #bc8cff; --issue: #f85149; --todo: #d29922; --work: #3fb950;
44
+ }
45
+ :root[data-theme="light"] {
46
+ --bg: #ffffff; --panel: #f6f8fa; --line: #d0d7de; --fg: #1f2328; --muted: #59636e;
47
+ --accent: #0969da; --danger: #d1242f; --ok: #1a7f37;
48
+ }
49
+ * { box-sizing: border-box; }
50
+ html, body { margin: 0; }
51
+ body {
52
+ background: var(--color-background-primary, var(--bg));
53
+ color: var(--color-text-primary, var(--fg));
54
+ font: 13px/1.5 var(--font-sans, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);
55
+ padding: 12px 14px;
56
+ }
57
+ h1 { font-size: 15px; margin: 0; }
58
+ h2 { font-size: 12px; margin: 14px 0 6px; color: var(--color-text-secondary, var(--muted));
59
+ text-transform: uppercase; letter-spacing: .4px; }
60
+ .head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
61
+ .head .brand { color: var(--accent); font-weight: 700; letter-spacing: .5px; font-size: 12px; }
62
+ .head .totals { color: var(--color-text-secondary, var(--muted)); font-size: 12px; margin-left: auto; }
63
+ .muted { color: var(--color-text-secondary, var(--muted)); }
64
+ .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 4px 18px; }
65
+ .bar { display: flex; height: 12px; border-radius: 6px; overflow: hidden;
66
+ border: 1px solid var(--color-border-primary, var(--line)); background: var(--color-background-secondary, var(--panel)); }
67
+ .bar > span { display: block; height: 100%; }
68
+ .legend { display: flex; flex-wrap: wrap; gap: 4px 14px; margin-top: 6px; font-size: 12px; }
69
+ .dot { display: inline-block; width: 8px; height: 8px; border-radius: 4px; margin-right: 5px; }
70
+ .chips { display: flex; flex-wrap: wrap; gap: 6px; }
71
+ .chip { border: 1px solid var(--color-border-primary, var(--line)); border-radius: 10px;
72
+ padding: 1px 9px; font-size: 12px; background: var(--color-background-secondary, var(--panel)); }
73
+ .chip b { color: var(--accent); font-weight: 600; margin-left: 4px; }
74
+ .rows .row { display: flex; gap: 8px; align-items: center; padding: 3px 0; font-size: 12px; }
75
+ .rows .row .grow { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
76
+ .kind-pill { display: inline-block; padding: 0 7px; border-radius: 10px; font-size: 11px;
77
+ font-weight: 600; color: #04121f; }
78
+ .spark { width: 100%; height: 56px; display: block; }
79
+ .footer { margin-top: 14px; display: flex; align-items: center; gap: 10px; }
80
+ .footer .stamp { color: var(--color-text-secondary, var(--muted)); font-size: 11px; }
81
+ button {
82
+ font: inherit; color: var(--color-text-primary, var(--fg));
83
+ background: var(--color-background-secondary, var(--panel));
84
+ border: 1px solid var(--color-border-primary, var(--line)); border-radius: 6px;
85
+ padding: 3px 10px; cursor: pointer;
86
+ }
87
+ button:hover { border-color: var(--accent); }
88
+ button[hidden] { display: none; }
89
+ #status { color: var(--color-text-secondary, var(--muted)); }
90
90
  `;
91
91
  /**
92
92
  * Shared snippet runtime: XSS-safe DOM helpers plus a minimal MCP Apps bridge.
@@ -96,442 +96,442 @@ button[hidden] { display: none; }
96
96
  * JSON island (used by the classic MCP-UI embedded variant) when no host bridge
97
97
  * answers. All dynamic values reach the DOM via textContent, never markup.
98
98
  */
99
- const SNIPPET_RUNTIME_JS = String.raw `
100
- var KIND_COLORS = { SPEC: "#58a6ff", ARCH: "#bc8cff", ISSUE: "#f85149", TODO: "#d29922", WORK: "#3fb950" };
101
-
102
- function $(sel) { return document.querySelector(sel); }
103
-
104
- function h(tag, props) {
105
- var node = document.createElement(tag);
106
- applyProps(node, props);
107
- for (var i = 2; i < arguments.length; i += 1) appendKid(node, arguments[i]);
108
- return node;
109
- }
110
-
111
- function s(tag, props) {
112
- var node = document.createElementNS("http://www.w3.org/2000/svg", tag);
113
- for (var key in (props || {})) node.setAttribute(key, props[key]);
114
- for (var i = 2; i < arguments.length; i += 1) appendKid(node, arguments[i]);
115
- return node;
116
- }
117
-
118
- function applyProps(node, props) {
119
- for (var key in (props || {})) {
120
- var value = props[key];
121
- if (key === "class") node.className = value;
122
- else if (key === "text") node.textContent = value;
123
- else if (key.indexOf("on") === 0 && typeof value === "function") node.addEventListener(key.slice(2), value);
124
- else if (value !== undefined && value !== null) node.setAttribute(key, value);
125
- }
126
- }
127
-
128
- function appendKid(node, kid) {
129
- node.append(kid && kid.nodeType ? kid : document.createTextNode(kid == null ? "" : kid));
130
- }
131
-
132
- function kindPill(kind) {
133
- return h("span", { class: "kind-pill", style: "background:" + (KIND_COLORS[kind] || "#8b949e") }, kind);
134
- }
135
-
136
- function augmentApp(appName, render) {
137
- var nextId = 1;
138
- var pending = {};
139
- // Renderers that paint to a canvas (cytoscape) can't read live CSS
140
- // variables, so they register a callback here to recompute colors and
141
- // repaint when the host theme changes. Reset on every render.
142
- var themeHandlers = [];
143
- var bridge = {
144
- connected: false,
145
- callTool: null,
146
- onThemeChange: function (handler) { themeHandlers.push(handler); },
147
- };
148
- var lastData = null;
149
- // Top-level (no embedding frame) means window.parent === window: any
150
- // handshake we posted would loop straight back to ourselves.
151
- var topLevel = window.parent === window;
152
- // Hold outbound notifications until the handshake settles (spec ordering:
153
- // nothing before ui/notifications/initialized) or we give up on a host.
154
- var canNotify = false;
155
-
156
- function post(message) { window.parent.postMessage(message, "*"); }
157
- function notify(method, params) {
158
- var message = { jsonrpc: "2.0", method: method };
159
- if (params) message.params = params;
160
- post(message);
161
- }
162
- function request(method, params, timeoutMs) {
163
- return new Promise(function (resolve, reject) {
164
- var id = nextId; nextId += 1;
165
- var timer = timeoutMs
166
- ? setTimeout(function () { delete pending[id]; reject(new Error(method + " timed out")); }, timeoutMs)
167
- : null;
168
- pending[id] = function (result, error) {
169
- if (timer) clearTimeout(timer);
170
- if (error) reject(new Error(error.message || "host error"));
171
- else resolve(result);
172
- };
173
- post({ jsonrpc: "2.0", id: id, method: method, params: params || {} });
174
- });
175
- }
176
-
177
- function applyHostContext(context) {
178
- if (!context) return;
179
- if (context.theme) document.documentElement.setAttribute("data-theme", context.theme);
180
- var variables = context.styles && context.styles.variables;
181
- if (variables) {
182
- for (var name in variables) {
183
- if (name.indexOf("--") === 0) document.documentElement.style.setProperty(name, String(variables[name]));
184
- }
185
- }
186
- // Now that the effective colors are updated, let canvas renderers repaint
187
- // (CSS variables never reach a <canvas>, so they must do it themselves).
188
- for (var i = 0; i < themeHandlers.length; i += 1) {
189
- try { themeHandlers[i](); } catch (error) { /* a broken repaint must not wedge the frame */ }
190
- }
191
- }
192
-
193
- function reportSize() {
194
- if (!canNotify) return;
195
- notify("ui/notifications/size-changed", {
196
- width: document.documentElement.scrollWidth,
197
- height: document.documentElement.scrollHeight,
198
- });
199
- }
200
-
201
- function setStatus(text) {
202
- var status = $("#status");
203
- if (status) status.textContent = text;
204
- }
205
-
206
- function show(data) {
207
- lastData = data;
208
- themeHandlers = []; // the previous render's handlers reference stale nodes
209
- var app = $("#app");
210
- app.replaceChildren();
211
- try {
212
- render(app, data, bridge);
213
- } catch (error) {
214
- app.replaceChildren(h("p", { class: "muted" }, "Failed to render: " + (error && error.message)));
215
- }
216
- requestAnimationFrame(reportSize);
217
- }
218
-
219
- function showMessage(text) {
220
- var app = $("#app");
221
- app.replaceChildren(h("p", { class: "muted", id: "status" }, text));
222
- requestAnimationFrame(reportSize);
223
- }
224
-
225
- window.addEventListener("message", function (event) {
226
- // Only the embedding host may drive this view: drop self-echoes when
227
- // rendered top-level and anything a co-resident frame tries to inject.
228
- if (topLevel || event.source !== window.parent) return;
229
- var message = event.data;
230
- if (!message || message.jsonrpc !== "2.0") return;
231
- if (message.id !== undefined && message.method === undefined) {
232
- var handler = pending[message.id];
233
- if (handler) { delete pending[message.id]; handler(message.result, message.error); }
234
- return;
235
- }
236
- if (message.method === "ui/notifications/tool-result") {
237
- var result = message.params || {};
238
- if (result.structuredContent) {
239
- show(result.structuredContent);
240
- } else {
241
- // Text-only results (e.g. "no such project") must still surface
242
- // instead of leaving the frame on "Waiting for tool result…".
243
- var texts = [];
244
- var blocks = result.content || [];
245
- for (var i = 0; i < blocks.length; i += 1) {
246
- if (blocks[i] && blocks[i].type === "text") texts.push(blocks[i].text);
247
- }
248
- showMessage(texts.length ? texts.join("\n") : "Tool returned no renderable data.");
249
- }
250
- return;
251
- }
252
- if (message.method === "ui/notifications/host-context-changed") {
253
- applyHostContext(message.params);
254
- requestAnimationFrame(reportSize);
255
- return;
256
- }
257
- if (message.id !== undefined && message.method !== undefined) {
258
- if (message.method === "ping" || message.method === "ui/resource-teardown") {
259
- post({ jsonrpc: "2.0", id: message.id, result: {} });
260
- } else {
261
- post({ jsonrpc: "2.0", id: message.id, error: { code: -32601, message: "Method not found" } });
262
- }
263
- }
264
- });
265
-
266
- var inlineNode = document.getElementById("augment-data");
267
- var inlineData = null;
268
- if (inlineNode) {
269
- try { inlineData = JSON.parse(inlineNode.textContent); } catch (error) { inlineData = null; }
270
- }
271
- var hasInlineData = Boolean(inlineData);
272
- if (inlineData) show(inlineData);
273
-
274
- if (topLevel) {
275
- if (!lastData) setStatus("No MCP Apps host detected and no inline data.");
276
- } else {
277
- // No hard deadline on the handshake: a slow host may still answer after
278
- // the status flips, and the pending entry stays live so a late response
279
- // completes the connection instead of being dropped.
280
- var statusTimer = setTimeout(function () {
281
- if (bridge.connected) return;
282
- if (hasInlineData) {
283
- // Classic embedded variant only: no SEP-1865 handshake occurs, yet
284
- // the inline content is already shown, so a size hint is safe and
285
- // useful. A conformant host completes ui/initialize first (clearing
286
- // this timer), so it never receives an out-of-lifecycle notification.
287
- canNotify = true;
288
- reportSize();
289
- } else {
290
- setStatus("No MCP Apps host detected and no inline data.");
291
- }
292
- }, 3000);
293
- request("ui/initialize", {
294
- protocolVersion: "2026-01-26",
295
- appInfo: { name: appName, version: "1" },
296
- appCapabilities: {},
297
- }).then(function (result) {
298
- clearTimeout(statusTimer);
299
- bridge.connected = true;
300
- bridge.callTool = function (name, args) {
301
- return request("tools/call", { name: name, arguments: args || {} }, 120000);
302
- };
303
- notify("ui/notifications/initialized");
304
- canNotify = true;
305
- if (result) applyHostContext(result.hostContext);
306
- reportSize();
307
- if (!lastData) setStatus("Waiting for tool result…");
308
- });
309
- }
310
-
311
- if (window.ResizeObserver) {
312
- var scheduled = false;
313
- new ResizeObserver(function () {
314
- if (scheduled) return;
315
- scheduled = true;
316
- requestAnimationFrame(function () { scheduled = false; reportSize(); });
317
- }).observe(document.body);
318
- }
319
- }
99
+ const SNIPPET_RUNTIME_JS = String.raw `
100
+ var KIND_COLORS = { SPEC: "#58a6ff", ARCH: "#bc8cff", ISSUE: "#f85149", TODO: "#d29922", WORK: "#3fb950" };
101
+
102
+ function $(sel) { return document.querySelector(sel); }
103
+
104
+ function h(tag, props) {
105
+ var node = document.createElement(tag);
106
+ applyProps(node, props);
107
+ for (var i = 2; i < arguments.length; i += 1) appendKid(node, arguments[i]);
108
+ return node;
109
+ }
110
+
111
+ function s(tag, props) {
112
+ var node = document.createElementNS("http://www.w3.org/2000/svg", tag);
113
+ for (var key in (props || {})) node.setAttribute(key, props[key]);
114
+ for (var i = 2; i < arguments.length; i += 1) appendKid(node, arguments[i]);
115
+ return node;
116
+ }
117
+
118
+ function applyProps(node, props) {
119
+ for (var key in (props || {})) {
120
+ var value = props[key];
121
+ if (key === "class") node.className = value;
122
+ else if (key === "text") node.textContent = value;
123
+ else if (key.indexOf("on") === 0 && typeof value === "function") node.addEventListener(key.slice(2), value);
124
+ else if (value !== undefined && value !== null) node.setAttribute(key, value);
125
+ }
126
+ }
127
+
128
+ function appendKid(node, kid) {
129
+ node.append(kid && kid.nodeType ? kid : document.createTextNode(kid == null ? "" : kid));
130
+ }
131
+
132
+ function kindPill(kind) {
133
+ return h("span", { class: "kind-pill", style: "background:" + (KIND_COLORS[kind] || "#8b949e") }, kind);
134
+ }
135
+
136
+ function augmentApp(appName, render) {
137
+ var nextId = 1;
138
+ var pending = {};
139
+ // Renderers that paint to a canvas (cytoscape) can't read live CSS
140
+ // variables, so they register a callback here to recompute colors and
141
+ // repaint when the host theme changes. Reset on every render.
142
+ var themeHandlers = [];
143
+ var bridge = {
144
+ connected: false,
145
+ callTool: null,
146
+ onThemeChange: function (handler) { themeHandlers.push(handler); },
147
+ };
148
+ var lastData = null;
149
+ // Top-level (no embedding frame) means window.parent === window: any
150
+ // handshake we posted would loop straight back to ourselves.
151
+ var topLevel = window.parent === window;
152
+ // Hold outbound notifications until the handshake settles (spec ordering:
153
+ // nothing before ui/notifications/initialized) or we give up on a host.
154
+ var canNotify = false;
155
+
156
+ function post(message) { window.parent.postMessage(message, "*"); }
157
+ function notify(method, params) {
158
+ var message = { jsonrpc: "2.0", method: method };
159
+ if (params) message.params = params;
160
+ post(message);
161
+ }
162
+ function request(method, params, timeoutMs) {
163
+ return new Promise(function (resolve, reject) {
164
+ var id = nextId; nextId += 1;
165
+ var timer = timeoutMs
166
+ ? setTimeout(function () { delete pending[id]; reject(new Error(method + " timed out")); }, timeoutMs)
167
+ : null;
168
+ pending[id] = function (result, error) {
169
+ if (timer) clearTimeout(timer);
170
+ if (error) reject(new Error(error.message || "host error"));
171
+ else resolve(result);
172
+ };
173
+ post({ jsonrpc: "2.0", id: id, method: method, params: params || {} });
174
+ });
175
+ }
176
+
177
+ function applyHostContext(context) {
178
+ if (!context) return;
179
+ if (context.theme) document.documentElement.setAttribute("data-theme", context.theme);
180
+ var variables = context.styles && context.styles.variables;
181
+ if (variables) {
182
+ for (var name in variables) {
183
+ if (name.indexOf("--") === 0) document.documentElement.style.setProperty(name, String(variables[name]));
184
+ }
185
+ }
186
+ // Now that the effective colors are updated, let canvas renderers repaint
187
+ // (CSS variables never reach a <canvas>, so they must do it themselves).
188
+ for (var i = 0; i < themeHandlers.length; i += 1) {
189
+ try { themeHandlers[i](); } catch (error) { /* a broken repaint must not wedge the frame */ }
190
+ }
191
+ }
192
+
193
+ function reportSize() {
194
+ if (!canNotify) return;
195
+ notify("ui/notifications/size-changed", {
196
+ width: document.documentElement.scrollWidth,
197
+ height: document.documentElement.scrollHeight,
198
+ });
199
+ }
200
+
201
+ function setStatus(text) {
202
+ var status = $("#status");
203
+ if (status) status.textContent = text;
204
+ }
205
+
206
+ function show(data) {
207
+ lastData = data;
208
+ themeHandlers = []; // the previous render's handlers reference stale nodes
209
+ var app = $("#app");
210
+ app.replaceChildren();
211
+ try {
212
+ render(app, data, bridge);
213
+ } catch (error) {
214
+ app.replaceChildren(h("p", { class: "muted" }, "Failed to render: " + (error && error.message)));
215
+ }
216
+ requestAnimationFrame(reportSize);
217
+ }
218
+
219
+ function showMessage(text) {
220
+ var app = $("#app");
221
+ app.replaceChildren(h("p", { class: "muted", id: "status" }, text));
222
+ requestAnimationFrame(reportSize);
223
+ }
224
+
225
+ window.addEventListener("message", function (event) {
226
+ // Only the embedding host may drive this view: drop self-echoes when
227
+ // rendered top-level and anything a co-resident frame tries to inject.
228
+ if (topLevel || event.source !== window.parent) return;
229
+ var message = event.data;
230
+ if (!message || message.jsonrpc !== "2.0") return;
231
+ if (message.id !== undefined && message.method === undefined) {
232
+ var handler = pending[message.id];
233
+ if (handler) { delete pending[message.id]; handler(message.result, message.error); }
234
+ return;
235
+ }
236
+ if (message.method === "ui/notifications/tool-result") {
237
+ var result = message.params || {};
238
+ if (result.structuredContent) {
239
+ show(result.structuredContent);
240
+ } else {
241
+ // Text-only results (e.g. "no such project") must still surface
242
+ // instead of leaving the frame on "Waiting for tool result…".
243
+ var texts = [];
244
+ var blocks = result.content || [];
245
+ for (var i = 0; i < blocks.length; i += 1) {
246
+ if (blocks[i] && blocks[i].type === "text") texts.push(blocks[i].text);
247
+ }
248
+ showMessage(texts.length ? texts.join("\n") : "Tool returned no renderable data.");
249
+ }
250
+ return;
251
+ }
252
+ if (message.method === "ui/notifications/host-context-changed") {
253
+ applyHostContext(message.params);
254
+ requestAnimationFrame(reportSize);
255
+ return;
256
+ }
257
+ if (message.id !== undefined && message.method !== undefined) {
258
+ if (message.method === "ping" || message.method === "ui/resource-teardown") {
259
+ post({ jsonrpc: "2.0", id: message.id, result: {} });
260
+ } else {
261
+ post({ jsonrpc: "2.0", id: message.id, error: { code: -32601, message: "Method not found" } });
262
+ }
263
+ }
264
+ });
265
+
266
+ var inlineNode = document.getElementById("augment-data");
267
+ var inlineData = null;
268
+ if (inlineNode) {
269
+ try { inlineData = JSON.parse(inlineNode.textContent); } catch (error) { inlineData = null; }
270
+ }
271
+ var hasInlineData = Boolean(inlineData);
272
+ if (inlineData) show(inlineData);
273
+
274
+ if (topLevel) {
275
+ if (!lastData) setStatus("No MCP Apps host detected and no inline data.");
276
+ } else {
277
+ // No hard deadline on the handshake: a slow host may still answer after
278
+ // the status flips, and the pending entry stays live so a late response
279
+ // completes the connection instead of being dropped.
280
+ var statusTimer = setTimeout(function () {
281
+ if (bridge.connected) return;
282
+ if (hasInlineData) {
283
+ // Classic embedded variant only: no SEP-1865 handshake occurs, yet
284
+ // the inline content is already shown, so a size hint is safe and
285
+ // useful. A conformant host completes ui/initialize first (clearing
286
+ // this timer), so it never receives an out-of-lifecycle notification.
287
+ canNotify = true;
288
+ reportSize();
289
+ } else {
290
+ setStatus("No MCP Apps host detected and no inline data.");
291
+ }
292
+ }, 3000);
293
+ request("ui/initialize", {
294
+ protocolVersion: "2026-01-26",
295
+ appInfo: { name: appName, version: "1" },
296
+ appCapabilities: {},
297
+ }).then(function (result) {
298
+ clearTimeout(statusTimer);
299
+ bridge.connected = true;
300
+ bridge.callTool = function (name, args) {
301
+ return request("tools/call", { name: name, arguments: args || {} }, 120000);
302
+ };
303
+ notify("ui/notifications/initialized");
304
+ canNotify = true;
305
+ if (result) applyHostContext(result.hostContext);
306
+ reportSize();
307
+ if (!lastData) setStatus("Waiting for tool result…");
308
+ });
309
+ }
310
+
311
+ if (window.ResizeObserver) {
312
+ var scheduled = false;
313
+ new ResizeObserver(function () {
314
+ if (scheduled) return;
315
+ scheduled = true;
316
+ requestAnimationFrame(function () { scheduled = false; reportSize(); });
317
+ }).observe(document.body);
318
+ }
319
+ }
320
320
  `;
321
- const INSIGHTS_RENDER_JS = String.raw `
322
- function formatChars(count) {
323
- if (count >= 1000000) return (count / 1000000).toFixed(1) + "M";
324
- if (count >= 1000) return (count / 1000).toFixed(1) + "k";
325
- return String(count);
326
- }
327
-
328
- function section(title) {
329
- var kids = [h("h2", {}, title)];
330
- for (var i = 1; i < arguments.length; i += 1) kids.push(arguments[i]);
331
- var wrap = h("div", {});
332
- for (var j = 0; j < kids.length; j += 1) wrap.append(kids[j]);
333
- return wrap;
334
- }
335
-
336
- function kindBar(byKind, total) {
337
- var bar = h("div", { class: "bar" });
338
- var legend = h("div", { class: "legend" });
339
- for (var i = 0; i < byKind.length; i += 1) {
340
- var entry = byKind[i];
341
- if (entry.count > 0 && total > 0) {
342
- bar.append(h("span", {
343
- style: "width:" + (100 * entry.count / total) + "%;background:" + (KIND_COLORS[entry.kind] || "#8b949e"),
344
- title: entry.kind + ": " + entry.count,
345
- }));
346
- }
347
- legend.append(h("span", {},
348
- h("span", { class: "dot", style: "background:" + (KIND_COLORS[entry.kind] || "#8b949e") }),
349
- entry.kind + " " + entry.count));
350
- }
351
- return h("div", {}, bar, legend);
352
- }
353
-
354
- function sparkline(weeks) {
355
- var width = 300, height = 56, pad = 2;
356
- var max = 1;
357
- for (var i = 0; i < weeks.length; i += 1) max = Math.max(max, weeks[i].created + weeks[i].updated);
358
- var slot = (width - pad * 2) / weeks.length;
359
- var svg = s("svg", { class: "spark", viewBox: "0 0 " + width + " " + height, preserveAspectRatio: "none" });
360
- for (var j = 0; j < weeks.length; j += 1) {
361
- var week = weeks[j];
362
- var x = pad + j * slot;
363
- var createdH = Math.round((height - 14) * week.created / max);
364
- var updatedH = Math.round((height - 14) * week.updated / max);
365
- var base = height - 12;
366
- var title = week.start + ": " + week.created + " created, " + week.updated + " updated";
367
- var group = s("g", {});
368
- group.append(s("title", {}, title));
369
- if (updatedH > 0) group.append(s("rect", { x: x + 1, y: base - createdH - updatedH, width: Math.max(1, slot - 2), height: updatedH, fill: "var(--muted)", opacity: "0.55" }));
370
- if (createdH > 0) group.append(s("rect", { x: x + 1, y: base - createdH, width: Math.max(1, slot - 2), height: createdH, fill: "var(--accent)" }));
371
- group.append(s("rect", { x: x + 1, y: base + 2, width: Math.max(1, slot - 2), height: 2, fill: "var(--line)" }));
372
- svg.append(group);
373
- }
374
- return svg;
375
- }
376
-
377
- function renderInsights(app, data, bridge) {
378
- var totals = data.totals || { memories: 0, links: 0, distinct_tags: 0, total_chars: 0 };
379
- app.append(h("div", { class: "head" },
380
- h("span", { class: "brand" }, "augment"),
381
- h("h1", {}, "Memory insights — " + data.project.name),
382
- h("span", { class: "totals" },
383
- totals.memories + " memories · " + totals.links + " links · " + formatChars(totals.total_chars) + " chars")));
384
-
385
- app.append(section("Memories by kind", kindBar(data.by_kind || [], totals.memories)));
386
-
387
- var grid = h("div", { class: "grid" });
388
- if ((data.links_by_type || []).length > 0) {
389
- var linkRows = h("div", { class: "rows" });
390
- for (var i = 0; i < data.links_by_type.length; i += 1) {
391
- var entry = data.links_by_type[i];
392
- linkRows.append(h("div", { class: "row" },
393
- h("span", { class: "grow" }, entry.type),
394
- h("span", { class: "muted" }, String(entry.count))));
395
- }
396
- linkRows.append(h("div", { class: "row muted" }, data.orphan_count + " memories with no links"));
397
- grid.append(section("Links", linkRows));
398
- }
399
- if ((data.top_connected || []).length > 0) {
400
- var hubRows = h("div", { class: "rows" });
401
- for (var j = 0; j < data.top_connected.length; j += 1) {
402
- var hub = data.top_connected[j];
403
- hubRows.append(h("div", { class: "row" },
404
- kindPill(hub.kind),
405
- h("span", { class: "grow", title: hub.name }, hub.name),
406
- h("span", { class: "muted" }, "×" + hub.degree)));
407
- }
408
- grid.append(section("Most connected", hubRows));
409
- }
410
- if (grid.childNodes.length > 0) app.append(grid);
411
-
412
- if ((data.top_tags || []).length > 0) {
413
- var chips = h("div", { class: "chips" });
414
- for (var k = 0; k < data.top_tags.length; k += 1) {
415
- var tag = data.top_tags[k];
416
- chips.append(h("span", { class: "chip" }, tag.tag, h("b", {}, String(tag.count))));
417
- }
418
- app.append(section("Top tags", chips));
419
- }
420
-
421
- app.append(section("Activity (created vs updated, last 12 weeks)", sparkline(data.activity || [])));
422
-
423
- var refresh = h("button", { hidden: "", onclick: function () {
424
- refresh.disabled = true;
425
- bridge.callTool("memory_insights", { project_name: data.project.name }).then(function (result) {
426
- refresh.disabled = false;
427
- if (result && result.structuredContent) {
428
- app.replaceChildren();
429
- renderInsights(app, result.structuredContent, bridge);
430
- }
431
- }).catch(function () { refresh.disabled = false; });
432
- } }, "Refresh");
433
- if (bridge.connected) refresh.removeAttribute("hidden");
434
- app.append(h("div", { class: "footer" }, refresh,
435
- h("span", { class: "stamp" }, "generated " + String(data.generated_at || "").replace("T", " ").slice(0, 16))));
436
- }
437
-
438
- augmentApp("augment-memory-insights", renderInsights);
321
+ const INSIGHTS_RENDER_JS = String.raw `
322
+ function formatChars(count) {
323
+ if (count >= 1000000) return (count / 1000000).toFixed(1) + "M";
324
+ if (count >= 1000) return (count / 1000).toFixed(1) + "k";
325
+ return String(count);
326
+ }
327
+
328
+ function section(title) {
329
+ var kids = [h("h2", {}, title)];
330
+ for (var i = 1; i < arguments.length; i += 1) kids.push(arguments[i]);
331
+ var wrap = h("div", {});
332
+ for (var j = 0; j < kids.length; j += 1) wrap.append(kids[j]);
333
+ return wrap;
334
+ }
335
+
336
+ function kindBar(byKind, total) {
337
+ var bar = h("div", { class: "bar" });
338
+ var legend = h("div", { class: "legend" });
339
+ for (var i = 0; i < byKind.length; i += 1) {
340
+ var entry = byKind[i];
341
+ if (entry.count > 0 && total > 0) {
342
+ bar.append(h("span", {
343
+ style: "width:" + (100 * entry.count / total) + "%;background:" + (KIND_COLORS[entry.kind] || "#8b949e"),
344
+ title: entry.kind + ": " + entry.count,
345
+ }));
346
+ }
347
+ legend.append(h("span", {},
348
+ h("span", { class: "dot", style: "background:" + (KIND_COLORS[entry.kind] || "#8b949e") }),
349
+ entry.kind + " " + entry.count));
350
+ }
351
+ return h("div", {}, bar, legend);
352
+ }
353
+
354
+ function sparkline(weeks) {
355
+ var width = 300, height = 56, pad = 2;
356
+ var max = 1;
357
+ for (var i = 0; i < weeks.length; i += 1) max = Math.max(max, weeks[i].created + weeks[i].updated);
358
+ var slot = (width - pad * 2) / weeks.length;
359
+ var svg = s("svg", { class: "spark", viewBox: "0 0 " + width + " " + height, preserveAspectRatio: "none" });
360
+ for (var j = 0; j < weeks.length; j += 1) {
361
+ var week = weeks[j];
362
+ var x = pad + j * slot;
363
+ var createdH = Math.round((height - 14) * week.created / max);
364
+ var updatedH = Math.round((height - 14) * week.updated / max);
365
+ var base = height - 12;
366
+ var title = week.start + ": " + week.created + " created, " + week.updated + " updated";
367
+ var group = s("g", {});
368
+ group.append(s("title", {}, title));
369
+ if (updatedH > 0) group.append(s("rect", { x: x + 1, y: base - createdH - updatedH, width: Math.max(1, slot - 2), height: updatedH, fill: "var(--muted)", opacity: "0.55" }));
370
+ if (createdH > 0) group.append(s("rect", { x: x + 1, y: base - createdH, width: Math.max(1, slot - 2), height: createdH, fill: "var(--accent)" }));
371
+ group.append(s("rect", { x: x + 1, y: base + 2, width: Math.max(1, slot - 2), height: 2, fill: "var(--line)" }));
372
+ svg.append(group);
373
+ }
374
+ return svg;
375
+ }
376
+
377
+ function renderInsights(app, data, bridge) {
378
+ var totals = data.totals || { memories: 0, links: 0, distinct_tags: 0, total_chars: 0 };
379
+ app.append(h("div", { class: "head" },
380
+ h("span", { class: "brand" }, "augment"),
381
+ h("h1", {}, "Memory insights — " + data.project.name),
382
+ h("span", { class: "totals" },
383
+ totals.memories + " memories · " + totals.links + " links · " + formatChars(totals.total_chars) + " chars")));
384
+
385
+ app.append(section("Memories by kind", kindBar(data.by_kind || [], totals.memories)));
386
+
387
+ var grid = h("div", { class: "grid" });
388
+ if ((data.links_by_type || []).length > 0) {
389
+ var linkRows = h("div", { class: "rows" });
390
+ for (var i = 0; i < data.links_by_type.length; i += 1) {
391
+ var entry = data.links_by_type[i];
392
+ linkRows.append(h("div", { class: "row" },
393
+ h("span", { class: "grow" }, entry.type),
394
+ h("span", { class: "muted" }, String(entry.count))));
395
+ }
396
+ linkRows.append(h("div", { class: "row muted" }, data.orphan_count + " memories with no links"));
397
+ grid.append(section("Links", linkRows));
398
+ }
399
+ if ((data.top_connected || []).length > 0) {
400
+ var hubRows = h("div", { class: "rows" });
401
+ for (var j = 0; j < data.top_connected.length; j += 1) {
402
+ var hub = data.top_connected[j];
403
+ hubRows.append(h("div", { class: "row" },
404
+ kindPill(hub.kind),
405
+ h("span", { class: "grow", title: hub.name }, hub.name),
406
+ h("span", { class: "muted" }, "×" + hub.degree)));
407
+ }
408
+ grid.append(section("Most connected", hubRows));
409
+ }
410
+ if (grid.childNodes.length > 0) app.append(grid);
411
+
412
+ if ((data.top_tags || []).length > 0) {
413
+ var chips = h("div", { class: "chips" });
414
+ for (var k = 0; k < data.top_tags.length; k += 1) {
415
+ var tag = data.top_tags[k];
416
+ chips.append(h("span", { class: "chip" }, tag.tag, h("b", {}, String(tag.count))));
417
+ }
418
+ app.append(section("Top tags", chips));
419
+ }
420
+
421
+ app.append(section("Activity (created vs updated, last 12 weeks)", sparkline(data.activity || [])));
422
+
423
+ var refresh = h("button", { hidden: "", onclick: function () {
424
+ refresh.disabled = true;
425
+ bridge.callTool("memory_insights", { project_name: data.project.name }).then(function (result) {
426
+ refresh.disabled = false;
427
+ if (result && result.structuredContent) {
428
+ app.replaceChildren();
429
+ renderInsights(app, result.structuredContent, bridge);
430
+ }
431
+ }).catch(function () { refresh.disabled = false; });
432
+ } }, "Refresh");
433
+ if (bridge.connected) refresh.removeAttribute("hidden");
434
+ app.append(h("div", { class: "footer" }, refresh,
435
+ h("span", { class: "stamp" }, "generated " + String(data.generated_at || "").replace("T", " ").slice(0, 16))));
436
+ }
437
+
438
+ augmentApp("augment-memory-insights", renderInsights);
439
439
  `;
440
- const GRAPH_RENDER_JS = String.raw `
441
- function renderGraph(app, data, bridge) {
442
- app.append(h("div", { class: "head" },
443
- h("span", { class: "brand" }, "augment"),
444
- h("h1", {}, "Memory graph — " + data.project.name),
445
- h("span", { class: "totals" }, data.total_memories + " memories · " + data.total_links + " links")));
446
- if (data.truncated) {
447
- app.append(h("p", { class: "muted", style: "margin:4px 0 0" },
448
- "Showing the " + data.nodes.length + " most recently updated memories."));
449
- }
450
-
451
- var stage = h("div", { style: "position:relative;margin-top:8px" });
452
- var cyHost = h("div", { style: "height:380px;border:1px solid var(--color-border-primary, var(--line));border-radius:6px" });
453
- var info = h("div", { class: "muted", style: "padding:6px 2px;min-height:24px;font-size:12px" },
454
- "Tap a node to inspect it.");
455
- stage.append(cyHost);
456
- app.append(stage, info);
457
-
458
- var elements = [];
459
- for (var i = 0; i < data.nodes.length; i += 1) {
460
- var node = data.nodes[i];
461
- elements.push({ data: { id: "m" + node.id, label: node.name, kind: node.kind,
462
- color: KIND_COLORS[node.kind] || "#8b949e", tags: node.tags, degree: node.degree } });
463
- }
464
- for (var j = 0; j < data.edges.length; j += 1) {
465
- var edge = data.edges[j];
466
- elements.push({ data: { id: "l" + edge.id, source: "m" + edge.source, target: "m" + edge.target, label: edge.type } });
467
- }
468
-
469
- // Cytoscape paints to canvas, so CSS custom properties don't resolve —
470
- // read the effective theme colors off the document instead.
471
- var fgColor = getComputedStyle(document.body).color || "#e6edf3";
472
- var cy = cytoscape({
473
- container: cyHost,
474
- elements: elements,
475
- minZoom: 0.2,
476
- maxZoom: 2.5,
477
- style: [
478
- { selector: "node", style: {
479
- "label": "data(label)", "color": fgColor, "font-size": 10, "text-wrap": "wrap",
480
- "text-max-width": 110, "text-valign": "bottom", "text-margin-y": 4,
481
- "background-color": "data(color)", "border-color": "data(color)",
482
- "width": 20, "height": 20, "border-width": 2 } },
483
- { selector: "node:selected", style: { "border-color": "#ffffff", "border-width": 3 } },
484
- { selector: "edge", style: {
485
- "label": "data(label)", "font-size": 8, "color": "#8b949e", "curve-style": "bezier",
486
- "width": 1.2, "line-color": "#484f58", "target-arrow-color": "#484f58",
487
- "target-arrow-shape": "triangle", "text-rotation": "autorotate" } },
488
- ],
489
- });
490
- cy.layout({ name: "cose", animate: false, padding: 24, nodeRepulsion: 9000, idealEdgeLength: 80 }).run();
491
-
492
- // Node label color is baked onto the canvas at render time; a later host
493
- // theme switch would leave it stale (e.g. light text on a light panel).
494
- // Re-read the effective color and restyle in place — positions/zoom/pan
495
- // are preserved (no relayout).
496
- bridge.onThemeChange(function () {
497
- cy.nodes().style("color", getComputedStyle(document.body).color || "#e6edf3");
498
- });
499
-
500
- cy.on("tap", "node", function (event) {
501
- var d = event.target.data();
502
- info.replaceChildren(
503
- kindPill(d.kind), " ",
504
- h("b", {}, d.label),
505
- h("span", { class: "muted" }, " ×" + d.degree + " links" +
506
- (d.tags && d.tags.length ? " · " + d.tags.join(", ") : "")));
507
- });
508
- }
509
-
510
- augmentApp("augment-memory-graph", renderGraph);
440
+ const GRAPH_RENDER_JS = String.raw `
441
+ function renderGraph(app, data, bridge) {
442
+ app.append(h("div", { class: "head" },
443
+ h("span", { class: "brand" }, "augment"),
444
+ h("h1", {}, "Memory graph — " + data.project.name),
445
+ h("span", { class: "totals" }, data.total_memories + " memories · " + data.total_links + " links")));
446
+ if (data.truncated) {
447
+ app.append(h("p", { class: "muted", style: "margin:4px 0 0" },
448
+ "Showing the " + data.nodes.length + " most recently updated memories."));
449
+ }
450
+
451
+ var stage = h("div", { style: "position:relative;margin-top:8px" });
452
+ var cyHost = h("div", { style: "height:380px;border:1px solid var(--color-border-primary, var(--line));border-radius:6px" });
453
+ var info = h("div", { class: "muted", style: "padding:6px 2px;min-height:24px;font-size:12px" },
454
+ "Tap a node to inspect it.");
455
+ stage.append(cyHost);
456
+ app.append(stage, info);
457
+
458
+ var elements = [];
459
+ for (var i = 0; i < data.nodes.length; i += 1) {
460
+ var node = data.nodes[i];
461
+ elements.push({ data: { id: "m" + node.id, label: node.name, kind: node.kind,
462
+ color: KIND_COLORS[node.kind] || "#8b949e", tags: node.tags, degree: node.degree } });
463
+ }
464
+ for (var j = 0; j < data.edges.length; j += 1) {
465
+ var edge = data.edges[j];
466
+ elements.push({ data: { id: "l" + edge.id, source: "m" + edge.source, target: "m" + edge.target, label: edge.type } });
467
+ }
468
+
469
+ // Cytoscape paints to canvas, so CSS custom properties don't resolve —
470
+ // read the effective theme colors off the document instead.
471
+ var fgColor = getComputedStyle(document.body).color || "#e6edf3";
472
+ var cy = cytoscape({
473
+ container: cyHost,
474
+ elements: elements,
475
+ minZoom: 0.2,
476
+ maxZoom: 2.5,
477
+ style: [
478
+ { selector: "node", style: {
479
+ "label": "data(label)", "color": fgColor, "font-size": 10, "text-wrap": "wrap",
480
+ "text-max-width": 110, "text-valign": "bottom", "text-margin-y": 4,
481
+ "background-color": "data(color)", "border-color": "data(color)",
482
+ "width": 20, "height": 20, "border-width": 2 } },
483
+ { selector: "node:selected", style: { "border-color": "#ffffff", "border-width": 3 } },
484
+ { selector: "edge", style: {
485
+ "label": "data(label)", "font-size": 8, "color": "#8b949e", "curve-style": "bezier",
486
+ "width": 1.2, "line-color": "#484f58", "target-arrow-color": "#484f58",
487
+ "target-arrow-shape": "triangle", "text-rotation": "autorotate" } },
488
+ ],
489
+ });
490
+ cy.layout({ name: "cose", animate: false, padding: 24, nodeRepulsion: 9000, idealEdgeLength: 80 }).run();
491
+
492
+ // Node label color is baked onto the canvas at render time; a later host
493
+ // theme switch would leave it stale (e.g. light text on a light panel).
494
+ // Re-read the effective color and restyle in place — positions/zoom/pan
495
+ // are preserved (no relayout).
496
+ bridge.onThemeChange(function () {
497
+ cy.nodes().style("color", getComputedStyle(document.body).color || "#e6edf3");
498
+ });
499
+
500
+ cy.on("tap", "node", function (event) {
501
+ var d = event.target.data();
502
+ info.replaceChildren(
503
+ kindPill(d.kind), " ",
504
+ h("b", {}, d.label),
505
+ h("span", { class: "muted" }, " ×" + d.degree + " links" +
506
+ (d.tags && d.tags.length ? " · " + d.tags.join(", ") : "")));
507
+ });
508
+ }
509
+
510
+ augmentApp("augment-memory-graph", renderGraph);
511
511
  `;
512
512
  function snippetDocument(options) {
513
513
  const dataIsland = options.inlineData === undefined
514
514
  ? ""
515
515
  : `\n <script type="application/json" id="augment-data">${escapeInlineJson(options.inlineData)}</script>`;
516
- return `<!DOCTYPE html>
517
- <html lang="en">
518
- <head>
519
- <meta charset="utf-8" />
520
- <meta name="viewport" content="width=device-width, initial-scale=1" />
521
- <title>${options.title}</title>
522
- <style>${SNIPPET_CSS}</style>${options.headExtra ?? ""}
523
- </head>
524
- <body>
525
- <div id="app"><p id="status" class="muted">Connecting to host…</p></div>${dataIsland}
526
- <script>
527
- (() => {
528
- "use strict";
529
- ${SNIPPET_RUNTIME_JS}
530
- ${options.bodyJs}
531
- })();
532
- </script>
533
- </body>
534
- </html>
516
+ return `<!DOCTYPE html>
517
+ <html lang="en">
518
+ <head>
519
+ <meta charset="utf-8" />
520
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
521
+ <title>${options.title}</title>
522
+ <style>${SNIPPET_CSS}</style>${options.headExtra ?? ""}
523
+ </head>
524
+ <body>
525
+ <div id="app"><p id="status" class="muted">Connecting to host…</p></div>${dataIsland}
526
+ <script>
527
+ (() => {
528
+ "use strict";
529
+ ${SNIPPET_RUNTIME_JS}
530
+ ${options.bodyJs}
531
+ })();
532
+ </script>
533
+ </body>
534
+ </html>
535
535
  `;
536
536
  }
537
537
  /**