dsh-builtin-browser 0.4.7 → 0.4.9

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 (44) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/README.en.md +62 -491
  3. package/README.md +44 -470
  4. package/client.js +10 -10
  5. package/cordis.patch.yml +4 -4
  6. package/desktop-bridge/plugin-browser-bridge.js +1 -1
  7. package/docs/README.md +8 -4
  8. package/docs/architecture.md +82 -11
  9. package/docs/development.md +38 -0
  10. package/docs/limitations.md +49 -0
  11. package/docs/tool-reference.md +42 -42
  12. package/docs/user-guide.md +93 -37
  13. package/docs/why-browser.md +3 -3
  14. package/lib/browser/types.d.ts +2 -2
  15. package/lib/browser-electron/entry.d.ts +2 -2
  16. package/lib/browser-electron/entry.js +1 -1
  17. package/lib/browser-electron/history-store.d.ts +1 -1
  18. package/lib/browser-electron/history-store.js +1 -1
  19. package/lib/browser-electron/host-main.js +3 -3
  20. package/lib/browser-electron/provider.d.ts +9 -9
  21. package/lib/browser-electron/provider.js +14 -14
  22. package/lib/browser-electron/remote-host.js +1 -1
  23. package/lib/browser-electron/settings-store.d.ts +4 -4
  24. package/lib/browser-electron/system-browser.d.ts +60 -0
  25. package/lib/browser-electron/system-browser.js +257 -23
  26. package/lib/index.d.ts +1 -1
  27. package/lib/index.js +1 -1
  28. package/lib/tool-browser/element-fields.d.ts +4 -4
  29. package/lib/tool-browser/element-fields.js +6 -6
  30. package/lib/tool-browser/index.d.ts +6 -6
  31. package/lib/tool-browser/index.js +161 -139
  32. package/package.json +2 -2
  33. package/scripts/verify-host-compat.mjs +6 -6
  34. package/src/browser/types.ts +2 -2
  35. package/src/browser-electron/entry.ts +2 -2
  36. package/src/browser-electron/history-store.ts +1 -1
  37. package/src/browser-electron/host-main.ts +3 -3
  38. package/src/browser-electron/provider.ts +15 -15
  39. package/src/browser-electron/remote-host.ts +1 -1
  40. package/src/browser-electron/settings-store.ts +4 -4
  41. package/src/browser-electron/system-browser.ts +244 -16
  42. package/src/index.ts +1 -1
  43. package/src/tool-browser/element-fields.ts +6 -6
  44. package/src/tool-browser/index.ts +164 -140
