dsh-builtin-browser 0.1.8 → 0.1.10

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.
@@ -94,7 +94,8 @@ export declare class BrowserRuntime extends Service {
94
94
  flushAuth(session: BrowserSessionId): Promise<readonly ExportedCookie[]>;
95
95
  /** Import cookies into the session through the provider. */
96
96
  restoreAuth(session: BrowserSessionId, cookies: readonly ExportedCookie[]): Promise<number>;
97
- /** Close the session through the selected provider. Idempotent. */
97
+ /** Close the session through the selected provider. Idempotent; a missing
98
+ * provider is treated as already-closed so teardown paths stay no-ops. */
98
99
  close(session: BrowserSessionId): Promise<void>;
99
100
  }
100
101
  export default BrowserRuntime;
@@ -158,9 +158,22 @@ export class BrowserRuntime extends Service {
158
158
  async restoreAuth(session, cookies) {
159
159
  return this.resolveProvider().restoreAuth(session, cookies);
160
160
  }
161
- /** Close the session through the selected provider. Idempotent. */
161
+ /** Close the session through the selected provider. Idempotent; a missing
162
+ * provider is treated as already-closed so teardown paths stay no-ops. */
162
163
  async close(session) {
163
- return this.resolveProvider().close(session);
164
+ try {
165
+ await this.resolveProvider().close(session);
166
+ }
167
+ catch (error) {
168
+ const code = error instanceof BrowserError ? error.code : undefined;
169
+ if (code === 'BROWSER_PROVIDER_UNAVAILABLE'
170
+ || code === 'BROWSER_PROVIDER_CONFIGURED_MISSING'
171
+ || code === 'BROWSER_PROVIDER_CONFIGURED_UNAVAILABLE'
172
+ || code === 'BROWSER_PROVIDER_AMBIGUOUS') {
173
+ return; // provider gone; nothing to close
174
+ }
175
+ throw error;
176
+ }
164
177
  }
165
178
  }
166
179
  export default BrowserRuntime;
