dsh-builtin-browser 0.2.0 → 0.3.1

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 (34) hide show
  1. package/README.en.md +50 -5
  2. package/README.md +62 -10
  3. package/client.js +44 -8
  4. package/desktop-bridge/install.mjs +98 -0
  5. package/desktop-bridge/plugin-browser-bridge.js +385 -0
  6. package/lib/browser-electron/bridge-connection.d.ts +58 -0
  7. package/lib/browser-electron/bridge-connection.js +133 -0
  8. package/lib/browser-electron/desktop-bridge-host.d.ts +124 -0
  9. package/lib/browser-electron/desktop-bridge-host.js +297 -0
  10. package/lib/browser-electron/entry.js +81 -13
  11. package/lib/browser-electron/history-store.d.ts +5 -1
  12. package/lib/browser-electron/history-store.js +12 -1
  13. package/lib/browser-electron/provider.d.ts +42 -1
  14. package/lib/browser-electron/provider.js +96 -24
  15. package/lib/browser-electron/remote-host.d.ts +9 -1
  16. package/lib/browser-electron/remote-host.js +17 -3
  17. package/lib/browser-electron/settings-store.d.ts +5 -0
  18. package/lib/browser-electron/settings-store.js +24 -1
  19. package/lib/browser-electron/system-browser.d.ts +132 -0
  20. package/lib/browser-electron/system-browser.js +380 -0
  21. package/lib/browser-electron/virtual-cursor.d.ts +27 -2
  22. package/lib/browser-electron/virtual-cursor.js +77 -7
  23. package/lib/tool-browser/index.js +51 -15
  24. package/package.json +2 -1
  25. package/src/browser-electron/bridge-connection.ts +145 -0
  26. package/src/browser-electron/desktop-bridge-host.ts +305 -0
  27. package/src/browser-electron/entry.ts +84 -13
  28. package/src/browser-electron/history-store.ts +11 -2
  29. package/src/browser-electron/provider.ts +124 -25
  30. package/src/browser-electron/remote-host.ts +17 -2
  31. package/src/browser-electron/settings-store.ts +30 -1
  32. package/src/browser-electron/system-browser.ts +371 -0
  33. package/src/browser-electron/virtual-cursor.ts +76 -6
  34. package/src/tool-browser/index.ts +51 -15
@@ -11,7 +11,7 @@ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
11
11
  import { homedir } from 'node:os';
12
12
  import { dirname, isAbsolute, join, resolve, sep } from 'node:path';
13
13
  import { HistoryStore } from './history-store.js';
14
- import { paintCursor } from './virtual-cursor.js';
14
+ import { forgetCursor, paintCursor } from './virtual-cursor.js';
15
15
  import { BrowserError } from '../browser/types.js';
