@angelky7/dsh-browser-control 1.6.0 → 1.7.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/README.en.md CHANGED
@@ -64,8 +64,8 @@ In one line: **for "use MY browser" tasks (logged-in Bilibili, Zhihu, admin pane
64
64
 
65
65
  | File | Purpose |
66
66
  |---|---|
67
- | [DSH-Browser-Control-v1.6.0.zip](https://github.com/angelky7/dsh-browser-control/releases/download/v1.6.0/DSH-Browser-Control-v1.6.0.zip) | Chrome extension (unzip and load; this release adds the windows commands — reinstall required) |
68
- | [dsh-browser-control-plugin-v1.6.0.zip](https://github.com/angelky7/dsh-browser-control/releases/download/v1.6.0/dsh-browser-control-plugin-v1.6.0.zip) | dsh plugin (offline fallback; online installs use Option A/B) |
67
+ | [DSH-Browser-Control-v1.7.0.zip](https://github.com/angelky7/dsh-browser-control/releases/download/v1.7.0/DSH-Browser-Control-v1.7.0.zip) | Chrome extension (unzip and load; this release adds the history navigation commands — reinstall required) |
68
+ | [dsh-browser-control-plugin-v1.7.0.zip](https://github.com/angelky7/dsh-browser-control/releases/download/v1.7.0/dsh-browser-control-plugin-v1.7.0.zip) | dsh plugin (offline fallback; online installs use Option A/B) |
69
69
 
70
70
  ## Install the Chrome extension (30 seconds)
71
71
 
@@ -104,7 +104,7 @@ npm install @angelky7/dsh-browser-control
104
104
 
105
105
  ```bash
106
106
  # Straight from GitHub
107
- dsh plugin --profile web add "github:angelky7/dsh-browser-control#v1.6.0"
107
+ dsh plugin --profile web add "github:angelky7/dsh-browser-control#v1.7.0"
108
108
 
109
109
  # Local checkout for debugging (note: the explicit file: prefix is required)
110
110
  dsh plugin --profile web add "file:D:\path\to\dsh-browser-control"
@@ -256,9 +256,9 @@ curl -X POST http://127.0.0.1:9777/api/command \
256
256
  | `browser_dialog` | Read or set the native-dialog policy accept/dismiss/manual, plus the recent dialog log (v1.0.9+) |
257
257
  | `browser_hold` | Put or remove a cancellable take-over overlay on a tab (tells the user, stops mis-clicks); `covered:false` says why it could not cover the page. A soft layer, not a security boundary |
258
258
  | `browser_cookies` | Read/set/remove cookies visible to a tab (`action: get/set/remove`; get takes a `domain` filter, set/remove need a `url` or `domain` anchor, remove re-checks and reports `remaining`). **Cookie values are credentials** — a get puts them fully into the model-visible text; see SECURITY.md |
259
- | `browser_tabs` | Tab management (list/open/close/activate/group/ungroup; see "What agent tabs look like") |
259
+ | `browser_tabs` | Tab management (list/open/close/activate/group/ungroup; v1.7.0+ history back/forward — same arrival judging as navigate, reports unchanged at the history edge) |
260
260
  | `browser_evaluate` | Run arbitrary JS |
261
- | `browser_screenshot` | Capture page screenshot |
261
+ | `browser_screenshot` | Capture page screenshot (v1.7.0+ receipt carries the real pixel size; fullPage captures the whole scrollable page, selector captures one element) |
262
262
  | `browser_console_log` | Captured console entries, listed with their text (v1.0.7+) |
263
263
  | `browser_network_log` | Captured HTTP requests, listed with method/status/URL (v1.0.7+) |
264
264
  | `browser_network_clear` | Clear the network log buffer on its own (v1.0.7+) |
package/README.md CHANGED
@@ -64,8 +64,8 @@ Chrome 扩展(CDP 驱动)
64
64
 
65
65
  | 文件 | 说明 |
66
66
  |---|---|
67
- | [DSH-Browser-Control-v1.6.0.zip](https://github.com/angelky7/dsh-browser-control/releases/download/v1.6.0/DSH-Browser-Control-v1.6.0.zip) | Chrome 扩展(解压后加载;这一版扩展新增 windows 窗口控制命令,**必须重装**) |
68
- | [dsh-browser-control-plugin-v1.6.0.zip](https://github.com/angelky7/dsh-browser-control/releases/download/v1.6.0/dsh-browser-control-plugin-v1.6.0.zip) | dsh 插件(离线兜底,在线装直接用方式 A/B) |
67
+ | [DSH-Browser-Control-v1.7.0.zip](https://github.com/angelky7/dsh-browser-control/releases/download/v1.7.0/DSH-Browser-Control-v1.7.0.zip) | Chrome 扩展(解压后加载;这一版扩展新增 tabs.back/forward 历史导航命令,**必须重装**) |
68
+ | [dsh-browser-control-plugin-v1.7.0.zip](https://github.com/angelky7/dsh-browser-control/releases/download/v1.7.0/dsh-browser-control-plugin-v1.7.0.zip) | dsh 插件(离线兜底,在线装直接用方式 A/B) |
69
69
 
70
70
  ## 安装 Chrome 扩展(30 秒)
71
71
 
@@ -104,7 +104,7 @@ npm install @angelky7/dsh-browser-control
104
104
 
105
105
  ```bash
106
106
  # 直接从 GitHub 安装
107
- dsh plugin --profile web add "github:angelky7/dsh-browser-control#v1.6.0"
107
+ dsh plugin --profile web add "github:angelky7/dsh-browser-control#v1.7.0"
108
108
 
109
109
  # 本地目录调试(注意:必须显式 file: 前缀)
110
110
  dsh plugin --profile web add "file:D:\path\to\dsh-browser-control"
@@ -250,9 +250,9 @@ curl -X POST http://127.0.0.1:9777/api/command \
250
250
  | `browser_dialog` | 读改原生弹窗应答策略 accept/dismiss/manual,并回最近弹窗记录(v1.0.9+) |
251
251
  | `browser_hold` | 在某个标签页上加/撤一层可取消的接管浮层(告知用户 + 防误触);`covered:false` 时说明盖不住的原因。软层,不是安全边界 |
252
252
  | `browser_cookies` | 读/设/删某标签页可见的 cookie(`action: get/set/remove`;get 带 `domain` 过滤,set/remove 需要 `url` 或 `domain` 锚点,remove 复查并回报 `remaining`)。**cookie 的值是凭据**,get 会把值完整放进模型可见文本——见 SECURITY.md |
253
- | `browser_tabs` | 标签页管理(列表/新建/关闭/切换/归组/拆组;见「agent 开的标签长什么样」) |
253
+ | `browser_tabs` | 标签页管理(列表/新建/关闭/切换/归组/拆组;v1.7.0 起支持历史 back/forward——落地判据与 navigate 同一套,历史尽头如实报 unchanged) |
254
254
  | `browser_evaluate` | 执行任意 JS |
255
- | `browser_screenshot` | 截取页面截图 |
255
+ | `browser_screenshot` | 截取页面截图(v1.7.0 起回执带**真实像素尺寸**;fullPage 一张截全页、selector 只截一个元素) |
256
256
  | `browser_console_log` | 抓取页面 console 日志并逐条给出正文(v1.0.7+) |
257
257
  | `browser_network_log` | 抓取 HTTP 请求/响应并逐条给出方法/状态/URL(v1.0.7+) |
258
258
  | `browser_network_clear` | 单独清空网络日志缓冲(v1.0.7+) |
package/lib/index.js CHANGED
@@ -117,6 +117,8 @@ const WRITE_COMMANDS = /* @__PURE__ */ new Set([
117
117
  "tabs.closeMany",
118
118
  "tabs.activate",
119
119
  "dialog",
120
+ "tabs.back",
121
+ "tabs.forward",
120
122
  "cleanup",
121
123
  "hold",
122
124
  "windows.open"
@@ -1200,6 +1202,17 @@ function closeTabIds(args) {
1200
1202
  if (ids.length === 0) throw new Error("close requires tabId or tabIds");
1201
1203
  return ids;
1202
1204
  }
1205
+ /** Actual pixel dimensions straight from the PNG's IHDR chunk (width/height are
1206
+ * big-endian at offsets 16/20 after the 8-byte signature + 4-byte length + 4-byte
1207
+ * type). The receipt should say how big the picture IS — fullPage captures are
1208
+ * often 3-5 viewports tall and the model cannot tell from the byte count. */
1209
+ function pngDimensions(buffer) {
1210
+ if (buffer.length < 24 || buffer.toString("latin1", 12, 16) !== "IHDR") return {};
1211
+ return {
1212
+ width: buffer.readUInt32BE(16),
1213
+ height: buffer.readUInt32BE(20)
1214
+ };
1215
+ }
1203
1216
  /** Write one screenshot payload to the shots directory and return its durable location. */
1204
1217
  async function saveScreenshot(controller, payload) {
1205
1218
  const dir = controller.shotsDir;
@@ -1210,12 +1223,15 @@ async function saveScreenshot(controller, payload) {
1210
1223
  const file = path.join(dir, `${stamp}-${random}.${payload.format === "jpeg" ? "jpg" : "png"}`);
1211
1224
  const buffer = Buffer.from(payload.base64, "base64");
1212
1225
  await writeFile(file, buffer);
1226
+ const dims = payload.format === "png" ? pngDimensions(buffer) : {};
1213
1227
  return {
1214
1228
  file,
1215
1229
  bytes: buffer.length,
1216
1230
  tabId: payload.tabId,
1217
1231
  title: payload.tabTitle,
1218
- url: payload.tabUrl
1232
+ url: payload.tabUrl,
1233
+ ...dims,
1234
+ ...payload.elementRect === void 0 ? {} : { elementRect: payload.elementRect }
1219
1235
  };
1220
1236
  }
1221
1237
  /** Write one PDF payload to `path` (absolute or relative to `controller.shotsDir`)
@@ -2041,12 +2057,12 @@ function applyBrowserTools(ctx, controller) {
2041
2057
  }));
2042
2058
  ctx.tools.register(defineTool({
2043
2059
  name: "browser_tabs",
2044
- description: "List tabs, or open/close/activate/group/ungroup. `close` takes one tab (`tabId`) or several (`tabIds`); the result accounts for every id, so a retry is safe.",
2060
+ description: "List tabs, or open/close/activate/group/ungroup, or go back/forward in the tab's history. `close` takes one tab (`tabId`) or several (`tabIds`); the result accounts for every id, so a retry is safe.",
2045
2061
  parameters: {
2046
2062
  action: {
2047
2063
  type: "string",
2048
2064
  required: true,
2049
- description: "One of: list, open, close, activate, group, ungroup."
2065
+ description: "One of: list, open, close, activate, group, ungroup, back, forward."
2050
2066
  },
2051
2067
  url: {
2052
2068
  type: "string",
@@ -2054,7 +2070,7 @@ function applyBrowserTools(ctx, controller) {
2054
2070
  },
2055
2071
  tabId: {
2056
2072
  type: "number",
2057
- description: "Target tab for close/activate/group."
2073
+ description: "Target tab for close/activate/group/back/forward; defaults to the active tab for back/forward."
2058
2074
  },
2059
2075
  tabIds: {
2060
2076
  type: "array",
@@ -2071,11 +2087,11 @@ function applyBrowserTools(ctx, controller) {
2071
2087
  },
2072
2088
  waitUntil: {
2073
2089
  type: "string",
2074
- description: "For open: when the new tab counts as arrived — same tiers as browser_navigate (load default / domcontentloaded / commit / none)."
2090
+ description: "For open/back/forward: when the navigation counts as arrived — same tiers as browser_navigate (load default / domcontentloaded / commit / none). back/forward report unchanged:true + a reason when the tab is already at the oldest/newest history entry."
2075
2091
  },
2076
2092
  timeoutMs: {
2077
2093
  type: "number",
2078
- description: "For open: wait budget, overriding the waitUntil tier (load 15000, commit/domcontentloaded 4000)."
2094
+ description: "For open/back/forward: wait budget, overriding the waitUntil tier (load 15000, commit/domcontentloaded 4000)."
2079
2095
  },
2080
2096
  windowId: {
2081
2097
  type: "number",
@@ -2121,6 +2137,18 @@ function applyBrowserTools(ctx, controller) {
2121
2137
  return ids.length === 1 ? await controller.execute("tabs.close", { tabId: ids[0] }, exec.signal) : await controller.execute("tabs.closeMany", { tabIds: ids }, exec.signal);
2122
2138
  }
2123
2139
  case "activate": return await controller.execute("tabs.activate", { tabId: requireTabId(args, "activate") }, exec.signal);
2140
+ case "back":
2141
+ case "forward": {
2142
+ const waitUntil = args.waitUntil === void 0 ? "load" : String(args.waitUntil);
2143
+ if (!(waitUntil in NAV_TIERS)) throw new Error(`browser_tabs ${args.action}: waitUntil must be one of none, commit, domcontentloaded, load — got "${waitUntil}"`);
2144
+ const params = { waitUntil };
2145
+ if (args.tabId !== void 0) params.tabId = args.tabId;
2146
+ let budgetMs = NAV_TIERS[waitUntil];
2147
+ if (typeof args.timeoutMs === "number" && Number.isFinite(args.timeoutMs)) budgetMs = Math.min(NAV_MAX_MS, Math.max(100, Math.trunc(args.timeoutMs)));
2148
+ params.timeoutMs = budgetMs;
2149
+ if (args.action === "back") return await controller.execute("tabs.back", params, exec.signal, budgetMs + BRIDGE_MARGIN_MS);
2150
+ return await controller.execute("tabs.forward", params, exec.signal, budgetMs + BRIDGE_MARGIN_MS);
2151
+ }
2124
2152
  case "group": return await controller.execute("tabs.group", { tabId: requireTabId(args, "group") }, exec.signal);
2125
2153
  case "ungroup": {
2126
2154
  const params = {};
@@ -2326,7 +2354,7 @@ function applyBrowserTools(ctx, controller) {
2326
2354
  }));
2327
2355
  ctx.tools.register(defineTool({
2328
2356
  name: "browser_screenshot",
2329
- description: "Capture the tab as PNG/JPEG, save it under the configured shots directory, and return the absolute file path.",
2357
+ description: "Capture the tab as PNG/JPEG, save it under the configured shots directory, and return the absolute file path plus the image's real pixel size. fullPage captures the whole scrollable page in one shot (sticky/fixed headers may repeat); selector captures one element.",
2330
2358
  parameters: {
2331
2359
  tabId: {
2332
2360
  type: "number",
@@ -2334,7 +2362,11 @@ function applyBrowserTools(ctx, controller) {
2334
2362
  },
2335
2363
  fullPage: {
2336
2364
  type: "boolean",
2337
- description: "Capture beyond the viewport."
2365
+ description: "Capture the entire scrollable page instead of just the viewport. The receipt's width/height show the real size."
2366
+ },
2367
+ selector: {
2368
+ type: "string",
2369
+ description: "CSS selector: capture just that element (scrolled into view); the receipt carries its rect."
2338
2370
  },
2339
2371
  format: {
2340
2372
  type: "string",
@@ -2359,12 +2391,36 @@ function applyBrowserTools(ctx, controller) {
2359
2391
  required: true
2360
2392
  },
2361
2393
  title: { type: "string" },
2362
- url: { type: "string" }
2394
+ url: { type: "string" },
2395
+ width: { type: "number" },
2396
+ height: { type: "number" },
2397
+ elementRect: {
2398
+ type: "object",
2399
+ additionalProperties: false,
2400
+ properties: {
2401
+ x: {
2402
+ type: "number",
2403
+ required: true
2404
+ },
2405
+ y: {
2406
+ type: "number",
2407
+ required: true
2408
+ },
2409
+ w: {
2410
+ type: "number",
2411
+ required: true
2412
+ },
2413
+ h: {
2414
+ type: "number",
2415
+ required: true
2416
+ }
2417
+ }
2418
+ }
2363
2419
  }
2364
2420
  },
2365
2421
  render: (_args, value) => [{
2366
2422
  type: "text",
2367
- text: `Saved ${value.file} (${value.bytes} bytes)`
2423
+ text: `Saved ${value.file} (${value.bytes} bytes${value.width !== void 0 && value.height !== void 0 ? `, ${value.width}×${value.height}` : ""}${value.elementRect !== void 0 ? `, element at ${value.elementRect.x},${value.elementRect.y} size ${value.elementRect.w}×${value.elementRect.h}` : ""})`
2368
2424
  }]
2369
2425
  },
2370
2426
  presentCall: () => ({
@@ -2376,6 +2432,7 @@ function applyBrowserTools(ctx, controller) {
2376
2432
  const params = { format: args.format === "jpeg" ? "jpeg" : "png" };
2377
2433
  if (args.tabId !== void 0) params.tabId = args.tabId;
2378
2434
  if (args.fullPage !== void 0) params.fullPage = args.fullPage;
2435
+ if (args.selector !== void 0) params.selector = args.selector;
2379
2436
  return saveScreenshot(controller, await controller.execute("screenshot", params, exec.signal));
2380
2437
  }
2381
2438
  }));
@@ -41,6 +41,9 @@ export const HOLD_ABORTED_MARKER = 'browser-hold-aborted:';
41
41
  const WRITE_COMMANDS = new Set([
42
42
  'nav', 'click', 'input', 'press', 'scroll', 'eval', 'emulate',
43
43
  'tabs.open', 'tabs.close', 'tabs.closeMany', 'tabs.activate', 'dialog',
44
+ // 历史导航与 nav 同族:超时/掉线后重试可能**多跳一条**历史(第一次 back 其实
45
+ // 已经落了地),报 unknown 才诚实。
46
+ 'tabs.back', 'tabs.forward',
44
47
  // 这条不是扩展命令,而是插件本地命令 `browser_cleanup`:它逐条 rm,中途失败
45
48
  // (Windows 上被占用的截图会 EPERM)时前面的文件已经没了,报 not_started
46
49
  // 就是告诉模型「什么都没发生,重试安全」,而那正好是假的。
@@ -309,6 +309,15 @@ function closeTabIds(args) {
309
309
  throw new Error('close requires tabId or tabIds');
310
310
  return ids;
311
311
  }
312
+ /** Actual pixel dimensions straight from the PNG's IHDR chunk (width/height are
313
+ * big-endian at offsets 16/20 after the 8-byte signature + 4-byte length + 4-byte
314
+ * type). The receipt should say how big the picture IS — fullPage captures are
315
+ * often 3-5 viewports tall and the model cannot tell from the byte count. */
316
+ function pngDimensions(buffer) {
317
+ if (buffer.length < 24 || buffer.toString('latin1', 12, 16) !== 'IHDR')
318
+ return {};
319
+ return { width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20) };
320
+ }
312
321
  /** Write one screenshot payload to the shots directory and return its durable location. */
313
322
  async function saveScreenshot(controller, payload) {
314
323
  const dir = controller.shotsDir;
@@ -320,12 +329,15 @@ async function saveScreenshot(controller, payload) {
320
329
  const file = path.join(dir, `${stamp}-${random}.${payload.format === 'jpeg' ? 'jpg' : 'png'}`);
321
330
  const buffer = Buffer.from(payload.base64, 'base64');
322
331
  await writeFile(file, buffer);
332
+ const dims = payload.format === 'png' ? pngDimensions(buffer) : {};
323
333
  return {
324
334
  file,
325
335
  bytes: buffer.length,
326
336
  tabId: payload.tabId,
327
337
  title: payload.tabTitle,
328
338
  url: payload.tabUrl,
339
+ ...dims,
340
+ ...(payload.elementRect === undefined ? {} : { elementRect: payload.elementRect }),
329
341
  };
330
342
  }
331
343
  /** Write one PDF payload to `path` (absolute or relative to `controller.shotsDir`)
@@ -881,16 +893,16 @@ function applyBrowserTools(ctx, controller) {
881
893
  }));
882
894
  ctx.tools.register(defineTool({
883
895
  name: 'browser_tabs',
884
- description: 'List tabs, or open/close/activate/group/ungroup. `close` takes one tab (`tabId`) or several (`tabIds`); the result accounts for every id, so a retry is safe.',
896
+ description: 'List tabs, or open/close/activate/group/ungroup, or go back/forward in the tab\'s history. `close` takes one tab (`tabId`) or several (`tabIds`); the result accounts for every id, so a retry is safe.',
885
897
  parameters: {
886
- action: { type: 'string', required: true, description: 'One of: list, open, close, activate, group, ungroup.' },
898
+ action: { type: 'string', required: true, description: 'One of: list, open, close, activate, group, ungroup, back, forward.' },
887
899
  url: { type: 'string', description: 'URL for the open action.' },
888
- tabId: { type: 'number', description: 'Target tab for close/activate/group.' },
900
+ tabId: { type: 'number', description: 'Target tab for close/activate/group/back/forward; defaults to the active tab for back/forward.' },
889
901
  tabIds: { type: 'array', items: { type: 'number' }, description: 'For close: every tab to close in one call. The result reports each id (closed / alreadyGone / failed) instead of stopping at the first failure.' },
890
902
  active: { type: 'boolean', description: 'Whether a newly opened tab becomes active; defaults to true.' },
891
903
  group: { type: 'boolean', description: 'For open: put the new tab in the DSH tab group. Defaults to true; pass false to keep it loose.' },
892
- waitUntil: { type: 'string', description: 'For open: when the new tab counts as arrived — same tiers as browser_navigate (load default / domcontentloaded / commit / none).' },
893
- timeoutMs: { type: 'number', description: 'For open: wait budget, overriding the waitUntil tier (load 15000, commit/domcontentloaded 4000).' },
904
+ waitUntil: { type: 'string', description: 'For open/back/forward: when the navigation counts as arrived — same tiers as browser_navigate (load default / domcontentloaded / commit / none). back/forward report unchanged:true + a reason when the tab is already at the oldest/newest history entry.' },
905
+ timeoutMs: { type: 'number', description: 'For open/back/forward: wait budget, overriding the waitUntil tier (load 15000, commit/domcontentloaded 4000).' },
894
906
  windowId: { type: 'number', description: 'For ungroup: only dissolve that window’s DSH group; omit to clear every window.' },
895
907
  },
896
908
  output: {
@@ -934,6 +946,28 @@ function applyBrowserTools(ctx, controller) {
934
946
  }
935
947
  case 'activate':
936
948
  return await controller.execute('tabs.activate', { tabId: requireTabId(args, 'activate') }, exec.signal);
949
+ case 'back':
950
+ case 'forward': {
951
+ // Same wait-shape as open: tier default load, explicit budget, bridge
952
+ // margin so the reply can outlive the tier without the bridge timing out.
953
+ // 两条各写一个字面量调用点(surface 台对账 execute 字面量,拼名字会瞎)。
954
+ const waitUntil = args.waitUntil === undefined ? 'load' : String(args.waitUntil);
955
+ if (!(waitUntil in NAV_TIERS)) {
956
+ throw new Error(`browser_tabs ${args.action}: waitUntil must be one of none, commit, domcontentloaded, load — got "${waitUntil}"`);
957
+ }
958
+ const params = { waitUntil };
959
+ if (args.tabId !== undefined)
960
+ params.tabId = args.tabId;
961
+ let budgetMs = NAV_TIERS[waitUntil];
962
+ if (typeof args.timeoutMs === 'number' && Number.isFinite(args.timeoutMs)) {
963
+ budgetMs = Math.min(NAV_MAX_MS, Math.max(100, Math.trunc(args.timeoutMs)));
964
+ }
965
+ params.timeoutMs = budgetMs;
966
+ if (args.action === 'back') {
967
+ return await controller.execute('tabs.back', params, exec.signal, budgetMs + BRIDGE_MARGIN_MS);
968
+ }
969
+ return await controller.execute('tabs.forward', params, exec.signal, budgetMs + BRIDGE_MARGIN_MS);
970
+ }
937
971
  case 'group':
938
972
  return await controller.execute('tabs.group', { tabId: requireTabId(args, 'group') }, exec.signal);
939
973
  case 'ungroup': {
@@ -1078,10 +1112,11 @@ function applyBrowserTools(ctx, controller) {
1078
1112
  }));
1079
1113
  ctx.tools.register(defineTool({
1080
1114
  name: 'browser_screenshot',
1081
- description: 'Capture the tab as PNG/JPEG, save it under the configured shots directory, and return the absolute file path.',
1115
+ description: 'Capture the tab as PNG/JPEG, save it under the configured shots directory, and return the absolute file path plus the image\'s real pixel size. fullPage captures the whole scrollable page in one shot (sticky/fixed headers may repeat); selector captures one element.',
1082
1116
  parameters: {
1083
1117
  tabId: { type: 'number', description: 'Target tab; defaults to the active tab.' },
1084
- fullPage: { type: 'boolean', description: 'Capture beyond the viewport.' },
1118
+ fullPage: { type: 'boolean', description: 'Capture the entire scrollable page instead of just the viewport. The receipt\'s width/height show the real size.' },
1119
+ selector: { type: 'string', description: 'CSS selector: capture just that element (scrolled into view); the receipt carries its rect.' },
1085
1120
  format: { type: 'string', description: '"png" (default) or "jpeg".' },
1086
1121
  },
1087
1122
  output: {
@@ -1094,9 +1129,24 @@ function applyBrowserTools(ctx, controller) {
1094
1129
  tabId: { type: 'number', required: true },
1095
1130
  title: { type: 'string' },
1096
1131
  url: { type: 'string' },
1132
+ width: { type: 'number' },
1133
+ height: { type: 'number' },
1134
+ elementRect: {
1135
+ type: 'object',
1136
+ additionalProperties: false,
1137
+ properties: {
1138
+ x: { type: 'number', required: true },
1139
+ y: { type: 'number', required: true },
1140
+ w: { type: 'number', required: true },
1141
+ h: { type: 'number', required: true },
1142
+ },
1143
+ },
1097
1144
  },
1098
1145
  },
1099
- render: (_args, value) => [{ type: 'text', text: `Saved ${value.file} (${value.bytes} bytes)` }],
1146
+ render: (_args, value) => [{
1147
+ type: 'text',
1148
+ text: `Saved ${value.file} (${value.bytes} bytes${value.width !== undefined && value.height !== undefined ? `, ${value.width}×${value.height}` : ''}${value.elementRect !== undefined ? `, element at ${value.elementRect.x},${value.elementRect.y} size ${value.elementRect.w}×${value.elementRect.h}` : ''})`,
1149
+ }],
1100
1150
  },
1101
1151
  presentCall: () => ({ card: 'generic', title: 'Browser screenshot', kind: 'other' }),
1102
1152
  async execute(args, exec) {
@@ -1106,6 +1156,8 @@ function applyBrowserTools(ctx, controller) {
1106
1156
  params.tabId = args.tabId;
1107
1157
  if (args.fullPage !== undefined)
1108
1158
  params.fullPage = args.fullPage;
1159
+ if (args.selector !== undefined)
1160
+ params.selector = args.selector;
1109
1161
  const payload = await controller.execute('screenshot', params, exec.signal);
1110
1162
  return saveScreenshot(controller, payload);
1111
1163
  },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@angelky7/dsh-browser-control",
3
3
  "description": "Local WebSocket bridge between dsh and the DSH Browser Control browser extension, with model-facing browser_* tools and a Settings-managed on/off switch",
4
- "version": "1.6.0",
4
+ "version": "1.7.0",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",
@@ -37,7 +37,7 @@
37
37
  "build": "tsc -p tsconfig.json && tsdown",
38
38
  "typecheck": "tsc -p tsconfig.json --noEmit",
39
39
  "check": "npm run typecheck && npm run build && publint",
40
- "verify": "node verify/verify_config_patch.mjs && node verify/verify_peers.mjs && node verify/verify_surface.mjs && node verify/verify_tab_groups.mjs && node verify/verify_tabs_close_many.mjs && node verify/verify_cdp_retry.mjs && node verify/verify_snapshot_ax.mjs && node verify/verify_hard_lock.mjs && node verify/verify_policy.mjs && node verify/verify_cookies.mjs && node verify/verify_extension_e2e.mjs && node verify/verify_plugin.mjs legacy && node verify/verify_plugin.mjs conflict && node verify/verify_plugin.mjs new && node verify/verify_http.mjs && node verify/verify_errors.mjs && node verify/verify_hold.mjs && node verify/verify_protocol.mjs && node verify/verify_bundle_http.mjs && node verify/verify_nav_report.mjs && node verify/verify_nav_landing.mjs && node verify/verify_render_contract.mjs && node verify/verify_press_events.mjs && node verify/verify_nav_timeout.mjs && node verify/verify_nav_waituntil.mjs && node verify/verify_archive.mjs && node verify/verify_windows.mjs"
40
+ "verify": "node verify/verify_config_patch.mjs && node verify/verify_peers.mjs && node verify/verify_surface.mjs && node verify/verify_tab_groups.mjs && node verify/verify_tabs_close_many.mjs && node verify/verify_cdp_retry.mjs && node verify/verify_snapshot_ax.mjs && node verify/verify_hard_lock.mjs && node verify/verify_policy.mjs && node verify/verify_cookies.mjs && node verify/verify_extension_e2e.mjs && node verify/verify_plugin.mjs legacy && node verify/verify_plugin.mjs conflict && node verify/verify_plugin.mjs new && node verify/verify_http.mjs && node verify/verify_errors.mjs && node verify/verify_hold.mjs && node verify/verify_protocol.mjs && node verify/verify_bundle_http.mjs && node verify/verify_nav_report.mjs && node verify/verify_nav_landing.mjs && node verify/verify_render_contract.mjs && node verify/verify_press_events.mjs && node verify/verify_nav_timeout.mjs && node verify/verify_nav_waituntil.mjs && node verify/verify_archive.mjs && node verify/verify_windows.mjs && node verify/verify_history_nav.mjs"
41
41
  },
42
42
  "publishConfig": {
43
43
  "access": "public",