@assistant-ui/vscode 0.0.0-stage → 0.0.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/LICENSE +21 -0
- package/README.md +159 -2
- package/dist/external-url.d.ts +3 -0
- package/dist/external-url.d.ts.map +1 -0
- package/dist/external-url.js +19 -0
- package/dist/external-url.js.map +1 -0
- package/dist/host/html.d.ts +65 -0
- package/dist/host/html.d.ts.map +1 -0
- package/dist/host/html.js +126 -0
- package/dist/host/html.js.map +1 -0
- package/dist/host/router.d.ts +24 -0
- package/dist/host/router.d.ts.map +1 -0
- package/dist/host/router.js +166 -0
- package/dist/host/router.js.map +1 -0
- package/dist/host/serve.d.ts +34 -0
- package/dist/host/serve.d.ts.map +1 -0
- package/dist/host/serve.js +93 -0
- package/dist/host/serve.js.map +1 -0
- package/dist/host.d.ts +5 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +4 -0
- package/dist/protocol.d.ts +46 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +30 -0
- package/dist/protocol.js.map +1 -0
- package/dist/route-url.d.ts +3 -0
- package/dist/route-url.d.ts.map +1 -0
- package/dist/route-url.js +7 -0
- package/dist/route-url.js.map +1 -0
- package/dist/webview/csp-nonce.d.ts +6 -0
- package/dist/webview/csp-nonce.d.ts.map +1 -0
- package/dist/webview/csp-nonce.js +12 -0
- package/dist/webview/csp-nonce.js.map +1 -0
- package/dist/webview/fetch.d.ts +14 -0
- package/dist/webview/fetch.d.ts.map +1 -0
- package/dist/webview/fetch.js +164 -0
- package/dist/webview/fetch.js.map +1 -0
- package/dist/webview/id.d.ts +2 -0
- package/dist/webview/id.d.ts.map +1 -0
- package/dist/webview/id.js +5 -0
- package/dist/webview/id.js.map +1 -0
- package/dist/webview/links.d.ts +14 -0
- package/dist/webview/links.d.ts.map +1 -0
- package/dist/webview/links.js +44 -0
- package/dist/webview/links.js.map +1 -0
- package/dist/webview/rpc.d.ts +3 -0
- package/dist/webview/rpc.d.ts.map +1 -0
- package/dist/webview/rpc.js +32 -0
- package/dist/webview/rpc.js.map +1 -0
- package/dist/webview/storage.d.ts +8 -0
- package/dist/webview/storage.d.ts.map +1 -0
- package/dist/webview/storage.js +24 -0
- package/dist/webview/storage.js.map +1 -0
- package/dist/webview/vscode-api.d.ts +11 -0
- package/dist/webview/vscode-api.d.ts.map +1 -0
- package/dist/webview/vscode-api.js +22 -0
- package/dist/webview/vscode-api.js.map +1 -0
- package/dist/webview.d.ts +7 -0
- package/dist/webview.d.ts.map +1 -0
- package/dist/webview.js +7 -0
- package/package.json +87 -3
- package/src/external-url.ts +19 -0
- package/src/host/html.ts +224 -0
- package/src/host/router.ts +253 -0
- package/src/host/serve.ts +150 -0
- package/src/host.ts +27 -0
- package/src/protocol.ts +131 -0
- package/src/route-url.ts +6 -0
- package/src/theme.css +327 -0
- package/src/webview/csp-nonce.ts +10 -0
- package/src/webview/fetch.ts +210 -0
- package/src/webview/id.ts +4 -0
- package/src/webview/links.ts +56 -0
- package/src/webview/rpc.ts +35 -0
- package/src/webview/storage.ts +24 -0
- package/src/webview/vscode-api.ts +31 -0
- package/src/webview.ts +14 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"links.js","names":[],"sources":["../../src/webview/links.ts"],"sourcesContent":["import { DEFAULT_EXTERNAL_SCHEMES, parseExternalUrl } from \"../external-url\";\nimport { webviewPort, type VSCodeBridgePort } from \"./fetch\";\nimport { callHost } from \"./rpc\";\n\nexport type LinkInterceptorOptions = {\n /** URL schemes to open outside the webview, such as `\"https:\"`. */\n schemes?: readonly string[];\n onError?: (error: unknown) => void;\n port?: VSCodeBridgePort;\n};\n\n/**\n * Opens absolute links and `window.open` URLs through the extension host's\n * `openExternal`, since a webview cannot navigate away or open windows.\n * Returns a function that removes the listeners and restores `window.open`.\n */\nexport function installLinkInterceptor({\n schemes = DEFAULT_EXTERNAL_SCHEMES,\n onError = (error) => console.error(error),\n port = webviewPort,\n}: LinkInterceptorOptions = {}): () => void {\n const openExternal = (url: string) => {\n callHost(port, \"openExternal\", [url]).catch(onError);\n };\n\n const onClick = (event: MouseEvent) => {\n if (event.defaultPrevented) return;\n if (event.button !== (event.type === \"auxclick\" ? 1 : 0)) return;\n if (!(event.target instanceof Element)) return;\n const anchor = event.target.closest(\"a[href]\");\n const url = parseExternalUrl(anchor?.getAttribute(\"href\"), schemes);\n if (!url) return;\n event.preventDefault();\n openExternal(url);\n };\n\n let installed = true;\n const originalOpen = window.open;\n const patchedOpen: typeof window.open = function (url, ...rest) {\n const external = installed ? parseExternalUrl(url, schemes) : null;\n if (!external) return originalOpen.call(window, url, ...rest);\n openExternal(external);\n return null;\n };\n\n document.addEventListener(\"click\", onClick);\n document.addEventListener(\"auxclick\", onClick);\n window.open = patchedOpen;\n\n return () => {\n installed = false;\n document.removeEventListener(\"click\", onClick);\n document.removeEventListener(\"auxclick\", onClick);\n if (window.open === patchedOpen) window.open = originalOpen;\n };\n}\n"],"mappings":";;;;;;;;;AAgBA,SAAgB,uBAAuB,EACrC,UAAU,0BACV,WAAW,UAAU,QAAQ,MAAM,KAAK,GACxC,OAAO,gBACmB,CAAC,GAAe;CAC1C,MAAM,gBAAgB,QAAgB;EACpC,SAAS,MAAM,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,OAAO;CACrD;CAEA,MAAM,WAAW,UAAsB;EACrC,IAAI,MAAM,kBAAkB;EAC5B,IAAI,MAAM,YAAY,MAAM,SAAS,aAAa,IAAI,IAAI;EAC1D,IAAI,EAAE,MAAM,kBAAkB,UAAU;EACxC,MAAM,SAAS,MAAM,OAAO,QAAQ,SAAS;EAC7C,MAAM,MAAM,iBAAiB,QAAQ,aAAa,MAAM,GAAG,OAAO;EAClE,IAAI,CAAC,KAAK;EACV,MAAM,eAAe;EACrB,aAAa,GAAG;CAClB;CAEA,IAAI,YAAY;CAChB,MAAM,eAAe,OAAO;CAC5B,MAAM,cAAkC,SAAU,KAAK,GAAG,MAAM;EAC9D,MAAM,WAAW,YAAY,iBAAiB,KAAK,OAAO,IAAI;EAC9D,IAAI,CAAC,UAAU,OAAO,aAAa,KAAK,QAAQ,KAAK,GAAG,IAAI;EAC5D,aAAa,QAAQ;EACrB,OAAO;CACT;CAEA,SAAS,iBAAiB,SAAS,OAAO;CAC1C,SAAS,iBAAiB,YAAY,OAAO;CAC7C,OAAO,OAAO;CAEd,aAAa;EACX,YAAY;EACZ,SAAS,oBAAoB,SAAS,OAAO;EAC7C,SAAS,oBAAoB,YAAY,OAAO;EAChD,IAAI,OAAO,SAAS,aAAa,OAAO,OAAO;CACjD;AACF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rpc.d.ts","sourceRoot":"","sources":["../../src/webview/rpc.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,mBAAgB;AAMhD,eAAO,MAAM,QAAQ,GACnB,MAAM,gBAAgB,EACtB,QAAQ,MAAM,EACd,QAAQ,OAAO,EAAE,KAChB,OAAO,CAAC,OAAO,CAuBjB,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { VSCODE_BRIDGE_CHANNEL, isHostToWebviewMessage } from "../protocol.js";
|
|
2
|
+
import { randomIdPrefix } from "./id.js";
|
|
3
|
+
// src/webview/rpc.ts
|
|
4
|
+
const idPrefix = `rpc-${randomIdPrefix()}`;
|
|
5
|
+
let nextId = 0;
|
|
6
|
+
const callHost = (port, method, params) => {
|
|
7
|
+
const id = `${idPrefix}-${++nextId}`;
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const unsubscribe = port.onMessage((message) => {
|
|
10
|
+
if (!isHostToWebviewMessage(message) || message.id !== id) return;
|
|
11
|
+
if (message.kind !== "rpc:response") return;
|
|
12
|
+
unsubscribe();
|
|
13
|
+
if (message.ok) resolve(message.result);
|
|
14
|
+
else reject(new Error(message.message));
|
|
15
|
+
});
|
|
16
|
+
try {
|
|
17
|
+
port.postMessage({
|
|
18
|
+
channel: VSCODE_BRIDGE_CHANNEL,
|
|
19
|
+
kind: "rpc:request",
|
|
20
|
+
id,
|
|
21
|
+
method,
|
|
22
|
+
params
|
|
23
|
+
});
|
|
24
|
+
} catch (error) {
|
|
25
|
+
unsubscribe();
|
|
26
|
+
reject(error);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
export { callHost };
|
|
31
|
+
|
|
32
|
+
//# sourceMappingURL=rpc.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rpc.js","names":[],"sources":["../../src/webview/rpc.ts"],"sourcesContent":["import { isHostToWebviewMessage, VSCODE_BRIDGE_CHANNEL } from \"../protocol\";\nimport type { VSCodeBridgePort } from \"./fetch\";\nimport { randomIdPrefix } from \"./id\";\n\nconst idPrefix = `rpc-${randomIdPrefix()}`;\nlet nextId = 0;\n\nexport const callHost = (\n port: VSCodeBridgePort,\n method: string,\n params: unknown[],\n): Promise<unknown> => {\n const id = `${idPrefix}-${++nextId}`;\n return new Promise((resolve, reject) => {\n const unsubscribe = port.onMessage((message) => {\n if (!isHostToWebviewMessage(message) || message.id !== id) return;\n if (message.kind !== \"rpc:response\") return;\n unsubscribe();\n if (message.ok) resolve(message.result);\n else reject(new Error(message.message));\n });\n try {\n port.postMessage({\n channel: VSCODE_BRIDGE_CHANNEL,\n kind: \"rpc:request\",\n id,\n method,\n params,\n });\n } catch (error) {\n unsubscribe();\n reject(error);\n }\n });\n};\n"],"mappings":";;;AAIA,MAAM,WAAW,OAAO,eAAe;AACvC,IAAI,SAAS;AAEb,MAAa,YACX,MACA,QACA,WACqB;CACrB,MAAM,KAAK,GAAG,SAAS,GAAG,EAAE;CAC5B,OAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,cAAc,KAAK,WAAW,YAAY;GAC9C,IAAI,CAAC,uBAAuB,OAAO,KAAK,QAAQ,OAAO,IAAI;GAC3D,IAAI,QAAQ,SAAS,gBAAgB;GACrC,YAAY;GACZ,IAAI,QAAQ,IAAI,QAAQ,QAAQ,MAAM;QACjC,OAAO,IAAI,MAAM,QAAQ,OAAO,CAAC;EACxC,CAAC;EACD,IAAI;GACF,KAAK,YAAY;IACf,SAAS;IACT,MAAM;IACN;IACA;IACA;GACF,CAAC;EACH,SAAS,OAAO;GACd,YAAY;GACZ,OAAO,KAAK;EACd;CACF,CAAC;AACH"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AsyncStorageLike } from "@assistant-ui/core/react";
|
|
2
|
+
import { type VSCodeBridgePort } from "./fetch.js";
|
|
3
|
+
/**
|
|
4
|
+
* Creates an `AsyncStorageLike` backed by the `storage` Memento passed to
|
|
5
|
+
* `serveWebviewHost`, for `createLocalStorageAdapter({ storage })`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createVSCodeStorage(port?: VSCodeBridgePort): AsyncStorageLike;
|
|
8
|
+
//# sourceMappingURL=storage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/webview/storage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,EAAe,KAAK,gBAAgB,EAAE,mBAAgB;AAG7D;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,GAAE,gBAA8B,GACnC,gBAAgB,CAalB"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { webviewPort } from "./fetch.js";
|
|
2
|
+
import { callHost } from "./rpc.js";
|
|
3
|
+
// src/webview/storage.ts
|
|
4
|
+
/**
|
|
5
|
+
* Creates an `AsyncStorageLike` backed by the `storage` Memento passed to
|
|
6
|
+
* `serveWebviewHost`, for `createLocalStorageAdapter({ storage })`.
|
|
7
|
+
*/
|
|
8
|
+
function createVSCodeStorage(port = webviewPort) {
|
|
9
|
+
return {
|
|
10
|
+
getItem: async (key) => {
|
|
11
|
+
const value = await callHost(port, "storage.getItem", [key]);
|
|
12
|
+
return typeof value === "string" ? value : null;
|
|
13
|
+
},
|
|
14
|
+
setItem: async (key, value) => {
|
|
15
|
+
await callHost(port, "storage.setItem", [key, value]);
|
|
16
|
+
},
|
|
17
|
+
removeItem: async (key) => {
|
|
18
|
+
await callHost(port, "storage.removeItem", [key]);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export { createVSCodeStorage };
|
|
23
|
+
|
|
24
|
+
//# sourceMappingURL=storage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.js","names":[],"sources":["../../src/webview/storage.ts"],"sourcesContent":["import type { AsyncStorageLike } from \"@assistant-ui/core/react\";\nimport { webviewPort, type VSCodeBridgePort } from \"./fetch\";\nimport { callHost } from \"./rpc\";\n\n/**\n * Creates an `AsyncStorageLike` backed by the `storage` Memento passed to\n * `serveWebviewHost`, for `createLocalStorageAdapter({ storage })`.\n */\nexport function createVSCodeStorage(\n port: VSCodeBridgePort = webviewPort,\n): AsyncStorageLike {\n return {\n getItem: async (key) => {\n const value = await callHost(port, \"storage.getItem\", [key]);\n return typeof value === \"string\" ? value : null;\n },\n setItem: async (key, value) => {\n await callHost(port, \"storage.setItem\", [key, value]);\n },\n removeItem: async (key) => {\n await callHost(port, \"storage.removeItem\", [key]);\n },\n };\n}\n"],"mappings":";;;;;;;AAQA,SAAgB,oBACd,OAAyB,aACP;CAClB,OAAO;EACL,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,SAAS,MAAM,mBAAmB,CAAC,GAAG,CAAC;GAC3D,OAAO,OAAO,UAAU,WAAW,QAAQ;EAC7C;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,SAAS,MAAM,mBAAmB,CAAC,KAAK,KAAK,CAAC;EACtD;EACA,YAAY,OAAO,QAAQ;GACzB,MAAM,SAAS,MAAM,sBAAsB,CAAC,GAAG,CAAC;EAClD;CACF;AACF"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type VSCodeApi<TState = unknown> = {
|
|
2
|
+
postMessage(message: unknown): void;
|
|
3
|
+
getState(): TState | undefined;
|
|
4
|
+
setState<T extends TState | undefined>(newState: T): T;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Returns the webview's VS Code API. `acquireVsCodeApi()` may only be called
|
|
8
|
+
* once per webview, so every caller in the webview must go through this.
|
|
9
|
+
*/
|
|
10
|
+
export declare function getVSCodeApi<TState = unknown>(): VSCodeApi<TState>;
|
|
11
|
+
//# sourceMappingURL=vscode-api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vscode-api.d.ts","sourceRoot":"","sources":["../../src/webview/vscode-api.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,SAAS,CAAC,MAAM,GAAG,OAAO,IAAI;IACxC,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACpC,QAAQ,IAAI,MAAM,GAAG,SAAS,CAAC;IAC/B,QAAQ,CAAC,CAAC,SAAS,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,CAAC,GAAG,CAAC,CAAC;CACxD,CAAC;AAIF;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,GAAG,OAAO,KAAK,SAAS,CAAC,MAAM,CAAC,CAgBlE"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { VSCODE_BRIDGE_CHANNEL } from "../protocol.js";
|
|
2
|
+
// src/webview/vscode-api.ts
|
|
3
|
+
let api;
|
|
4
|
+
/**
|
|
5
|
+
* Returns the webview's VS Code API. `acquireVsCodeApi()` may only be called
|
|
6
|
+
* once per webview, so every caller in the webview must go through this.
|
|
7
|
+
*/
|
|
8
|
+
function getVSCodeApi() {
|
|
9
|
+
if (api) return api;
|
|
10
|
+
const acquire = globalThis.acquireVsCodeApi;
|
|
11
|
+
if (typeof acquire !== "function") throw new Error("acquireVsCodeApi is not defined: getVSCodeApi() must run inside a VS Code webview.");
|
|
12
|
+
api = acquire();
|
|
13
|
+
api.postMessage({
|
|
14
|
+
channel: VSCODE_BRIDGE_CHANNEL,
|
|
15
|
+
kind: "fetch:reset",
|
|
16
|
+
id: ""
|
|
17
|
+
});
|
|
18
|
+
return api;
|
|
19
|
+
}
|
|
20
|
+
export { getVSCodeApi };
|
|
21
|
+
|
|
22
|
+
//# sourceMappingURL=vscode-api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vscode-api.js","names":[],"sources":["../../src/webview/vscode-api.ts"],"sourcesContent":["import { VSCODE_BRIDGE_CHANNEL } from \"../protocol\";\n\nexport type VSCodeApi<TState = unknown> = {\n postMessage(message: unknown): void;\n getState(): TState | undefined;\n setState<T extends TState | undefined>(newState: T): T;\n};\n\nlet api: VSCodeApi | undefined;\n\n/**\n * Returns the webview's VS Code API. `acquireVsCodeApi()` may only be called\n * once per webview, so every caller in the webview must go through this.\n */\nexport function getVSCodeApi<TState = unknown>(): VSCodeApi<TState> {\n if (api) return api as VSCodeApi<TState>;\n const acquire = (globalThis as { acquireVsCodeApi?: () => VSCodeApi })\n .acquireVsCodeApi;\n if (typeof acquire !== \"function\") {\n throw new Error(\n \"acquireVsCodeApi is not defined: getVSCodeApi() must run inside a VS Code webview.\",\n );\n }\n api = acquire();\n api.postMessage({\n channel: VSCODE_BRIDGE_CHANNEL,\n kind: \"fetch:reset\",\n id: \"\",\n });\n return api as VSCodeApi<TState>;\n}\n"],"mappings":";;AAQA,IAAI;;;;;AAMJ,SAAgB,eAAoD;CAClE,IAAI,KAAK,OAAO;CAChB,MAAM,UAAW,WACd;CACH,IAAI,OAAO,YAAY,YACrB,MAAM,IAAI,MACR,oFACF;CAEF,MAAM,QAAQ;CACd,IAAI,YAAY;EACd,SAAS;EACT,MAAM;EACN,IAAI;CACN,CAAC;CACD,OAAO;AACT"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { getVSCodeApi, type VSCodeApi } from "./webview/vscode-api.js";
|
|
2
|
+
export { getCspNonce } from "./webview/csp-nonce.js";
|
|
3
|
+
export { createVSCodeFetch, vscodeFetch, type VSCodeBridgePort, type VSCodeFetch, } from "./webview/fetch.js";
|
|
4
|
+
export { installLinkInterceptor, type LinkInterceptorOptions, } from "./webview/links.js";
|
|
5
|
+
export { createVSCodeStorage } from "./webview/storage.js";
|
|
6
|
+
export * from "./protocol.js";
|
|
7
|
+
//# sourceMappingURL=webview.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webview.d.ts","sourceRoot":"","sources":["../src/webview.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,KAAK,SAAS,EAAE,gCAA6B;AACpE,OAAO,EAAE,WAAW,EAAE,+BAA4B;AAClD,OAAO,EACL,iBAAiB,EACjB,WAAW,EACX,KAAK,gBAAgB,EACrB,KAAK,WAAW,GACjB,2BAAwB;AACzB,OAAO,EACL,sBAAsB,EACtB,KAAK,sBAAsB,GAC5B,2BAAwB;AACzB,OAAO,EAAE,mBAAmB,EAAE,6BAA0B;AACxD,8BAA2B"}
|
package/dist/webview.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { VSCODE_BRIDGE_CHANNEL, VSCODE_VIRTUAL_ORIGIN, isHostToWebviewMessage, isWebviewToHostMessage } from "./protocol.js";
|
|
2
|
+
import { getVSCodeApi } from "./webview/vscode-api.js";
|
|
3
|
+
import { getCspNonce } from "./webview/csp-nonce.js";
|
|
4
|
+
import { createVSCodeFetch, vscodeFetch } from "./webview/fetch.js";
|
|
5
|
+
import { installLinkInterceptor } from "./webview/links.js";
|
|
6
|
+
import { createVSCodeStorage } from "./webview/storage.js";
|
|
7
|
+
export { VSCODE_BRIDGE_CHANNEL, VSCODE_VIRTUAL_ORIGIN, createVSCodeFetch, createVSCodeStorage, getCspNonce, getVSCodeApi, installLinkInterceptor, isHostToWebviewMessage, isWebviewToHostMessage, vscodeFetch };
|
package/package.json
CHANGED
|
@@ -1,6 +1,90 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assistant-ui/vscode",
|
|
3
|
-
"version": "0.0.0
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Run assistant-ui inside VS Code extension webviews by tunnelling fetch to the extension host",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"vscode",
|
|
7
|
+
"vscode-extension",
|
|
8
|
+
"webview",
|
|
9
|
+
"assistant-ui",
|
|
10
|
+
"ai",
|
|
11
|
+
"chat"
|
|
12
|
+
],
|
|
13
|
+
"author": "AgentbaseAI Inc.",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"homepage": "https://www.assistant-ui.com/",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/assistant-ui/assistant-ui.git",
|
|
19
|
+
"directory": "packages/vscode"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/assistant-ui/assistant-ui/issues"
|
|
23
|
+
},
|
|
24
|
+
"type": "module",
|
|
25
|
+
"exports": {
|
|
26
|
+
"./webview": {
|
|
27
|
+
"types": "./dist/webview.d.ts",
|
|
28
|
+
"default": "./dist/webview.js"
|
|
29
|
+
},
|
|
30
|
+
"./host": {
|
|
31
|
+
"types": "./dist/host.d.ts",
|
|
32
|
+
"default": "./dist/host.js"
|
|
33
|
+
},
|
|
34
|
+
"./theme.css": {
|
|
35
|
+
"style": "./src/theme.css",
|
|
36
|
+
"default": "./src/theme.css"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"dist",
|
|
41
|
+
"src",
|
|
42
|
+
"!src/**/*.test.ts",
|
|
43
|
+
"!src/**/*.test.tsx",
|
|
44
|
+
"!src/testUtils.ts",
|
|
45
|
+
"README.md"
|
|
46
|
+
],
|
|
47
|
+
"sideEffects": [
|
|
48
|
+
"./src/theme.css"
|
|
49
|
+
],
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public",
|
|
52
|
+
"provenance": true
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@assistant-ui/core": "^0.3.32"
|
|
56
|
+
},
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"@types/react": "*",
|
|
59
|
+
"react": "^18 || ^19"
|
|
60
|
+
},
|
|
61
|
+
"peerDependenciesMeta": {
|
|
62
|
+
"react": {
|
|
63
|
+
"optional": true
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"@assistant-ui/store": "0.3.22",
|
|
68
|
+
"@assistant-ui/tap": "0.9.24",
|
|
69
|
+
"@assistant-ui/x-buildutils": "0.0.32",
|
|
70
|
+
"@assistant-ui/x-react18": "0.0.0",
|
|
71
|
+
"@testing-library/react": "^16.3.3",
|
|
72
|
+
"@types/node": "^26.6.5",
|
|
73
|
+
"@types/react": "^19.3.0",
|
|
74
|
+
"ai": "^7.0.153",
|
|
75
|
+
"jsdom": "^30.1.3",
|
|
76
|
+
"react": "^19.3.1",
|
|
77
|
+
"react-dom": "^19.3.1",
|
|
78
|
+
"tailwindcss": "^4.3.3",
|
|
79
|
+
"vitest": "^5.0.5"
|
|
80
|
+
},
|
|
81
|
+
"scripts": {
|
|
82
|
+
"build": "aui-build",
|
|
83
|
+
"test": "vitest run",
|
|
84
|
+
"test:coverage": "vitest run --coverage --coverage.reporter=text --coverage.reporter=html --coverage.reporter=json-summary",
|
|
85
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
|
|
86
|
+
"test:peer-react18": "vitest run --config vitest.peer-react18.config.ts",
|
|
87
|
+
"test:watch": "vitest",
|
|
88
|
+
"test:types:peer-react18": "aui-react18-types"
|
|
89
|
+
}
|
|
6
90
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const DEFAULT_EXTERNAL_SCHEMES: readonly string[] = [
|
|
2
|
+
"http:",
|
|
3
|
+
"https:",
|
|
4
|
+
"mailto:",
|
|
5
|
+
];
|
|
6
|
+
|
|
7
|
+
export const parseExternalUrl = (
|
|
8
|
+
value: unknown,
|
|
9
|
+
schemes: readonly string[],
|
|
10
|
+
): string | null => {
|
|
11
|
+
if (typeof value !== "string" && !(value instanceof URL)) return null;
|
|
12
|
+
let url: URL;
|
|
13
|
+
try {
|
|
14
|
+
url = new URL(value);
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return schemes.includes(url.protocol) ? url.href : null;
|
|
19
|
+
};
|
package/src/host/html.ts
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
export type WebviewCspSource = {
|
|
2
|
+
/** A source list, such as VS Code's `'self' https://*.vscode-cdn.net`. */
|
|
3
|
+
readonly cspSource: string;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export type WebviewResourceLike<U> = WebviewCspSource & {
|
|
7
|
+
asWebviewUri(uri: U): { toString(): string };
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type WebviewCspMode = "strict" | "relaxed";
|
|
11
|
+
|
|
12
|
+
export type WebviewCspOptions = {
|
|
13
|
+
nonce: string;
|
|
14
|
+
/**
|
|
15
|
+
* `"relaxed"` swaps the style nonce for `'unsafe-inline'`, for libraries
|
|
16
|
+
* that inject `<style>` tags without a nonce.
|
|
17
|
+
*/
|
|
18
|
+
csp?: WebviewCspMode;
|
|
19
|
+
/**
|
|
20
|
+
* Extra `script-src` sources, one host or scheme per entry. Code-split
|
|
21
|
+
* chunks need none: a module script's imports inherit its nonce.
|
|
22
|
+
*/
|
|
23
|
+
scriptSrc?: readonly string[];
|
|
24
|
+
connectSrc?: readonly string[];
|
|
25
|
+
/**
|
|
26
|
+
* Extra `img-src` sources, such as `"https:"`. Remote images are blocked by
|
|
27
|
+
* default, because an image URL in model output can carry chat data away.
|
|
28
|
+
*/
|
|
29
|
+
imgSrc?: readonly string[];
|
|
30
|
+
/** Extra `media-src` sources, blocked by default like `imgSrc`. */
|
|
31
|
+
mediaSrc?: readonly string[];
|
|
32
|
+
frameSrc?: readonly string[];
|
|
33
|
+
/** Adds `'wasm-unsafe-eval'` to `script-src` for WebAssembly modules. */
|
|
34
|
+
wasmUnsafeEval?: boolean;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type WebviewSurface = "sidebar" | "editor" | "panel";
|
|
38
|
+
|
|
39
|
+
export type RenderWebviewHtmlOptions<U> = Omit<WebviewCspOptions, "nonce"> & {
|
|
40
|
+
scripts: readonly U[];
|
|
41
|
+
styles?: readonly U[];
|
|
42
|
+
title?: string;
|
|
43
|
+
lang?: string;
|
|
44
|
+
/** Defaults to a fresh random nonce per render. */
|
|
45
|
+
nonce?: string;
|
|
46
|
+
/** Sets `data-aui-vscode-surface` for `@assistant-ui/vscode/theme.css`. */
|
|
47
|
+
surface?: WebviewSurface;
|
|
48
|
+
/** Id of the empty mount element; `null` omits it. */
|
|
49
|
+
rootId?: string | null;
|
|
50
|
+
/** `"module"` (default) or `"classic"` deferred scripts. */
|
|
51
|
+
scriptType?: "module" | "classic";
|
|
52
|
+
htmlAttributes?: Readonly<Record<string, string>>;
|
|
53
|
+
bodyAttributes?: Readonly<Record<string, string>>;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const NONCE_PATTERN = /^[A-Za-z0-9+/_-]+={0,2}$/;
|
|
57
|
+
const ATTRIBUTE_NAME_PATTERN = /^[A-Za-z_:][-A-Za-z0-9_:.]*$/;
|
|
58
|
+
const INVALID_SOURCE_PATTERN = /[\s;,'"]/;
|
|
59
|
+
|
|
60
|
+
const HTML_ESCAPES: Record<string, string> = {
|
|
61
|
+
"&": "&",
|
|
62
|
+
"<": "<",
|
|
63
|
+
">": ">",
|
|
64
|
+
'"': """,
|
|
65
|
+
"'": "'",
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const escapeHtml = (value: string) =>
|
|
69
|
+
value.replace(/[&<>"']/g, (char) => HTML_ESCAPES[char] ?? char);
|
|
70
|
+
|
|
71
|
+
const assertNonce = (nonce: string) => {
|
|
72
|
+
if (!NONCE_PATTERN.test(nonce)) {
|
|
73
|
+
throw new Error(`Invalid CSP nonce: ${JSON.stringify(nonce)}`);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const sources = (directive: string, values: readonly string[]) => {
|
|
78
|
+
for (const value of values) {
|
|
79
|
+
if (value === "" || INVALID_SOURCE_PATTERN.test(value)) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Invalid ${directive} source: ${JSON.stringify(value)}. Pass one host or scheme source per entry.`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return values;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const cspSourceList = (cspSource: string) => {
|
|
89
|
+
const list = cspSource.split(/\s+/).filter((value) => value !== "");
|
|
90
|
+
if (list.length === 0) throw new Error("Invalid cspSource: empty");
|
|
91
|
+
sources(
|
|
92
|
+
"cspSource",
|
|
93
|
+
list.filter((value) => value !== "'self'"),
|
|
94
|
+
);
|
|
95
|
+
return list.join(" ");
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const attributes = (values: Readonly<Record<string, string>>) =>
|
|
99
|
+
Object.entries(values)
|
|
100
|
+
.map(([name, value]) => {
|
|
101
|
+
if (!ATTRIBUTE_NAME_PATTERN.test(name)) {
|
|
102
|
+
throw new Error(`Invalid attribute name: ${JSON.stringify(name)}`);
|
|
103
|
+
}
|
|
104
|
+
return ` ${name}="${escapeHtml(value)}"`;
|
|
105
|
+
})
|
|
106
|
+
.join("");
|
|
107
|
+
|
|
108
|
+
/** Returns a base64 nonce from 16 cryptographically random bytes. */
|
|
109
|
+
export function createCspNonce(): string {
|
|
110
|
+
const crypto = globalThis.crypto;
|
|
111
|
+
if (typeof crypto?.getRandomValues !== "function") {
|
|
112
|
+
throw new Error(
|
|
113
|
+
"createCspNonce() needs globalThis.crypto.getRandomValues (Node.js 19 or later).",
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
117
|
+
return btoa(String.fromCharCode(...bytes));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Returns the Content-Security-Policy for a webview whose scripts carry
|
|
122
|
+
* `nonce` and whose other resources load through `asWebviewUri`.
|
|
123
|
+
*/
|
|
124
|
+
export function createWebviewCsp(
|
|
125
|
+
webview: WebviewCspSource,
|
|
126
|
+
options: WebviewCspOptions,
|
|
127
|
+
): string {
|
|
128
|
+
const { nonce, csp = "strict" } = options;
|
|
129
|
+
assertNonce(nonce);
|
|
130
|
+
const cspSource = cspSourceList(webview.cspSource);
|
|
131
|
+
const frameSrc = sources("frame-src", options.frameSrc ?? []);
|
|
132
|
+
|
|
133
|
+
const directives: [string, ...string[]][] = [
|
|
134
|
+
["default-src", "'none'"],
|
|
135
|
+
[
|
|
136
|
+
"script-src",
|
|
137
|
+
`'nonce-${nonce}'`,
|
|
138
|
+
...sources("script-src", options.scriptSrc ?? []),
|
|
139
|
+
...(options.wasmUnsafeEval ? ["'wasm-unsafe-eval'"] : []),
|
|
140
|
+
],
|
|
141
|
+
[
|
|
142
|
+
"style-src",
|
|
143
|
+
cspSource,
|
|
144
|
+
csp === "relaxed" ? "'unsafe-inline'" : `'nonce-${nonce}'`,
|
|
145
|
+
],
|
|
146
|
+
[
|
|
147
|
+
"img-src",
|
|
148
|
+
cspSource,
|
|
149
|
+
"blob:",
|
|
150
|
+
"data:",
|
|
151
|
+
...sources("img-src", options.imgSrc ?? []),
|
|
152
|
+
],
|
|
153
|
+
[
|
|
154
|
+
"media-src",
|
|
155
|
+
cspSource,
|
|
156
|
+
"blob:",
|
|
157
|
+
"data:",
|
|
158
|
+
...sources("media-src", options.mediaSrc ?? []),
|
|
159
|
+
],
|
|
160
|
+
["font-src", cspSource, "data:"],
|
|
161
|
+
[
|
|
162
|
+
"connect-src",
|
|
163
|
+
cspSource,
|
|
164
|
+
...sources("connect-src", options.connectSrc ?? []),
|
|
165
|
+
],
|
|
166
|
+
["frame-src", ...(frameSrc.length > 0 ? frameSrc : ["'none'"])],
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
return directives.map((directive) => directive.join(" ")).join("; ");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Renders the HTML document for a webview: a CSP meta tag, the nonce in
|
|
174
|
+
* `<meta property="csp-nonce">` for `getCspNonce()`, the stylesheets, a mount
|
|
175
|
+
* element, and the scripts, each carrying the nonce.
|
|
176
|
+
*/
|
|
177
|
+
export function renderWebviewHtml<U>(
|
|
178
|
+
webview: WebviewResourceLike<U>,
|
|
179
|
+
options: RenderWebviewHtmlOptions<U>,
|
|
180
|
+
): string {
|
|
181
|
+
const {
|
|
182
|
+
scripts,
|
|
183
|
+
styles = [],
|
|
184
|
+
title = "",
|
|
185
|
+
lang = "en",
|
|
186
|
+
nonce = createCspNonce(),
|
|
187
|
+
surface,
|
|
188
|
+
rootId = "root",
|
|
189
|
+
scriptType = "module",
|
|
190
|
+
htmlAttributes = {},
|
|
191
|
+
bodyAttributes = {},
|
|
192
|
+
...cspOptions
|
|
193
|
+
} = options;
|
|
194
|
+
|
|
195
|
+
const csp = createWebviewCsp(webview, { ...cspOptions, nonce });
|
|
196
|
+
const url = (uri: U) => escapeHtml(webview.asWebviewUri(uri).toString());
|
|
197
|
+
const scriptAttributes =
|
|
198
|
+
scriptType === "module" ? ' type="module"' : " defer";
|
|
199
|
+
|
|
200
|
+
return [
|
|
201
|
+
"<!doctype html>",
|
|
202
|
+
`<html${attributes({ lang, ...htmlAttributes })}>`,
|
|
203
|
+
"<head>",
|
|
204
|
+
'<meta charset="utf-8">',
|
|
205
|
+
`<meta http-equiv="Content-Security-Policy" content="${escapeHtml(csp)}">`,
|
|
206
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
207
|
+
`<meta property="csp-nonce" nonce="${nonce}">`,
|
|
208
|
+
`<title>${escapeHtml(title)}</title>`,
|
|
209
|
+
...styles.map((uri) => `<link rel="stylesheet" href="${url(uri)}">`),
|
|
210
|
+
"</head>",
|
|
211
|
+
`<body${attributes({
|
|
212
|
+
...(surface ? { "data-aui-vscode-surface": surface } : {}),
|
|
213
|
+
...bodyAttributes,
|
|
214
|
+
})}>`,
|
|
215
|
+
...(rootId === null ? [] : [`<div id="${escapeHtml(rootId)}"></div>`]),
|
|
216
|
+
...scripts.map(
|
|
217
|
+
(uri) =>
|
|
218
|
+
`<script${scriptAttributes} nonce="${nonce}" src="${url(uri)}"></script>`,
|
|
219
|
+
),
|
|
220
|
+
"</body>",
|
|
221
|
+
"</html>",
|
|
222
|
+
"",
|
|
223
|
+
].join("\n");
|
|
224
|
+
}
|