16
16
  /**
17
17
  * Page-context human-verification (CAPTCHA / bot-detection) detection. Runs
@@ -393,6 +393,11 @@ export class ElectronBrowserProvider {
393
393
  async settleDocument(handle, before, signal) {
394
394
  const started = Date.now();
395
395
  const deadline = started + SETTLE_TIMEOUT_MS;
396
+ // The document is being replaced, so the synthetic pointer drawn into the old one
397
+ // is gone. Dropping the remembered position is what lets it be painted again:
398
+ // without this, a paint at coincidentally identical coordinates would be skipped
399
+ // and the pointer would never come back after a navigation.
400
+ forgetCursor(handle);
396
401
  for (;;) {
397
402
  if (signal?.aborted === true)
398
403
  return;
@@ -870,21 +875,39 @@ export class ElectronBrowserProvider {
870
875
  // iframes stay opaque — the browser forbids reading them).
871
876
  let content = ''
872
877
  if (fmt === 'txt') {
873
- const parts = []
874
- const textWalk = (node) => {
875
- if (node.nodeType === Node.TEXT_NODE) { const t = (node.textContent || '').trim(); if (t) parts.push(t); return }
876
- if (node.nodeType !== Node.ELEMENT_NODE) return
877
- const tag = node.tagName.toLowerCase()
878
- if (tag === 'script' || tag === 'style' || tag === 'noscript') return
879
- if (tag === 'iframe') {
880
- try { const d = node.contentDocument; if (d && d.body) for (const c of d.body.childNodes) textWalk(c) } catch { /* cross-origin */ }
881
- return
878
+ // Prefer the browser's own rendered text.
879
+ //
880
+ // Walking text nodes and joining them is not equivalent: pages that wrap
881
+ // every character in its own element — one-character spans for a
882
+ // per-character animation, which is common — produce one text node per
883
+ // letter, and joining those with spaces turns every word into a column of
884
+ // letters. The rendered text reassembles them the way a reader sees them,
885
+ // and it also honours visibility, so hidden text does not leak out.
886
+ // (No backticks in this comment: it lives inside a template literal.)
887
+ const rendered = typeof root.innerText === 'string' ? root.innerText : ''
888
+ if (rendered.trim() !== '') {
889
+ content = rendered
890
+ } else {
891
+ // Fallback for nodes without layout (a detached root, or a fragment):
892
+ // collect text directly, still piercing shadow roots and same-origin
893
+ // iframes, and join without inventing separators beyond what the markup
894
+ // already implies.
895
+ const parts = []
896
+ const textWalk = (node) => {
897
+ if (node.nodeType === Node.TEXT_NODE) { const t = (node.textContent || '').trim(); if (t) parts.push(t); return }
898
+ if (node.nodeType !== Node.ELEMENT_NODE) return
899
+ const tag = node.tagName.toLowerCase()
900
+ if (tag === 'script' || tag === 'style' || tag === 'noscript') return
901
+ if (tag === 'iframe') {
902
+ try { const d = node.contentDocument; if (d && d.body) for (const c of d.body.childNodes) textWalk(c) } catch { /* cross-origin */ }
903
+ return
904
+ }
905
+ if (node.shadowRoot) for (const c of node.shadowRoot.childNodes) textWalk(c)
906
+ for (const child of node.childNodes) textWalk(child)
882
907
  }
883
- if (node.shadowRoot) for (const c of node.shadowRoot.childNodes) textWalk(c)
884
- for (const child of node.childNodes) textWalk(child)
908
+ textWalk(root)
909
+ content = parts.join(' ')
885
910
  }
886
- textWalk(root)
887
- content = parts.join(' ')
888
911
  }
889
912
  else if (fmt === 'html') content = root.outerHTML || ''
890
913
  else if (fmt === 'json') content = JSON.stringify(root)
@@ -952,6 +975,15 @@ export class ElectronBrowserProvider {
952
975
  y = Number(out.y);
953
976
  }
954
977
  else {
978
+ // A coordinate only exists because somebody read a picture. Under the
979
+ // non-visual strategy the plugin must not accept one: a model that cannot
980
+ // see cannot have produced it, so the coordinates are either copied from an
981
+ // earlier vision pass or invented, and honouring them turns a silent
982
+ // mis-click into a mystery. Refusing here, with a message that says what to
983
+ // do instead, converts that into a usable instruction.
984
+ if (this.settingsSource !== undefined && this.settingsSource().vision.strategy === 'nonVisual') {
985
+ throw new BrowserError('browser: the configured non-visual strategy refuses coordinate clicks — pass a semantic target instead, e.g. target { by: "text", value: "Sign in" }, which locates the element from the DOM and clicks its centre', 'BROWSER_NON_VISUAL_COORDINATES');
986
+ }
955
987
  x = request.x;
956
988
  y = request.y;
957
989
  }
@@ -975,7 +1007,9 @@ export class ElectronBrowserProvider {
975
1007
  }
976
1008
  // Paint before the press so the ripple is already on screen when the click