@@ -111,6 +111,16 @@ export declare class SystemBrowserViewHost implements ElectronBrowserViewHost {
111
111
  */
112
112
  private clientSuspect;
113
113
  private child;
114
+ /**
115
+ * True when the browser in use was adopted from a previous run rather than launched here.
116
+ *
117
+ * Worth recording because the two differ on teardown: a browser this host spawned is its to
118
+ * kill, while one it merely took over belongs to a process that is already gone — closing it is
119
+ * still right (it holds the profile nobody else can use), but it is a decision, not cleanup.
120
+ */
121
+ private adoptedRunning;
122
+ /** Whether this connection's surplus blank pages have been collapsed already. */
123
+ private blanksNormalized;
114
124
  private starting;
115
125
  /**
116
126
  * Browsers that ignored a kill.
@@ -167,6 +177,25 @@ export declare class SystemBrowserViewHost implements ElectronBrowserViewHost {
167
177
  */
168
178
  private forgetBrowserState;
169
179
  private ensureClient;
180
+ /**
181
+ * Take over a browser already serving this profile, instead of racing it.
182
+ *
183
+ * A previous DSH process leaves its browser running: the child handle died with that process, so
184
+ * nothing here knows about it, and `start()`'s cleanup branch is skipped because it keys on
185
+ * `this.child`. Launching then does something worse than fail — on Windows Chrome hands the
186
+ * request to the instance already holding the profile and exits 0, and that exit is read as a
187
+ * launch failure. The user is told to go kill the leftovers by hand, on every restart, which is
188
+ * exactly what was reported.
189
+ *
190
+ * The launch loop deliberately refuses to trust a `<profile>/DevToolsActivePort` left by a
191
+ * previous launch, and it is right to: a stale port number can be reused by something that is
192
+ * not a browser at all. What makes adoption safe HERE is that this runs BEFORE launching, on a
193
+ * file that lives inside this plugin's own profile directory — only a browser started with this
194
+ * `--user-data-dir` writes it — and the endpoint is then asked to identify itself.
195
+ * @param why - a short phrase for the log, naming which attempt this is.
196
+ * @returns a connected client for the running browser, or undefined when there is none.
197
+ */
198
+ private adoptRunningBrowser;
170
199
  /**
171
200
  * Launch the browser with a private profile and connect over CDP.
172
201
  *
@@ -236,6 +265,37 @@ export declare class SystemBrowserViewHost implements ElectronBrowserViewHost {
236
265
  private ensureSession;
237
266
  /** Create and attach the target for one view. Serialised by {@link ensureSession}. */
238
267
  private buildSession;
268
+ /**
269
+ * Take an unclaimed blank page, if the browser has one to spare.
270
+ *
271
+ * "Unclaimed" is what matters: a blank page the plugin is already driving belongs to a view, and
272
+ * handing the same target to a second view would make two views share one page. The page kept by
273
+ * {@link normalizeBlankPages} is exactly the unclaimed one this is looking for.
274
+ * @param client - the connected browser.
275
+ * @returns the target id to reuse, or undefined when a new page has to be created.
276
+ */
277
+ private adoptBlankTarget;
278
+ /** Create one page, refusing to go on without an id. */
279
+ private createTarget;
280
+ /**
281
+ * Collapse the blank pages a freshly started browser comes with down to one.
282
+ *
283
+ * Chrome opens more than one page when it is given a URL on top of its own start page, and this
284
+ * is the browser's behaviour rather than a session restore: measured on a cleanly exited profile
285
+ * as well as a crashed one, both pages appear within a second of launch. There is nothing to
286
+ * configure away — by the time a client can talk to the browser, both already exist.
287
+ *
288
+ * Left alone the surplus page is invisible to the plugin's tab model and therefore unclosable
289
+ * through the tools, so it survives every navigation and accumulates for as long as the window
290
+ * lives. One is kept deliberately: {@link buildSession} adopts it instead of creating a third.
291
+ *
292
+ * `Target.setDiscoverTargets` has to come first. Without it the browser may omit pages that
293
+ * existed before this client connected — which is precisely the page that needs closing, and the
294
+ * reason the first attempt at this fix failed silently.
295
+ * @param client - the connected browser.
296
+ * @returns how many surplus blank pages were closed.
297
+ */
298
+ private normalizeBlankPages;
239
299
  /** Views whose session is being created right now, so a second caller waits for the first. */
240
300
  private readonly sessionBuilding;
241
301
  /** sessionId -> targetId, so a destroyed view can close the right page. */
@@ -36,6 +36,15 @@ import { join } from 'node:path';
36
36
  * exited, and that exit was reported as a bad browser path.
37
37
  */
38
38
  const EXIT_GRACE_MS = 3_000;
39
+ /**
40
+ * How long a browser is given to close itself after being asked, before it is killed.
41
+ *
42
+ * Long enough for Chrome to flush its profile, which is what keeps the next launch from reading
43
+ * the profile as crashed and offering a session restore. Deliberately not the same knob as
44
+ * {@link EXIT_GRACE_MS}: that one waits for a browser to die because a NEW one cannot start until
45
+ * it does, this one merely prefers a graceful exit to a kill.
46
+ */
47
+ const CLOSE_GRACE_MS = 6_000;
39
48
  /**
40
49
  * Ceiling on one attempt to ask a discovered CDP port who it is.
41
50
  *
@@ -147,6 +156,21 @@ const INSTALL_LOCATIONS = {
147
156
  * @param env - the environment to resolve against.
148
157
  * @returns the path with %NAME% references replaced, or the entry unchanged when unknown.
149
158
  */
159
+ /**
160
+ * Whether a target URL is one of the blanks a browser opens by itself.
161
+ *
162
+ * Deliberately narrow: `about:blank` and Chrome's own new-tab pages, and nothing else. A wider
163
+ * test ("has no path", "starts with chrome://") would sooner or later match a page the user is
164
+ * reading, and the caller closes what this returns.
165
+ * @param url - the target's reported URL.
166
+ */
167
+ function isBlankUrl(url) {
168
+ if (typeof url !== 'string')
169
+ return false;
170
+ return url === 'about:blank'
171
+ || url.startsWith('chrome://newtab')
172
+ || url.startsWith('chrome://new-tab-page');
173
+ }
150
174
  function expandLocation(location, env) {
151
175
  return location.replace(/%([^%]+)%/g, (whole, name) => env[name] ?? env[name.toUpperCase()] ?? env[name.toLowerCase()] ?? whole);
152
176
  }
@@ -385,6 +409,16 @@ export class SystemBrowserViewHost {
385
409
  */
386
410
  clientSuspect = false;
387
411
  child;
412
+ /**
413
+ * True when the browser in use was adopted from a previous run rather than launched here.
414
+ *
415
+ * Worth recording because the two differ on teardown: a browser this host spawned is its to
416
+ * kill, while one it merely took over belongs to a process that is already gone — closing it is
417
+ * still right (it holds the profile nobody else can use), but it is a decision, not cleanup.
418
+ */
419
+ adoptedRunning = false;
420
+ /** Whether this connection's surplus blank pages have been collapsed already. */
421
+ blanksNormalized = false;
388
422
  starting;
389
423
  /**
390
424
  * Browsers that ignored a kill.
@@ -468,6 +502,10 @@ export class SystemBrowserViewHost {
468
502
  forgetBrowserState() {
469
503
  this.views.clear();
470
504
  this.sessions.clear();
505
+ // A new browser comes with its own surplus pages, so the tidy-up runs again for it. Without
506
+ // this, a restart would inherit the previous connection's "already handled" and leave the new
507
+ // browser's extra blank page in place for the rest of the session.
508
+ this.blanksNormalized = false;
471
509
  }
472
510
  async ensureClient() {
473
511
  // A disposed host must never start a browser: doing so would spawn a process
@@ -501,7 +539,78 @@ export class SystemBrowserViewHost {
501
539
  }
502
540
  // Concurrent first calls share one startup rather than racing two browsers.
503
541
  this.starting ??= this.start().finally(() => { this.starting = undefined; });
504
- return await this.starting;
542
+ const client = await this.starting;
543
+ // Once per connection. A browser opens more than one page when it starts, and the surplus is
544
+ // invisible to the plugin's tab model — see normalizeBlankPages for why that matters.
545
+ if (!this.blanksNormalized) {
546
+ this.blanksNormalized = true;
547
+ await this.normalizeBlankPages(client);
548
+ }
549
+ return client;
550
+ }
551
+ /**
552
+ * Take over a browser already serving this profile, instead of racing it.
553
+ *
554
+ * A previous DSH process leaves its browser running: the child handle died with that process, so
555
+ * nothing here knows about it, and `start()`'s cleanup branch is skipped because it keys on
556
+ * `this.child`. Launching then does something worse than fail — on Windows Chrome hands the
557
+ * request to the instance already holding the profile and exits 0, and that exit is read as a
558
+ * launch failure. The user is told to go kill the leftovers by hand, on every restart, which is
559
+ * exactly what was reported.
560
+ *
561
+ * The launch loop deliberately refuses to trust a `<profile>/DevToolsActivePort` left by a
562
+ * previous launch, and it is right to: a stale port number can be reused by something that is
563
+ * not a browser at all. What makes adoption safe HERE is that this runs BEFORE launching, on a
564
+ * file that lives inside this plugin's own profile directory — only a browser started with this
565
+ * `--user-data-dir` writes it — and the endpoint is then asked to identify itself.
566
+ * @param why - a short phrase for the log, naming which attempt this is.
567
+ * @returns a connected client for the running browser, or undefined when there is none.
568
+ */
569
+ async adoptRunningBrowser(why) {
570
+ const portFile = join(this.profileDir, 'DevToolsActivePort');
571
+ let port;
572
+ try {
573
+ if (!existsSync(portFile))
574
+ return undefined;
575
+ const value = Number(readFileSync(portFile, 'utf8').split('\n')[0]?.trim() ?? '');
576
+ if (!Number.isInteger(value) || value <= 0)
577
+ return undefined;
578
+ port = value;
579
+ }
580
+ catch {
581
+ return undefined;
582
+ }
583
+ try {
584
+ const version = await (await fetch(`http://127.0.0.1:${port}/json/version`, {
585
+ signal: AbortSignal.timeout(PORT_PROBE_MS),
586
+ })).json();
587
+ if (typeof version.webSocketDebuggerUrl !== 'string')
588
+ return undefined;
589
+ // The port number alone proves nothing — it can be reused once the browser that owned it
590
+ // exits. The endpoint's own answer is the evidence, and it has to name a browser.
591
+ if (typeof version.Browser !== 'string' || !/chrome|chromium|edge/i.test(version.Browser))
592
+ return undefined;
593
+ const client = new CdpClient(version.webSocketDebuggerUrl);
594
+ await client.whenReady();
595
+ // Asked through the protocol too, so a websocket that merely accepts connections is not
596
+ // mistaken for a browser that answers.
597
+ const info = await client.send('Browser.getVersion', {});
598
+ if (typeof info?.product !== 'string' || !/chrome|chromium|edge/i.test(info.product)) {
599
+ client.close();
600
+ return undefined;
601
+ }
602
+ if (this.disposed) {
603
+ client.close();
604
+ return undefined;
605
+ }
606
+ this.client = client;
607
+ this.adoptedRunning = true;
608
+ return client;
609
+ }
610
+ catch {
611
+ // No endpoint, nothing listening, or not a browser: all of them mean "launch one".
612
+ return undefined;
613
+ }
505
614
  }
506
615
  /**
507
616
  * Launch the browser with a private profile and connect over CDP.
@@ -512,6 +621,12 @@ export class SystemBrowserViewHost {
512
621
  * @returns the connected client.
513
622
  */
514
623
  async start() {
624
+ // Before spawning anything: is a browser for THIS profile already running? A DSH restart
625
+ // leaves one behind, and launching into it is the failure reported as "exited immediately
626
+ // after launch (exit code 0)" that required killing seven processes by hand.
627
+ const adopted = await this.adoptRunningBrowser('before launch');
628
+ if (adopted !== undefined)
629
+ return adopted;
515
630
  const args = [
516
631
  '--remote-debugging-port=0',
517
632
  `--user-data-dir=${this.profileDir}`,
@@ -569,15 +684,14 @@ export class SystemBrowserViewHost {
569
684
  });
570
685
  });
571
686
  }
