@harborclient/sdk 1.3.8 → 1.3.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 (36) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/dist/components/AnchorMenuPanel/index.d.ts.map +1 -1
  3. package/dist/components/AnchorMenuPanel/index.js +13 -5
  4. package/dist/components/Autocomplete/useAutocomplete.d.ts +14 -1
  5. package/dist/components/Autocomplete/useAutocomplete.d.ts.map +1 -1
  6. package/dist/components/Autocomplete/useAutocomplete.js +28 -1
  7. package/dist/components/RoundButton/index.d.ts +5 -1
  8. package/dist/components/RoundButton/index.d.ts.map +1 -1
  9. package/dist/components/RoundButton/index.js +2 -2
  10. package/dist/components/RowActionsMenu/index.d.ts.map +1 -1
  11. package/dist/components/RowActionsMenu/index.js +22 -11
  12. package/dist/components/Sidebar/index.d.ts +4 -1
  13. package/dist/components/Sidebar/index.d.ts.map +1 -1
  14. package/dist/components/Sidebar/index.js +4 -2
  15. package/dist/components/SidebarItem/sidebarItemClasses.d.ts +3 -2
  16. package/dist/components/SidebarItem/sidebarItemClasses.d.ts.map +1 -1
  17. package/dist/components/SidebarItem/sidebarItemClasses.js +3 -2
  18. package/dist/components/SidebarRail/SidebarRailSeparator.d.ts +3 -3
  19. package/dist/components/SidebarRail/SidebarRailSeparator.js +3 -3
  20. package/dist/components/SidebarSection/SectionItem.js +1 -1
  21. package/dist/components/TabBar/index.d.ts.map +1 -1
  22. package/dist/components/TabBar/index.js +1 -1
  23. package/dist/components/VariableInput/index.d.ts +14 -1
  24. package/dist/components/VariableInput/index.d.ts.map +1 -1
  25. package/dist/components/VariableInput/index.js +4 -2
  26. package/dist/components/rowActionsMenuHelpers.d.ts +4 -0
  27. package/dist/components/rowActionsMenuHelpers.d.ts.map +1 -1
  28. package/dist/components/rowActionsMenuHelpers.js +6 -1
  29. package/dist/runtime/createBridgedPluginContext.js +87 -0
  30. package/dist/runtime/reactHost.d.ts +32 -0
  31. package/dist/runtime/webpageHandle.d.ts +50 -0
  32. package/dist/runtime/webpageHandle.js +273 -0
  33. package/dist/snippets.d.ts +67 -0
  34. package/dist/types.d.ts +105 -1
  35. package/dist/types.d.ts.map +1 -1
  36. package/package.json +1 -1
