@maxhealth.tech/prefab 0.2.2 → 0.2.3
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/app.d.ts +1 -1
- package/dist/app.js +1 -1
- package/dist/renderer/auto.d.ts +5 -0
- package/dist/renderer/auto.d.ts.map +1 -1
- package/dist/renderer/auto.js +42 -8
- package/dist/renderer/auto.js.map +1 -1
- package/dist/renderer/bridge.d.ts +34 -8
- package/dist/renderer/bridge.d.ts.map +1 -1
- package/dist/renderer/bridge.js +281 -20
- package/dist/renderer/bridge.js.map +1 -1
- package/dist/renderer.auto.min.js +11 -11
- package/dist/renderer.min.js +10 -10
- package/package.json +1 -1
package/dist/app.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type { ComponentJSON } from './core/component.js';
|
|
|
9
9
|
import type { Action, ActionJSON } from './actions/types.js';
|
|
10
10
|
import type { PipeFn } from './rx/pipes.js';
|
|
11
11
|
/** Package version — injected by build script, updated at release time. */
|
|
12
|
-
export declare const VERSION = "0.2.
|
|
12
|
+
export declare const VERSION = "0.2.3";
|
|
13
13
|
export interface Theme {
|
|
14
14
|
light?: Record<string, string>;
|
|
15
15
|
dark?: Record<string, string>;
|
package/dist/app.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import {} from './core/component.js';
|
|
8
8
|
import { drainAutoState } from './rx/state-collector.js';
|
|
9
9
|
/** Package version — injected by build script, updated at release time. */
|
|
10
|
-
export const VERSION = '0.2.
|
|
10
|
+
export const VERSION = '0.2.3';
|
|
11
11
|
export class PrefabApp {
|
|
12
12
|
title;
|
|
13
13
|
view;
|
package/dist/renderer/auto.d.ts
CHANGED
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
* Designed for CSP-restricted environments (VS Code webviews, sandboxed iframes)
|
|
10
10
|
* where inline `<script>` blocks are forbidden.
|
|
11
11
|
*
|
|
12
|
+
* Supports all three Bridge protocols:
|
|
13
|
+
* - prefab:* (MistralOS)
|
|
14
|
+
* - ui/* JSON-RPC (VS Code, Claude, ChatGPT)
|
|
15
|
+
* - ext-apps SDK (legacy)
|
|
16
|
+
*
|
|
12
17
|
* The host HTML only needs:
|
|
13
18
|
* ```html
|
|
14
19
|
* <div id="root"></div>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auto.d.ts","sourceRoot":"","sources":["../../src/renderer/auto.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"auto.d.ts","sourceRoot":"","sources":["../../src/renderer/auto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG"}
|
package/dist/renderer/auto.js
CHANGED
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
* Designed for CSP-restricted environments (VS Code webviews, sandboxed iframes)
|
|
10
10
|
* where inline `<script>` blocks are forbidden.
|
|
11
11
|
*
|
|
12
|
+
* Supports all three Bridge protocols:
|
|
13
|
+
* - prefab:* (MistralOS)
|
|
14
|
+
* - ui/* JSON-RPC (VS Code, Claude, ChatGPT)
|
|
15
|
+
* - ext-apps SDK (legacy)
|
|
16
|
+
*
|
|
12
17
|
* The host HTML only needs:
|
|
13
18
|
* ```html
|
|
14
19
|
* <div id="root"></div>
|
|
@@ -24,20 +29,50 @@ function isPrefabWire(data) {
|
|
|
24
29
|
function isPrefabUpdate(data) {
|
|
25
30
|
return PrefabRenderer.isPrefabUpdate(data);
|
|
26
31
|
}
|
|
32
|
+
function extractWireData(payload) {
|
|
33
|
+
if (isPrefabWire(payload))
|
|
34
|
+
return payload;
|
|
35
|
+
if (payload !== null && typeof payload === 'object') {
|
|
36
|
+
const obj = payload;
|
|
37
|
+
// MCP Apps tool-result: { content: [...], structuredContent: { $prefab, view } }
|
|
38
|
+
if (isPrefabWire(obj.structuredContent))
|
|
39
|
+
return obj.structuredContent;
|
|
40
|
+
// Try parsing text content items
|
|
41
|
+
if (Array.isArray(obj.content)) {
|
|
42
|
+
for (const raw of obj.content) {
|
|
43
|
+
const item = raw;
|
|
44
|
+
if (item.type === 'text' && typeof item.text === 'string') {
|
|
45
|
+
try {
|
|
46
|
+
const parsed = JSON.parse(item.text);
|
|
47
|
+
if (isPrefabWire(parsed))
|
|
48
|
+
return parsed;
|
|
49
|
+
}
|
|
50
|
+
catch { /* skip non-JSON */ }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
27
57
|
async function boot() {
|
|
28
58
|
const root = document.querySelector(ROOT_SELECTOR);
|
|
29
59
|
if (!root) {
|
|
30
60
|
console.error(`[prefab:auto] Mount target "${ROOT_SELECTOR}" not found`);
|
|
31
61
|
return;
|
|
32
62
|
}
|
|
63
|
+
const el = root;
|
|
33
64
|
const ui = await app();
|
|
34
65
|
let mounted;
|
|
66
|
+
function mount(data) {
|
|
67
|
+
if (mounted)
|
|
68
|
+
mounted.destroy();
|
|
69
|
+
mounted = ui.mount(el, data);
|
|
70
|
+
}
|
|
35
71
|
// ── Tool result handler (structuredContent from MCP) ───────────────────
|
|
36
72
|
ui.onToolResult((result) => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
mounted = ui.mount(root, result);
|
|
73
|
+
const wire = extractWireData(result);
|
|
74
|
+
if (wire) {
|
|
75
|
+
mount(wire);
|
|
41
76
|
}
|
|
42
77
|
else if (isPrefabUpdate(result) && mounted) {
|
|
43
78
|
mounted.update(result);
|
|
@@ -45,10 +80,9 @@ async function boot() {
|
|
|
45
80
|
});
|
|
46
81
|
// ── Tool input handler (initial data or dynamic args) ──────────────────
|
|
47
82
|
ui.onToolInput((args) => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
mounted = ui.mount(root, args);
|
|
83
|
+
const wire = extractWireData(args);
|
|
84
|
+
if (wire) {
|
|
85
|
+
mount(wire);
|
|
52
86
|
}
|
|
53
87
|
else if (isPrefabUpdate(args) && mounted) {
|
|
54
88
|
mounted.update(args);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auto.js","sourceRoot":"","sources":["../../src/renderer/auto.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"auto.js","sourceRoot":"","sources":["../../src/renderer/auto.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,18 +2,27 @@
|
|
|
2
2
|
* Bridge -- PostMessage-based communication between a prefab app
|
|
3
3
|
* (running in an iframe) and its host (the parent window).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Triple protocol:
|
|
6
|
+
* 1. prefab:* — custom protocol (MistralOS, self-hosted)
|
|
7
|
+
* 2. ui/* — MCP Apps JSON-RPC 2.0 (VS Code, Claude, ChatGPT, Goose)
|
|
8
|
+
* 3. ext-apps — @modelcontextprotocol/ext-apps SDK (legacy fallback)
|
|
8
9
|
*
|
|
9
|
-
* Protocol detection: tries prefab:init first (
|
|
10
|
-
*
|
|
10
|
+
* Protocol detection: tries prefab:init first (1s timeout),
|
|
11
|
+
* then ui/initialize JSON-RPC (1.5s), then ext-apps SDK.
|
|
11
12
|
*
|
|
12
13
|
* prefab:* messages:
|
|
13
14
|
* App → Host: prefab:init, prefab:tool-call, prefab:send-message,
|
|
14
15
|
* prefab:request-mode, prefab:open-link, prefab:update-context
|
|
15
16
|
* Host → App: prefab:init-response, prefab:tool-input, prefab:tool-result,
|
|
16
17
|
* prefab:tool-cancelled, prefab:theme-update, prefab:state-update
|
|
18
|
+
*
|
|
19
|
+
* ui/* JSON-RPC messages (MCP Apps spec 2026-01-26):
|
|
20
|
+
* App → Host: ui/initialize, ui/notifications/initialized,
|
|
21
|
+
* ui/notifications/size-changed, ui/open-link, ui/message,
|
|
22
|
+
* ui/request-display-mode, ui/update-model-context
|
|
23
|
+
* Host → App: ui/notifications/tool-input, ui/notifications/tool-result,
|
|
24
|
+
* ui/notifications/tool-cancelled, ui/notifications/tool-input-partial,
|
|
25
|
+
* ui/notifications/host-context-changed, ui/resource-teardown
|
|
17
26
|
*/
|
|
18
27
|
import type { McpTransport } from './actions.js';
|
|
19
28
|
export interface BridgeMessage<T extends string = string> {
|
|
@@ -54,12 +63,17 @@ export declare class Bridge {
|
|
|
54
63
|
private cleanup;
|
|
55
64
|
private pending;
|
|
56
65
|
private callIdCounter;
|
|
66
|
+
private rpcPending;
|
|
67
|
+
private rpcIdCounter;
|
|
68
|
+
private sentRpcIds;
|
|
69
|
+
private postFn;
|
|
57
70
|
private extApp;
|
|
58
71
|
constructor(hostOrigin?: string);
|
|
59
|
-
/** Start listening for prefab:*
|
|
72
|
+
/** Start listening for messages (prefab:* and JSON-RPC ui/*). */
|
|
60
73
|
connect(): void;
|
|
61
74
|
/**
|
|
62
|
-
* Init handshake. Tries prefab:init first,
|
|
75
|
+
* Init handshake. Tries prefab:init first, then ui/initialize JSON-RPC,
|
|
76
|
+
* then falls back to ext-apps SDK.
|
|
63
77
|
*/
|
|
64
78
|
initialize(appCapabilities: AppCapabilities): Promise<HostContext>;
|
|
65
79
|
/** Create an McpTransport that routes through the active protocol. */
|
|
@@ -77,10 +91,22 @@ export declare class Bridge {
|
|
|
77
91
|
/** Disconnect and clean up. */
|
|
78
92
|
disconnect(): void;
|
|
79
93
|
/** Which protocol is active after initialize(). */
|
|
80
|
-
get activeProtocol(): 'prefab' | 'ext-apps';
|
|
94
|
+
get activeProtocol(): 'prefab' | 'jsonrpc' | 'ext-apps';
|
|
81
95
|
private initPrefab;
|
|
82
96
|
private createPrefabTransport;
|
|
83
97
|
private sendPrefab;
|
|
98
|
+
/** Handle incoming JSON-RPC message. */
|
|
99
|
+
private handleJsonRpc;
|
|
100
|
+
/** Initialize via MCP Apps JSON-RPC ui/initialize handshake. */
|
|
101
|
+
private initJsonRpc;
|
|
102
|
+
/** Create transport that routes tool calls via JSON-RPC tools/call. */
|
|
103
|
+
private createJsonRpcTransport;
|
|
104
|
+
/** Send a JSON-RPC request and return a promise for the response. */
|
|
105
|
+
private sendRpcRequest;
|
|
106
|
+
/** Send a JSON-RPC notification (no id, no response expected). */
|
|
107
|
+
private sendRpcNotification;
|
|
108
|
+
/** Low-level: post a JSON-RPC envelope. Uses acquireVsCodeApi if available. */
|
|
109
|
+
private postJsonRpc;
|
|
84
110
|
private initExtApps;
|
|
85
111
|
private wireExtAppsEvents;
|
|
86
112
|
private createExtAppsTransport;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../../src/renderer/bridge.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../../src/renderer/bridge.ts"],"names":[],"mappings":"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"}
|
package/dist/renderer/bridge.js
CHANGED
|
@@ -2,19 +2,34 @@
|
|
|
2
2
|
* Bridge -- PostMessage-based communication between a prefab app
|
|
3
3
|
* (running in an iframe) and its host (the parent window).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Triple protocol:
|
|
6
|
+
* 1. prefab:* — custom protocol (MistralOS, self-hosted)
|
|
7
|
+
* 2. ui/* — MCP Apps JSON-RPC 2.0 (VS Code, Claude, ChatGPT, Goose)
|
|
8
|
+
* 3. ext-apps — @modelcontextprotocol/ext-apps SDK (legacy fallback)
|
|
8
9
|
*
|
|
9
|
-
* Protocol detection: tries prefab:init first (
|
|
10
|
-
*
|
|
10
|
+
* Protocol detection: tries prefab:init first (1s timeout),
|
|
11
|
+
* then ui/initialize JSON-RPC (1.5s), then ext-apps SDK.
|
|
11
12
|
*
|
|
12
13
|
* prefab:* messages:
|
|
13
14
|
* App → Host: prefab:init, prefab:tool-call, prefab:send-message,
|
|
14
15
|
* prefab:request-mode, prefab:open-link, prefab:update-context
|
|
15
16
|
* Host → App: prefab:init-response, prefab:tool-input, prefab:tool-result,
|
|
16
17
|
* prefab:tool-cancelled, prefab:theme-update, prefab:state-update
|
|
18
|
+
*
|
|
19
|
+
* ui/* JSON-RPC messages (MCP Apps spec 2026-01-26):
|
|
20
|
+
* App → Host: ui/initialize, ui/notifications/initialized,
|
|
21
|
+
* ui/notifications/size-changed, ui/open-link, ui/message,
|
|
22
|
+
* ui/request-display-mode, ui/update-model-context
|
|
23
|
+
* Host → App: ui/notifications/tool-input, ui/notifications/tool-result,
|
|
24
|
+
* ui/notifications/tool-cancelled, ui/notifications/tool-input-partial,
|
|
25
|
+
* ui/notifications/host-context-changed, ui/resource-teardown
|
|
17
26
|
*/
|
|
27
|
+
/** Type guard: is this a JSON-RPC 2.0 envelope? */
|
|
28
|
+
function isJsonRpcEnvelope(msg) {
|
|
29
|
+
return (msg !== null &&
|
|
30
|
+
typeof msg === 'object' &&
|
|
31
|
+
msg.jsonrpc === '2.0');
|
|
32
|
+
}
|
|
18
33
|
// ── Bridge Class ─────────────────────────────────────────────────────────────
|
|
19
34
|
export class Bridge {
|
|
20
35
|
hostOrigin;
|
|
@@ -24,58 +39,84 @@ export class Bridge {
|
|
|
24
39
|
// prefab:* state
|
|
25
40
|
pending = new Map();
|
|
26
41
|
callIdCounter = 0;
|
|
42
|
+
// JSON-RPC state
|
|
43
|
+
rpcPending = new Map();
|
|
44
|
+
rpcIdCounter = 0;
|
|
45
|
+
sentRpcIds = new Set();
|
|
46
|
+
postFn;
|
|
27
47
|
// ext-apps state (lazy)
|
|
28
48
|
extApp;
|
|
29
49
|
constructor(hostOrigin = '*') {
|
|
30
50
|
this.hostOrigin = hostOrigin;
|
|
31
51
|
}
|
|
32
|
-
/** Start listening for prefab:*
|
|
52
|
+
/** Start listening for messages (prefab:* and JSON-RPC ui/*). */
|
|
33
53
|
connect() {
|
|
34
54
|
if (typeof window === 'undefined')
|
|
35
55
|
return;
|
|
56
|
+
// Detect VS Code webview API (provides postMessage without origin restrictions)
|
|
57
|
+
const vscodeApi = detectVsCodeApi();
|
|
58
|
+
if (vscodeApi) {
|
|
59
|
+
this.postFn = (msg) => vscodeApi.postMessage(msg);
|
|
60
|
+
}
|
|
36
61
|
const handler = (event) => {
|
|
37
62
|
if (this.hostOrigin !== '*' && event.origin !== this.hostOrigin)
|
|
38
63
|
return;
|
|
39
64
|
const msg = event.data;
|
|
40
|
-
|
|
65
|
+
// ── JSON-RPC 2.0 messages (ui/* protocol) ───────────────────────────
|
|
66
|
+
if (isJsonRpcEnvelope(msg)) {
|
|
67
|
+
this.handleJsonRpc(msg);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
// ── prefab:* messages ───────────────────────────────────────────────
|
|
71
|
+
const bmsg = msg;
|
|
72
|
+
if (!bmsg?.type.startsWith('prefab:'))
|
|
41
73
|
return;
|
|
42
74
|
// Resolve pending tool-call promises
|
|
43
|
-
if (
|
|
44
|
-
const p = this.pending.get(
|
|
75
|
+
if (bmsg.type === 'prefab:tool-call-response' && bmsg.id) {
|
|
76
|
+
const p = this.pending.get(bmsg.id);
|
|
45
77
|
if (p) {
|
|
46
|
-
this.pending.delete(
|
|
47
|
-
if (
|
|
48
|
-
p.reject(new Error(
|
|
78
|
+
this.pending.delete(bmsg.id);
|
|
79
|
+
if (bmsg.payload?.error != null) {
|
|
80
|
+
p.reject(new Error(bmsg.payload.error));
|
|
49
81
|
}
|
|
50
82
|
else {
|
|
51
|
-
p.resolve(
|
|
83
|
+
p.resolve(bmsg.payload?.result);
|
|
52
84
|
}
|
|
53
85
|
return;
|
|
54
86
|
}
|
|
55
87
|
}
|
|
56
88
|
// Dispatch to registered listeners
|
|
57
|
-
const handlers = this.listeners.get(
|
|
89
|
+
const handlers = this.listeners.get(bmsg.type);
|
|
58
90
|
if (handlers) {
|
|
59
91
|
for (const fn of handlers)
|
|
60
|
-
fn(
|
|
92
|
+
fn(bmsg.payload ?? {});
|
|
61
93
|
}
|
|
62
94
|
};
|
|
63
95
|
window.addEventListener('message', handler);
|
|
64
96
|
this.cleanup = () => window.removeEventListener('message', handler);
|
|
65
97
|
}
|
|
66
98
|
/**
|
|
67
|
-
* Init handshake. Tries prefab:init first,
|
|
99
|
+
* Init handshake. Tries prefab:init first, then ui/initialize JSON-RPC,
|
|
100
|
+
* then falls back to ext-apps SDK.
|
|
68
101
|
*/
|
|
69
102
|
async initialize(appCapabilities) {
|
|
70
103
|
try {
|
|
71
104
|
return await this.initPrefab(appCapabilities);
|
|
72
105
|
}
|
|
73
106
|
catch {
|
|
74
|
-
|
|
107
|
+
try {
|
|
108
|
+
return await this.initJsonRpc(appCapabilities);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return this.initExtApps(appCapabilities);
|
|
112
|
+
}
|
|
75
113
|
}
|
|
76
114
|
}
|
|
77
115
|
/** Create an McpTransport that routes through the active protocol. */
|
|
78
116
|
createTransport() {
|
|
117
|
+
if (this.protocol === 'jsonrpc') {
|
|
118
|
+
return this.createJsonRpcTransport();
|
|
119
|
+
}
|
|
79
120
|
if (this.protocol === 'ext-apps' && this.extApp) {
|
|
80
121
|
return this.createExtAppsTransport();
|
|
81
122
|
}
|
|
@@ -83,7 +124,10 @@ export class Bridge {
|
|
|
83
124
|
}
|
|
84
125
|
/** Request a display mode change. */
|
|
85
126
|
requestMode(mode) {
|
|
86
|
-
if (this.protocol === '
|
|
127
|
+
if (this.protocol === 'jsonrpc') {
|
|
128
|
+
void this.sendRpcRequest('ui/request-display-mode', { mode });
|
|
129
|
+
}
|
|
130
|
+
else if (this.protocol === 'ext-apps' && this.extApp) {
|
|
87
131
|
void this.extApp.requestDisplayMode({ mode });
|
|
88
132
|
}
|
|
89
133
|
else {
|
|
@@ -92,7 +136,10 @@ export class Bridge {
|
|
|
92
136
|
}
|
|
93
137
|
/** Request the host to open a URL. */
|
|
94
138
|
openLink(url, target) {
|
|
95
|
-
if (this.protocol === '
|
|
139
|
+
if (this.protocol === 'jsonrpc') {
|
|
140
|
+
void this.sendRpcRequest('ui/open-link', { url, target });
|
|
141
|
+
}
|
|
142
|
+
else if (this.protocol === 'ext-apps' && this.extApp) {
|
|
96
143
|
void this.extApp.openLink({ url });
|
|
97
144
|
}
|
|
98
145
|
else {
|
|
@@ -101,7 +148,10 @@ export class Bridge {
|
|
|
101
148
|
}
|
|
102
149
|
/** Send context updates to the host. */
|
|
103
150
|
updateContext(context) {
|
|
104
|
-
if (this.protocol === '
|
|
151
|
+
if (this.protocol === 'jsonrpc') {
|
|
152
|
+
void this.sendRpcRequest('ui/update-model-context', { structuredContent: context });
|
|
153
|
+
}
|
|
154
|
+
else if (this.protocol === 'ext-apps' && this.extApp) {
|
|
105
155
|
void this.extApp.updateModelContext({ structuredContent: context });
|
|
106
156
|
}
|
|
107
157
|
else {
|
|
@@ -130,6 +180,11 @@ export class Bridge {
|
|
|
130
180
|
p.reject(new Error('Bridge disconnected'));
|
|
131
181
|
}
|
|
132
182
|
this.pending.clear();
|
|
183
|
+
for (const [, p] of this.rpcPending) {
|
|
184
|
+
p.reject(new Error('Bridge disconnected'));
|
|
185
|
+
}
|
|
186
|
+
this.rpcPending.clear();
|
|
187
|
+
this.sentRpcIds.clear();
|
|
133
188
|
}
|
|
134
189
|
/** Which protocol is active after initialize(). */
|
|
135
190
|
get activeProtocol() {
|
|
@@ -187,6 +242,194 @@ export class Bridge {
|
|
|
187
242
|
const msg = { type, payload, id };
|
|
188
243
|
target.postMessage(msg, this.hostOrigin);
|
|
189
244
|
}
|
|
245
|
+
// ── JSON-RPC ui/* protocol (MCP Apps spec 2026-01-26) ────────────────
|
|
246
|
+
/** Handle incoming JSON-RPC message. */
|
|
247
|
+
handleJsonRpc(msg) {
|
|
248
|
+
// Response to a request we sent (has no 'method' → must be JsonRpcResponse)
|
|
249
|
+
if (!('method' in msg)) {
|
|
250
|
+
const p = this.rpcPending.get(msg.id);
|
|
251
|
+
if (p) {
|
|
252
|
+
this.rpcPending.delete(msg.id);
|
|
253
|
+
if (msg.error) {
|
|
254
|
+
p.reject(new Error(msg.error.message));
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
p.resolve(msg.result);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
// Request or notification from host (JsonRpcRequest)
|
|
263
|
+
const method = msg.method;
|
|
264
|
+
const params = msg.params ?? {};
|
|
265
|
+
// Skip our own outgoing requests bounced back (same-window postMessage in tests)
|
|
266
|
+
if (msg.id != null && this.sentRpcIds.delete(msg.id))
|
|
267
|
+
return;
|
|
268
|
+
// Auto-acknowledge host requests (those with an id) so we don't deadlock
|
|
269
|
+
if (msg.id != null) {
|
|
270
|
+
this.postJsonRpc({ jsonrpc: '2.0', id: msg.id, result: {} });
|
|
271
|
+
}
|
|
272
|
+
// Map ui/* notifications to internal prefab:* events
|
|
273
|
+
switch (method) {
|
|
274
|
+
case 'ui/notifications/tool-input':
|
|
275
|
+
this.dispatch('prefab:tool-input', { args: params.arguments ?? params });
|
|
276
|
+
break;
|
|
277
|
+
case 'ui/notifications/tool-input-partial':
|
|
278
|
+
this.dispatch('prefab:tool-input-partial', { args: params.arguments ?? params });
|
|
279
|
+
break;
|
|
280
|
+
case 'ui/notifications/tool-result':
|
|
281
|
+
this.dispatch('prefab:tool-result', { result: params });
|
|
282
|
+
break;
|
|
283
|
+
case 'ui/notifications/tool-cancelled':
|
|
284
|
+
this.dispatch('prefab:tool-cancelled', params);
|
|
285
|
+
break;
|
|
286
|
+
case 'ui/notifications/host-context-changed': {
|
|
287
|
+
const theme = {};
|
|
288
|
+
if (typeof params.theme === 'string') {
|
|
289
|
+
theme.colorScheme = params.theme;
|
|
290
|
+
}
|
|
291
|
+
if (typeof params.styles === 'object' && params.styles !== null) {
|
|
292
|
+
const styles = params.styles;
|
|
293
|
+
if (styles.variables)
|
|
294
|
+
theme.variables = styles.variables;
|
|
295
|
+
}
|
|
296
|
+
this.dispatch('prefab:theme-update', theme);
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
case 'ui/resource-teardown':
|
|
300
|
+
this.dispatch('prefab:teardown', params);
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/** Initialize via MCP Apps JSON-RPC ui/initialize handshake. */
|
|
305
|
+
initJsonRpc(appCapabilities) {
|
|
306
|
+
return new Promise((resolve, reject) => {
|
|
307
|
+
const id = ++this.rpcIdCounter;
|
|
308
|
+
let settled = false;
|
|
309
|
+
this.rpcPending.set(id, {
|
|
310
|
+
resolve: (result) => {
|
|
311
|
+
if (settled)
|
|
312
|
+
return;
|
|
313
|
+
settled = true;
|
|
314
|
+
this.protocol = 'jsonrpc';
|
|
315
|
+
const r = (result ?? {});
|
|
316
|
+
const hostInfo = (r.hostInfo ?? {});
|
|
317
|
+
const hostCtx = (r.hostContext ?? {});
|
|
318
|
+
const hostCaps = (r.hostCapabilities ?? {});
|
|
319
|
+
// Parse theme from hostContext
|
|
320
|
+
let theme;
|
|
321
|
+
if (typeof hostCtx.theme === 'string' || typeof hostCtx.styles === 'object') {
|
|
322
|
+
theme = {};
|
|
323
|
+
if (typeof hostCtx.theme === 'string') {
|
|
324
|
+
theme.colorScheme = hostCtx.theme;
|
|
325
|
+
}
|
|
326
|
+
if (typeof hostCtx.styles === 'object' && hostCtx.styles !== null) {
|
|
327
|
+
const styles = hostCtx.styles;
|
|
328
|
+
if (styles.variables)
|
|
329
|
+
theme.variables = styles.variables;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// Send ui/notifications/initialized to confirm readiness
|
|
333
|
+
this.postJsonRpc({
|
|
334
|
+
jsonrpc: '2.0',
|
|
335
|
+
method: 'ui/notifications/initialized',
|
|
336
|
+
params: {},
|
|
337
|
+
});
|
|
338
|
+
resolve({
|
|
339
|
+
hostName: hostInfo.name,
|
|
340
|
+
hostVersion: hostInfo.version,
|
|
341
|
+
capabilities: {
|
|
342
|
+
toast: true,
|
|
343
|
+
navigation: hostCaps.openLinks != null,
|
|
344
|
+
messaging: true,
|
|
345
|
+
displayModes: (hostCtx.availableDisplayModes ?? []),
|
|
346
|
+
},
|
|
347
|
+
theme,
|
|
348
|
+
toolInput: undefined,
|
|
349
|
+
meta: hostCtx,
|
|
350
|
+
});
|
|
351
|
+
},
|
|
352
|
+
reject: (err) => {
|
|
353
|
+
if (settled)
|
|
354
|
+
return;
|
|
355
|
+
settled = true;
|
|
356
|
+
reject(err);
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
// Send ui/initialize request
|
|
360
|
+
this.postJsonRpc({
|
|
361
|
+
jsonrpc: '2.0',
|
|
362
|
+
id,
|
|
363
|
+
method: 'ui/initialize',
|
|
364
|
+
params: {
|
|
365
|
+
protocolVersion: '2026-01-26',
|
|
366
|
+
capabilities: {},
|
|
367
|
+
clientInfo: { name: 'prefab', version: '0.2' },
|
|
368
|
+
appCapabilities: {
|
|
369
|
+
...(appCapabilities.displayModes && {
|
|
370
|
+
availableDisplayModes: appCapabilities.displayModes,
|
|
371
|
+
}),
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
// Timeout — fall through to ext-apps
|
|
376
|
+
setTimeout(() => {
|
|
377
|
+
if (!settled) {
|
|
378
|
+
settled = true;
|
|
379
|
+
this.rpcPending.delete(id);
|
|
380
|
+
reject(new Error('ui/initialize timeout'));
|
|
381
|
+
}
|
|
382
|
+
}, 1500);
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
/** Create transport that routes tool calls via JSON-RPC tools/call. */
|
|
386
|
+
createJsonRpcTransport() {
|
|
387
|
+
return {
|
|
388
|
+
callTool: (name, args) => {
|
|
389
|
+
return this.sendRpcRequest('tools/call', { name, arguments: args });
|
|
390
|
+
},
|
|
391
|
+
sendMessage: (message) => {
|
|
392
|
+
return this.sendRpcRequest('ui/message', {
|
|
393
|
+
role: 'user',
|
|
394
|
+
content: { type: 'text', text: message },
|
|
395
|
+
});
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
/** Send a JSON-RPC request and return a promise for the response. */
|
|
400
|
+
sendRpcRequest(method, params) {
|
|
401
|
+
const id = ++this.rpcIdCounter;
|
|
402
|
+
return new Promise((resolve, reject) => {
|
|
403
|
+
this.rpcPending.set(id, { resolve, reject });
|
|
404
|
+
this.postJsonRpc({ jsonrpc: '2.0', id, method, params });
|
|
405
|
+
setTimeout(() => {
|
|
406
|
+
if (this.rpcPending.has(id)) {
|
|
407
|
+
this.rpcPending.delete(id);
|
|
408
|
+
reject(new Error(`JSON-RPC '${method}' timed out`));
|
|
409
|
+
}
|
|
410
|
+
}, 30000);
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
/** Send a JSON-RPC notification (no id, no response expected). */
|
|
414
|
+
sendRpcNotification(method, params) {
|
|
415
|
+
this.postJsonRpc({ jsonrpc: '2.0', method, params });
|
|
416
|
+
}
|
|
417
|
+
/** Low-level: post a JSON-RPC envelope. Uses acquireVsCodeApi if available. */
|
|
418
|
+
postJsonRpc(msg) {
|
|
419
|
+
if (typeof window === 'undefined')
|
|
420
|
+
return;
|
|
421
|
+
// Track outgoing request ids to filter self-messages in same-window envs
|
|
422
|
+
if (msg.id != null && typeof msg.method === 'string') {
|
|
423
|
+
this.sentRpcIds.add(msg.id);
|
|
424
|
+
}
|
|
425
|
+
if (this.postFn) {
|
|
426
|
+
this.postFn(msg);
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
const target = window.parent !== window ? window.parent : window;
|
|
430
|
+
target.postMessage(msg, this.hostOrigin);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
190
433
|
// ── ext-apps fallback ──────────────────────────────────────────────────
|
|
191
434
|
async initExtApps(appCapabilities) {
|
|
192
435
|
const { App, PostMessageTransport } = await import('@modelcontextprotocol/ext-apps');
|
|
@@ -299,4 +542,22 @@ export function isIframe() {
|
|
|
299
542
|
return true;
|
|
300
543
|
}
|
|
301
544
|
}
|
|
545
|
+
/** Detect VS Code webview API (acquireVsCodeApi). Returns the API object or undefined. */
|
|
546
|
+
function detectVsCodeApi() {
|
|
547
|
+
if (typeof window === 'undefined')
|
|
548
|
+
return undefined;
|
|
549
|
+
const win = window;
|
|
550
|
+
if (typeof win.acquireVsCodeApi === 'function') {
|
|
551
|
+
try {
|
|
552
|
+
const api = win.acquireVsCodeApi();
|
|
553
|
+
if (api !== null && typeof api === 'object' && 'postMessage' in api) {
|
|
554
|
+
return api;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
catch {
|
|
558
|
+
return undefined;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return undefined;
|
|
562
|
+
}
|
|
302
563
|
//# sourceMappingURL=bridge.js.map
|