@@ -294,7 +294,8 @@ export interface BrowserProvider {
294
294
  export interface BrowserHistoryEntry {
295
295
  /** Monotonic sequence number within the session. */
296
296
  readonly seq: number;
297
- /** The operation name: navigate | execute | click | type | replay. */
297
+ /** The operation name. Replayable: navigate | execute | click | type.
298
+ * Also recorded (non-replayable): fill | download | flushAuth | restoreAuth. */
298
299
  readonly action: string;
299
300
  /** The operation's arguments. */
300
301
  readonly params: Record<string, unknown>;
@@ -28,10 +28,15 @@ export function apply(ctx, config) {
28
28
  // External host (desktop shell) wins; otherwise self-host. The self-hosted
29
29
  // child is disposed with the fiber, mirroring the shell's lifetime.
30
30
  const host = config.viewHost ?? new RemoteElectronViewHost(defaultHostMainPath());
31
+ // Own the disposer on THIS plugin's fiber: registerBrowserProvider's effect
32
+ // is bound to the seam's own fiber (the browser row), so a reload of this
33
+ // row would otherwise collide with the still-registered provider
34
+ // (BROWSER_DUPLICATE_PROVIDER) or leave a stale provider behind.
35
+ const unregister = ctx.browser.registerBrowserProvider(new ElectronBrowserProvider(host, { httpOnly: config.httpOnly }));
31
36
  ctx.effect(() => () => {
37
+ unregister();
32
38
  if (config.viewHost === undefined && host instanceof RemoteElectronViewHost) {
33
39
  host.dispose();
34
40
  }
35
41
  });
36
- ctx.browser.registerBrowserProvider(new ElectronBrowserProvider(host, { httpOnly: config.httpOnly }));
37
42
  }
@@ -18,6 +18,8 @@ import { createInterface } from 'node:readline';
18
18
  import { createConnection } from 'node:net';
19
19
  /** CDP protocol version attached to every view's debugger. */
20
20
  const CDP_VERSION = '1.3';
21
+ /** Download cap: the body is shipped base64 as one JSON line; bound the memory. */
22
+ const MAX_DOWNLOAD_BYTES = 256 * 1024 * 1024;
21
23
  /** Views by the id the parent assigned at createView time. */
22
24
  const views = new Map();
23
25
  /** The single browser window; created lazily on first createView. */
@@ -48,12 +50,17 @@ async function handle(op, msg) {
48
50
  window.on('closed', () => { window = undefined; });
49
51
  }
50
52
  const view = new WebContentsView();
53
+ // Attach the debugger BEFORE the view can be seen: an attach failure
54
+ // then leaves nothing in the window (no visible ghost view).
55
+ view.webContents.debugger.attach(CDP_VERSION);
56
+ // New views start hidden: with several sessions/tabs only the shown
57
+ // one may be visible (they stack in contentView child order).
51
58
  view.setVisible(false);
52
59
  window.contentView.addChildView(view);
53
60
  const [width, height] = window.getContentSize();
54
61
  view.setBounds({ x: 0, y: 0, width: width ?? 0, height: height ?? 0 });
55
- view.setVisible(true);
56
- view.webContents.debugger.attach(CDP_VERSION);
62
+ if (views.size === 0)
63
+ view.setVisible(true);
57
64
  views.set(viewId, { webContentsView: view });
58
65
  reply(msg.id, { ok: true });
59
66
  return;
@@ -80,8 +87,23 @@ async function handle(op, msg) {
80
87
  if (viewId === undefined)
81
88
  throw new Error('showView missing viewId');
82
89
  const entry = views.get(viewId);
83
- if (entry !== undefined)
90
+ if (entry !== undefined) {
91
+ // Hide every other view, then show and RAISE the target so the
92
+ // human actually sees the active tab/session (topmost child wins).
93
+ for (const v of views.values()) {
94
+ if (v === entry)
95
+ continue;
96
+ try {
97
+ v.webContentsView.setVisible(false);
98
+ }
99
+ catch { /* destroyed */ }
100
+ }
84
101
  entry.webContentsView.setVisible(true);
102
+ if (window !== undefined) {
103
+ window.contentView.removeChildView(entry.webContentsView);
104
+ window.contentView.addChildView(entry.webContentsView);
105
+ }
106
+ }
85
107
  reply(msg.id, { ok: true });
86
108
  return;
87
109
  }
@@ -119,6 +141,7 @@ async function handle(op, msg) {
119
141
  if (!r.ok) throw new Error('HTTP ' + r.status)
120
142
  const b = await r.arrayBuffer()
121
143
  const bytes = new Uint8Array(b)
144
+ if (bytes.length > ${String(MAX_DOWNLOAD_BYTES)}) throw new Error('download too large (limit ' + ${String(MAX_DOWNLOAD_BYTES)} + ' bytes, got ' + bytes.length + ')')
122
145
  let bin = ''
123
146
  for (let i = 0; i < bytes.length; i += 0x8000) bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000))
124
147
  return btoa(bin)
@@ -144,16 +167,21 @@ async function handle(op, msg) {
144
167
  // Export the session's cookies so login state can be saved/restored
145
168
  // across browser hosts (or shared with another machine).
146
169
  const cookies = await entry.webContentsView.webContents.session.cookies.get({});
147
- const exported = cookies.map(c => ({
148
- url: `http${c.secure ? 's' : ''}://${c.domain.startsWith('.') ? c.domain.slice(1) : c.domain}${c.path}`,
149
- name: c.name,
150
- value: c.value,
151
- domain: c.domain,
152
- path: c.path,
153
- secure: c.secure,
154
- httpOnly: c.httpOnly,
155
- expirationDate: c.expirationDate,
156
- }));
170
+ const exported = cookies.map(c => {
171
+ const host = c.domain.startsWith('.') ? c.domain.slice(1) : c.domain;
172
+ // IPv6 literal domains need brackets in a URL (e.g. http://[::1]/).
173
+ const hostPart = host.includes(':') && !host.startsWith('[') ? `[${host}]` : host;
174
+ return {
175
+ url: `http${c.secure ? 's' : ''}://${hostPart}${c.path}`,
176
+ name: c.name,
177
+ value: c.value,
178
+ domain: c.domain,
179
+ path: c.path,
180
+ secure: c.secure,
181
+ httpOnly: c.httpOnly,
182
+ expirationDate: c.expirationDate,
183
+ };
184
+ });
157
185
  reply(msg.id, { ok: true, result: { cookies: exported } });
158
186
  return;
159
187
  }
@@ -167,6 +195,7 @@ async function handle(op, msg) {
167
195
  throw new Error(`restoreAuth: unknown view ${viewId}`);
168
196
  if (!Array.isArray(cookies))
169
197
  throw new Error('restoreAuth missing cookies array');
198
+ let restored = 0;
170
199
  for (const c of cookies) {
171
200
  if (typeof c.url !== 'string' || typeof c.name !== 'string' || typeof c.value !== 'string')
172
201
  continue;
@@ -180,8 +209,9 @@ async function handle(op, msg) {
180
209
  ...typeof c.httpOnly === 'boolean' ? { httpOnly: c.httpOnly } : {},
181
210
  ...typeof c.expirationDate === 'number' ? { expirationDate: c.expirationDate } : {},
182
211
  });
212
+ restored++;
183
213
  }
184
- reply(msg.id, { ok: true, result: { restored: cookies.length } });
214
+ reply(msg.id, { ok: true, result: { restored } });
185
215
  return;
186
216
  }