@@ -7,6 +7,7 @@ import {
7
7
  import { bridgeInvoke, bridgeOn } from './hcBridge.js';
8
8
  import { createPluginDatabaseApi } from './pluginDatabaseApi.js';
9
9
  import { setHostReact } from './reactHost.js';
10
+ import { openWebpage } from './webpageHandle.js';
10
11
 
11
12
  /** @type {Map<string, Set<(...args: unknown[]) => void | Promise<void>>>} */
12
13
  const commandHandlers = new Map();
@@ -352,6 +353,52 @@ export function createBridgedPluginContext({ pluginId, mode, contributionId, rea
352
353
  */
353
354
  const assertAi = () => assertPermission('ai');
354
355
 
356
+ /**
357
+ * Asserts browser permission for embedded webpage control.
358
+ */
359
+ const assertBrowser = () => assertPermission('browser');
360
+
361
+ /**
362
+ * Invokes a webpage session op on the host renderer via the plugin bridge.
363
+ *
364
+ * @param {Record<string, unknown>} req - ScriptWebpageRequest-shaped payload.
365
+ * @returns {Promise<unknown>} Host session result.
366
+ */
367
+ const callWebpage = async (req) => {
368
+ const op = String(req.op ?? '');
369
+ switch (op) {
370
+ case 'open':
371
+ return bridgeInvoke('webpage.open', { url: req.url, reuse: req.reuse });
372
+ case 'focus':
373
+ return bridgeInvoke('webpage.focus', { tabId: req.tabId });
374
+ case 'close':
375
+ return bridgeInvoke('webpage.close', { tabId: req.tabId });
376
+ case 'query':
377
+ return bridgeInvoke('webpage.query', {
378
+ tabId: req.tabId,
379
+ selector: req.selector,
380
+ all: req.all,
381
+ maxElements: req.maxElements
382
+ });
383
+ case 'evaluate':
384
+ return bridgeInvoke('webpage.evaluate', {
385
+ tabId: req.tabId,
386
+ expression: req.expression
387
+ });
388
+ case 'injectScript':
389
+ return bridgeInvoke('webpage.injectScript', { tabId: req.tabId, source: req.source });
390
+ case 'injectStylesheet':
391
+ return bridgeInvoke('webpage.injectStylesheet', { tabId: req.tabId, css: req.css });
392
+ case 'screenshot':
393
+ return bridgeInvoke('webpage.screenshot', {
394
+ tabId: req.tabId,
395
+ fullPage: req.fullPage === true
396
+ });
397
+ default:
398
+ throw new Error(`Unsupported webpage bridge op: ${op}`);
399
+ }
400
+ };
401
+
355
402
  /**
356
403
  * Asserts that a contribution id is declared in manifest.contributes.
357
404
  *
@@ -482,6 +529,18 @@ export function createBridgedPluginContext({ pluginId, mode, contributionId, rea
482
529
  assertPermission('filesystem:write');
483
530
  await bridgeInvoke('fs.writeFile', { path, content });
484
531
  },
532
+ writeBytes: async (path, bytes) => {
533
+ assertPermission('filesystem:write');
534
+ const u8 =
535
+ bytes instanceof Uint8Array
536
+ ? bytes
537
+ : new Uint8Array(/** @type {ArrayLike<number>} */ (bytes));
538
+ let binary = '';
539
+ for (let i = 0; i < u8.length; i += 1) {
540
+ binary += String.fromCharCode(u8[i] ?? 0);
541
+ }
542
+ return bridgeInvoke('fs.writeBytes', { path, base64: btoa(binary) });
543
+ },
485
544
  watchFile: (path, listener) => {
486
545
  assertPermission('filesystem:read');
487
546
  const unsubscribe = bridgeOn(`fs.watch:${path}`, () => {
@@ -1243,6 +1302,34 @@ export function createBridgedPluginContext({ pluginId, mode, contributionId, rea
1243
1302
  selection: input?.selection
1244
1303
  });
1245
1304
  }
1305
+ },
1306
+ /**
1307
+ * Opens or reuses an embedded browser tab and returns a control handle.
1308
+ *
1309
+ * Requires the `browser` permission. Same semantics as request-script `hc.webpage`.
1310
+ *
1311
+ * @param {string} [url] - Optional URL; omit to bind the active browser tab.
1312
+ * @param {{ reuse?: boolean }} [options] - Optional `{ reuse }` (default true).
1313
+ * @returns {Promise<import('../types').PluginWebpageHandle>} Webpage handle.
1314
+ */
1315
+ webpage: async (url, options) => {
1316
+ assertBrowser();
1317
+ /**
1318
+ * Writes screenshot PNG bytes via the plugin filesystem bridge.
1319
+ *
1320
+ * @param {string} path - Relative or absolute allowlisted path.
1321
+ * @param {string} pngBase64 - Base64-encoded PNG payload.
1322
+ * @returns {Promise<string>} Absolute written path.
1323
+ */
1324
+ const writeScreenshotBytes = async (path, pngBase64) => {
1325
+ assertPermission('filesystem:write');
1326
+ const result = await bridgeInvoke('fs.writeBytes', { path, base64: pngBase64 });
1327
+ if (typeof result !== 'string' || !result.trim()) {
1328
+ throw new Error('hc.webpage().screenshot failed to resolve write path');
1329
+ }
1330
+ return result;
1331
+ };
1332
+ return openWebpage(callWebpage, url, options, writeScreenshotBytes);
1246
1333
  }
1247
1334
  };
1248
1335
  }
