dsh-builtin-browser 0.4.9 → 0.4.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.
@@ -82,6 +82,7 @@ const defaultLauncher: BrowserLauncher = (path, args) =>
82
82
  import { spawn, type ChildProcess } from 'node:child_process'
83
83
  import { randomUUID } from 'node:crypto'
84
84
  import type { ElectronBrowserViewHost, ElectronViewHandle } from './provider.js'
85
+ import type { CarrierPage } from '../browser/types.js'
85
86
 
86
87
  /** Which browser the user asked for. */
87
88
  export type BrowserChannel = 'bundled' | 'chrome' | 'edge' | 'auto' | 'sidebar'
@@ -187,6 +188,19 @@ const INSTALL_LOCATIONS: Record<DetectedBrowser['kind'], Partial<Record<NodeJS.P
187
188
  * @param env - the environment to resolve against.
188
189
  * @returns the path with %NAME% references replaced, or the entry unchanged when unknown.
189
190
  */
191
+ /**
192
+ * Whether a browser's self-reported product name is one this carrier could have launched.
193
+ *
194
+ * `edg`, not `edge`: Edge reports itself as `Edg/155.0.4283.45`, both over HTTP and over the
195
+ * protocol. Testing for the full word rejected a live Edge that had just been started, so the
196
+ * call failed with the hand-off error this check was added to prevent — while the browser was
197
+ * running the whole time, answering on its own port.
198
+ * @param product - the product string a browser reports about itself.
199
+ */
200
+ function isBrowserProduct(product: unknown): boolean {
201
+ return typeof product === 'string' && /chrome|chromium|edg/i.test(product)
202
+ }
203
+
190
204
  /**
191
205
  * Whether a target URL is one of the blanks a browser opens by itself.
192
206
  *
@@ -619,13 +633,13 @@ export class SystemBrowserViewHost implements ElectronBrowserViewHost {
619
633
  if (typeof version.webSocketDebuggerUrl !== 'string') return undefined
620
634
  // The port number alone proves nothing — it can be reused once the browser that owned it
621
635
  // exits. The endpoint's own answer is the evidence, and it has to name a browser.
622
- if (typeof version.Browser !== 'string' || !/chrome|chromium|edge/i.test(version.Browser)) return undefined
636
+ if (!isBrowserProduct(version.Browser)) return undefined
623
637
  const client = new CdpClient(version.webSocketDebuggerUrl)
624
638
  await client.whenReady()
625
639
  // Asked through the protocol too, so a websocket that merely accepts connections is not
626
640
  // mistaken for a browser that answers.
627
641
  const info = await client.send('Browser.getVersion', {}) as { product?: string }
628
- if (typeof info?.product !== 'string' || !/chrome|chromium|edge/i.test(info.product)) {
642
+ if (!isBrowserProduct(info?.product)) {
629
643
  client.close()
630
644
  return undefined
631
645
  }
@@ -867,6 +881,69 @@ export class SystemBrowserViewHost implements ElectronBrowserViewHost {
867
881
  return !this.disposed
868
882
  }
869
883
 
884
+ /**
885
+ * Every page the browser currently holds, including ones this plugin did not create.
886
+ *
887
+ * Chrome and Edge own their own page creation. A click on a link that opens a new page — or any
888
+ * `window.open` — produces a page the provider never asked for, and nothing reports it back.
889
+ * Those pages are real: they hold memory and stay until something closes them. Measured on a real
890
+ * window: four pages present while `dsh_browser_list_tabs` reported one.
891
+ *
892
+ * Discovery is already on at connection time (see the blank-page normalisation), so
893
+ * `Target.getTargets` also lists pages that existed before this client connected.
894
+ * @returns the browser's pages, or an empty list when it cannot answer.
895
+ */
896
+ /**
897
+ * Every page the browser holds that THIS HOST did not open.
898
+ *
899
+ * Chrome and Edge own their own page creation. A click on a link that opens a new page — or any
900
+ * `window.open` — produces a page the provider never asked for, and nothing reports it back.
901
+ * Those pages are real: they hold memory and stay until something closes them. Measured on a real
902
+ * window: four pages present while `dsh_browser_list_tabs` reported one.
903
+ *
904
+ * Pages this host opened are EXCLUDED here rather than deduplicated by the caller, because the
905
+ * caller cannot do it: the provider knows a tab by its own id, the carrier knows a page by its
906
+ * CDP target id, and the two never match. The first version returned everything and relied on the
907
+ * caller to filter, which reported the same page twice.
908
+ *
909
+ * Discovery is already on at connection time (see the blank-page normalisation), so
910
+ * `Target.getTargets` also lists pages that existed before this client connected.
911
+ * @returns the browser's pages that are not ours, or an empty list when it cannot answer.
912
+ */
913
+ async listPages(): Promise<readonly CarrierPage[]> {
914
+ try {
915
+ const client = await this.ensureClient()
916
+ const listed = await client.send('Target.getTargets', {}) as {
917
+ targetInfos?: Array<{ targetId?: string; type?: string; url?: string; title?: string }>
918
+ }
919
+ const ours = new Set(this.sessions.values())
920
+ return (listed?.targetInfos ?? [])
921
+ .filter(entry => entry.type === 'page' && typeof entry.targetId === 'string')
922
+ .filter(entry => !ours.has(entry.targetId as string))
923
+ .map(entry => ({
924
+ id: entry.targetId as string,
925
+ url: typeof entry.url === 'string' ? entry.url : '',
926
+ ...typeof entry.title === 'string' && entry.title !== '' ? { title: entry.title } : {},
927
+ }))
928
+ } catch {
929
+ // A listing must not fail harder than driving does; the next command reports a real problem.
930
+ return []
931
+ }
932
+ }
933
+
934
+ /**
935
+ * Close one page by the target id {@link listPages} reported.
936
+ *
937
+ * `Target.closeTarget` is the browser's own close, so it works for pages this plugin never
938
+ * created. The page may already be gone if the human closed it in the meantime — the caller
939
+ * asked for it to be closed and it is, so that is not an error.
940
+ * @param id - the CDP target id.
941
+ */
942
+ async closePage(id: string): Promise<void> {
943
+ const client = await this.ensureClient()
944
+ await client.send('Target.closeTarget', { targetId: id }).catch(() => undefined)
945
+ }
946
+
870
947
  createView(): ElectronViewHandle {
871
948
  const viewId = randomUUID()
872
949
  return {