187
217
  default:
@@ -228,6 +258,12 @@ void app.whenReady().then(() => {
228
258
  socket.on('error', error => {
229
259
  process.stderr.write(`[dsh-browser host] socket error: ${String(error)}\n`);
230
260
  });
261
+ // The parent owns our lifetime: when it closes the socket (dispose) or dies
262
+ // without cleanup, exit so no zombie Electron window is left behind.
263
+ socket.on('close', () => {
264
+ process.stderr.write('[dsh-browser host] parent connection closed, exiting\n');
265
+ app.exit(0);
266
+ });
231
267
  // Keep the process alive until the parent closes the socket or kills us.
232
268
  });
233
269
  // Diagnostics go to stderr, which the parent never parses as protocol.
@@ -21,11 +21,11 @@ const CHALLENGE_DETECT_EXPRESSION = `(() => {
21
21
  const lower = (title + '\\n' + bodyText).toLowerCase()
22
22
  const frameSrcs = [...document.querySelectorAll('iframe')].map(f => f.src || '').join(' ')
23
23
  const framesLower = frameSrcs.toLowerCase()
24
- const hasCfInterstitial = /just a moment|checking your browser|attention required|cf_chl|challenge-platform/i.test(lower)
24
+ const hasCfInterstitial = /just a moment|checking your browser|attention required|cf_chl/i.test(lower)
25
25
  || !!document.querySelector('#challenge-running, #challenge-stage, #cf-chl-container')
26
26
  const hasHCaptcha = !!window.hcaptcha || !!document.querySelector('.h-captcha') || /hcaptcha\\.com/i.test(framesLower)
27
27
  const hasRecaptcha = !!window.grecaptcha || !!document.querySelector('.g-recaptcha') || /recaptcha\\/api|google\\.com\\/recaptcha/i.test(framesLower)
28
- const hasTurnstile = !!window.turnstile || /challenges\\.cloudflare\\.com/i.test(framesLower) || /turnstile/i.test(lower)
28
+ const hasTurnstile = !!window.turnstile || /challenges\\.cloudflare\\.com/i.test(framesLower) || /turnstile|challenge-platform/i.test(lower)
29
29
  const verifyWording = /verify you are human|verify you are not a robot|\\u4eba\\u673a\\u9a8c\\u8bc1|\\u5b89\\u5168\\u9a8c\\u8bc1|enable javascript and cookies|\\u8bf7.*\\u9a8c\\u8bc1/i.test(lower)
30
30
  if (hasCfInterstitial) return { blocked: true, kind: 'cloudflare', reason: 'Cloudflare "Just a moment" interstitial' }
31
31
  if (hasHCaptcha) return { blocked: true, kind: 'hcaptcha', reason: 'hCaptcha verification' }
@@ -80,7 +80,7 @@ export class ElectronBrowserProvider {
80
80
  open() {
81
81
  const handle = this.host.createView();
82
82
  const id = `browser:${randomUUID()}`;
83
- this.sessions.set(id, { id, tabs: [{ id: `tab:${randomUUID()}`, handle }], activeIndex: 0, history: [] });
83
+ this.sessions.set(id, { id, tabs: [{ id: `tab:${randomUUID()}`, handle }], activeIndex: 0, history: [], nextSeq: 1 });
84
84
  return Promise.resolve(id);
85
85
  }
86
86
  /** Open a URL in the active tab (default) or a new tab. */
@@ -175,15 +175,19 @@ export class ElectronBrowserProvider {
175
175
  }
176
176
  }
177
177
  signal?.throwIfAborted();
178
- const result = await handle.sendCommand(CDP_PAGE_NAVIGATE, { url });
178
+ // Page.navigate can hang on an unreachable/slow host; bound it like the
179
+ // evaluate paths so a wedged navigation surfaces as an error instead of
180
+ // blocking the tool call forever.
181
+ const timeoutMs = 30_000;
182
+ const result = await withTimeout(handle.sendCommand(CDP_PAGE_NAVIGATE, { url }), timeoutMs, signal, `browser: navigation timed out after ${timeoutMs}ms`);
179
183
  // Page.navigate resolves even when the navigation fails; surface the
180
184
  // failure instead of leaving a silent white screen.
181
185
  const errorText = result.errorText;
182
186
  if (typeof errorText === 'string' && errorText !== '') {
183
- this.record(s, 'navigate', { url }, false, { error: errorText });
184
187
  throw new BrowserError(`browser: navigation to "${url}" failed: ${errorText}`, 'BROWSER_NAVIGATION_FAILED');
185
188
  }
186
189
  this.record(s, 'navigate', { url }, true);
190
+ this.showActive(s);
187
191
  }
188
192
  catch (error) {
189
193
  if (!(error instanceof BrowserError && error.code === 'BROWSER_NAVIGATION_BLOCKED')) {
@@ -230,10 +234,16 @@ export class ElectronBrowserProvider {
230
234
  return { ok: false, exception };
231
235
  }
232
236
  const value = result.result?.value ?? null;
233
- this.record(s, 'execute', { script: request.script }, true, { result: typeof value === 'string' ? value.slice(0, 500) : JSON.stringify(value).slice(0, 500) });
237
+ this.record(s, 'execute', {
238
+ script: request.script,
239
+ ...request.args !== undefined && request.args.length > 0 ? { args: request.args } : {},
240
+ }, true, { result: typeof value === 'string' ? value.slice(0, 500) : JSON.stringify(value).slice(0, 500) });
234
241
  return { ok: true, value };
235
242
  }
236
243
  catch (error) {
244
+ if (error instanceof Error && error.name === 'TimeoutError') {
245
+ throw new BrowserError(`browser: execute timed out after ${request.timeoutMs ?? 30_000}ms`, 'BROWSER_EXECUTE_TIMEOUT', { cause: error });
246
+ }
237
247
  throw new BrowserError(`browser: execute failed: ${String(error)}`, 'BROWSER_EXECUTE_FAILED', { cause: error });
238
248
  }
239
249
  }
@@ -333,13 +343,10 @@ export class ElectronBrowserProvider {
333
343
  return { ok: true, content: content.slice(0, ${String(maxChars)}), truncated }
334
344
  })()`;