@@ -0,0 +1,32 @@
1
+ import type * as React from 'react';
2
+ import type * as ReactDOM from 'react-dom';
3
+
4
+ /**
5
+ * Installs the HarborClient renderer React instance for plugin JSX and hooks.
6
+ *
7
+ * @param react - React namespace from the host.
8
+ */
9
+ export function setHostReact(react: typeof React): void;
10
+
11
+ /**
12
+ * Returns the installed host React instance.
13
+ *
14
+ * @returns Host React namespace.
15
+ * @throws When {@link setHostReact} has not been called yet.
16
+ */
17
+ export function requireHostReact(): typeof React;
18
+
19
+ /**
20
+ * Installs the HarborClient renderer React DOM instance for plugin portals.
21
+ *
22
+ * @param reactDom - React DOM namespace from the host shim.
23
+ */
24
+ export function setHostReactDom(reactDom: typeof ReactDOM): void;
25
+
26
+ /**
27
+ * Returns the installed host React DOM instance.
28
+ *
29
+ * @returns Host React DOM namespace.
30
+ * @throws When {@link setHostReactDom} has not been called yet.
31
+ */
32
+ export function requireHostReactDom(): typeof ReactDOM;
@@ -0,0 +1,50 @@
1
+ import type { PluginWebpageHandle } from '../types';
2
+
3
+ /**
4
+ * Throws when a webpage bridge result is an `{ error }` object.
5
+ *
6
+ * @param result - Raw bridge result.
7
+ * @returns The result when it is not an error.
8
+ * @throws When the bridge returned `{ error: string }`.
9
+ */
10
+ export function unwrapWebpageBridgeResult(result: unknown): unknown;
11
+
12
+ /**
13
+ * Normalizes the optional second argument to `hc.webpage(url, options)`.
14
+ *
15
+ * @param options - User-provided options.
16
+ * @returns Normalized open options.
17
+ */
18
+ export function normalizeWebpageOpenOptions(options?: unknown): { reuse?: boolean };
19
+
20
+ /**
21
+ * Builds a webpage handle whose methods call the host webpage bridge.
22
+ *
23
+ * @param tab - Opened tab metadata from the bridge.
24
+ * @param callWebpage - Bridge transport.
25
+ * @returns Plain-object handle for the plugin world.
26
+ */
27
+ export function createWebpageHandle(
28
+ tab: {
29
+ tabId: string;
30
+ url: string;
31
+ title: string;
32
+ canGoBack?: boolean;
33
+ canGoForward?: boolean;
34
+ },
35
+ callWebpage: (req: Record<string, unknown>) => Promise<unknown>
36
+ ): PluginWebpageHandle;
37
+
38
+ /**
39
+ * Opens or reuses an embedded browser tab and returns a control handle.
40
+ *
41
+ * @param callWebpage - Bridge transport that accepts ScriptWebpageRequest-shaped payloads.
42
+ * @param url - Optional URL; omit to bind the active browser tab.
43
+ * @param openOptions - Optional `{ reuse }` (default true).
44
+ * @returns Webpage handle.
45
+ */
46
+ export function openWebpage(
47
+ callWebpage: (req: Record<string, unknown>) => Promise<unknown>,
48
+ url?: unknown,
49
+ openOptions?: unknown
50
+ ): Promise<PluginWebpageHandle>;
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Throws when a webpage bridge result is an `{ error }` object.
3
+ *
4
+ * @param {unknown} result - Raw bridge result.
5
+ * @returns {unknown} The result when it is not an error.
6
+ * @throws {Error} When the bridge returned `{ error: string }`.
7
+ */
8
+ export function unwrapWebpageBridgeResult(result) {
9
+ if (
10
+ result != null &&
11
+ typeof result === 'object' &&
12
+ !Array.isArray(result) &&
13
+ Object.keys(result).length === 1 &&
14
+ 'error' in result &&
15
+ typeof (/** @type {{ error: unknown }} */ (result).error) === 'string'
16
+ ) {
17
+ throw new Error(/** @type {{ error: string }} */ (result).error);
18
+ }
19
+ return result;
20
+ }
21
+
22
+ /**
23
+ * Normalizes the optional second argument to `hc.webpage(url, options)`.
24
+ *
25
+ * @param {unknown} [options] - User-provided options.
26
+ * @returns {{ reuse?: boolean }} Normalized open options.
27
+ */
28
+ export function normalizeWebpageOpenOptions(options) {
29
+ if (options == null) {
30
+ return {};
31
+ }
32
+ if (typeof options !== 'object' || Array.isArray(options)) {
33
+ throw new Error('hc.webpage options must be an object');
34
+ }
35
+ const raw = /** @type {Record<string, unknown>} */ (options);
36
+ if (!('reuse' in raw) || raw.reuse === undefined) {
37
+ return {};
38
+ }
39
+ if (typeof raw.reuse !== 'boolean') {
40
+ throw new Error('hc.webpage options.reuse must be a boolean');
41
+ }
42
+ return { reuse: raw.reuse };
43
+ }
44
+
45
+ /**
46
+ * Normalizes optional `hc.webpage().screenshot` options.
47
+ *
48
+ * @param {unknown} [options] - User-provided options (`fullPage` optional).
49
+ * @returns {{ fullPage: boolean }} Normalized options (default `fullPage: false`).
50
+ * @throws {Error} When options is present but not a plain object, or `fullPage` is not a boolean.
51
+ */
52
+ export function normalizeWebpageScreenshotOptions(options) {
53
+ if (options == null) {
54
+ return { fullPage: false };
55
+ }
56
+ if (typeof options !== 'object' || Array.isArray(options)) {
57
+ throw new Error('hc.webpage().screenshot options must be an object');
58
+ }
59
+ const raw = /** @type {Record<string, unknown>} */ (options);
60
+ if (!('fullPage' in raw) || raw.fullPage === undefined) {
61
+ return { fullPage: false };
62
+ }
63
+ if (typeof raw.fullPage !== 'boolean') {
64
+ throw new Error('hc.webpage().screenshot options.fullPage must be a boolean');
65
+ }
66
+ return { fullPage: raw.fullPage };
67
+ }
68
+
69
+ /**
70
+ * Builds a webpage handle whose methods call the host webpage bridge.
71
+ *
72
+ * @param {{
73
+ * tabId: string;
74
+ * url: string;
75
+ * title: string;
76
+ * canGoBack?: boolean;
77
+ * canGoForward?: boolean;
78
+ * }} tab - Opened tab metadata from the bridge.
79
+ * @param {(req: Record<string, unknown>) => Promise<unknown>} callWebpage - Bridge transport.
80
+ * @param {(path: string, pngBase64: string) => Promise<string>} [writeScreenshotBytes] - Optional
81
+ * writer that saves PNG base64 under an allowlisted path and returns the absolute path.
82
+ * @returns {import('../types').PluginWebpageHandle} Plain-object handle for the plugin world.
83
+ */
84
+ export function createWebpageHandle(tab, callWebpage, writeScreenshotBytes) {
85
+ const tabId = tab.tabId;
86
+ return {
87
+ tabId,
88
+ url: tab.url,
89
+ title: tab.title,
90
+ canGoBack: tab.canGoBack === true,
91
+ canGoForward: tab.canGoForward === true,
92
+ /**
93
+ * Focuses this browser tab in the HarborClient tab bar.
94
+ *
95
+ * @returns {Promise<void>} Resolves when the tab is focused.
96
+ */
97
+ focus: async () => {
98
+ unwrapWebpageBridgeResult(await callWebpage({ op: 'focus', tabId }));
99
+ },
100
+ /**
101
+ * Closes this browser tab, honoring page leave prompts.
102
+ *
103
+ * @returns {Promise<boolean>} True when closed; false when the user chose to stay.
104
+ */
105
+ close: async () => {
106
+ const result = /** @type {{ closed: boolean }} */ (
107
+ unwrapWebpageBridgeResult(await callWebpage({ op: 'close', tabId }))
108
+ );
109
+ return result.closed === true;
110
+ },
111
+ /**
112
+ * Captures the visible viewport (or full page) as PNG and writes it via the filesystem bridge.
113
+ *
114
+ * @param {unknown} path - Relative (plugin root) or absolute allowlisted path.
115
+ * @param {unknown} [screenshotOptions] - Optional `{ fullPage }` (default false).
116
+ * @returns {Promise<{ path: string }>} Absolute path of the written file.
117
+ */
118
+ screenshot: async (path, screenshotOptions) => {
119
+ const { fullPage } = normalizeWebpageScreenshotOptions(screenshotOptions);
120
+ const pathText = String(path ?? '').trim();
121
+ if (!pathText) {
122
+ throw new Error('hc.webpage().screenshot requires a path');
123
+ }
124
+ if (!writeScreenshotBytes) {
125
+ throw new Error('hc.webpage().screenshot requires hc.fs.writeBytes');
126
+ }
127
+ const capture = /** @type {{ pngBase64?: string }} */ (
128
+ unwrapWebpageBridgeResult(await callWebpage({ op: 'screenshot', tabId, fullPage }))
129
+ );
130
+ if (!capture || typeof capture.pngBase64 !== 'string' || !capture.pngBase64) {
131
+ throw new Error('hc.webpage().screenshot did not return image data');
132
+ }
133
+ const absolutePath = await writeScreenshotBytes(pathText, capture.pngBase64);
134
+ return { path: absolutePath };
135
+ },
136
+ dom: {
137
+ /**
138
+ * Queries the live page DOM with a CSS selector.
139
+ *
140
+ * @param {string} selector - CSS selector.
141
+ * @param {{ all?: boolean; maxElements?: number }} [queryOptions] - Optional query flags.
142
+ * @returns {Promise<{ selector: string; matchCount: number; elements: unknown[] }>}
143
+ */
144
+ query: async (selector, queryOptions) => {
145
+ const selectorText = String(selector ?? '').trim();
146
+ if (!selectorText) {
147
+ throw new Error('hc.webpage().dom.query requires a selector');
148
+ }
149
+ let all;
150
+ let maxElements;
151
+ if (queryOptions != null) {
152
+ if (typeof queryOptions !== 'object' || Array.isArray(queryOptions)) {
153
+ throw new Error('hc.webpage().dom.query options must be an object');
154
+ }
155
+ const raw = /** @type {Record<string, unknown>} */ (queryOptions);
156
+ if ('all' in raw && raw.all !== undefined) {
157
+ if (typeof raw.all !== 'boolean') {
158
+ throw new Error('hc.webpage().dom.query options.all must be a boolean');
159
+ }
160
+ all = raw.all;
161
+ }
162
+ if ('maxElements' in raw && raw.maxElements !== undefined) {
163
+ if (typeof raw.maxElements !== 'number' || !Number.isFinite(raw.maxElements)) {
164
+ throw new Error('hc.webpage().dom.query options.maxElements must be a finite number');
165
+ }
166
+ maxElements = raw.maxElements;
167
+ }
168
+ }
169
+ return /** @type {{ selector: string; matchCount: number; elements: unknown[] }} */ (
170
+ unwrapWebpageBridgeResult(
171
+ await callWebpage({ op: 'query', tabId, selector: selectorText, all, maxElements })
172
+ )
173
+ );
174
+ },
175
+ /**
176
+ * Evaluates JavaScript in the page main world and returns the result.
177
+ *
178
+ * @param {string} expression - JavaScript source that returns a JSON-serializable value.
179
+ * @returns {Promise<unknown>} Evaluation result.
180
+ */
181
+ evaluate: async (expression) => {
182
+ const expressionText = String(expression ?? '').trim();
183
+ if (!expressionText) {
184
+ throw new Error('hc.webpage().dom.evaluate requires an expression');
185
+ }
186
+ const result = /** @type {{ value: unknown }} */ (
187
+ unwrapWebpageBridgeResult(
188
+ await callWebpage({ op: 'evaluate', tabId, expression: expressionText })
189
+ )
190
+ );
191
+ return result.value;
192
+ },
193
+ /**
194
+ * Injects and runs JavaScript source in the page main world.
195
+ *
196
+ * @param {string} source - JavaScript source to inject.
197
+ * @returns {Promise<unknown>} Evaluation result from the injected script.
198
+ */
199
+ injectScript: async (source) => {
200
+ const sourceText = String(source ?? '');
201
+ if (!sourceText.trim()) {
202
+ throw new Error('hc.webpage().dom.injectScript requires source');
203
+ }
204
+ const result = /** @type {{ value: unknown }} */ (
205
+ unwrapWebpageBridgeResult(
206
+ await callWebpage({ op: 'injectScript', tabId, source: sourceText })
207
+ )
208
+ );
209
+ return result.value;
210
+ },
211
+ /**
212
+ * Injects a CSS stylesheet into the page.
213
+ *
214
+ * @param {string} css - Stylesheet source.
215
+ * @returns {Promise<string>} Electron insertion key.
216
+ */
217
+ injectStylesheet: async (css) => {
218
+ const cssText = String(css ?? '');
219
+ if (!cssText.trim()) {
220
+ throw new Error('hc.webpage().dom.injectStylesheet requires css');
221
+ }
222
+ const result = /** @type {{ key: string }} */ (
223
+ unwrapWebpageBridgeResult(
224
+ await callWebpage({ op: 'injectStylesheet', tabId, css: cssText })
225
+ )
226
+ );
227
+ return result.key;
228
+ }
229
+ }
230
+ };
231
+ }
232
+
233
+ /**
234
+ * Opens or reuses an embedded browser tab and returns a control handle.
235
+ *
236
+ * @param {(req: Record<string, unknown>) => Promise<unknown>} callWebpage - Bridge transport
237
+ * that accepts ScriptWebpageRequest-shaped payloads (`op` plus fields).
238
+ * @param {unknown} [url] - Optional URL; omit to bind the active browser tab.
239
+ * @param {unknown} [openOptions] - Optional `{ reuse }` (default true).
240
+ * @param {(path: string, pngBase64: string) => Promise<string>} [writeScreenshotBytes] - Optional
241
+ * writer used by `page.screenshot`.
242
+ * @returns {Promise<import('../types').PluginWebpageHandle>} Webpage handle.
243
+ */
244
+ export async function openWebpage(callWebpage, url, openOptions, writeScreenshotBytes) {
245
+ const normalizedOptions = normalizeWebpageOpenOptions(openOptions);
246
+ let openUrl;
247
+ if (url !== undefined && url !== null) {
248
+ const trimmed = String(url).trim();
249
+ if (!trimmed) {
250
+ throw new Error('hc.webpage requires a non-empty url when provided');
251
+ }
252
+ openUrl = trimmed;
253
+ }
254
+ const opened = /** @type {{
255
+ tabId: string;
256
+ url: string;
257
+ title: string;
258
+ canGoBack?: boolean;
259
+ canGoForward?: boolean;
260
+ }} */ (
261
+ unwrapWebpageBridgeResult(
262
+ await callWebpage({
263
+ op: 'open',
264
+ url: openUrl,
265
+ reuse: normalizedOptions.reuse
266
+ })
267
+ )
268
+ );
269
+ if (!opened || typeof opened.tabId !== 'string') {
270
+ throw new Error('hc.webpage open did not return a tab');
271
+ }
272
+ return createWebpageHandle(opened, callWebpage, writeScreenshotBytes);
273
+ }
@@ -239,6 +239,8 @@ interface HcInfoApi {
239
239
  readonly workflowActionId: string;
240
240
  /** 0-based index of the workflow action currently executing, or -1 when not in a workflow. */
241
241
  readonly workflowActionIteration: number;
242
+ /** UUID of the live page (website) for this script run, or empty when not a live page. */
243
+ readonly livepageId: string;
242
244
  }
