@harborclient/sdk 1.4.5 → 1.5.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/CHANGELOG.md CHANGED
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 1.5.0 - 2026-08-01
8
+
9
+ - feat(live-page): introduce live page functionality and refactor related components. (`16f1f304`)
10
+
7
11
  ## 1.4.5 - 2026-08-01
8
12
 
9
13
  - feat(websites): implement Add Live Page modal and import functionality. (`1bdbf8a4`)
@@ -5,9 +5,9 @@ import {
5
5
  registerContributionHeaderActions
6
6
  } from './contributionRegistry.js';
7
7
  import { bridgeInvoke, bridgeOn } from './hcBridge.js';
8
+ import { openLivePage } from './livePageHandle.js';
8
9
  import { createPluginDatabaseApi } from './pluginDatabaseApi.js';
9
10
  import { setHostReact } from './reactHost.js';
10
- import { openWebpage } from './webpageHandle.js';
11
11
 
12
12
  /** @type {Map<string, Set<(...args: unknown[]) => void | Promise<void>>>} */
13
13
  const commandHandlers = new Map();
@@ -354,7 +354,7 @@ export function createBridgedPluginContext({ pluginId, mode, contributionId, rea
354
354
  const assertAi = () => assertPermission('ai');
355
355
 
356
356
  /**
357
- * Asserts browser permission for embedded webpage control.
357
+ * Asserts browser permission for embedded live page control.
358
358
  */
359
359
  const assertBrowser = () => assertPermission('browser');
360
360
 
@@ -364,43 +364,56 @@ export function createBridgedPluginContext({ pluginId, mode, contributionId, rea
364
364
  const assertLiveServer = () => assertPermission('live-server');
365
365
 
366
366
  /**
367
- * Invokes a webpage session op on the host renderer via the plugin bridge.
367
+ * Asserts live-pages permission for saved live page (website) APIs.
368
+ */
369
+ const assertLivePages = () => assertPermission('live-pages');
370
+
371
+ /**
372
+ * Invokes a livePage session op on the host renderer via the plugin bridge.
368
373
  *
369
- * @param {Record<string, unknown>} req - ScriptWebpageRequest-shaped payload.
374
+ * @param {Record<string, unknown>} req - ScriptLivePageRequest-shaped payload.
370
375
  * @returns {Promise<unknown>} Host session result.
371
376
  */
372
- const callWebpage = async (req) => {
377
+ const callLivePage = async (req) => {
373
378
  const op = String(req.op ?? '');
374
379
  switch (op) {
375
380
  case 'open':
376
- return bridgeInvoke('webpage.open', { url: req.url, reuse: req.reuse });
381
+ return bridgeInvoke('livePage.open', { url: req.url, reuse: req.reuse });
377
382
  case 'focus':
378
- return bridgeInvoke('webpage.focus', { tabId: req.tabId });
383
+ return bridgeInvoke('livePage.focus', { tabId: req.tabId });
379
384
  case 'close':
380
- return bridgeInvoke('webpage.close', { tabId: req.tabId });
385
+ return bridgeInvoke('livePage.close', { tabId: req.tabId });
381
386
  case 'query':
382
- return bridgeInvoke('webpage.query', {
387
+ return bridgeInvoke('livePage.query', {
383
388
  tabId: req.tabId,
384
389
  selector: req.selector,
385
390
  all: req.all,
386
391
  maxElements: req.maxElements
387
392
  });
388
393
  case 'evaluate':
389
- return bridgeInvoke('webpage.evaluate', {
394
+ return bridgeInvoke('livePage.evaluate', {
390
395
  tabId: req.tabId,
391
396
  expression: req.expression
392
397
  });
393
398
  case 'injectScript':
394
- return bridgeInvoke('webpage.injectScript', { tabId: req.tabId, source: req.source });
399
+ return bridgeInvoke('livePage.injectScript', { tabId: req.tabId, source: req.source });
395
400
  case 'injectStylesheet':
396
- return bridgeInvoke('webpage.injectStylesheet', { tabId: req.tabId, css: req.css });
401
+ return bridgeInvoke('livePage.injectStylesheet', { tabId: req.tabId, css: req.css });
397
402
  case 'screenshot':
398
- return bridgeInvoke('webpage.screenshot', {
403
+ return bridgeInvoke('livePage.screenshot', {
399
404
  tabId: req.tabId,
400
405
  fullPage: req.fullPage === true
401
406
  });
407
+ case 'goBack':
408
+ return bridgeInvoke('livePage.goBack', { tabId: req.tabId });
409
+ case 'goForward':
410
+ return bridgeInvoke('livePage.goForward', { tabId: req.tabId });
411
+ case 'reload':
412
+ return bridgeInvoke('livePage.reload', { tabId: req.tabId });
413
+ case 'navigate':
414
+ return bridgeInvoke('livePage.navigate', { tabId: req.tabId, url: req.url });
402
415
  default:
403
- throw new Error(`Unsupported webpage bridge op: ${op}`);
416
+ throw new Error(`Unsupported livePage bridge op: ${op}`);
404
417
  }
405
418
  };
406
419
 
@@ -860,6 +873,28 @@ export function createBridgedPluginContext({ pluginId, mode, contributionId, rea
860
873
  });
861
874
  });
862
875
  },
876
+ registerLivePageChromeAction: (action) => {
877
+ assertManifestContribution('livePageChromeActions', action.id);
878
+ if (!isAgent) {
879
+ return noopDisposable();
880
+ }
881
+ void bridgeInvoke('registerContribution', {
882
+ kind: 'livePageChromeActions',
883
+ contribution: {
884
+ pluginId,
885
+ id: action.id,
886
+ title: action.title,
887
+ command: action.command,
888
+ icon: action.icon
889
+ }
890
+ });
891
+ return track(() => {
892
+ void bridgeInvoke('unregisterContribution', {
893
+ kind: 'livePageChromeActions',
894
+ contributionId: action.id
895
+ });
896
+ });
897
+ },
863
898
  registerScriptEditorAction: (action) => {
864
899
  assertManifestContribution('scriptEditorActions', action.id);
865
900
  if (!isAgent) {
@@ -1341,6 +1376,28 @@ export function createBridgedPluginContext({ pluginId, mode, contributionId, rea
1341
1376
  });
1342
1377
  }
1343
1378
  },
1379
+ livePages: {
1380
+ list: async () => {
1381
+ assertLivePages();
1382
+ return bridgeInvoke('livePages.list');
1383
+ },
1384
+ get: async (idOrUuid) => {
1385
+ assertLivePages();
1386
+ return bridgeInvoke('livePages.get', { idOrUuid });
1387
+ },
1388
+ create: async (input) => {
1389
+ assertLivePages();
1390
+ return bridgeInvoke('livePages.create', { input });
1391
+ },
1392
+ update: async (input) => {
1393
+ assertLivePages();
1394
+ return bridgeInvoke('livePages.update', { input });
1395
+ },
1396
+ delete: async (id) => {
1397
+ assertLivePages();
1398
+ await bridgeInvoke('livePages.delete', { id });
1399
+ }
1400
+ },
1344
1401
  ai: {
1345
1402
  registerChatPointer: (config) => {
1346
1403
  assertAi();
@@ -1375,13 +1432,13 @@ export function createBridgedPluginContext({ pluginId, mode, contributionId, rea
1375
1432
  /**
1376
1433
  * Opens or reuses an embedded browser tab and returns a control handle.
1377
1434
  *
1378
- * Requires the `browser` permission. Same semantics as request-script `hc.webpage`.
1435
+ * Requires the `browser` permission. Same semantics as request-script `hc.livePage`.
1379
1436
  *
1380
1437
  * @param {string} [url] - Optional URL; omit to bind the active browser tab.
1381
1438
  * @param {{ reuse?: boolean }} [options] - Optional `{ reuse }` (default true).
1382
- * @returns {Promise<import('../types').PluginWebpageHandle>} Webpage handle.
1439
+ * @returns {Promise<import('../types').PluginLivePageHandle>} Webpage handle.
1383
1440
  */
1384
- webpage: async (url, options) => {
1441
+ livePage: async (url, options) => {
1385
1442
  assertBrowser();
1386
1443
  /**
1387
1444
  * Writes screenshot PNG bytes via the plugin filesystem bridge.
@@ -1394,11 +1451,11 @@ export function createBridgedPluginContext({ pluginId, mode, contributionId, rea
1394
1451
  assertPermission('filesystem:write');
1395
1452
  const result = await bridgeInvoke('fs.writeBytes', { path, base64: pngBase64 });
1396
1453
  if (typeof result !== 'string' || !result.trim()) {
1397
- throw new Error('hc.webpage().screenshot failed to resolve write path');
1454
+ throw new Error('hc.livePage().screenshot failed to resolve write path');
1398
1455
  }
1399
1456
  return result;
1400
1457
  };
1401
- return openWebpage(callWebpage, url, options, writeScreenshotBytes);
1458
+ return openLivePage(callLivePage, url, options, writeScreenshotBytes);
1402
1459
  }
1403
1460
  };
1404
1461
  }
@@ -0,0 +1,50 @@
1
+ import type { PluginLivePageHandle } from '../types';
2
+
3
+ /**
4
+ * Throws when a live-page 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 unwrapLivePageBridgeResult(result: unknown): unknown;
11
+
12
+ /**
13
+ * Normalizes the optional second argument to `hc.livePage(url, options)`.
14
+ *
15
+ * @param options - User-provided options.
16
+ * @returns Normalized open options.
17
+ */
18
+ export function normalizeLivePageOpenOptions(options?: unknown): { reuse?: boolean };
19
+
20
+ /**
21
+ * Builds a live-page handle whose methods call the host live-page bridge.
22
+ *
23
+ * @param tab - Opened tab metadata from the bridge.
24
+ * @param callLivePage - Bridge transport.
25
+ * @returns Plain-object handle for the plugin world.
26
+ */
27
+ export function createLivePageHandle(
28
+ tab: {
29
+ tabId: string;
30
+ url: string;
31
+ title: string;
32
+ canGoBack?: boolean;
33
+ canGoForward?: boolean;
34
+ },
35
+ callLivePage: (req: Record<string, unknown>) => Promise<unknown>
36
+ ): PluginLivePageHandle;
37
+
38
+ /**
39
+ * Opens or reuses an embedded browser tab and returns a control handle.
40
+ *
41
+ * @param callLivePage - Bridge transport that accepts ScriptLivePageRequest-shaped payloads.
42
+ * @param url - Optional URL; omit to bind the active browser tab.
43
+ * @param openOptions - Optional `{ reuse }` (default true).
44
+ * @returns Live-page handle.
45
+ */
46
+ export function openLivePage(
47
+ callLivePage: (req: Record<string, unknown>) => Promise<unknown>,
48
+ url?: unknown,
49
+ openOptions?: unknown
50
+ ): Promise<PluginLivePageHandle>;
@@ -1,11 +1,11 @@
1
1
  /**
2
- * Throws when a webpage bridge result is an `{ error }` object.
2
+ * Throws when a live-page bridge result is an `{ error }` object.
3
3
  *
4
4
  * @param {unknown} result - Raw bridge result.
5
5
  * @returns {unknown} The result when it is not an error.
6
6
  * @throws {Error} When the bridge returned `{ error: string }`.
7
7
  */
8
- export function unwrapWebpageBridgeResult(result) {
8
+ export function unwrapLivePageBridgeResult(result) {
9
9
  if (
10
10
  result != null &&
11
11
  typeof result === 'object' &&
@@ -20,54 +20,54 @@ export function unwrapWebpageBridgeResult(result) {
20
20
  }
21
21
 
22
22
  /**
23
- * Normalizes the optional second argument to `hc.webpage(url, options)`.
23
+ * Normalizes the optional second argument to `hc.livePage(url, options)`.
24
24
  *
25
25
  * @param {unknown} [options] - User-provided options.
26
26
  * @returns {{ reuse?: boolean }} Normalized open options.
27
27
  */
28
- export function normalizeWebpageOpenOptions(options) {
28
+ export function normalizeLivePageOpenOptions(options) {
29
29
  if (options == null) {
30
30
  return {};
31
31
  }
32
32
  if (typeof options !== 'object' || Array.isArray(options)) {
33
- throw new Error('hc.webpage options must be an object');
33
+ throw new Error('hc.livePage options must be an object');
34
34
  }
35
35
  const raw = /** @type {Record<string, unknown>} */ (options);
36
36
  if (!('reuse' in raw) || raw.reuse === undefined) {
37
37
  return {};
38
38
  }
39
39
  if (typeof raw.reuse !== 'boolean') {
40
- throw new Error('hc.webpage options.reuse must be a boolean');
40
+ throw new Error('hc.livePage options.reuse must be a boolean');
41
41
  }
42
42
  return { reuse: raw.reuse };
43
43
  }
44
44
 
45
45
  /**
46
- * Normalizes optional `hc.webpage().screenshot` options.
46
+ * Normalizes optional `hc.livePage().screenshot` options.
47
47
  *
48
48
  * @param {unknown} [options] - User-provided options (`fullPage` optional).
49
49
  * @returns {{ fullPage: boolean }} Normalized options (default `fullPage: false`).
50
50
  * @throws {Error} When options is present but not a plain object, or `fullPage` is not a boolean.
51
51
  */
52
- export function normalizeWebpageScreenshotOptions(options) {
52
+ export function normalizeLivePageScreenshotOptions(options) {
53
53
  if (options == null) {
54
54
  return { fullPage: false };
55
55
  }
56
56
  if (typeof options !== 'object' || Array.isArray(options)) {
57
- throw new Error('hc.webpage().screenshot options must be an object');
57
+ throw new Error('hc.livePage().screenshot options must be an object');
58
58
  }
59
59
  const raw = /** @type {Record<string, unknown>} */ (options);
60
60
  if (!('fullPage' in raw) || raw.fullPage === undefined) {
61
61
  return { fullPage: false };
62
62
  }
63
63
  if (typeof raw.fullPage !== 'boolean') {
64
- throw new Error('hc.webpage().screenshot options.fullPage must be a boolean');
64
+ throw new Error('hc.livePage().screenshot options.fullPage must be a boolean');
65
65
  }
66
66
  return { fullPage: raw.fullPage };
67
67
  }
68
68
 
69
69
  /**
70
- * Builds a webpage handle whose methods call the host webpage bridge.
70
+ * Builds a live-page handle whose methods call the host live page bridge.
71
71
  *
72
72
  * @param {{
73
73
  * tabId: string;
@@ -76,14 +76,15 @@ export function normalizeWebpageScreenshotOptions(options) {
76
76
  * canGoBack?: boolean;
77
77
  * canGoForward?: boolean;
78
78
  * }} tab - Opened tab metadata from the bridge.
79
- * @param {(req: Record<string, unknown>) => Promise<unknown>} callWebpage - Bridge transport.
79
+ * @param {(req: Record<string, unknown>) => Promise<unknown>} callLivePage - Bridge transport.
80
80
  * @param {(path: string, pngBase64: string) => Promise<string>} [writeScreenshotBytes] - Optional
81
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.
82
+ * @returns {import('../types').PluginLivePageHandle} Plain-object handle for the plugin world.
83
83
  */
84
- export function createWebpageHandle(tab, callWebpage, writeScreenshotBytes) {
84
+ export function createLivePageHandle(tab, callLivePage, writeScreenshotBytes) {
85
85
  const tabId = tab.tabId;
86
- return {
86
+ /** @type {import('../types').PluginLivePageHandle} */
87
+ const handle = {
87
88
  tabId,
88
89
  url: tab.url,
89
90
  title: tab.title,
@@ -95,7 +96,7 @@ export function createWebpageHandle(tab, callWebpage, writeScreenshotBytes) {
95
96
  * @returns {Promise<void>} Resolves when the tab is focused.
96
97
  */
97
98
  focus: async () => {
98
- unwrapWebpageBridgeResult(await callWebpage({ op: 'focus', tabId }));
99
+ unwrapLivePageBridgeResult(await callLivePage({ op: 'focus', tabId }));
99
100
  },
100
101
  /**
101
102
  * Closes this browser tab, honoring page leave prompts.
@@ -104,10 +105,59 @@ export function createWebpageHandle(tab, callWebpage, writeScreenshotBytes) {
104
105
  */
105
106
  close: async () => {
106
107
  const result = /** @type {{ closed: boolean }} */ (
107
- unwrapWebpageBridgeResult(await callWebpage({ op: 'close', tabId }))
108
+ unwrapLivePageBridgeResult(await callLivePage({ op: 'close', tabId }))
108
109
  );
109
110
  return result.closed === true;
110
111
  },
112
+ /**
113
+ * Navigates history back one entry and waits for load.
114
+ *
115
+ * @returns {Promise<void>} Resolves when load finishes.
116
+ */
117
+ goBack: async () => {
118
+ applyLivePageNavSnapshot(
119
+ handle,
120
+ unwrapLivePageBridgeResult(await callLivePage({ op: 'goBack', tabId }))
121
+ );
122
+ },
123
+ /**
124
+ * Navigates history forward one entry and waits for load.
125
+ *
126
+ * @returns {Promise<void>} Resolves when load finishes.
127
+ */
128
+ goForward: async () => {
129
+ applyLivePageNavSnapshot(
130
+ handle,
131
+ unwrapLivePageBridgeResult(await callLivePage({ op: 'goForward', tabId }))
132
+ );
133
+ },
134
+ /**
135
+ * Reloads the current page and waits for load.
136
+ *
137
+ * @returns {Promise<void>} Resolves when load finishes.
138
+ */
139
+ reload: async () => {
140
+ applyLivePageNavSnapshot(
141
+ handle,
142
+ unwrapLivePageBridgeResult(await callLivePage({ op: 'reload', tabId }))
143
+ );
144
+ },
145
+ /**
146
+ * Loads a URL in this tab and waits for load.
147
+ *
148
+ * @param {unknown} url - Absolute http(s) or about:blank URL.
149
+ * @returns {Promise<void>} Resolves when load finishes.
150
+ */
151
+ navigate: async (url) => {
152
+ const urlText = String(url ?? '').trim();
153
+ if (!urlText) {
154
+ throw new Error('hc.livePage().navigate requires a non-empty url');
155
+ }
156
+ applyLivePageNavSnapshot(
157
+ handle,
158
+ unwrapLivePageBridgeResult(await callLivePage({ op: 'navigate', tabId, url: urlText }))
159
+ );
160
+ },
111
161
  /**
112
162
  * Captures the visible viewport (or full page) as PNG and writes it via the filesystem bridge.
113
163
  *
@@ -116,19 +166,19 @@ export function createWebpageHandle(tab, callWebpage, writeScreenshotBytes) {
116
166
  * @returns {Promise<{ path: string }>} Absolute path of the written file.
117
167
  */
118
168
  screenshot: async (path, screenshotOptions) => {
119
- const { fullPage } = normalizeWebpageScreenshotOptions(screenshotOptions);
169
+ const { fullPage } = normalizeLivePageScreenshotOptions(screenshotOptions);
120
170
  const pathText = String(path ?? '').trim();
121
171
  if (!pathText) {
122
- throw new Error('hc.webpage().screenshot requires a path');
172
+ throw new Error('hc.livePage().screenshot requires a path');
123
173
  }
124
174
  if (!writeScreenshotBytes) {
125
- throw new Error('hc.webpage().screenshot requires hc.fs.writeBytes');
175
+ throw new Error('hc.livePage().screenshot requires hc.fs.writeBytes');
126
176
  }
127
177
  const capture = /** @type {{ pngBase64?: string }} */ (
128
- unwrapWebpageBridgeResult(await callWebpage({ op: 'screenshot', tabId, fullPage }))
178
+ unwrapLivePageBridgeResult(await callLivePage({ op: 'screenshot', tabId, fullPage }))
129
179
  );
130
180
  if (!capture || typeof capture.pngBase64 !== 'string' || !capture.pngBase64) {
131
- throw new Error('hc.webpage().screenshot did not return image data');
181
+ throw new Error('hc.livePage().screenshot did not return image data');
132
182
  }
133
183
  const absolutePath = await writeScreenshotBytes(pathText, capture.pngBase64);
134
184
  return { path: absolutePath };
@@ -144,31 +194,33 @@ export function createWebpageHandle(tab, callWebpage, writeScreenshotBytes) {
144
194
  query: async (selector, queryOptions) => {
145
195
  const selectorText = String(selector ?? '').trim();
146
196
  if (!selectorText) {
147
- throw new Error('hc.webpage().dom.query requires a selector');
197
+ throw new Error('hc.livePage().dom.query requires a selector');
148
198
  }
149
199
  let all;
150
200
  let maxElements;
151
201
  if (queryOptions != null) {
152
202
  if (typeof queryOptions !== 'object' || Array.isArray(queryOptions)) {
153
- throw new Error('hc.webpage().dom.query options must be an object');
203
+ throw new Error('hc.livePage().dom.query options must be an object');
154
204
  }
155
205
  const raw = /** @type {Record<string, unknown>} */ (queryOptions);
156
206
  if ('all' in raw && raw.all !== undefined) {
157
207
  if (typeof raw.all !== 'boolean') {
158
- throw new Error('hc.webpage().dom.query options.all must be a boolean');
208
+ throw new Error('hc.livePage().dom.query options.all must be a boolean');
159
209
  }
160
210
  all = raw.all;
161
211
  }
162
212
  if ('maxElements' in raw && raw.maxElements !== undefined) {
163
213
  if (typeof raw.maxElements !== 'number' || !Number.isFinite(raw.maxElements)) {
164
- throw new Error('hc.webpage().dom.query options.maxElements must be a finite number');
214
+ throw new Error(
215
+ 'hc.livePage().dom.query options.maxElements must be a finite number'
216
+ );
165
217
  }
166
218
  maxElements = raw.maxElements;
167
219
  }
168
220
  }
169
221
  return /** @type {{ selector: string; matchCount: number; elements: unknown[] }} */ (
170
- unwrapWebpageBridgeResult(
171
- await callWebpage({ op: 'query', tabId, selector: selectorText, all, maxElements })
222
+ unwrapLivePageBridgeResult(
223
+ await callLivePage({ op: 'query', tabId, selector: selectorText, all, maxElements })
172
224
  )
173
225
  );
174
226
  },
@@ -181,11 +233,11 @@ export function createWebpageHandle(tab, callWebpage, writeScreenshotBytes) {
181
233
  evaluate: async (expression) => {
182
234
  const expressionText = String(expression ?? '').trim();
183
235
  if (!expressionText) {
184
- throw new Error('hc.webpage().dom.evaluate requires an expression');
236
+ throw new Error('hc.livePage().dom.evaluate requires an expression');
185
237
  }
186
238
  const result = /** @type {{ value: unknown }} */ (
187
- unwrapWebpageBridgeResult(
188
- await callWebpage({ op: 'evaluate', tabId, expression: expressionText })
239
+ unwrapLivePageBridgeResult(
240
+ await callLivePage({ op: 'evaluate', tabId, expression: expressionText })
189
241
  )
190
242
  );
191
243
  return result.value;
@@ -199,11 +251,11 @@ export function createWebpageHandle(tab, callWebpage, writeScreenshotBytes) {
199
251
  injectScript: async (source) => {
200
252
  const sourceText = String(source ?? '');
201
253
  if (!sourceText.trim()) {
202
- throw new Error('hc.webpage().dom.injectScript requires source');
254
+ throw new Error('hc.livePage().dom.injectScript requires source');
203
255
  }
204
256
  const result = /** @type {{ value: unknown }} */ (
205
- unwrapWebpageBridgeResult(
206
- await callWebpage({ op: 'injectScript', tabId, source: sourceText })
257
+ unwrapLivePageBridgeResult(
258
+ await callLivePage({ op: 'injectScript', tabId, source: sourceText })
207
259
  )
208
260
  );
209
261
  return result.value;
@@ -217,37 +269,63 @@ export function createWebpageHandle(tab, callWebpage, writeScreenshotBytes) {
217
269
  injectStylesheet: async (css) => {
218
270
  const cssText = String(css ?? '');
219
271
  if (!cssText.trim()) {
220
- throw new Error('hc.webpage().dom.injectStylesheet requires css');
272
+ throw new Error('hc.livePage().dom.injectStylesheet requires css');
221
273
  }
222
274
  const result = /** @type {{ key: string }} */ (
223
- unwrapWebpageBridgeResult(
224
- await callWebpage({ op: 'injectStylesheet', tabId, css: cssText })
275
+ unwrapLivePageBridgeResult(
276
+ await callLivePage({ op: 'injectStylesheet', tabId, css: cssText })
225
277
  )
226
278
  );
227
279
  return result.key;
228
280
  }
229
281
  }
230
282
  };
283
+ return handle;
284
+ }
285
+
286
+ /**
287
+ * Copies navigation fields from a bridge result onto a live-page handle.
288
+ *
289
+ * @param {import('../types').PluginLivePageHandle} handle - Mutable handle to update.
290
+ * @param {unknown} result - Bridge result with url/title/history flags.
291
+ */
292
+ function applyLivePageNavSnapshot(handle, result) {
293
+ if (result == null || typeof result !== 'object' || Array.isArray(result)) {
294
+ return;
295
+ }
296
+ const snapshot = /** @type {Record<string, unknown>} */ (result);
297
+ if (typeof snapshot.url === 'string') {
298
+ handle.url = snapshot.url;
299
+ }
300
+ if (typeof snapshot.title === 'string') {
301
+ handle.title = snapshot.title;
302
+ }
303
+ if (typeof snapshot.canGoBack === 'boolean') {
304
+ handle.canGoBack = snapshot.canGoBack;
305
+ }
306
+ if (typeof snapshot.canGoForward === 'boolean') {
307
+ handle.canGoForward = snapshot.canGoForward;
308
+ }
231
309
  }
232
310
 
233
311
  /**
234
312
  * Opens or reuses an embedded browser tab and returns a control handle.
235
313
  *
236
- * @param {(req: Record<string, unknown>) => Promise<unknown>} callWebpage - Bridge transport
237
- * that accepts ScriptWebpageRequest-shaped payloads (`op` plus fields).
314
+ * @param {(req: Record<string, unknown>) => Promise<unknown>} callLivePage - Bridge transport
315
+ * that accepts ScriptLivePageRequest-shaped payloads (`op` plus fields).
238
316
  * @param {unknown} [url] - Optional URL; omit to bind the active browser tab.
239
317
  * @param {unknown} [openOptions] - Optional `{ reuse }` (default true).
240
318
  * @param {(path: string, pngBase64: string) => Promise<string>} [writeScreenshotBytes] - Optional
241
319
  * writer used by `page.screenshot`.
242
- * @returns {Promise<import('../types').PluginWebpageHandle>} Webpage handle.
320
+ * @returns {Promise<import('../types').PluginLivePageHandle>} Live-page handle.
243
321
  */
244
- export async function openWebpage(callWebpage, url, openOptions, writeScreenshotBytes) {
245
- const normalizedOptions = normalizeWebpageOpenOptions(openOptions);
322
+ export async function openLivePage(callLivePage, url, openOptions, writeScreenshotBytes) {
323
+ const normalizedOptions = normalizeLivePageOpenOptions(openOptions);
246
324
  let openUrl;
247
325
  if (url !== undefined && url !== null) {
248
326
  const trimmed = String(url).trim();
249
327
  if (!trimmed) {
250
- throw new Error('hc.webpage requires a non-empty url when provided');
328
+ throw new Error('hc.livePage requires a non-empty url when provided');
251
329
  }
252
330
  openUrl = trimmed;
253
331
  }
@@ -258,8 +336,8 @@ export async function openWebpage(callWebpage, url, openOptions, writeScreenshot
258
336
  canGoBack?: boolean;
259
337
  canGoForward?: boolean;
260
338
  }} */ (
261
- unwrapWebpageBridgeResult(
262
- await callWebpage({
339
+ unwrapLivePageBridgeResult(
340
+ await callLivePage({
263
341
  op: 'open',
264
342
  url: openUrl,
265
343
  reuse: normalizedOptions.reuse
@@ -267,7 +345,7 @@ export async function openWebpage(callWebpage, url, openOptions, writeScreenshot
267
345
  )
268
346
  );
269
347
  if (!opened || typeof opened.tabId !== 'string') {
270
- throw new Error('hc.webpage open did not return a tab');
348
+ throw new Error('hc.livePage open did not return a tab');
271
349
  }
272
- return createWebpageHandle(opened, callWebpage, writeScreenshotBytes);
350
+ return createLivePageHandle(opened, callLivePage, writeScreenshotBytes);
273
351
  }
@@ -271,9 +271,9 @@ interface HcSendRequestResponse {
271
271
  }
272
272
 
273
273
  /**
274
- * Live DOM helpers on a webpage handle from hc.webpage.
274
+ * Live DOM helpers on a live-page handle from hc.livePage.
275
275
  */
276
- interface HcWebpageDom {
276
+ interface HcLivePageDom {
277
277
  /**
278
278
  * Queries the live page DOM with a CSS selector.
279
279
  */
@@ -299,15 +299,19 @@ interface HcWebpageDom {
299
299
  }
300
300
 
301
301
  /**
302
- * Handle returned by hc.webpage for an embedded browser tab.
302
+ * Handle returned by hc.livePage for an embedded browser tab.
303
303
  */
304
- interface HcWebpageHandle {
304
+ interface HcLivePageHandle {
305
305
  readonly tabId: string;
306
- readonly url: string;
307
- readonly title: string;
308
- readonly canGoBack: boolean;
309
- readonly canGoForward: boolean;
310
- readonly dom: HcWebpageDom;
306
+ /** Current page URL. Updated after navigate / goBack / goForward / reload. */
307
+ url: string;
308
+ /** Document title. Updated after navigate / goBack / goForward / reload. */
309
+ title: string;
310
+ /** Whether history can go back. Updated after navigation helpers. */
311
+ canGoBack: boolean;
312
+ /** Whether history can go forward. Updated after navigation helpers. */
313
+ canGoForward: boolean;
314
+ readonly dom: HcLivePageDom;
311
315
  /**
312
316
  * Focuses this browser tab in the tab bar.
313
317
  */
@@ -316,6 +320,24 @@ interface HcWebpageHandle {
316
320
  * Closes this browser tab. Returns false when the user cancels a leave prompt.
317
321
  */
318
322
  close(): Promise<boolean>;
323
+ /**
324
+ * Navigates history back one entry and waits for load. Updates url/title/history flags.
325
+ */
326
+ goBack(): Promise<void>;
327
+ /**
328
+ * Navigates history forward one entry and waits for load. Updates url/title/history flags.
329
+ */
330
+ goForward(): Promise<void>;
331
+ /**
332
+ * Reloads the current page and waits for load. Updates url/title/history flags.
333
+ */
334
+ reload(): Promise<void>;
335
+ /**
336
+ * Loads a URL in this tab and waits for load. Updates url/title/history flags.
337
+ *
338
+ * @param url - Absolute http(s) or about:blank URL.
339
+ */
340
+ navigate(url: string): Promise<void>;
319
341
  /**
320
342
  * Captures the visible viewport as PNG and writes it under the script file access root.
321
343
  *
@@ -465,13 +487,13 @@ interface HcScriptApi {
465
487
  ask(prompt: string, options?: HcAskOptions): Promise<string | null>;
466
488
  /**
467
489
  * Opens or reuses an embedded browser tab and returns a control handle.
468
- * Requires Settings → General → Allow script webpage access.
490
+ * Requires Settings → General → Allow script live page access.
469
491
  *
470
492
  * @param url - Optional URL to open or reuse; omit to bind the active browser tab.
471
493
  * @param options - Optional `{ reuse }` (default true).
472
- * @throws When webpage access is disabled or unavailable in this context.
494
+ * @throws When live-page access is disabled or unavailable in this context.
473
495
  */
474
- webpage(url?: string, options?: { reuse?: boolean }): Promise<HcWebpageHandle>;
496
+ livePage(url?: string, options?: { reuse?: boolean }): Promise<HcLivePageHandle>;
475
497
  /**
476
498
  * Resolves after the given delay. Use for pacing between script steps.
477
499
  *