335
345
  // Honor a per-call timeout: content evaluation can hang on a heavy page,
336
- // so a caller-supplied budget bounds it. AbortSignal.any merges the
337
- // caller's signal with a timeout.
346
+ // so a caller-supplied budget bounds it. Unlike a bare signal entry check,
347
+ // withTimeout also interrupts a call already in flight.
338
348
  const timeoutMs = request.timeoutMs ?? 30_000;
339
- const timeoutSignal = AbortSignal.timeout(timeoutMs);
340
- const result = signal !== undefined
341
- ? await handleSendEvaluate(tab.handle, script, AbortSignal.any([signal, timeoutSignal]))
342
- : await handleSendEvaluate(tab.handle, script, timeoutSignal);
349
+ const result = await withTimeout(handleSendEvaluate(tab.handle, script), timeoutMs, signal, `browser: content timed out after ${timeoutMs}ms`);
343
350
  if (!result.ok)
344
351
  throw new BrowserError(`browser: content evaluation failed: ${result.exception}`, 'BROWSER_CONTENT_FAILED');
345
352
  const value = result.value;
@@ -362,7 +369,9 @@ export class ElectronBrowserProvider {
362
369
  const { handle } = this.activeTab(s);
363
370
  signal?.throwIfAborted();
364
371
  await handle.sendCommand('Input.insertText', { text: request.text });
365
- this.record(s, 'type', { text: request.text.slice(0, 200) }, true);
372
+ // Store the full text so replay re-issues the same input; the history
373
+ // tool truncates long values when rendering.
374
+ this.record(s, 'type', { text: request.text }, true);
366
375
  }
