@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.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +159 -2
  3. package/dist/external-url.d.ts +3 -0
  4. package/dist/external-url.d.ts.map +1 -0
  5. package/dist/external-url.js +19 -0
  6. package/dist/external-url.js.map +1 -0
  7. package/dist/host/html.d.ts +65 -0
  8. package/dist/host/html.d.ts.map +1 -0
  9. package/dist/host/html.js +126 -0
  10. package/dist/host/html.js.map +1 -0
  11. package/dist/host/router.d.ts +24 -0
  12. package/dist/host/router.d.ts.map +1 -0
  13. package/dist/host/router.js +166 -0
  14. package/dist/host/router.js.map +1 -0
  15. package/dist/host/serve.d.ts +34 -0
  16. package/dist/host/serve.d.ts.map +1 -0
  17. package/dist/host/serve.js +93 -0
  18. package/dist/host/serve.js.map +1 -0
  19. package/dist/host.d.ts +5 -0
  20. package/dist/host.d.ts.map +1 -0
  21. package/dist/host.js +4 -0
  22. package/dist/protocol.d.ts +46 -0
  23. package/dist/protocol.d.ts.map +1 -0
  24. package/dist/protocol.js +30 -0
  25. package/dist/protocol.js.map +1 -0
  26. package/dist/route-url.d.ts +3 -0
  27. package/dist/route-url.d.ts.map +1 -0
  28. package/dist/route-url.js +7 -0
  29. package/dist/route-url.js.map +1 -0
  30. package/dist/webview/csp-nonce.d.ts +6 -0
  31. package/dist/webview/csp-nonce.d.ts.map +1 -0
  32. package/dist/webview/csp-nonce.js +12 -0
  33. package/dist/webview/csp-nonce.js.map +1 -0
  34. package/dist/webview/fetch.d.ts +14 -0
  35. package/dist/webview/fetch.d.ts.map +1 -0
  36. package/dist/webview/fetch.js +164 -0
  37. package/dist/webview/fetch.js.map +1 -0
  38. package/dist/webview/id.d.ts +2 -0
  39. package/dist/webview/id.d.ts.map +1 -0
  40. package/dist/webview/id.js +5 -0
  41. package/dist/webview/id.js.map +1 -0
  42. package/dist/webview/links.d.ts +14 -0
  43. package/dist/webview/links.d.ts.map +1 -0
  44. package/dist/webview/links.js +44 -0
  45. package/dist/webview/links.js.map +1 -0
  46. package/dist/webview/rpc.d.ts +3 -0
  47. package/dist/webview/rpc.d.ts.map +1 -0
  48. package/dist/webview/rpc.js +32 -0
  49. package/dist/webview/rpc.js.map +1 -0
  50. package/dist/webview/storage.d.ts +8 -0
  51. package/dist/webview/storage.d.ts.map +1 -0
  52. package/dist/webview/storage.js +24 -0
  53. package/dist/webview/storage.js.map +1 -0
  54. package/dist/webview/vscode-api.d.ts +11 -0
  55. package/dist/webview/vscode-api.d.ts.map +1 -0
  56. package/dist/webview/vscode-api.js +22 -0
  57. package/dist/webview/vscode-api.js.map +1 -0
  58. package/dist/webview.d.ts +7 -0
  59. package/dist/webview.d.ts.map +1 -0
  60. package/dist/webview.js +7 -0
  61. package/package.json +87 -3
  62. package/src/external-url.ts +19 -0
  63. package/src/host/html.ts +224 -0
  64. package/src/host/router.ts +253 -0
  65. package/src/host/serve.ts +150 -0
  66. package/src/host.ts +27 -0
  67. package/src/protocol.ts +131 -0
  68. package/src/route-url.ts +6 -0
  69. package/src/theme.css +327 -0
  70. package/src/webview/csp-nonce.ts +10 -0
  71. package/src/webview/fetch.ts +210 -0
  72. package/src/webview/id.ts +4 -0
  73. package/src/webview/links.ts +56 -0
  74. package/src/webview/rpc.ts +35 -0
  75. package/src/webview/storage.ts +24 -0
  76. package/src/webview/vscode-api.ts +31 -0
  77. package/src/webview.ts +14 -0
