@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
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,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
|
+
}
|