367
376
  /**
368
377
  * Fill a form's fields in one batch. Runs one page-context script that
@@ -421,7 +430,15 @@ export class ElectronBrowserProvider {
421
430
  return vis.length > 0 ? vis : all
422
431
  }
423
432
  for (const spec of specs) {
424
- const els = candidates(spec)
433
+ let els
434
+ try {
435
+ els = candidates(spec)
436
+ } catch (e) {
437
+ // A malformed selector must not abort the whole batch; report the
438
+ // field as failed and continue with the rest.
439
+ out.push({ ok: false, error: String(e), target: describe(spec) })
440
+ continue
441
+ }
425
442
  if (els.length === 0) { out.push({ ok: false, error: 'field not found', target: describe(spec) }); continue }
426
443
  const el = els[0]
427
444
  const tag = el.tagName
@@ -481,7 +498,9 @@ export class ElectronBrowserProvider {
481
498
  let submitted = false
482
499
  if (${submitFlag}) {
483
500
  let last = null
484
- for (const spec of specs) { const els = candidates(spec); if (els.length > 0) { last = els[0]; break } }
501
+ for (const spec of specs) {
502
+ try { const els = candidates(spec); if (els.length > 0) { last = els[0]; break } } catch { /* skip */ }
503
+ }
485
504
  const form = last && (last.form || last.closest('form'))
486
505
  if (form) { form.requestSubmit(); submitted = true }
487
506
  }
@@ -512,7 +531,10 @@ export class ElectronBrowserProvider {
512
531
  if (typeof downloadable.download !== 'function') {
513
532
  throw new BrowserError('browser: download is only available on the self-hosted browser', 'BROWSER_DOWNLOAD_UNSUPPORTED');
514
533
  }
515
- await downloadable.download(request.url, request.savePath);
534
+ // The child fetches in-page with awaitPromise; a slow/hung network can
535
+ // block it well past the tool budget, so bound it like every other call.
536
+ const timeoutMs = 60_000;
537
+ await withTimeout(downloadable.download(request.url, request.savePath), timeoutMs, signal, `browser: download timed out after ${timeoutMs}ms`);
516
538
  this.record(s, 'download', { url: request.url, savePath: request.savePath }, true, { result: request.savePath });
517
539
  return { path: request.savePath };
518
540
  }
@@ -527,7 +549,8 @@ export class ElectronBrowserProvider {
527
549
  if (typeof host.flushAuth !== 'function') {
528
550
  throw new BrowserError('browser: auth export is only available on the self-hosted browser', 'BROWSER_AUTH_UNSUPPORTED');
529
551
  }
530
- const cookies = await host.flushAuth();
552
+ const timeoutMs = 30_000;
553
+ const cookies = await withTimeout(host.flushAuth(), timeoutMs, undefined, `browser: auth export timed out after ${timeoutMs}ms`);
531
554
  this.record(s, 'flushAuth', {}, true, { result: `${cookies.length} cookies` });
532
555
  return cookies;
533
556
  }
@@ -539,7 +562,8 @@ export class ElectronBrowserProvider {
539
562
  if (typeof host.restoreAuth !== 'function') {
540
563
  throw new BrowserError('browser: auth restore is only available on the self-hosted browser', 'BROWSER_AUTH_UNSUPPORTED');
541
564
  }
542
- const restored = await host.restoreAuth(cookies);
565
+ const timeoutMs = 30_000;
566
+ const restored = await withTimeout(host.restoreAuth(cookies), timeoutMs, undefined, `browser: auth restore timed out after ${timeoutMs}ms`);
543
567
  this.record(s, 'restoreAuth', { count: cookies.length }, true, { result: `${restored} cookies` });
544
568
  return restored;
545
569
  }
@@ -553,7 +577,10 @@ export class ElectronBrowserProvider {
553
577
  // a clip this yields the full-page image (CDP default is the viewport).
554
578
  params.captureBeyondViewport = true;
555
579
  }
556
- const result = await handle.sendCommand(CDP_PAGE_CAPTURE_SCREENSHOT, params);
580
+ // Bound the capture: a view that has not painted can wedge
581
+ // Page.captureScreenshot indefinitely (observed after view re-showing).
582
+ const timeoutMs = 30_000;
583
+ const result = await withTimeout(handle.sendCommand(CDP_PAGE_CAPTURE_SCREENSHOT, params), timeoutMs, signal, `browser: screenshot timed out after ${timeoutMs}ms`);
557
584
  const data = result.data;