package/src/theme.css ADDED
@@ -0,0 +1,327 @@
1
+ @custom-variant dark (&:is(.vscode-dark *, .vscode-high-contrast:not(.vscode-high-contrast-light) *));
2
+
3
+ @theme inline {
4
+ --color-background: var(--background);
5
+ --color-foreground: var(--foreground);
6
+ --color-card: var(--card);
7
+ --color-card-foreground: var(--card-foreground);
8
+ --color-popover: var(--popover);
9
+ --color-popover-foreground: var(--popover-foreground);
10
+ --color-primary: var(--primary);
11
+ --color-primary-foreground: var(--primary-foreground);
12
+ --color-secondary: var(--secondary);
13
+ --color-secondary-foreground: var(--secondary-foreground);
14
+ --color-muted: var(--muted);
15
+ --color-muted-foreground: var(--muted-foreground);
16
+ --color-accent: var(--accent);
17
+ --color-accent-foreground: var(--accent-foreground);
18
+ --color-destructive: var(--destructive);
19
+ --color-border: var(--border);
20
+ --color-input: var(--input);
21
+ --color-ring: var(--ring);
22
+ --color-chart-1: var(--chart-1);
23
+ --color-chart-2: var(--chart-2);
24
+ --color-chart-3: var(--chart-3);
25
+ --color-chart-4: var(--chart-4);
26
+ --color-chart-5: var(--chart-5);
27
+ --color-sidebar: var(--sidebar);
28
+ --color-sidebar-foreground: var(--sidebar-foreground);
29
+ --color-sidebar-primary: var(--sidebar-primary);
30
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
31
+ --color-sidebar-accent: var(--sidebar-accent);
32
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
33
+ --color-sidebar-border: var(--sidebar-border);
34
+ --color-sidebar-ring: var(--sidebar-ring);
35
+ --radius-sm: calc(var(--radius) - 4px);
36
+ --radius-md: calc(var(--radius) - 2px);
37
+ --radius-lg: var(--radius);
38
+ --radius-xl: calc(var(--radius) + 4px);
39
+ --radius-2xl: calc(var(--radius) + 8px);
40
+ --radius-3xl: 1.5rem;
41
+ --font-sans: var(--vscode-font-family, ui-sans-serif, system-ui, sans-serif);
42
+ --font-mono: var(
43
+ --vscode-editor-font-family,
44
+ ui-monospace,
45
+ SFMono-Regular,
46
+ Menlo,
47
+ Consolas,
48
+ monospace
49
+ );
50
+ }
51
+
52
+ /*
53
+ * Tokens live on body, not :root: VS Code sets its theme classes on body, and
54
+ * body-scoped values win over a template's :root values in any import order.
55
+ */
56
+ @layer base {
57
+ body {
58
+ --aui-vscode-background: var(
59
+ --vscode-sideBar-background,
60
+ var(--vscode-editor-background, Canvas)
61
+ );
62
+ --aui-vscode-foreground: var(
63
+ --vscode-sideBar-foreground,
64
+ var(--vscode-foreground, CanvasText)
65
+ );
66
+
67
+ --radius: 0.375rem;
68
+ --background: var(--aui-vscode-background);
69
+ --foreground: var(--aui-vscode-foreground);
70
+ --card: var(--vscode-editorWidget-background, var(--background));
71
+ --card-foreground: var(--vscode-editorWidget-foreground, var(--foreground));
72
+ --popover: var(
73
+ --vscode-menu-background,
74
+ var(--vscode-editorWidget-background, var(--background))
75
+ );
76
+ --popover-foreground: var(
77
+ --vscode-menu-foreground,
78
+ var(--vscode-editorWidget-foreground, var(--foreground))
79
+ );
80
+ --primary: var(--vscode-button-background, #0078d4);
81
+ --primary-foreground: var(--vscode-button-foreground, #ffffff);
82
+ /*
83
+ * Derived from the surface: the VS Code candidates equal a surface in some
84
+ * default theme (input and editor in Light Modern) or are transparent
85
+ * (secondary buttons in Dark Modern).
86
+ */
87
+ --secondary: color-mix(in srgb, var(--foreground) 12%, var(--background));
88
+ --secondary-foreground: var(--foreground);
89
+ --muted: color-mix(in srgb, var(--foreground) 6%, var(--background));
90
+ --muted-foreground: var(--vscode-descriptionForeground, GrayText);
91
+ --accent: var(
92
+ --vscode-list-hoverBackground,
93
+ var(
94
+ --vscode-toolbar-hoverBackground,
95
+ color-mix(in srgb, var(--foreground) 10%, transparent)
96
+ )
97
+ );
98
+ --accent-foreground: var(--vscode-list-hoverForeground, var(--foreground));
99
+ --destructive: var(--vscode-errorForeground, light-dark(#e51400, #f14c4c));
100
+ --border: var(
101
+ --vscode-panel-border,
102
+ var(
103
+ --vscode-widget-border,
104
+ color-mix(in srgb, var(--foreground) 15%, transparent)
105
+ )
106
+ );
107
+ --input: var(
108
+ --vscode-input-border,
109
+ var(--vscode-dropdown-border, var(--border))
110
+ );
111
+ --ring: var(--vscode-focusBorder, #0078d4);
112
+ --chart-1: var(--vscode-charts-blue, #3794ff);
113
+ --chart-2: var(--vscode-charts-green, #89d185);
114
+ --chart-3: var(--vscode-charts-yellow, #cca700);
115
+ --chart-4: var(--vscode-charts-orange, #d18616);
116
+ --chart-5: var(--vscode-charts-purple, #b180d7);
117
+ --sidebar: var(--vscode-sideBar-background, var(--background));
118
+ --sidebar-foreground: var(
119
+ --vscode-sideBar-foreground,
120
+ var(--vscode-foreground, var(--foreground))
121
+ );
122
+ --sidebar-primary: var(--primary);
123
+ --sidebar-primary-foreground: var(--primary-foreground);
124
+ --sidebar-accent: var(--accent);
125
+ --sidebar-accent-foreground: var(--accent-foreground);
126
+ --sidebar-border: var(--vscode-sideBar-border, var(--border));
127
+ --sidebar-ring: var(--ring);
128
+
129
+ background-color: var(--background);
130
+ color: var(--foreground);
131
+ font-family: var(
132
+ --vscode-font-family,
133
+ ui-sans-serif,
134
+ system-ui,
135
+ sans-serif
136
+ );
137
+ font-size: var(--vscode-font-size, 13px);
138
+ font-weight: var(--vscode-font-weight, normal);
139
+ }
140
+
141
+ body:is(
142
+ [data-aui-vscode-surface="editor"],
143
+ [data-aui-vscode-surface="editor"] *
144
+ ) {
145
+ --aui-vscode-background: var(--vscode-editor-background, Canvas);
146
+ --aui-vscode-foreground: var(
147
+ --vscode-editor-foreground,
148
+ var(--vscode-foreground, CanvasText)
149
+ );
150
+ }
151
+
152
+ body:is(
153
+ [data-aui-vscode-surface="panel"],
154
+ [data-aui-vscode-surface="panel"] *
155
+ ) {
156
+ --aui-vscode-background: var(
157
+ --vscode-panel-background,
158
+ var(--vscode-editor-background, Canvas)
159
+ );
160
+ --aui-vscode-foreground: var(--vscode-foreground, CanvasText);
161
+ }
162
+
163
+ body:is(.vscode-light, .vscode-high-contrast-light) {
164
+ color-scheme: light;
165
+ }
166
+
167
+ /* Light Modern sets --vscode-descriptionForeground to its foreground colour. */
168
+ body.vscode-light:not(.vscode-high-contrast) {
169
+ --muted-foreground: color-mix(
170
+ in srgb,
171
+ var(--foreground) 72%,
172
+ var(--background)
173
+ );
174
+ }
175
+
176
+ body:is(
177
+ .vscode-dark,
178
+ .vscode-high-contrast:not(.vscode-high-contrast-light)
179
+ ) {
180
+ color-scheme: dark;
181
+ }
182
+
183
+ /* Dark High Contrast sets --vscode-button-background to its black background. */
184
+ body:is(.vscode-high-contrast, .vscode-high-contrast-light) {
185
+ --primary: var(--vscode-textLink-foreground, LinkText);
186
+ --primary-foreground: var(--background);
187
+ --border: var(--vscode-contrastBorder, CanvasText);
188
+ --input: var(--vscode-contrastBorder, CanvasText);
189
+ --ring: var(
190
+ --vscode-contrastActiveBorder,
191
+ var(--vscode-focusBorder, Highlight)
192
+ );
193
+ --sidebar-border: var(--border);
194
+ }
195
+
196
+ /*
197
+ * Recent VS Code releases inject the webview defaults as
198
+ * @layer vscode-default, which ranks below Tailwind's layers but still
199
+ * applies wherever preflight sets nothing, and which the unlayered
200
+ * revert-layer rules below would fall back to. revert here drops them to the
201
+ * browser defaults.
202
+ */
203
+ :where(img, video) {
204
+ max-height: revert;
205
+ }
206
+
207
+ :where(a code) {
208
+ color: revert;
209
+ }
210
+
211
+ :where(a, input, select, textarea):focus {
212
+ outline: revert;
213
+ outline-offset: revert;
214
+ }
215
+
216
+ :where(code, kbd) {
217
+ color: revert;
218
+ background-color: revert;
219
+ border-radius: revert;
220
+ }
221
+
222
+ :where(kbd) {
223
+ box-shadow: revert;
224
+ vertical-align: revert;
225
+ }
226
+
227
+ :where(blockquote) {
228
+ background: revert;
229
+ }
230
+ }
231
+
232
+ /*
233
+ * VS Code injects an unlayered default stylesheet into every webview, which
234
+ * outranks Tailwind's layers; revert-layer hands these properties back to them.
235
+ */
236
+ body {
237
+ margin: revert-layer;
238
+ padding: revert-layer;
239
+ background-color: revert-layer;
240
+ color: revert-layer;
241
+ font-family: revert-layer;
242
+ font-size: revert-layer;
243
+ font-weight: revert-layer;
244
+ }
245
+
246
+ img,
247
+ video {
248
+ max-width: revert-layer;
249
+ max-height: revert-layer;
250
+ }
251
+
252
+ a,
253
+ a code,
254
+ a:hover {
255
+ color: revert-layer;
256
+ }
257
+
258
+ a:focus,
259
+ input:focus,
260
+ select:focus,
261
+ textarea:focus {
262
+ outline: revert-layer;
263
+ outline-offset: revert-layer;
264
+ }
265
+
266
+ code,
267
+ pre code {
268
+ font-family: revert-layer;
269
+ color: revert-layer;
270
+ background-color: revert-layer;
271
+ padding: revert-layer;
272
+ border-radius: revert-layer;
273
+ }
274
+
275
+ blockquote {
276
+ background: revert-layer;
277
+ border-color: revert-layer;
278
+ }
279
+
280
+ :is(.vscode-high-contrast, .vscode-high-contrast-light)
281
+ :is(
282
+ [data-radix-popper-content-wrapper] > *,
283
+ [role="dialog"],
284
+ [role="menu"],
285
+ [role="listbox"],
286
+ [role="tooltip"]
287
+ ) {
288
+ outline: 1px solid var(--vscode-contrastBorder, CanvasText);
289
+ outline-offset: -1px;
290
+ }
291
+
292
+ :is(.vscode-high-contrast, .vscode-high-contrast-light)
293
+ :is(
294
+ .aui-user-message-content,
295
+ [data-slot="composer-quote"],
296
+ [data-slot="chat-panel-user-message"],
297
+ [data-slot="chat-panel-composer"]
298
+ ) {
299
+ outline: 1px solid var(--vscode-contrastBorder, CanvasText);
300
+ outline-offset: -1px;
301
+ }
302
+
303
+ :is(.vscode-high-contrast, .vscode-high-contrast-light)
304
+ [data-slot="aui_thread-list-item"]:is([data-active], :hover) {
305
+ outline: 1px dashed
306
+ var(--vscode-contrastActiveBorder, var(--vscode-focusBorder, Highlight));
307
+ outline-offset: -1px;
308
+ }
309
+
310
+ :is(.vscode-high-contrast, .vscode-high-contrast-light)
311
+ [data-slot="aui_thread-list-item"][data-active] {
312
+ outline-style: solid;
313
+ }
314
+
315
+ :is(.vscode-high-contrast, .vscode-high-contrast-light)
316
+ [data-slot="thread-list"]
317
+ [aria-current="true"] {
318
+ outline: 1px solid
319
+ var(--vscode-contrastActiveBorder, var(--vscode-focusBorder, Highlight));
320
+ outline-offset: -1px;
321
+ }
322
+
323
+ :is(.vscode-high-contrast, .vscode-high-contrast-light) :focus-visible {
324
+ outline: 1px solid
325
+ var(--vscode-contrastActiveBorder, var(--vscode-focusBorder, Highlight));
326
+ outline-offset: -1px;
327
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Returns the nonce `renderWebviewHtml` wrote to `<meta property="csp-nonce">`,
3
+ * for libraries that inject `<style>` or `<script>` tags at runtime.
4
+ */
5
+ export function getCspNonce(): string | undefined {
6
+ const meta = globalThis.document?.querySelector<HTMLMetaElement>(
7
+ 'meta[property="csp-nonce"]',
8
+ );
9
+ return meta?.nonce || meta?.getAttribute("nonce") || undefined;
10
+ }
@@ -0,0 +1,210 @@
1
+ import {
2
+ isHostToWebviewMessage,
3
+ VSCODE_BRIDGE_CHANNEL,
4
+ VSCODE_VIRTUAL_ORIGIN,
5
+ type HostToWebviewMessage,
6
+ type WebviewToHostMessage,
7
+ } from "../protocol";
8
+ import { isForeignHttpUrl } from "../route-url";
9
+ import { getVSCodeApi } from "./vscode-api";
10
+ import { randomIdPrefix } from "./id";
11
+
12
+ export type VSCodeBridgePort = {
13
+ postMessage(message: WebviewToHostMessage): void;
14
+ onMessage(listener: (message: unknown) => void): () => void;
15
+ };
16
+
17
+ export type VSCodeFetch = (
18
+ input: RequestInfo | URL,
19
+ init?: RequestInit,
20
+ ) => Promise<Response>;
21
+
22
+ const NULL_BODY_STATUSES = new Set([101, 204, 205, 304]);
23
+
24
+ const idPrefix = randomIdPrefix();
25
+ let nextId = 0;
26
+
27
+ export const webviewPort: VSCodeBridgePort = {
28
+ postMessage: (message) => getVSCodeApi().postMessage(message),
29
+ onMessage: (listener) => {
30
+ const handler = (event: MessageEvent) => listener(event.data);
31
+ globalThis.addEventListener("message", handler);
32
+ return () => globalThis.removeEventListener("message", handler);
33
+ },
34
+ };
35
+
36
+ const abortReason = (signal: AbortSignal): unknown =>
37
+ signal.reason ?? new DOMException("This operation was aborted", "AbortError");
38
+
39
+ const readBody = async (
40
+ body: ReadableStream<Uint8Array>,
41
+ signal: AbortSignal,
42
+ ) => {
43
+ const reader = body.getReader();
44
+ const onAbort = () => {
45
+ reader.cancel(signal.reason).catch(() => undefined);
46
+ };
47
+ signal.addEventListener("abort", onAbort, { once: true });
48
+ try {
49
+ const parts: Uint8Array[] = [];
50
+ let size = 0;
51
+ while (true) {
52
+ const { done, value } = await reader.read();
53
+ if (signal.aborted) throw abortReason(signal);
54
+ if (done) break;
55
+ parts.push(value);
56
+ size += value.byteLength;
57
+ }
58
+ const out = new Uint8Array(size);
59
+ let offset = 0;
60
+ for (const part of parts) {
61
+ out.set(part, offset);
62
+ offset += part.byteLength;
63
+ }
64
+ return out;
65
+ } finally {
66
+ signal.removeEventListener("abort", onAbort);
67
+ reader.releaseLock();
68
+ }
69
+ };
70
+
71
+ const toRequest = (input: RequestInfo | URL, init?: RequestInit) => {
72
+ const request =
73
+ input instanceof Request
74
+ ? new Request(input, init)
75
+ : new Request(new URL(String(input), VSCODE_VIRTUAL_ORIGIN), init);
76
+ if (isForeignHttpUrl(new URL(request.url))) {
77
+ throw new TypeError("vscodeFetch only reaches the extension host's routes");
78
+ }
79
+ return request;
80
+ };
81
+
82
+ /**
83
+ * Creates a `fetch` that sends each request to `serveWebviewRoutes` in the
84
+ * extension host and streams the response back over `port`.
85
+ */
86
+ export function createVSCodeFetch(
87
+ port: VSCodeBridgePort = webviewPort,
88
+ ): VSCodeFetch {
89
+ return async (input, init) => {
90
+ const request = toRequest(input, init);
91
+ const { signal } = request;
92
+ if (signal.aborted) throw abortReason(signal);
93
+
94
+ const body = request.body ? await readBody(request.body, signal) : null;
95
+ if (signal.aborted) throw abortReason(signal);
96
+
97
+ const id = `${idPrefix}-${++nextId}`;
98
+
99
+ return new Promise<Response>((resolve, reject) => {
100
+ let controller: ReadableStreamDefaultController<Uint8Array> | undefined;
101
+ let responded = false;
102
+ let finished = false;
103
+
104
+ const finish = () => {
105
+ finished = true;
106
+ unsubscribe();
107
+ signal.removeEventListener("abort", onAbort);
108
+ };
109
+
110
+ const fail = (error: unknown) => {
111
+ if (finished) return;
112
+ finish();
113
+ if (controller) controller.error(error);
114
+ else reject(error);
115
+ };
116
+
117
+ const sendAbort = () => {
118
+ try {
119
+ port.postMessage({
120
+ channel: VSCODE_BRIDGE_CHANNEL,
121
+ kind: "fetch:abort",
122
+ id,
123
+ });
124
+ } catch {
125
+ return;
126
+ }
127
+ };
128
+
129
+ const onAbort = () => {
130
+ if (finished) return;
131
+ fail(abortReason(signal));
132
+ sendAbort();
133
+ };
134
+
135
+ const onHead = (
136
+ message: Extract<HostToWebviewMessage, { kind: "fetch:head" }>,
137
+ ) => {
138
+ const stream =
139
+ request.method === "HEAD" || NULL_BODY_STATUSES.has(message.status)
140
+ ? null
141
+ : new ReadableStream<Uint8Array>({
142
+ start: (c) => {
143
+ controller = c;
144
+ },
145
+ cancel: () => {
146
+ if (finished) return;
147
+ finish();
148
+ sendAbort();
149
+ },
150
+ });
151
+ let response: Response;
152
+ try {
153
+ response = new Response(stream, {
154
+ status: message.status,
155
+ statusText: message.statusText,
156
+ headers: message.headers,
157
+ });
158
+ } catch (error) {
159
+ finish();
160
+ reject(new TypeError("Failed to fetch", { cause: error }));
161
+ sendAbort();
162
+ return;
163
+ }
164
+ responded = true;
165
+ resolve(response);
166
+ };
167
+
168
+ const unsubscribe = port.onMessage((message) => {
169
+ if (finished) return;
170
+ if (!isHostToWebviewMessage(message) || message.id !== id) return;
171
+ switch (message.kind) {
172
+ case "fetch:head":
173
+ onHead(message);
174
+ break;
175
+ case "fetch:chunk":
176
+ controller?.enqueue(message.chunk);
177
+ break;
178
+ case "fetch:end":
179
+ finish();
180
+ if (controller) controller.close();
181
+ else if (!responded) {
182
+ reject(new TypeError("Response ended before its head"));
183
+ }
184
+ break;
185
+ case "fetch:error":
186
+ fail(new TypeError(message.message));
187
+ break;
188
+ }
189
+ });
190
+
191
+ signal.addEventListener("abort", onAbort, { once: true });
192
+
193
+ try {
194
+ port.postMessage({
195
+ channel: VSCODE_BRIDGE_CHANNEL,
196
+ kind: "fetch:request",
197
+ id,
198
+ url: request.url,
199
+ method: request.method,
200
+ headers: [...request.headers],
201
+ body,
202
+ });
203
+ } catch (error) {
204
+ fail(error);
205
+ }
206
+ });
207
+ };
208
+ }
209
+
210
+ export const vscodeFetch: VSCodeFetch = createVSCodeFetch();
@@ -0,0 +1,4 @@
1
+ export const randomIdPrefix = () =>
2
+ Array.from(globalThis.crypto.getRandomValues(new Uint8Array(8)), (byte) =>
3
+ byte.toString(16).padStart(2, "0"),
4
+ ).join("");
@@ -0,0 +1,56 @@
1
+ import { DEFAULT_EXTERNAL_SCHEMES, parseExternalUrl } from "../external-url";
2
+ import { webviewPort, type VSCodeBridgePort } from "./fetch";
3
+ import { callHost } from "./rpc";
4
+
5
+ export type LinkInterceptorOptions = {
6
+ /** URL schemes to open outside the webview, such as `"https:"`. */
7
+ schemes?: readonly string[];
8
+ onError?: (error: unknown) => void;
9
+ port?: VSCodeBridgePort;
10
+ };
11
+
12
+ /**
13
+ * Opens absolute links and `window.open` URLs through the extension host's
14
+ * `openExternal`, since a webview cannot navigate away or open windows.
15
+ * Returns a function that removes the listeners and restores `window.open`.
16
+ */
17
+ export function installLinkInterceptor({
18
+ schemes = DEFAULT_EXTERNAL_SCHEMES,
19
+ onError = (error) => console.error(error),
20
+ port = webviewPort,
21
+ }: LinkInterceptorOptions = {}): () => void {
22
+ const openExternal = (url: string) => {
23
+ callHost(port, "openExternal", [url]).catch(onError);
24
+ };
25
+
26
+ const onClick = (event: MouseEvent) => {
27
+ if (event.defaultPrevented) return;
28
+ if (event.button !== (event.type === "auxclick" ? 1 : 0)) return;
29
+ if (!(event.target instanceof Element)) return;
30
+ const anchor = event.target.closest("a[href]");
31
+ const url = parseExternalUrl(anchor?.getAttribute("href"), schemes);
32
+ if (!url) return;
33
+ event.preventDefault();
34
+ openExternal(url);
35
+ };
36
+
37
+ let installed = true;
38
+ const originalOpen = window.open;
39
+ const patchedOpen: typeof window.open = function (url, ...rest) {
40
+ const external = installed ? parseExternalUrl(url, schemes) : null;
41
+ if (!external) return originalOpen.call(window, url, ...rest);
42
+ openExternal(external);
43
+ return null;
44
+ };
45
+
46
+ document.addEventListener("click", onClick);
47
+ document.addEventListener("auxclick", onClick);
48
+ window.open = patchedOpen;
49
+
50
+ return () => {
51
+ installed = false;
52
+ document.removeEventListener("click", onClick);
53
+ document.removeEventListener("auxclick", onClick);
54
+ if (window.open === patchedOpen) window.open = originalOpen;
55
+ };
56
+ }
@@ -0,0 +1,35 @@
1
+ import { isHostToWebviewMessage, VSCODE_BRIDGE_CHANNEL } from "../protocol";
2
+ import type { VSCodeBridgePort } from "./fetch";
3
+ import { randomIdPrefix } from "./id";
4
+
5
+ const idPrefix = `rpc-${randomIdPrefix()}`;
6
+ let nextId = 0;
7
+
8
+ export const callHost = (
9
+ port: VSCodeBridgePort,
10
+ method: string,
11
+ params: unknown[],
12
+ ): Promise<unknown> => {
13
+ const id = `${idPrefix}-${++nextId}`;
14
+ return new Promise((resolve, reject) => {
15
+ const unsubscribe = port.onMessage((message) => {
16
+ if (!isHostToWebviewMessage(message) || message.id !== id) return;
17
+ if (message.kind !== "rpc:response") return;
18
+ unsubscribe();
19
+ if (message.ok) resolve(message.result);
20
+ else reject(new Error(message.message));
21
+ });
22
+ try {
23
+ port.postMessage({
24
+ channel: VSCODE_BRIDGE_CHANNEL,
25
+ kind: "rpc:request",
26
+ id,
27
+ method,
28
+ params,
29
+ });
30
+ } catch (error) {
31
+ unsubscribe();
32
+ reject(error);
33
+ }
34
+ });
35
+ };
@@ -0,0 +1,24 @@
1
+ import type { AsyncStorageLike } from "@assistant-ui/core/react";
2
+ import { webviewPort, type VSCodeBridgePort } from "./fetch";
3
+ import { callHost } from "./rpc";
4
+
5
+ /**
6
+ * Creates an `AsyncStorageLike` backed by the `storage` Memento passed to
7
+ * `serveWebviewHost`, for `createLocalStorageAdapter({ storage })`.
8
+ */
9
+ export function createVSCodeStorage(
10
+ port: VSCodeBridgePort = webviewPort,
11
+ ): AsyncStorageLike {
12
+ return {
13
+ getItem: async (key) => {
14
+ const value = await callHost(port, "storage.getItem", [key]);
15
+ return typeof value === "string" ? value : null;
16
+ },
17
+ setItem: async (key, value) => {
18
+ await callHost(port, "storage.setItem", [key, value]);
19
+ },
20
+ removeItem: async (key) => {
21
+ await callHost(port, "storage.removeItem", [key]);
22
+ },
23
+ };
24
+ }
@@ -0,0 +1,31 @@
1
+ import { VSCODE_BRIDGE_CHANNEL } from "../protocol";
2
+
3
+ export type VSCodeApi<TState = unknown> = {
4
+ postMessage(message: unknown): void;
5
+ getState(): TState | undefined;
6
+ setState<T extends TState | undefined>(newState: T): T;
7
+ };
8
+
9
+ let api: VSCodeApi | undefined;
10
+
11
+ /**
12
+ * Returns the webview's VS Code API. `acquireVsCodeApi()` may only be called
13
+ * once per webview, so every caller in the webview must go through this.
14
+ */
15
+ export function getVSCodeApi<TState = unknown>(): VSCodeApi<TState> {
16
+ if (api) return api as VSCodeApi<TState>;
17
+ const acquire = (globalThis as { acquireVsCodeApi?: () => VSCodeApi })
18
+ .acquireVsCodeApi;
19
+ if (typeof acquire !== "function") {
20
+ throw new Error(
21
+ "acquireVsCodeApi is not defined: getVSCodeApi() must run inside a VS Code webview.",
22
+ );
23
+ }
24
+ api = acquire();
25
+ api.postMessage({
26
+ channel: VSCODE_BRIDGE_CHANNEL,
27
+ kind: "fetch:reset",
28
+ id: "",
29
+ });
30
+ return api as VSCodeApi<TState>;
31
+ }