243
245
 
244
246
  /**
@@ -266,6 +268,62 @@ interface HcSendRequestResponse {
266
268
  json(): unknown;
267
269
  }
268
270
 
271
+ /**
272
+ * Live DOM helpers on a webpage handle from hc.webpage.
273
+ */
274
+ interface HcWebpageDom {
275
+ /**
276
+ * Queries the live page DOM with a CSS selector.
277
+ */
278
+ query(
279
+ selector: string,
280
+ options?: { all?: boolean; maxElements?: number }
281
+ ): Promise<{ selector: string; matchCount: number; elements: unknown[] }>;
282
+
283
+ /**
284
+ * Evaluates JavaScript in the page main world and returns the result.
285
+ */
286
+ evaluate(expression: string): Promise<unknown>;
287
+
288
+ /**
289
+ * Injects and runs JavaScript source in the page main world.
290
+ */
291
+ injectScript(source: string): Promise<unknown>;
292
+
293
+ /**
294
+ * Injects a CSS stylesheet into the page.
295
+ */
296
+ injectStylesheet(css: string): Promise<string>;
297
+ }
298
+
299
+ /**
300
+ * Handle returned by hc.webpage for an embedded browser tab.
301
+ */
302
+ interface HcWebpageHandle {
303
+ readonly tabId: string;
304
+ readonly url: string;
305
+ readonly title: string;
306
+ readonly canGoBack: boolean;
307
+ readonly canGoForward: boolean;
308
+ readonly dom: HcWebpageDom;
309
+ /**
310
+ * Focuses this browser tab in the tab bar.
311
+ */
312
+ focus(): Promise<void>;
313
+ /**
314
+ * Closes this browser tab. Returns false when the user cancels a leave prompt.
315
+ */
316
+ close(): Promise<boolean>;
317
+ /**
318
+ * Captures the visible viewport as PNG and writes it under the script file access root.
319
+ *
320
+ * @param path - Relative or absolute path under the script file root.
321
+ * @param options - Optional `{ fullPage }` (default false).
322
+ * @returns Absolute path of the written PNG.
323
+ */
324
+ screenshot(path: string, options?: { fullPage?: boolean }): Promise<{ path: string }>;
325
+ }
326
+
269
327
  /**
270
328
  * Element surface exposed by hc.response.document() for HTML bodies.
271
329
  */
@@ -403,6 +461,15 @@ interface HcScriptApi {
403
461
  * Omit model (or options) to use the first available model.
404
462
  */
405
463
  ask(prompt: string, options?: HcAskOptions): Promise<string | null>;
464
+ /**
465
+ * Opens or reuses an embedded browser tab and returns a control handle.
466
+ * Requires Settings → General → Allow script webpage access.
467
+ *
468
+ * @param url - Optional URL to open or reuse; omit to bind the active browser tab.
469
+ * @param options - Optional `{ reuse }` (default true).
470
+ * @throws When webpage access is disabled or unavailable in this context.
471
+ */
472
+ webpage(url?: string, options?: { reuse?: boolean }): Promise<HcWebpageHandle>;
406
473
  /**
407
474
  * Resolves after the given delay. Use for pacing between script steps.
408
475
  *