977
1009
  // lands — the cursor exists to show the human WHERE the agent acts.
978
- this.showCursor(handle, x, y, 'click');
1010
+ // Name the operation in the overlay: the pointer is the "agent is driving this
1011
+ // tab" signal, so the bubble is what lets a human follow along without the log.
1012
+ this.showCursor(handle, x, y, 'click', 'click', true);
979
1013
  const release = () => {
980
1014
  void handle
981
1015
  .sendCommand('Input.dispatchMouseEvent', {
@@ -1026,7 +1060,7 @@ export class ElectronBrowserProvider {
1026
1060
  const out = await this.runTargetScript(locateTab, script, locateMs, signal, 'BROWSER_TYPE_FAILED', 'browser: type');
1027
1061
  const cursorPoint = scriptPoint(out);
1028
1062
  if (cursorPoint !== undefined)
1029
- this.showCursor(locateTab.handle, cursorPoint.x, cursorPoint.y, 'click');
1063
+ this.showCursor(locateTab.handle, cursorPoint.x, cursorPoint.y, 'click', 'type', true);
1030
1064
  }
1031
1065
  const text = 'text' in request ? request.text : '';
1032
1066
  const timeoutMs = 30_000;
@@ -1879,11 +1913,14 @@ export class ElectronBrowserProvider {
1879
1913
  * @param x - viewport x in CSS pixels.
1880
1914
  * @param y - viewport y in CSS pixels.
1881
1915
  * @param action - click pulses a ripple; move only relocates.
1916
+ * @param label - short description of the operation, shown beside the pointer.
1917
+ * @param force - paint even when the pointer is already there (a click ripple must
1918
+ * always play; a bare move need not repeat).
1882
1919
  */
1883
- showCursor(handle, x, y, action) {
1920
+ showCursor(handle, x, y, action, label, force = false) {
1884
1921
  if (this.settingsSource !== undefined && !this.settingsSource().ui.virtualCursor)
1885
1922
  return;
1886
- paintCursor(handle, x, y, action, (target, expression) => handleSendEvaluate(target, expression));
1923
+ paintCursor(handle, x, y, action, (target, expression) => handleSendEvaluate(target, expression), label, force);
1887
1924
  }
1888
1925
  /**
1889
1926
  * Replay one recorded operation by sequence number. Navigate/click/type are
@@ -1967,6 +2004,13 @@ export class ElectronBrowserProvider {
1967
2004
  this.sessions.delete(session);
1968
2005
  for (const tab of existing.tabs)
1969
2006
  this.host.destroyView(tab.handle);
2007
+ // Releasing the browser at session end is a setting, not fixed behaviour
2008
+ // (requirements §3). A self-hosted carrier already tore its window down with
2009
+ // the views above; the desktop's sidebar does not, so it is asked explicitly
2010
+ // — and its cookies plus the browsing history outlive the page either way.
2011
+ if (this.settingsSource !== undefined && this.settingsSource().ui.closeWithSession) {
2012
+ void this.host.releasePage?.().catch(() => undefined);
2013
+ }
1970
2014
  }
1971
2015
  return Promise.resolve();
1972
2016
  }
@@ -2076,19 +2120,47 @@ export class ElectronBrowserProvider {
2076
2120
  this.notifyUserActionError(action, error);
2077
2121
  }
2078
2122
  }
2079
- /** Report a failed user action to the host UI (toolbar), when supported. */
2123
+ /**
2124
+ * Report a failed user action to the host UI (toolbar), when supported.
2125
+ *
2126
+ * Two properties matter here, because this runs inside the catch of an async
2127
+ * handler (issue #16):
2128
+ * - the receiver must be preserved. Reading the method off the host and calling
2129
+ * it unbound runs the host's implementation with `this === undefined`, so its
2130
+ * very first statement (`void this.ready()`) throws, the throw escapes the
2131
+ * async catch as an unhandled rejection, and the whole DSH host exits;
2132
+ * - this method must be incapable of throwing. Reporting a failed action is
2133
+ * diagnostics: it can never be allowed to become the failure.
2134
+ */
2080
2135
  notifyUserActionError(action, error) {
2081
- const notify = this.host.notifyUserActionError;
2082
2136
  const message = `action ${action.type} failed: ${String(error)}`;
2083
- if (typeof notify === 'function') {
2084
- notify(action.windowId, message);
2137
+ let reported = false;
2138
+ try {
2139
+ const host = this.host;
2140
+ const notify = host.notifyUserActionError;
2141
+ if (typeof notify === 'function') {
2142
+ // Called on its owner: an unbound call here kills the host process.
2143
+ notify.call(host, action.windowId, message);
2144
+ reported = true;
2145
+ }
2085
2146
  }
2086
- else {
2087
- process.stderr.write(`[dsh-browser] user action failed: ${message}\n`);
2147
+ catch (notifyError) {
2148
+ // Contained on purpose; the log line below still carries the original cause.
2149
+ process.stderr.write(`[dsh-browser] host notify failed: ${String(notifyError)}\n`);
2088
2150
  }
2151
+ if (!reported)
2152
+ process.stderr.write(`[dsh-browser] user action failed: ${message}\n`);
2089
2153
  }
2090
2154
  /** Ask the host to show the active tab's view, carrying the session label. */
2091
2155
  showActive(s) {
2156
+ // §3: whether the agent's page takes over the screen is the user's choice, not
2157
+ // a fixed behaviour. With auto-expand off the page still exists and is still
2158
+ // driven — it is simply folded away, so the agent works without claiming the
2159
+ // sidebar. Carriers that own no presentation (self-hosted) ignore the call.
2160
+ if (this.settingsSource !== undefined && !this.settingsSource().ui.autoExpandOnce) {
2161
+ void this.host.collapse?.().catch(() => undefined);
2162
+ return;
2163
+ }
2092
2164
  this.host.showView?.(this.activeTab(s).handle, s.label);
2093
2165
  }
2094
2166
  /** Read the current URL of a view through CDP. */
@@ -145,7 +145,15 @@ export declare class RemoteElectronViewHost implements ElectronBrowserViewHost {
145
145
  * on a cooldown heals it by itself while keeping the scan off the hot path.
146
146
  */
147
147
  available(): boolean;
148
- /** Ensure the child is up and ready (lazy on first use; restarts after a crash). */
148
+ /**
149
+ * Ensure the child is up and ready (lazy on first use; restarts after a crash).
150
+ *
151
+ * Never throws synchronously. Callers are fire-and-forget
152
+ * (`void this.ready().then(…).catch(…)`), and a synchronous throw escapes their
153
+ * `.catch` entirely — the shape that let a failed toolbar report take the whole
154
+ * DSH host down (issue #16). A disposed host is reported as a rejected promise
155
+ * instead, which those callers already handle.
156
+ */
149
157
  private ready;
150
158
  private start;
151
159
  /** The child died: tear down so the next use starts a fresh child. */
@@ -765,13 +765,27 @@ export class RemoteElectronViewHost {
765
765
  }
766
766
  return this.electronAvailable;
767
767
  }
768
- /** Ensure the child is up and ready (lazy on first use; restarts after a crash). */
768
+ /**
769
+ * Ensure the child is up and ready (lazy on first use; restarts after a crash).
770
+ *
771
+ * Never throws synchronously. Callers are fire-and-forget
772
+ * (`void this.ready().then(…).catch(…)`), and a synchronous throw escapes their
773
+ * `.catch` entirely — the shape that let a failed toolbar report take the whole
774
+ * DSH host down (issue #16). A disposed host is reported as a rejected promise
775
+ * instead, which those callers already handle.
776
+ */
769
777
  ready() {
770
778
  // Fail fast on a disposed host: a recovery path can re-enter ready() after
771
779
  // dispose() cleared its state, and starting fresh here would spawn a child
772
780
  // nobody will ever kill.
773
- if (this.disposed)
774
- throw new HostGoneError('browser host disposed');
781
+ if (this.disposed) {
782
+ const gone = Promise.reject(new HostGoneError('browser host disposed'));
783
+ // Keep the rejection observable to callers while marking it handled here:
784
+ // a fire-and-forget caller that forgot to attach a handler must not turn
785
+ // this into an unhandled rejection either.
786
+ gone.catch(() => undefined);
787
+ return gone;
788
+ }
775
789
  if (this.readyPromise !== undefined)
776
790
  return this.readyPromise;
777
791
  const started = this.start();
@@ -8,6 +8,7 @@
8
8
  * defaults" rather than an error.
9
9
  * @module dsh-browser/browser-electron/settings-store
10
10
  */
11
+ import type { BrowserChannel } from './system-browser.js';
11
12
  /** Vision/operation strategy the provider prefers when the model can see. */
12
13
  export type VisionStrategy = 'auto' | 'nonVisual';
13
14
  /** The full settings document, with every field resolved. */
@@ -35,6 +36,10 @@ export interface BrowserSettings {
35
36
  readonly vision: {
36
37
  readonly strategy: VisionStrategy;
37
38
  };
39
+ /** Which browser binary carries the agent's pages. */
40
+ readonly browser: {
41
+ readonly channel: BrowserChannel;
42
+ };
38
43
  /** Whether the agent may read cookies / export login state. */
39
44
  readonly credentials: {
40
45
  readonly allowRead: boolean;
@@ -17,6 +17,10 @@ export const DEFAULT_SETTINGS = {
17
17
  cookies: { persist: true },
18
18
  ui: { autoExpandOnce: true, closeWithSession: false, virtualCursor: true },
19
19
  vision: { strategy: 'auto' },
20
+ // Bundled by default: it needs nothing installed, and on the desktop the carrier
21
+ // is the sidebar anyway. Choosing a system browser is an explicit opt-in because
22
+ // it starts a real Chrome/Edge with its own profile.
23
+ browser: { channel: 'bundled' },
20
24
  credentials: { allowRead: true },
21
25
  };
22
26
  /**
@@ -32,6 +36,17 @@ export function settingsPath() {
32
36
  function bool(value, fallback) {
33
37
  return typeof value === 'boolean' ? value : fallback;
34
38
  }
39
+ /**
40
+ * Coerce the browser channel, keeping the default when it is not one of the
41
+ * known choices. A hand-edited typo must not silently select a different browser.
42
+ * @param value - the raw value from the settings document.
43
+ * @returns a valid channel.
44
+ */
45
+ function resolveChannel(value) {
46
+ return value === 'chrome' || value === 'edge' || value === 'auto' || value === 'bundled'
47
+ ? value
48
+ : DEFAULT_SETTINGS.browser.channel;
49
+ }
35
50
  /** Coerce one positive-integer field, keeping the default when invalid. */
36
51
  function count(value, fallback) {
37
52
  return typeof value === 'number' && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
@@ -55,6 +70,7 @@ export function resolveSettings(raw) {
55
70
  const ui = section(source, 'ui');
56
71
  const vision = section(source, 'vision');
57
72
  const credentials = section(source, 'credentials');
73
+ const browser = section(source, 'browser');
58
74
  return {
59
75
  history: {
60
76
  enabled: bool(history.enabled, DEFAULT_SETTINGS.history.enabled),
@@ -68,6 +84,7 @@ export function resolveSettings(raw) {
68
84
  virtualCursor: bool(ui.virtualCursor, DEFAULT_SETTINGS.ui.virtualCursor),
69
85
  },
70
86
  vision: { strategy: vision.strategy === 'nonVisual' ? 'nonVisual' : DEFAULT_SETTINGS.vision.strategy },
87
+ browser: { channel: resolveChannel(browser.channel) },
71
88
  credentials: { allowRead: bool(credentials.allowRead, DEFAULT_SETTINGS.credentials.allowRead) },
72
89
  };
73
90
  }
@@ -113,7 +130,13 @@ export class SettingsStore {
113
130
  if (this.cached !== undefined && mtimeMs === this.cachedMtimeMs)
114
131
  return this.cached;
115
132
  try {
116
- this.cached = text === undefined ? DEFAULT_SETTINGS : resolveSettings(JSON.parse(text));
133
+ // A leading BOM is what ordinary Windows editors (Notepad, PowerShell's
134
+ // `Set-Content -Encoding utf8`) leave behind, and `JSON.parse` rejects it.
135
+ // Without this strip, hand-editing the settings file silently discarded every
136
+ // value in it: the document looked correct, and the plugin quietly ran on
137
+ // defaults — including switches the user had just turned off.
138
+ const body = text?.replace(/^\uFEFF/, '');
139
+ this.cached = body === undefined ? DEFAULT_SETTINGS : resolveSettings(JSON.parse(body));
117
140
  }
118
141
  catch {
119
142
  // Malformed JSON: behave as defaults rather than fail startup.
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Drive the browser the user already has (Chrome / Edge / any Chromium) instead of
3
+ * the Electron we ship — the same approach Codex Browser Use takes.
4
+ *
5
+ * WHY
6
+ * The bundled Electron is a private copy with its own profile, so it can never see
7
+ * the logins the user already has in Chrome, and on a managed machine the user may
8
+ * simply prefer their own browser. Launching their browser with a *separate*
9
+ * `--user-data-dir` gives us a real Chromium we can drive over CDP without touching
10
+ * their everyday profile, bookmarks or windows: nothing of theirs is opened, locked
11
+ * or modified, and closing us never closes them.
12
+ *
13
+ * HOW
14
+ * Chromium exposes CDP on `--remote-debugging-port=0`, and writes the port it chose
15
+ * into `<profile>/DevToolsActivePort`. That file is the discovery mechanism — no
16
+ * port guessing, no collisions. From there everything is CDP over one WebSocket:
17
+ * `Target.createTarget` makes a page, `Target.attachToTarget` (flattened) gives a
18
+ * session for it, and the plugin's commands ride that session. Node 22 ships a global
19
+ * `WebSocket`, so this needs no dependency at all.
20
+ *
21
+ * WHAT IT IMPLEMENTS
22
+ * The same `ElectronBrowserViewHost` seam as the self-hosted and sidebar hosts, so
23
+ * the provider, the tools, history, the cursor and teardown rules are unchanged —
24
+ * only the carrier differs.
25
+ * @module dsh-browser/browser-electron/system-browser
26
+ */
27
+ import type { ElectronBrowserViewHost, ElectronViewHandle } from './provider.js';
28
+ /** Which browser the user asked for. */
29
+ export type BrowserChannel = 'bundled' | 'chrome' | 'edge' | 'auto';
30
+ /** A resolved browser installation. */
31
+ export interface DetectedBrowser {
32
+ /** Which product this is. */
33
+ readonly kind: 'chrome' | 'edge' | 'brave';
34
+ /** Absolute path to the executable. */
35
+ readonly path: string;
36
+ }
37
+ /**
38
+ * Find an installed Chromium browser.
39
+ *
40
+ * `auto` prefers Chrome, then Edge, then Brave — the order reflects how likely each
41
+ * is to be the browser a user actually chose rather than one Windows shipped.
42
+ * @param channel - the configured choice; `bundled` never resolves to a system browser.
43
+ * @param env - environment lookup, injected so tests need no real machine.
44
+ * @returns the detected browser, or undefined when the choice is unavailable.
45
+ */
46
+ export declare function detectBrowser(channel: BrowserChannel, env?: NodeJS.ProcessEnv): DetectedBrowser | undefined;
47
+ /** A system browser driven over CDP, presented as a browser view host. */
48
+ export declare class SystemBrowserViewHost implements ElectronBrowserViewHost {
49
+ private readonly browser;
50
+ private readonly profileDir;
51
+ private readonly extraArgs;
52
+ private readonly ephemeralDir?;
53
+ private readonly views;
54
+ private disposed;
55
+ /**
56
+ * The browser is started on FIRST USE, never on construction.
57
+ *
58
+ * A plugin that launches Chrome the moment its host loads is a plugin that takes
59
+ * over the machine before anyone asked it to: simply starting DSH would spawn a
60
+ * browser window. Registration must therefore be inert, and the process is only
61
+ * spawned when a view is actually needed.
62
+ */
63
+ private client;
64
+ private child;
65
+ private starting;
66
+ /**
67
+ * @param browser - the detected installation to launch on first use.
68
+ * @param profileDir - a plugin-owned directory; the user's own profile is never touched.
69
+ * @param extraArgs - additional Chromium switches.
70
+ * @param ephemeralDir - a directory to delete on release, when the user has turned
71
+ * persistence off so no login state outlives the session.
72
+ */
73
+ constructor(browser: DetectedBrowser, profileDir: string, extraArgs?: readonly string[], ephemeralDir?: string | undefined);
74
+ /** Which product this host would drive (for diagnostics). */
75
+ get kind(): DetectedBrowser['kind'];
76
+ /** Whether the browser has actually been started yet (diagnostics and tests). */
77
+ get started(): boolean;
78
+ /**
79
+ * The CDP client, starting the browser on first use.
80
+ * @returns the connected client.
81
+ */
82
+ private ensureClient;
83
+ /**
84
+ * Launch the browser with a private profile and connect over CDP.
85
+ *
86
+ * `--remote-debugging-port=0` plus the `DevToolsActivePort` file is the only
87
+ * reliable way to learn the port: a fixed port collides with whatever else the
88
+ * machine is running, and parsing stderr is fragile across versions.
89
+ * @returns the connected client.
90
+ */
91
+ private start;
92
+ /**
93
+ * Whether this host can back views.
94
+ *
95
+ * True while the host is usable — including before the browser has been started,
96
+ * since the first command is what starts it. Reporting "unavailable" here would
97
+ * make the plugin fall back for no reason.
98
+ */
99
+ available(): boolean;
100
+ createView(): ElectronViewHandle;
101
+ destroyView(handle: ElectronViewHandle): void;
102
+ /** Nothing to show: the browser owns its own windows. */
103
+ showView(): void;
104
+ /** Same as {@link showView}. */
105
+ presentView(): Promise<void>;
106
+ /** The browser owns window grouping. */
107
+ groupView(): void;
108
+ /**
109
+ * Bring the browser's window forward.
110
+ *
111
+ * The browser is a separate application, so there is no view to raise; focusing
112
+ * the process is the closest honest equivalent, and failing to do so (a minimized
113
+ * window, a locked session) is never worth an error.
114
+ */
115
+ focus(): Promise<void>;
116
+ /** The browser reports its own window lifecycle. */
117
+ onUserAction(): void;
118
+ /** The browser reports its own window lifecycle. */
119
+ onViewClosed(): void;
120
+ /**
121
+ * Close the browser we launched, if we ever launched one.
122
+ *
123
+ * The user's own windows belong to a different process and are never touched. A
124
+ * host whose browser was never started (the common case when nobody used the
125
+ * browser) simply tears down its bookkeeping.
126
+ */
127
+ dispose(): void;
128
+ /** The flattened session for a view, creating its page on first use. */
129
+ private ensureSession;
130
+ /** sessionId -> targetId, so a destroyed view can close the right page. */
131
+ private readonly sessions;
132
+ }