gpui-bridge 0.0.0-stage → 0.2.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/dist/app.js ADDED
@@ -0,0 +1,587 @@
1
+ // Runtime-agnostic client core: the App/UINode API and the handshake. It only
2
+ // needs a transport to the host process, so any JavaScript runtime can use it
3
+ // (Node: index.ts, txiki.js: txiki.ts).
4
+ import { PROTOCOL_VERSION, } from "./protocol.js";
5
+ import { Tree } from "./tree.js";
6
+ const HANDSHAKE_TIMEOUT_MS = 10_000;
7
+ export class HostExitedError extends Error {
8
+ exit;
9
+ constructor(message, exit) {
10
+ super(message);
11
+ this.exit = exit;
12
+ this.name = "HostExitedError";
13
+ }
14
+ }
15
+ function describeExit(exit, hostPath) {
16
+ if (exit.spawnError) {
17
+ const code = exit.spawnError.code;
18
+ return code === "ENOENT"
19
+ ? `gpui-bridge host not found at ${hostPath}. Build it with \`cargo build\` in host/ or pass hostPath.`
20
+ : `gpui-bridge host failed to start: ${exit.spawnError.message}`;
21
+ }
22
+ const how = exit.signal ? `signal ${exit.signal}` : `code ${exit.code}`;
23
+ const tail = exit.stderrTail.length ? `\nLast host output:\n ${exit.stderrTail.join("\n ")}` : "";
24
+ return `gpui-bridge host exited (${how}).${tail}`;
25
+ }
26
+ export class UINode {
27
+ app;
28
+ id;
29
+ type;
30
+ /** @internal */
31
+ constructor(app, id, type) {
32
+ this.app = app;
33
+ this.id = id;
34
+ this.type = type;
35
+ }
36
+ append(...children) {
37
+ for (const child of children) {
38
+ this.app.wire.queue({ op: "append", parent: this.id, child: child.id });
39
+ this.app.wire.attach(this.id, child.id);
40
+ }
41
+ return this;
42
+ }
43
+ insertBefore(child, before) {
44
+ this.app.wire.queue({ op: "insertBefore", parent: this.id, child: child.id, before: before.id });
45
+ this.app.wire.attach(this.id, child.id);
46
+ return this;
47
+ }
48
+ /** Merges props; `null` removes one. */
49
+ set(props) {
50
+ this.app.wire.queue({ op: "setProps", id: this.id, props });
51
+ return this;
52
+ }
53
+ /** Subscribes to an event; the host only emits subscribed events. */
54
+ on(event, handler) {
55
+ this.app.wire.setHandler(this.id, event, handler);
56
+ return this;
57
+ }
58
+ off(event) {
59
+ this.app.wire.setHandler(this.id, event, undefined);
60
+ return this;
61
+ }
62
+ /** Gives the node the keyboard focus on the next commit (an Input, a Textarea or a div with `keyDown`). */
63
+ focus() {
64
+ this.app.wire.queue({ op: "focus", id: this.id });
65
+ return this;
66
+ }
67
+ /** Removes the node and all its descendants. */
68
+ remove() {
69
+ this.app.wire.remove(this.id);
70
+ }
71
+ }
72
+ /** A window other than the main one (`App.createWindow`). */
73
+ export class AppWindow {
74
+ app;
75
+ id;
76
+ closedListeners = new Set();
77
+ eventListeners = new Map();
78
+ /** The subscription list last sent, to avoid one message per listener. */
79
+ eventNames = "";
80
+ /** @internal */
81
+ constructor(app, id) {
82
+ this.app = app;
83
+ this.id = id;
84
+ }
85
+ /** Updates the window; like `App.window`, it opens on the first commit that sets `root`. */
86
+ set(options) {
87
+ this.app.queueWindow(this.id, options);
88
+ return this;
89
+ }
90
+ /** Closes the window on the next commit. No `closed` event follows: that is only for the user closing it. */
91
+ close() {
92
+ this.app.queueCloseWindow(this.id);
93
+ }
94
+ on(event, listener) {
95
+ if (event === "closed") {
96
+ this.closedListeners.add(listener);
97
+ return this;
98
+ }
99
+ const listeners = this.eventListeners.get(event) ?? new Set();
100
+ listeners.add(listener);
101
+ this.eventListeners.set(event, listeners);
102
+ this.syncEvents();
103
+ return this;
104
+ }
105
+ off(event, listener) {
106
+ if (event === "closed") {
107
+ this.closedListeners.delete(listener);
108
+ return this;
109
+ }
110
+ const listeners = this.eventListeners.get(event);
111
+ listeners?.delete(listener);
112
+ if (listeners?.size === 0)
113
+ this.eventListeners.delete(event);
114
+ this.syncEvents();
115
+ return this;
116
+ }
117
+ /** @internal */
118
+ handleClosed() {
119
+ for (const listener of this.closedListeners)
120
+ listener();
121
+ }
122
+ /** @internal */
123
+ handleWindowEvent(payload) {
124
+ for (const listener of this.eventListeners.get(payload.event) ?? [])
125
+ listener(payload);
126
+ }
127
+ /** Sends the subscription list only when the set of subscribed events changes. */
128
+ syncEvents() {
129
+ const events = [...this.eventListeners.keys()];
130
+ const names = events.join(",");
131
+ if (names === this.eventNames)
132
+ return;
133
+ this.eventNames = names;
134
+ this.app.queueWindow(this.id, { events });
135
+ }
136
+ }
137
+ export class App {
138
+ host;
139
+ hostPath;
140
+ /** @internal Protocol operations queued until `commit`. */
141
+ wire = new Tree();
142
+ listeners = {
143
+ error: new Set(),
144
+ windowClosed: new Set(),
145
+ windowEvent: new Set(),
146
+ shortcut: new Set(),
147
+ notification: new Set(),
148
+ tray: new Set(),
149
+ exit: new Set(),
150
+ };
151
+ exitInfo;
152
+ closing = false;
153
+ nextWindowId = 2;
154
+ windows = new Map();
155
+ /** Windows given a root and not closed: the host exits when the user closes the last one. */
156
+ openWindows = new Set();
157
+ nextRequest = 1;
158
+ /** System actions waiting for their `result`. */
159
+ requests = new Map();
160
+ /** A tray keeps the host running after the last window closes. */
161
+ hasTray = false;
162
+ exitWaiters = [];
163
+ /** @internal */
164
+ constructor(host, hostPath) {
165
+ this.host = host;
166
+ this.hostPath = hostPath;
167
+ }
168
+ div(props = {}) {
169
+ return this.create("div", props);
170
+ }
171
+ text(props = {}) {
172
+ return this.create("text", props);
173
+ }
174
+ button(props = {}) {
175
+ return this.create("Button", props);
176
+ }
177
+ input(props = {}) {
178
+ return this.create("Input", props);
179
+ }
180
+ checkbox(props = {}) {
181
+ return this.create("Checkbox", props);
182
+ }
183
+ icon(props = {}) {
184
+ return this.create("Icon", props);
185
+ }
186
+ image(props = {}) {
187
+ return this.create("Image", props);
188
+ }
189
+ switch(props = {}) {
190
+ return this.create("Switch", props);
191
+ }
192
+ radioGroup(props = {}) {
193
+ return this.create("RadioGroup", props);
194
+ }
195
+ select(props = {}) {
196
+ return this.create("Select", props);
197
+ }
198
+ slider(props = {}) {
199
+ return this.create("Slider", props);
200
+ }
201
+ textarea(props = {}) {
202
+ return this.create("Textarea", props);
203
+ }
204
+ /** A modal dialog shown while `open`; append its body as children. */
205
+ dialog(props = {}) {
206
+ return this.create("Dialog", props);
207
+ }
208
+ /** A list that builds only the rows in view; append one child per row (PROTOCOL.md section 15). */
209
+ virtualList(props = {}) {
210
+ return this.create("VirtualList", props);
211
+ }
212
+ /** A data table with text cells; it virtualizes its rows. */
213
+ table(props = {}) {
214
+ return this.create("Table", props);
215
+ }
216
+ /** A tab bar; show the chosen tab's content yourself. */
217
+ tabs(props = {}) {
218
+ return this.create("Tabs", props);
219
+ }
220
+ /** One tab of a `Tabs`; append the nodes of its chip (an `Icon`, a `text`, a `Tag`…; a last `Button` is drawn after them). */
221
+ tab(props = {}) {
222
+ return this.create("Tab", props);
223
+ }
224
+ /** A menu bar: one button per menu (PROTOCOL.md section 10). */
225
+ menuBar(props = {}) {
226
+ return this.create("MenuBar", props);
227
+ }
228
+ /** An inline message; `close` when it is `closable` (PROTOCOL.md section 17). */
229
+ alert(props = {}) {
230
+ return this.create("Alert", props);
231
+ }
232
+ /** A count, a dot or an icon over its children. */
233
+ badge(props = {}) {
234
+ return this.create("Badge", props);
235
+ }
236
+ tag(props = {}) {
237
+ return this.create("Tag", props);
238
+ }
239
+ /** A key or shortcut, such as `ctrl-s`. */
240
+ kbd(props = {}) {
241
+ return this.create("Kbd", props);
242
+ }
243
+ /** A progress bar: `value` from 0 to 100. */
244
+ progress(props = {}) {
245
+ return this.create("Progress", props);
246
+ }
247
+ /** A spinner; give it a size with `class` (`size-4`). */
248
+ spinner(props = {}) {
249
+ return this.create("Spinner", props);
250
+ }
251
+ /** A loading placeholder. */
252
+ skeleton(props = {}) {
253
+ return this.create("Skeleton", props);
254
+ }
255
+ /** Stars: `change` with the chosen count. */
256
+ rating(props = {}) {
257
+ return this.create("Rating", props);
258
+ }
259
+ /** Page navigation: `change` with the chosen page (1-based). */
260
+ pagination(props = {}) {
261
+ return this.create("Pagination", props);
262
+ }
263
+ /** Content shown while `open`; toggle it with anything that takes a click. */
264
+ collapsible(props = {}) {
265
+ return this.create("Collapsible", props);
266
+ }
267
+ /** Sections as `items`; `change` with the id of the one toggled. */
268
+ accordion(props = {}) {
269
+ return this.create("Accordion", props);
270
+ }
271
+ /** A user picture, or their initial. */
272
+ avatar(props = {}) {
273
+ return this.create("Avatar", props);
274
+ }
275
+ /** A step indicator: `change` with the chosen step's `value`. */
276
+ stepper(props = {}) {
277
+ return this.create("Stepper", props);
278
+ }
279
+ /** A text field with increment and decrement buttons. */
280
+ numberInput(props = {}) {
281
+ return this.create("NumberInput", props);
282
+ }
283
+ /** A one-time code, one cell per digit. */
284
+ otpInput(props = {}) {
285
+ return this.create("OtpInput", props);
286
+ }
287
+ /** A form; append `Field`s to it. */
288
+ form(props = {}) {
289
+ return this.create("Form", props);
290
+ }
291
+ /** A label, an optional description and its children as the control. */
292
+ field(props = {}) {
293
+ return this.create("Field", props);
294
+ }
295
+ /** A titled box; append its content. */
296
+ groupBox(props = {}) {
297
+ return this.create("GroupBox", props);
298
+ }
299
+ /** Key-value rows. */
300
+ descriptionList(props = {}) {
301
+ return this.create("DescriptionList", props);
302
+ }
303
+ /** A status bar with text on the left and on the right. */
304
+ statusBar(props = {}) {
305
+ return this.create("StatusBar", props);
306
+ }
307
+ /** A toolbar; append its content. */
308
+ toolbar(props = {}) {
309
+ return this.create("Toolbar", props);
310
+ }
311
+ /** Panels the user can resize; append one child per panel. */
312
+ resizable(props = {}) {
313
+ return this.create("Resizable", props);
314
+ }
315
+ /** A month view: `change` with the chosen day (`YYYY-MM-DD`). */
316
+ calendar(props = {}) {
317
+ return this.create("Calendar", props);
318
+ }
319
+ /** Items in props; `change` with the chosen item's id, `expand`/`collapse` with a folder's id. */
320
+ tree(props = {}) {
321
+ return this.create("Tree", props);
322
+ }
323
+ /** A parsed patch; `change` with the selected lines. */
324
+ diff(props = {}) {
325
+ return this.create("Diff", props);
326
+ }
327
+ /** Slides as children; `change` with the selected index. */
328
+ carousel(props = {}) {
329
+ return this.create("Carousel", props);
330
+ }
331
+ /** Sections of entries in props; `change` with the clicked entry's id. */
332
+ sidebar(props = {}) {
333
+ return this.create("Sidebar", props);
334
+ }
335
+ /** A line, bar or pie chart of the points in `data`. */
336
+ chart(props = {}) {
337
+ return this.create("Chart", props);
338
+ }
339
+ /** A node of any type; the specific methods below only add typing. */
340
+ create(type, props = {}) {
341
+ return new UINode(this, this.wire.create(type, { ...props }), type);
342
+ }
343
+ /** Creates or updates the main window (window 1). It opens on the first commit that sets `root`. */
344
+ window(options) {
345
+ this.queueWindow(undefined, options);
346
+ return this;
347
+ }
348
+ /** Another window, with its own root; it opens on the first commit that sets `root`. */
349
+ createWindow(options = {}) {
350
+ const window = new AppWindow(this, this.nextWindowId++);
351
+ this.windows.set(window.id, window);
352
+ return window.set(options);
353
+ }
354
+ /** @internal */
355
+ queueWindow(id, options) {
356
+ const { root, ...rest } = options;
357
+ this.wire.queue({ op: "window", ...(id !== undefined && { id }), ...rest, ...(root && { root: root.id }) });
358
+ if (root)
359
+ this.openWindows.add(id ?? 1);
360
+ }
361
+ /** @internal */
362
+ queueCloseWindow(id) {
363
+ this.wire.queue({ op: "closeWindow", id });
364
+ this.openWindows.delete(id);
365
+ }
366
+ /** Native "open" dialog; resolves with the chosen paths, or `null` if cancelled. */
367
+ async openDialog(options = {}) {
368
+ return (await this.request({ op: "openDialog", request: 0, ...options })).paths ?? null;
369
+ }
370
+ /** Native "save" dialog; resolves with the chosen path, or `null` if cancelled. */
371
+ async saveDialog(options = {}) {
372
+ return (await this.request({ op: "saveDialog", request: 0, ...options })).path ?? null;
373
+ }
374
+ /** The clipboard's text, or `null` if it holds none. */
375
+ async readClipboard() {
376
+ return (await this.request({ op: "readClipboard", request: 0 })).text ?? null;
377
+ }
378
+ /** Shows a toast now (not on commit). */
379
+ notify(options) {
380
+ this.action({ op: "notify", ...options });
381
+ }
382
+ writeClipboard(text) {
383
+ this.action({ op: "writeClipboard", text });
384
+ }
385
+ /** Opens a URL with the system's handler (the browser for http). */
386
+ openUrl(url) {
387
+ this.action({ op: "openUrl", url });
388
+ }
389
+ /** Opens a file or folder with its default app. */
390
+ openPath(path) {
391
+ this.action({ op: "openPath", path });
392
+ }
393
+ /** Actions run when the host gets them, not on commit. */
394
+ action(message) {
395
+ if (this.exitInfo)
396
+ throw new HostExitedError(describeExit(this.exitInfo, this.hostPath), this.exitInfo);
397
+ this.host.send(message);
398
+ }
399
+ request(message) {
400
+ const request = this.nextRequest++;
401
+ return new Promise((resolve, reject) => {
402
+ this.action({ ...message, request });
403
+ this.requests.set(request, { resolve, reject });
404
+ });
405
+ }
406
+ /** App-wide keyboard shortcuts (`ctrl-s`, `f5`…), applied on the next commit; replaces the list. */
407
+ shortcuts(keys) {
408
+ this.wire.queue({ op: "shortcuts", keys });
409
+ return this;
410
+ }
411
+ /** Shows or updates the system tray icon on the next commit. */
412
+ tray(options) {
413
+ this.wire.queue({ op: "tray", ...options });
414
+ this.hasTray = true;
415
+ return this;
416
+ }
417
+ /** Removes the tray icon on the next commit; with no window open, the host then exits. */
418
+ removeTray() {
419
+ this.wire.queue({ op: "removeTray" });
420
+ this.hasTray = false;
421
+ if (this.openWindows.size === 0)
422
+ this.closing = true;
423
+ return this;
424
+ }
425
+ /** Sends every pending operation; the host renders once per commit. */
426
+ commit() {
427
+ if (this.exitInfo)
428
+ throw new HostExitedError(describeExit(this.exitInfo, this.hostPath), this.exitInfo);
429
+ this.wire.commit((message) => this.host.send(message));
430
+ }
431
+ on(event, listener) {
432
+ this.listeners[event].add(listener);
433
+ return this;
434
+ }
435
+ off(event, listener) {
436
+ this.listeners[event].delete(listener);
437
+ return this;
438
+ }
439
+ /** Closes the connection; resolves when the host has exited. */
440
+ close() {
441
+ this.closing = true;
442
+ this.host.close();
443
+ return this.exited();
444
+ }
445
+ /** Resolves when the host process has exited, for any reason. */
446
+ exited() {
447
+ if (this.exitInfo)
448
+ return Promise.resolve(this.exitInfo);
449
+ return new Promise((resolve) => this.exitWaiters.push(resolve));
450
+ }
451
+ /** @internal */
452
+ handleMessage(message) {
453
+ switch (message.type) {
454
+ case "event": {
455
+ const handler = this.wire.handler(message.id, message.event);
456
+ if (!handler)
457
+ return;
458
+ const type = this.wire.type(message.id);
459
+ const payload = message.event === "menu"
460
+ ? { item: message.value ?? "" }
461
+ : message.event === "keyDown"
462
+ ? { key: message.value ?? "" }
463
+ : message.event === "drop"
464
+ ? { paths: message.paths ?? [] }
465
+ : message.event === "sort"
466
+ ? message.value
467
+ : type === "Checkbox" || type === "Switch"
468
+ ? { checked: message.checked ?? false }
469
+ : message.value !== undefined
470
+ ? { value: message.value }
471
+ : type === "Input" || type === "Textarea"
472
+ ? { value: "" }
473
+ : {};
474
+ handler(payload);
475
+ return;
476
+ }
477
+ case "error": {
478
+ const error = { code: message.code, message: message.message, op: message.op };
479
+ // A rejected system action: its promise fails instead of waiting for a result.
480
+ const pending = message.request !== undefined ? this.requests.get(message.request) : undefined;
481
+ if (pending) {
482
+ this.requests.delete(message.request);
483
+ pending.reject(Object.assign(new Error(`gpui-bridge: host rejected '${error.op}': ${error.message}`), { host: error }));
484
+ return;
485
+ }
486
+ if (!this.emit("error", error))
487
+ console.warn(`gpui-bridge: host rejected '${error.op}': ${error.message}`);
488
+ return;
489
+ }
490
+ case "windowClosed":
491
+ this.openWindows.delete(message.id);
492
+ // The host exits after the last window, unless there is a tray.
493
+ if (this.openWindows.size === 0 && !this.hasTray)
494
+ this.closing = true;
495
+ this.windows.get(message.id)?.handleClosed();
496
+ this.emit("windowClosed", { id: message.id });
497
+ return;
498
+ case "windowEvent": {
499
+ const payload = {
500
+ id: message.id,
501
+ event: message.event,
502
+ x: message.x,
503
+ y: message.y,
504
+ width: message.width,
505
+ height: message.height,
506
+ visible: message.visible,
507
+ };
508
+ this.windows.get(message.id)?.handleWindowEvent(payload);
509
+ this.emit("windowEvent", payload);
510
+ return;
511
+ }
512
+ case "result":
513
+ this.requests.get(message.request)?.resolve(message);
514
+ this.requests.delete(message.request);
515
+ return;
516
+ case "notification":
517
+ this.emit("notification", { id: message.id, window: message.window });
518
+ return;
519
+ case "shortcut":
520
+ this.emit("shortcut", { keys: message.keys, window: message.window });
521
+ return;
522
+ case "tray":
523
+ this.emit("tray", message.event === "menu" ? { event: "menu", item: message.value ?? "" } : { event: "click" });
524
+ return;
525
+ case "ready":
526
+ return;
527
+ }
528
+ }
529
+ /** @internal */
530
+ handleExit(exit) {
531
+ this.exitInfo = exit;
532
+ // An exit after close() or windowClosed is expected; anything else is a crash.
533
+ const unexpected = !this.closing;
534
+ if (!this.emit("exit", exit) && unexpected)
535
+ console.error(describeExit(exit, this.hostPath));
536
+ for (const resolve of this.exitWaiters.splice(0))
537
+ resolve(exit);
538
+ for (const { reject } of this.requests.values())
539
+ reject(new HostExitedError(describeExit(exit, this.hostPath), exit));
540
+ this.requests.clear();
541
+ }
542
+ emit(event, payload) {
543
+ for (const listener of this.listeners[event])
544
+ listener(payload);
545
+ return this.listeners[event].size > 0;
546
+ }
547
+ }
548
+ /**
549
+ * Starts the host through `start` and completes the handshake. Rejects with a
550
+ * clear message if the host cannot start.
551
+ */
552
+ export function connect(hostPath, start) {
553
+ return new Promise((resolve, reject) => {
554
+ let app;
555
+ let ready = false;
556
+ const timer = setTimeout(() => {
557
+ host.kill();
558
+ reject(new Error(`gpui-bridge host did not answer the handshake within ${HANDSHAKE_TIMEOUT_MS} ms`));
559
+ }, HANDSHAKE_TIMEOUT_MS);
560
+ const host = start({
561
+ onMessage(message) {
562
+ if (ready)
563
+ return app.handleMessage(message);
564
+ if (message.type === "ready") {
565
+ ready = true;
566
+ clearTimeout(timer);
567
+ resolve(app);
568
+ }
569
+ else if (message.type === "error") {
570
+ clearTimeout(timer);
571
+ reject(new Error(`gpui-bridge handshake failed (${message.code}): ${message.message}`));
572
+ }
573
+ },
574
+ onCorruptLine(line) {
575
+ console.error(`gpui-bridge: host wrote a non-protocol line to stdout: ${line}`);
576
+ },
577
+ onExit(exit) {
578
+ if (ready)
579
+ return app.handleExit(exit);
580
+ clearTimeout(timer);
581
+ reject(new HostExitedError(describeExit(exit, hostPath), exit));
582
+ },
583
+ });
584
+ app = new App(host, hostPath);
585
+ host.send({ op: "hello", protocol: PROTOCOL_VERSION });
586
+ });
587
+ }
@@ -0,0 +1,18 @@
1
+ import type { HostEvents, HostTransport } from "./app.js";
2
+ import type { ClientMessage } from "./protocol.js";
3
+ export interface HostProcessOptions extends HostEvents {
4
+ path: string;
5
+ args: string[];
6
+ debug: boolean;
7
+ }
8
+ export declare class HostProcess implements HostTransport {
9
+ private readonly child;
10
+ private readonly stderrTail;
11
+ private exited;
12
+ constructor(options: HostProcessOptions);
13
+ get running(): boolean;
14
+ send(message: ClientMessage): void;
15
+ /** Closes stdin; the host exits on EOF. */
16
+ close(): void;
17
+ kill(): void;
18
+ }
@@ -0,0 +1,56 @@
1
+ // Spawns the host binary and speaks JSON Lines over its stdin/stdout.
2
+ import { spawn } from "node:child_process";
3
+ import { createInterface } from "node:readline";
4
+ const STDERR_TAIL_LINES = 20;
5
+ export class HostProcess {
6
+ child;
7
+ stderrTail = [];
8
+ exited = false;
9
+ constructor(options) {
10
+ this.child = spawn(options.path, options.args, { stdio: ["pipe", "pipe", "pipe"] });
11
+ createInterface({ input: this.child.stdout }).on("line", (line) => {
12
+ let message;
13
+ try {
14
+ message = JSON.parse(line);
15
+ }
16
+ catch {
17
+ options.onCorruptLine(line);
18
+ return;
19
+ }
20
+ options.onMessage(message);
21
+ });
22
+ createInterface({ input: this.child.stderr }).on("line", (line) => {
23
+ this.stderrTail.push(line);
24
+ if (this.stderrTail.length > STDERR_TAIL_LINES)
25
+ this.stderrTail.shift();
26
+ if (options.debug)
27
+ console.error(`[host] ${line}`);
28
+ });
29
+ // Writes racing a dying host fail with EPIPE; the exit handler reports the real cause.
30
+ this.child.stdin.on("error", () => { });
31
+ const finish = (exit) => {
32
+ if (this.exited)
33
+ return;
34
+ this.exited = true;
35
+ options.onExit({ ...exit, stderrTail: [...this.stderrTail] });
36
+ };
37
+ this.child.on("error", (spawnError) => finish({ code: null, signal: null, spawnError }));
38
+ // "close" (not "exit") so all stdout/stderr lines are delivered first.
39
+ this.child.on("close", (code, signal) => finish({ code, signal }));
40
+ }
41
+ get running() {
42
+ return !this.exited;
43
+ }
44
+ send(message) {
45
+ if (this.exited)
46
+ return;
47
+ this.child.stdin.write(JSON.stringify(message) + "\n");
48
+ }
49
+ /** Closes stdin; the host exits on EOF. */
50
+ close() {
51
+ this.child.stdin.end();
52
+ }
53
+ kill() {
54
+ this.child.kill();
55
+ }
56
+ }
@@ -0,0 +1,15 @@
1
+ import { type App } from "./app.js";
2
+ export * from "./app.js";
3
+ export * from "./protocol.js";
4
+ export interface AppOptions {
5
+ /** Host binary. Defaults to `$GPUI_BRIDGE_HOST`, then the platform package installed with this one, then the debug build in this repository. */
6
+ hostPath?: string;
7
+ /** Forward host logs (stderr) to the console. */
8
+ debug?: boolean;
9
+ /** Run the host without a window (`--no-window`), e.g. for tests. */
10
+ headless?: boolean;
11
+ }
12
+ /** The host to run: `$GPUI_BRIDGE_HOST`, then the packaged one, then a build in this repository. */
13
+ export declare function defaultHostPath(): string;
14
+ /** Starts the host and completes the handshake. Rejects with a clear message if the host cannot start. */
15
+ export declare function createApp(options?: AppOptions): Promise<App>;