558
585
  if (typeof data !== 'string') {
559
586
  throw new BrowserError('browser: screenshot returned no image data', 'BROWSER_SCREENSHOT_FAILED');
@@ -576,7 +603,7 @@ export class ElectronBrowserProvider {
576
603
  /** Append one operation to the session's history. */
577
604
  record(s, action, params, ok, detail) {
578
605
  const entry = {
579
- seq: s.history.length + 1,
606
+ seq: s.nextSeq++,
580
607
  action,
581
608
  params,
582
609
  ok,
@@ -636,8 +663,10 @@ export class ElectronBrowserProvider {
636
663
  const script = entry.params.script;
637
664
  if (typeof script !== 'string')
638
665
  throw new BrowserError(`browser: history seq ${seq} execute has no script`, 'BROWSER_HISTORY_INVALID');
639
- const result = await this.execute(session, { script });
640
- this.record(s, 'replay', { seq, of: entry.action, script }, result.ok, result.ok ? { result: String(result.value) } : { error: result.exception });
666
+ const recordedArgs = entry.params.args;
667
+ const args = Array.isArray(recordedArgs) ? recordedArgs.filter((a) => typeof a === 'string') : undefined;
668
+ const result = await this.execute(session, { script, ...args !== undefined && args.length > 0 ? { args } : {} });
669
+ this.record(s, 'replay', { seq, of: entry.action, script, ...args !== undefined && args.length > 0 ? { args } : {} }, result.ok, result.ok ? { result: String(result.value) } : { error: result.exception });
641
670
  return;
642
671
  }
643
672
  default:
@@ -682,7 +711,9 @@ export class ElectronBrowserProvider {
682
711
  }
683
712
  /** Read the current URL of a view through CDP. */
684
713
  async currentUrl(handle) {
685
- const result = await handleSendEvaluate(handle, 'location.href');
714
+ // Bound the read: a wedged renderer would otherwise hang listTabs.
715
+ const timeoutMs = 10_000;
716
+ const result = await withTimeout(handleSendEvaluate(handle, 'location.href'), timeoutMs, undefined, `browser: url read timed out after ${timeoutMs}ms`);
686
717
  return result.ok && typeof result.value === 'string' ? result.value : '';
687
718
  }
688
719
  }
@@ -703,7 +734,13 @@ function withTimeout(promise, ms, signal, message) {
703
734
  if (done)
704
735
  return;
705
736
  done = true;
706
- reject(new Error(message));
737
+ // A fired timeout must also release the abort listener; { once: true }
738
+ // only releases it on the next abort, which may never come.
739
+ if (signal !== undefined)
740
+ signal.removeEventListener('abort', onAbort);
741
+ const error = new Error(message);
742
+ error.name = 'TimeoutError';
743
+ reject(error);
707
744
  }, ms);
708
745
  const finish = (fn) => {
709
746
  if (done)
@@ -2,9 +2,9 @@
2
2
  * Self-hosted Electron browser host (parent side): an
3
3
  * {@link ElectronBrowserViewHost} implementation that spawns the plugin's own
4
4
  * Electron child process (host-main.js) and drives it over line-delimited
5
- * JSON-RPC on stdio. This is what makes the plugin work on surfaces without a
6
- * desktop shell's electronViewHost (plain dsh web): installing the plugin is
7
- * enough — the browser window appears on first use.
5
+ * JSON-RPC on a loopback TCP socket. This is what makes the plugin work on
6
+ * surfaces without a desktop shell's electronViewHost (plain dsh web):
7
+ * installing the plugin is enough — the browser window appears on first use.
8
8
  *
9
9
  * Protocol (one JSON object per line, both directions):
10
10
  * -> { id, op: 'createView' } | { id, op: 'destroyView', viewId } |
@@ -28,10 +28,13 @@ export declare class RemoteElectronViewHost implements ElectronBrowserViewHost {
28
28
  private pendingSocket;
29
29
  private readonly views;
30
30
  private readyPromise;
31
+ private disposed;
31
32
  constructor(hostMainPath: string);
32
- /** Ensure the child is up and ready (lazy on first use). */
33
+ /** Ensure the child is up and ready (lazy on first use; restarts after a crash). */
33
34
  private ready;
34
35
  private start;
36
+ /** The child died: tear down so the next use starts a fresh child. */
37
+ private onChildExit;
35
38
  createView(): ElectronViewHandle;
36
39
  private ensureView;
37
40
  showView(handle: ElectronViewHandle): void;