572
- // The port file belongs to the browser that just died, and it is NOT removed when the
573
- // process is killed: nothing gives a terminated browser a chance to clean up. The launch
574
- // loop below reads it to learn the port, so a stale file means connecting to whatever else
575
- // holds that port — or to the previous instance — and publishing a client this host does
576
- // not own and dispose() can never kill.
577
- try {
578
- rmSync(join(this.profileDir, 'DevToolsActivePort'), { force: true });
579
- }
580
- catch { /* nothing to remove */ }
687
+ // The port file is deliberately NOT deleted here, and that is a reversal.
688
+ //
689
+ // It used to be removed so the launch loop below could not read a previous browser's port. But
690
+ // the loop already refuses any file older than the spawn (`freshPort` compares mtimes), so the
691
+ // deletion never protected it — while it DID destroy the only record of a browser that may
692
+ // still be running for this profile. That record is what `adoptRunningBrowser` needs when the
693
+ // launch comes back as a hand-off: the browser was alive the whole time, and without the file
694
+ // there is no way to find it again.
581
695
  const spawnedAt = Date.now();
582
696
  let child;
583
697
  try {
@@ -728,6 +842,14 @@ export class SystemBrowserViewHost {
728
842
  const code = child.exitCode;
729
843
  const signal = child.signalCode;
730
844
  const clean = code === 0 && signal === null;
845
+ // Exit code 0 IS the hand-off: the request went to an instance already holding this profile,
846
+ // so that instance is our browser and it is running right now. Adopting it turns the
847
+ // reported "kill the leftovers by hand, on every restart" into a call that just works.
848
+ if (clean) {
849
+ const recovered = await this.adoptRunningBrowser('after a hand-off');
850
+ if (recovered !== undefined)
851
+ return recovered;
852
+ }
731
853
  throw new Error(clean
732
854
  ? `dsh-builtin-browser: ${target} exited immediately after launch (exit code 0). `
733
855
  + 'That means it handed the request to an instance already using this profile '
@@ -892,20 +1014,39 @@ export class SystemBrowserViewHost {
892
1014
  if (this.disposed)
893
1015
  return;
894
1016
  this.disposed = true;
1017
+ const client = this.client;
895
1018
  this.forgetBrowserState();
896
- this.client?.close();
897
- try {
898
- this.child?.kill();
899
- }
900
- catch { /* already gone */ }
901
- // And anything that ignored an earlier kill. They hold this profile, so leaving them
902
- // running means the next launch contends with a browser nobody owns.
903
- for (const leftover of this.stubborn.splice(0)) {
1019
+ this.client = undefined;
1020
+ // Ask before killing. `kill()` on Windows is TerminateProcess, which gives the browser no
1021
+ // chance to flush its profile — and Chrome records how it ended in that profile, so a
1022
+ // hard kill leaves it marked as a crash and the NEXT launch offers to restore a session.
1023
+ // That is a visible symptom (an extra window) traced all the way back to this line.
1024
+ if (client !== undefined) {
904
1025
  try {
905
- leftover.kill();
1026
+ void client.send('Browser.close', {}).catch(() => { });
906
1027
  }
907
1028
  catch { /* already gone */ }
908
1029
  }
1030
+ // Both the browser we launched and anything that ignored an earlier kill. They hold this
1031
+ // profile, so leaving them running means the next launch contends with a browser nobody owns.
1032
+ const doomed = [this.child, ...this.stubborn.splice(0)].filter((entry) => entry !== undefined);
1033
+ this.child = undefined;
1034
+ for (const dying of doomed) {
1035
+ /**
1036
+ * A bounded reprieve, then the kill we used to do immediately.
1037
+ *
1038
+ * The timer is unref'd so it never holds the process open: a DSH shutdown that is itself
1039
+ * ending does not wait for this, and the browser — having been asked to close — generally
1040
+ * gets there on its own. If it does not, the kill still lands while the process lives.
1041
+ */
1042
+ const force = () => { try {
1043
+ dying.kill();
1044
+ }
1045
+ catch { /* already gone */ } };
1046
+ const timer = setTimeout(force, CLOSE_GRACE_MS);
1047
+ timer.unref?.();
1048
+ dying.once('exit', () => { clearTimeout(timer); });
1049
+ }
909
1050
  const ephemeral = this.ephemeralDir;
910
1051
  if (ephemeral !== undefined && this.child !== undefined) {
911
1052
  // The browser flushes its profile while shutting down, so deleting later avoids
@@ -957,10 +1098,15 @@ export class SystemBrowserViewHost {
957
1098
  /** Create and attach the target for one view. Serialised by {@link ensureSession}. */
958
1099
  async buildSession(viewId) {
959
1100
  const client = await this.ensureClient();
960
- const created = await client.send('Target.createTarget', { url: 'about:blank' });
961
- const targetId = created?.targetId;
962
- if (typeof targetId !== 'string')
963
- throw new Error('dsh-builtin-browser: the browser did not create a page');
1101
+ // Adopt a blank page the browser opened on its own before creating another one.
1102
+ //
1103
+ // Chrome starts with more than one page when handed a URL on top of its own start page, so
1104
+ // creating a target here discarded the original and left it behind as a tab that is not in
1105
+ // the plugin's tab model — invisible to `dsh_browser_list_tabs`, and therefore unclosable
1106
+ // through the tools. Taking the blank instead makes the page the browser already opened the
1107
+ // one the caller gets.
1108
+ const adopted = await this.adoptBlankTarget(client);
1109
+ const targetId = adopted ?? await this.createTarget(client);
964
1110
  const attached = await client.send('Target.attachToTarget', { targetId, flatten: true });
965
1111
  const sessionId = attached?.sessionId;
966
1112
  if (typeof sessionId !== 'string')
@@ -970,6 +1116,94 @@ export class SystemBrowserViewHost {
970
1116
  this.views.set(viewId, { session: sessionId, client });
971
1117
  return sessionId;
972
1118
  }
1119
+ /**
1120
+ * Take an unclaimed blank page, if the browser has one to spare.
1121
+ *
1122
+ * "Unclaimed" is what matters: a blank page the plugin is already driving belongs to a view, and
1123
+ * handing the same target to a second view would make two views share one page. The page kept by
1124
+ * {@link normalizeBlankPages} is exactly the unclaimed one this is looking for.
1125
+ * @param client - the connected browser.
1126
+ * @returns the target id to reuse, or undefined when a new page has to be created.
1127
+ */
1128
+ async adoptBlankTarget(client) {
1129
+ try {
1130
+ const listed = await client.send('Target.getTargets', {});
1131
+ const claimed = new Set(this.sessions.values());
1132
+ for (const entry of listed?.targetInfos ?? []) {
1133
+ if (entry.type !== 'page' || typeof entry.targetId !== 'string')
1134
+ continue;
1135
+ if (!isBlankUrl(entry.url))
1136
+ continue;
1137
+ if (claimed.has(entry.targetId))
1138
+ continue;
1139
+ return entry.targetId;
1140
+ }
1141
+ return undefined;
1142
+ }
1143
+ catch {
1144
+ // Discovery is best-effort; creating a page is the behaviour this replaced.
1145
+ return undefined;
1146
+ }
1147
+ }
1148
+ /** Create one page, refusing to go on without an id. */
1149
+ async createTarget(client) {
1150
+ const created = await client.send('Target.createTarget', { url: 'about:blank' });
1151
+ const targetId = created?.targetId;
1152
+ if (typeof targetId !== 'string')
1153
+ throw new Error('dsh-builtin-browser: the browser did not create a page');
1154
+ return targetId;
1155
+ }
1156
+ /**
1157
+ * Collapse the blank pages a freshly started browser comes with down to one.
1158
+ *
1159
+ * Chrome opens more than one page when it is given a URL on top of its own start page, and this
1160
+ * is the browser's behaviour rather than a session restore: measured on a cleanly exited profile
1161
+ * as well as a crashed one, both pages appear within a second of launch. There is nothing to
1162
+ * configure away — by the time a client can talk to the browser, both already exist.
1163
+ *
1164
+ * Left alone the surplus page is invisible to the plugin's tab model and therefore unclosable
1165
+ * through the tools, so it survives every navigation and accumulates for as long as the window
1166
+ * lives. One is kept deliberately: {@link buildSession} adopts it instead of creating a third.
1167
+ *
1168
+ * `Target.setDiscoverTargets` has to come first. Without it the browser may omit pages that
1169
+ * existed before this client connected — which is precisely the page that needs closing, and the
1170
+ * reason the first attempt at this fix failed silently.
1171
+ * @param client - the connected browser.
1172
+ * @returns how many surplus blank pages were closed.
1173
+ */
1174
+ async normalizeBlankPages(client) {
1175
+ try {
1176
+ await client.send('Target.setDiscoverTargets', { discover: true });
1177
+ const listed = await client.send('Target.getTargets', {});
1178
+ const blanks = (listed?.targetInfos ?? []).filter(entry => entry.type === 'page' && typeof entry.targetId === 'string' && isBlankUrl(entry.url));
1179
+ const claimed = new Set(this.sessions.values());
1180
+ let closed = 0;
1181
+ // Keep the first unclaimed blank (that is the one buildSession will adopt) and close the rest.
1182
+ let kept = false;
1183
+ for (const entry of blanks) {
1184
+ const targetId = entry.targetId;
1185
+ if (claimed.has(targetId))
1186
+ continue;
1187
+ if (!kept) {
1188
+ kept = true;
1189
+ continue;
1190
+ }
1191
+ try {
1192
+ await client.send('Target.closeTarget', { targetId });
1193
+ closed += 1;
1194
+ }
1195
+ catch {
1196
+ // It may already be gone; one failure must not stop the rest.
1197
+ }
1198
+ }
1199
+ return closed;
1200
+ }
1201
+ catch {
1202
+ // A browser that will not answer Target commands has a real problem, and the next command
1203
+ // will report it. Turning a tidy-up into a functional error here would hide that.
1204
+ return 0;
1205
+ }
1206
+ }
973
1207
  /** Views whose session is being created right now, so a second caller waits for the first. */
974
1208
  sessionBuilding = new Map();
975
1209
  /** sessionId -> targetId, so a destroyed view can close the right page. */
package/lib/index.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * pieces. The cordis.patch.yml rows reference subpath exports:
4
4
  * - `dsh-builtin-browser/browser` -> the ctx.browser seam (Service)
5
5
  * - `dsh-builtin-browser/browser-electron` -> the Electron CDP provider
6
- * - `dsh-builtin-browser/tool-browser` -> the model-facing browser_* tools
6
+ * - `dsh-builtin-browser/tool-browser` -> the model-facing dsh_browser_* tools
7
7
  * The capability itself is composed by those rows; this root entry exists for
8
8
  * programmatic imports AND because the host's client-module scan identifies a
9
9
  * package's browser half through a loader row whose specifier resolves to the
package/lib/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * pieces. The cordis.patch.yml rows reference subpath exports:
4
4
  * - `dsh-builtin-browser/browser` -> the ctx.browser seam (Service)
5
5
  * - `dsh-builtin-browser/browser-electron` -> the Electron CDP provider
6
- * - `dsh-builtin-browser/tool-browser` -> the model-facing browser_* tools
6
+ * - `dsh-builtin-browser/tool-browser` -> the model-facing dsh_browser_* tools
7
7
  * The capability itself is composed by those rows; this root entry exists for
8
8
  * programmatic imports AND because the host's client-module scan identifies a
9
9
  * package's browser half through a loader row whose specifier resolves to the
@@ -8,9 +8,9 @@
8
8
  * and the tool's `output.schema` declared the properties. Nothing connected them, and the
9
9
  * result is this project's most repeatable bug:
10
10
  *
11
- * - `browser_a11y`'s `selector` — computed by the provider, declared by the schema,
11
+ * - `dsh_browser_a11y`'s `selector` — computed by the provider, declared by the schema,
12
12
  * printed by the renderer, and dropped by the projection. 134 tests stayed green.
13
- * - `browser_screenshot`'s `width`/`height` — measured by the provider, declared by the
13
+ * - `dsh_browser_screenshot`'s `width`/`height` — measured by the provider, declared by the
14
14
  * schema, and dropped by the projection. 172 tests stayed green. It was found a round
15
15
  * later, by reading the three places that agree and noticing the fourth did not.
16
16
  *
@@ -24,7 +24,7 @@
24
24
  * projection forgets is silently dropped. One table cannot get either wrong on its own.
25
25
  */
26
26
  /**
27
- * The `elements` item schema for browser_snapshot and browser_open.
27
+ * The `elements` item schema for dsh_browser_snapshot and dsh_browser_open.
28
28
  *
29
29
  * Written as a literal rather than generated, because `defineTool` validates the output schema
30
30
  * against its own `ValueSchemaSpec` and a generated `Record<string, …>` is too wide to satisfy
@@ -65,7 +65,7 @@ export declare const ELEMENT_ITEM_SCHEMA: {
65
65
  };
66
66
  };
67
67
  /**
68
- * The `nodes` item schema for browser_a11y.
68
+ * The `nodes` item schema for dsh_browser_a11y.
69
69
  *
70
70
  * Same reasoning as {@link ELEMENT_ITEM_SCHEMA}: literal for the type checker, table-backed by
71
71
  * test.
@@ -8,9 +8,9 @@
8
8
  * and the tool's `output.schema` declared the properties. Nothing connected them, and the
9
9
  * result is this project's most repeatable bug:
10
10
  *
11
- * - `browser_a11y`'s `selector` — computed by the provider, declared by the schema,
11
+ * - `dsh_browser_a11y`'s `selector` — computed by the provider, declared by the schema,
12
12
  * printed by the renderer, and dropped by the projection. 134 tests stayed green.
13
- * - `browser_screenshot`'s `width`/`height` — measured by the provider, declared by the
13
+ * - `dsh_browser_screenshot`'s `width`/`height` — measured by the provider, declared by the
14
14
  * schema, and dropped by the projection. 172 tests stayed green. It was found a round
15
15
  * later, by reading the three places that agree and noticing the fourth did not.
16
16
  *
@@ -24,7 +24,7 @@
24
24
  * projection forgets is silently dropped. One table cannot get either wrong on its own.
25
25
  */
26
26
  /**
27
- * The fields of one entry in `browser_snapshot` / `browser_open`'s `elements`.
27
+ * The fields of one entry in `dsh_browser_snapshot` / `dsh_browser_open`'s `elements`.
28
28
  *
29
29
  * `selector` is the reason this file exists; see the header. It was added to the schema and
30
30
  * the projection separately, and only one of them landed.
@@ -39,7 +39,7 @@ const ELEMENT_FIELDS = {
39
39
  frame: { schema: { type: 'boolean' } },
40
40
  };
41
41
  /**
42
- * The fields of one entry in `browser_a11y`'s `nodes`.
42
+ * The fields of one entry in `dsh_browser_a11y`'s `nodes`.
43
43
  *
44
44
  * Different shape from an element on purpose: a role and an accessible name carry meaning a
45
45
  * tag and a label string do not, and `depth` is what renders the tree.
@@ -80,7 +80,7 @@ function project(fields, source) {
80
80
  return out;
81
81
  }
82
82
  /**
83
- * The `elements` item schema for browser_snapshot and browser_open.
83
+ * The `elements` item schema for dsh_browser_snapshot and dsh_browser_open.
84
84
  *
85
85
  * Written as a literal rather than generated, because `defineTool` validates the output schema
86
86
  * against its own `ValueSchemaSpec` and a generated `Record<string, …>` is too wide to satisfy
@@ -102,7 +102,7 @@ export const ELEMENT_ITEM_SCHEMA = {
102
102
  },
103
103
  };
104
104
  /**
105
- * The `nodes` item schema for browser_a11y.
105
+ * The `nodes` item schema for dsh_browser_a11y.
106
106
  *
107
107
  * Same reasoning as {@link ELEMENT_ITEM_SCHEMA}: literal for the type checker, table-backed by
108
108
  * test.
@@ -1,14 +1,14 @@
1
1
  /**
2
- * Model-facing browser tools over `ctx.browser`: `browser_open`,
3
- * `browser_snapshot`, `browser_execute`, `browser_content`,
4
- * `browser_screenshot`, and tab management (`browser_list_tabs`,
5
- * `browser_switch_tab`, `browser_close_tab`, `browser_reset`).
2
+ * Model-facing browser tools over `ctx.browser`: `dsh_browser_open`,
3
+ * `dsh_browser_snapshot`, `dsh_browser_execute`, `dsh_browser_content`,
4
+ * `dsh_browser_screenshot`, and tab management (`dsh_browser_list_tabs`,
5
+ * `dsh_browser_switch_tab`, `dsh_browser_close_tab`, `dsh_browser_reset`).
6
6
  *
7
7
  * The tool layer owns only the model-facing schema, argument validation, and
8
8
  * result formatting — never provider selection or page driving, which belong
9
9
  * to the seam. Session lifecycle is owned here at the plugin level: each
10
10
  * calling task (a DSH session) gets its own browser session — the first
11
- * `browser_open` (or any tool when no session exists) opens it, and later
11
+ * `dsh_browser_open` (or any tool when no session exists) opens it, and later
12
12
  * tools in the same task reuse it. Concurrent tasks therefore never fight
13
13
  * over tabs, history, or navigation state.
14
14
  * @module dsh-browser/tool-browser
@@ -38,7 +38,7 @@ export interface Config {
38
38
  * Tools that only observe, plus the ones that undo a restriction.
39
39
  *
40
40
  * Never subject to an allow-list: restricting what the agent may *do* must not also
41
- * blind it or trap it, and the tool descriptions promise as much. browser_restrict
41
+ * blind it or trap it, and the tool descriptions promise as much. dsh_browser_restrict
42
42
  * is in here so a task that restricted everything can still lift it.
43
43
  */
44
44
  export declare const READ_ONLY_TOOLS: ReadonlySet<string>;