dsh-browser-plus 0.5.0 → 0.5.1
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 +131 -144
- package/README.en.md +27 -8
- package/README.md +27 -8
- package/client/index.js +185 -0
- package/cordis.patch.yml +24 -0
- package/docs/README.md +1 -1
- package/docs/SOAK-CHECKLIST.md +1 -1
- package/docs/tool-reference.md +5 -3
- package/docs/user-guide.md +20 -4
- package/lib/browser/runtime.d.ts +14 -1
- package/lib/browser/runtime.js +28 -0
- package/lib/browser/types.d.ts +96 -6
- package/lib/browser-electron/chrome-state.d.ts +24 -0
- package/lib/browser-electron/entry.d.ts +12 -5
- package/lib/browser-electron/entry.js +5 -2
- package/lib/browser-electron/host-main.d.ts +2 -1
- package/lib/browser-electron/host-main.js +205 -8
- package/lib/browser-electron/page-chrome.js +249 -14
- package/lib/browser-electron/provider.d.ts +60 -2
- package/lib/browser-electron/provider.js +300 -50
- package/lib/browser-electron/remote-host.d.ts +3 -1
- package/lib/browser-electron/remote-host.js +42 -1
- package/lib/client.js +185 -0
- package/lib/command-browser/index.d.ts +20 -0
- package/lib/command-browser/index.js +35 -0
- package/lib/http-browser/index.d.ts +28 -0
- package/lib/http-browser/index.js +110 -0
- package/lib/index.d.ts +11 -0
- package/lib/index.js +11 -0
- package/lib/task-todos/index.d.ts +25 -0
- package/lib/task-todos/index.js +100 -0
- package/lib/tool-browser/index.js +190 -95
- package/package.json +27 -2
- package/scripts/build-client.mjs +20 -0
- package/scripts/smoke-browser-tools.mjs +84 -1
- package/scripts/smoke-chrome-world.mjs +6 -4
- package/scripts/test-orb-drag.mjs +83 -0
- package/src/browser/runtime.ts +36 -0
- package/src/browser/types.ts +98 -6
- package/src/browser-electron/chrome-state.ts +18 -0
- package/src/browser-electron/entry.ts +17 -7
- package/src/browser-electron/host-main.ts +207 -11
- package/src/browser-electron/page-chrome.ts +249 -14
- package/src/browser-electron/provider.ts +328 -50
- package/src/browser-electron/remote-host.ts +49 -2
- package/src/command-browser/index.ts +61 -0
- package/src/http-browser/index.ts +139 -0
- package/src/index.ts +13 -0
- package/src/task-todos/index.ts +114 -0
- package/src/tool-browser/index.ts +193 -96
|
@@ -30,7 +30,7 @@ export const Config = z.object({
|
|
|
30
30
|
// meaning "the documented defaults" while an explicit [] still denies all.
|
|
31
31
|
writeRoots: z.array(z.string()).default(defaultWriteRoots()),
|
|
32
32
|
readRoots: z.array(z.string()).default(defaultWriteRoots()),
|
|
33
|
-
chromeWorld: z.union(['main', 'isolated']).default('
|
|
33
|
+
chromeWorld: z.union(['main', 'isolated']).default('isolated'),
|
|
34
34
|
userAgent: z.string(),
|
|
35
35
|
maskAutomation: z.boolean().default(true),
|
|
36
36
|
});
|
|
@@ -40,7 +40,10 @@ export function apply(ctx, config) {
|
|
|
40
40
|
// child is disposed with the fiber, mirroring the shell's lifetime.
|
|
41
41
|
const host = config.viewHost
|
|
42
42
|
?? new RemoteElectronViewHost(defaultHostMainPath(), {
|
|
43
|
-
|
|
43
|
+
// `?? 'isolated'` 不只是为了类型:入口可能拿到**未经 schema 解析**的配置
|
|
44
|
+
// (测试探针、其它调用方直接调 apply),那时缺省值不会自动出现,
|
|
45
|
+
// 而 remote-host 把 undefined 当成「不加 --chrome-world」→ 子进程退回 main ✗。
|
|
46
|
+
chromeWorld: config.chromeWorld ?? 'isolated',
|
|
44
47
|
maskAutomation: config.maskAutomation,
|
|
45
48
|
...config.userAgent === undefined ? {} : { userAgent: config.userAgent },
|
|
46
49
|
});
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* <- { id, op: 'ping' } | { id, op: 'createView', viewId, key?, label? } |
|
|
10
10
|
* { id, op: 'destroyView', viewId } | { id, op: 'showView', viewId } |
|
|
11
11
|
* { id, op: 'label', viewId, label } | { id, op: 'listWindows' } |
|
|
12
|
-
* { id, op: 'command', viewId, method, params }
|
|
12
|
+
* { id, op: 'command', viewId, method, params } |
|
|
13
|
+
* { id, op: 'printToPdf', viewId, options }
|
|
13
14
|
* -> { id, ok: true, result? } | { id, ok: false, err }
|
|
14
15
|
*
|
|
15
16
|
* The parent never parses stderr, so diagnostics may go there freely.
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* <- { id, op: 'ping' } | { id, op: 'createView', viewId, key?, label? } |
|
|
10
10
|
* { id, op: 'destroyView', viewId } | { id, op: 'showView', viewId } |
|
|
11
11
|
* { id, op: 'label', viewId, label } | { id, op: 'listWindows' } |
|
|
12
|
-
* { id, op: 'command', viewId, method, params }
|
|
12
|
+
* { id, op: 'command', viewId, method, params } |
|
|
13
|
+
* { id, op: 'printToPdf', viewId, options }
|
|
13
14
|
* -> { id, ok: true, result? } | { id, ok: false, err }
|
|
14
15
|
*
|
|
15
16
|
* The parent never parses stderr, so diagnostics may go there freely.
|
|
@@ -265,6 +266,12 @@ function saveBookmarksToDisk() {
|
|
|
265
266
|
* would silently reset the moment the user visited another site.
|
|
266
267
|
*/
|
|
267
268
|
let chromeBookmarkBar = false;
|
|
269
|
+
/**
|
|
270
|
+
* Where the human parked the floating orb. Remembered in the chrome prefs file
|
|
271
|
+
* rather than in the page: a page-side position is per ORIGIN and would jump
|
|
272
|
+
* back to the corner on the next site.
|
|
273
|
+
*/
|
|
274
|
+
let chromeOrbPosition;
|
|
268
275
|
let chromePrefsFile;
|
|
269
276
|
function loadPrefsFromDisk() {
|
|
270
277
|
try {
|
|
@@ -273,6 +280,13 @@ function loadPrefsFromDisk() {
|
|
|
273
280
|
if (typeof parsed !== 'object' || parsed === null)
|
|
274
281
|
return;
|
|
275
282
|
chromeBookmarkBar = parsed.bookmarkBar === true;
|
|
283
|
+
const orb = parsed.orbPosition;
|
|
284
|
+
if (typeof orb === 'object' && orb !== null) {
|
|
285
|
+
const { x, y } = orb;
|
|
286
|
+
if (typeof x === 'number' && typeof y === 'number' && Number.isFinite(x) && Number.isFinite(y)) {
|
|
287
|
+
chromeOrbPosition = { x: Math.round(x), y: Math.round(y) };
|
|
288
|
+
}
|
|
289
|
+
}
|
|
276
290
|
}
|
|
277
291
|
catch { /* first run, or an unreadable file */ }
|
|
278
292
|
}
|
|
@@ -280,7 +294,10 @@ function savePrefsToDisk() {
|
|
|
280
294
|
if (chromePrefsFile === undefined)
|
|
281
295
|
return;
|
|
282
296
|
try {
|
|
283
|
-
writeFileSync(chromePrefsFile, JSON.stringify({
|
|
297
|
+
writeFileSync(chromePrefsFile, JSON.stringify({
|
|
298
|
+
bookmarkBar: chromeBookmarkBar,
|
|
299
|
+
...chromeOrbPosition !== undefined ? { orbPosition: chromeOrbPosition } : {},
|
|
300
|
+
}, null, 2), 'utf8');
|
|
284
301
|
}
|
|
285
302
|
catch { /* read-only profile */ }
|
|
286
303
|
}
|
|
@@ -484,6 +501,13 @@ const taskViewIds = new Map();
|
|
|
484
501
|
const taskThumbnails = new Map();
|
|
485
502
|
const taskThumbnailVersions = new Map();
|
|
486
503
|
const taskStates = new Map();
|
|
504
|
+
/**
|
|
505
|
+
* The Agent's todo list per task, mirrored from the DSH session projection by the
|
|
506
|
+
* parent (see `setTaskTodos`). Kept here so the visible surface can be handed the
|
|
507
|
+
* current plan the moment it becomes visible — the list itself never rides in a
|
|
508
|
+
* task summary, because summaries reach every page's main world.
|
|
509
|
+
*/
|
|
510
|
+
const taskTodos = new Map();
|
|
487
511
|
const thumbnailTimers = new Map();
|
|
488
512
|
const thumbnailDirty = new Set();
|
|
489
513
|
let thumbnailCaptureInFlight = false;
|
|
@@ -574,6 +598,17 @@ function makeWindow() {
|
|
|
574
598
|
* the page and swallow clicks in the top 84px (the chrome is drawn there today).
|
|
575
599
|
*/
|
|
576
600
|
const CHROME_FRAME_HEIGHT = 84;
|
|
601
|
+
/** Height of the bookmark bar when it is shown. It becomes part of the chrome frame. */
|
|
602
|
+
const BOOKMARK_BAR_HEIGHT = 34;
|
|
603
|
+
/**
|
|
604
|
+
* How tall the chrome frame is right now: tab strip + toolbar, plus the bookmark
|
|
605
|
+
* bar when it is on. The page view starts below ALL of it, so a site's
|
|
606
|
+
* `position: fixed` header can never end up hidden under the bar — the same
|
|
607
|
+
* reason the toolbar itself lives in a view instead of a page padding.
|
|
608
|
+
*/
|
|
609
|
+
function chromeFrameHeight() {
|
|
610
|
+
return CHROME_FRAME_HEIGHT + (chromeBookmarkBar ? BOOKMARK_BAR_HEIGHT : 0);
|
|
611
|
+
}
|
|
577
612
|
let chromeFrame;
|
|
578
613
|
/** Why the frame view could not be created, if it could not. Published to the chrome. */
|
|
579
614
|
let chromeFrameError = '';
|
|
@@ -689,8 +724,22 @@ function handleChromeAction(view, viewId, chromeToken, params) {
|
|
|
689
724
|
workspacePanels = { tasks: action.tasks, trail: action.trail };
|
|
690
725
|
if (workspacePanels.tasks && visibleTaskKey !== undefined)
|
|
691
726
|
scheduleVisibleTaskThumbnail(visibleTaskKey);
|
|
727
|
+
// Opening the panel is the moment the other rows become visible, so hand
|
|
728
|
+
// the surface every image we already have for them.
|
|
729
|
+
if (workspacePanels.tasks) {
|
|
730
|
+
pushCachedTaskThumbnails();
|
|
731
|
+
pushCachedTaskTodos();
|
|
732
|
+
}
|
|
692
733
|
queueChromePatch({ op: 'panels.set', panels: workspacePanels });
|
|
693
734
|
}
|
|
735
|
+
else if (action.type === 'orb-move'
|
|
736
|
+
&& typeof action.x === 'number'
|
|
737
|
+
&& typeof action.y === 'number'
|
|
738
|
+
&& Number.isFinite(action.x)
|
|
739
|
+
&& Number.isFinite(action.y)) {
|
|
740
|
+
chromeOrbPosition = { x: Math.round(action.x), y: Math.round(action.y) };
|
|
741
|
+
savePrefsToDisk();
|
|
742
|
+
}
|
|
694
743
|
else if (action.type === 'set-control-owner'
|
|
695
744
|
&& typeof action.taskKey === 'string'
|
|
696
745
|
&& (action.control === 'agent' || action.control === 'human')
|
|
@@ -721,6 +770,9 @@ function handleChromeAction(view, viewId, chromeToken, params) {
|
|
|
721
770
|
// The bar is a profile-wide preference, so the host owns it and the
|
|
722
771
|
// chrome reads it back from the bootstrap / patch stream.
|
|
723
772
|
chromeBookmarkBar = action.visible;
|
|
773
|
+
// 书签栏现在占的是帧视图的高度,所以开关一变就得重排:
|
|
774
|
+
// 只发 patch 的话,帧还是 84px 高、页面视图还从 84 开始 —— 书签栏就会压住页面。
|
|
775
|
+
layoutViews();
|
|
724
776
|
savePrefsToDisk();
|
|
725
777
|
queueChromePatch({ op: 'bookmarkbar.set', visible: chromeBookmarkBar });
|
|
726
778
|
}
|
|
@@ -920,7 +972,7 @@ function ensureChromeFrame() {
|
|
|
920
972
|
}
|
|
921
973
|
catch { /* already attached */ }
|
|
922
974
|
win.contentView.addChildView(frame);
|
|
923
|
-
frame.setBounds({ x: 0, y: 0, width: win.getContentSize()[0] ?? 0, height:
|
|
975
|
+
frame.setBounds({ x: 0, y: 0, width: win.getContentSize()[0] ?? 0, height: chromeFrameHeight() });
|
|
924
976
|
// HIDDEN until the frame's chrome can drive the page. A visible frame sits on
|
|
925
977
|
// top of the page view, so a person's clicks land on it — and the frame's
|
|
926
978
|
// chrome can only do the things it can do *in its own document* today: its
|
|
@@ -974,11 +1026,12 @@ function layoutViews() {
|
|
|
974
1026
|
try {
|
|
975
1027
|
// Below the chrome frame: the page viewport is genuinely smaller now, so a
|
|
976
1028
|
// sticky/fixed header lands at the top of the page instead of under the chrome.
|
|
1029
|
+
const frameHeight = chromeFrameHeight();
|
|
977
1030
|
entry.webContentsView.setBounds({
|
|
978
1031
|
x: 0,
|
|
979
|
-
y:
|
|
1032
|
+
y: frameHeight,
|
|
980
1033
|
width: width ?? 0,
|
|
981
|
-
height: Math.max(0, (height ?? 0) -
|
|
1034
|
+
height: Math.max(0, (height ?? 0) - frameHeight),
|
|
982
1035
|
});
|
|
983
1036
|
}
|
|
984
1037
|
catch { /* destroyed */ }
|
|
@@ -998,7 +1051,7 @@ function raiseChromeFrame() {
|
|
|
998
1051
|
catch { /* not attached */ }
|
|
999
1052
|
try {
|
|
1000
1053
|
win.contentView.addChildView(chromeFrame);
|
|
1001
|
-
chromeFrame.setBounds({ x: 0, y: 0, width: win.getContentSize()[0] ?? 0, height:
|
|
1054
|
+
chromeFrame.setBounds({ x: 0, y: 0, width: win.getContentSize()[0] ?? 0, height: chromeFrameHeight() });
|
|
1002
1055
|
}
|
|
1003
1056
|
catch { /* destroyed */ }
|
|
1004
1057
|
}
|
|
@@ -1297,6 +1350,7 @@ function chromeWorkspaceState(selectedTaskKey = visibleTaskKey) {
|
|
|
1297
1350
|
trail: activeTraceForTask(selectedTaskKey),
|
|
1298
1351
|
bookmarks: chromeBookmarks,
|
|
1299
1352
|
bookmarkBar: chromeBookmarkBar,
|
|
1353
|
+
...chromeOrbPosition !== undefined ? { orbPosition: chromeOrbPosition } : {},
|
|
1300
1354
|
frameError: chromeFrameError,
|
|
1301
1355
|
windowProbe: (() => {
|
|
1302
1356
|
const win = window;
|
|
@@ -1329,7 +1383,10 @@ function chromeBootstrapScript(selectedTaskKey = visibleTaskKey) {
|
|
|
1329
1383
|
function chromePatchScript(operations) {
|
|
1330
1384
|
const patch = createPatch(chromeEpoch, ++chromeRevision, operations);
|
|
1331
1385
|
return ';window.__dshChromePatch = ' + JSON.stringify(patch)
|
|
1332
|
-
+ ';try { window.__dshChromeApply?.(window.__dshChromePatch) } catch {}'
|
|
1386
|
+
+ ';try { window.__dshChromeApply?.(window.__dshChromePatch) } catch {}'
|
|
1387
|
+
// 补丁里含 Agent 的计划(task.todos),而默认主世界下页面脚本读得到 window ——
|
|
1388
|
+
// 应用完立刻把这份明文抹掉,别让它一直挂在页面上。
|
|
1389
|
+
+ ';try { delete window.__dshChromePatch } catch { window.__dshChromePatch = undefined }';
|
|
1333
1390
|
}
|
|
1334
1391
|
function resetChromeDelivery() {
|
|
1335
1392
|
chromeEpoch += 1;
|
|
@@ -1425,6 +1482,51 @@ function queueFrameAddress() {
|
|
|
1425
1482
|
url = '';
|
|
1426
1483
|
runChromeScript(frame, `window.__dshChromeAddress && window.__dshChromeAddress(${JSON.stringify(url)})`);
|
|
1427
1484
|
}
|
|
1485
|
+
/**
|
|
1486
|
+
* Replay every task's last known image to the visible surface.
|
|
1487
|
+
*
|
|
1488
|
+
* A task's picture never travels in the task summaries: those are injected into
|
|
1489
|
+
* every visited page's main world, and the picture is screen content. So a
|
|
1490
|
+
* chrome surface only ever learns an image from a targeted 'task.thumbnail'
|
|
1491
|
+
* patch, and the capture path queues those for the VISIBLE task alone. Without
|
|
1492
|
+
* this replay, the surface that shows the panel paints its own task's image and
|
|
1493
|
+
* a placeholder for every other one — even for tasks the human already looked
|
|
1494
|
+
* at, which is exactly what the panel looked like before this existed.
|
|
1495
|
+
*
|
|
1496
|
+
* Safe by construction: the patches go to {@link chromeSurfaces}, i.e. the
|
|
1497
|
+
* visible task's page and the chrome frame, which are the only surfaces that
|
|
1498
|
+
* can show the panel in the first place.
|
|
1499
|
+
*/
|
|
1500
|
+
/** Store one task's plan and push it to the surface that shows the panel. */
|
|
1501
|
+
function updateTaskTodos(key, todos) {
|
|
1502
|
+
if (!Array.isArray(todos))
|
|
1503
|
+
return;
|
|
1504
|
+
const clean = [];
|
|
1505
|
+
for (const item of todos) {
|
|
1506
|
+
const entry = item;
|
|
1507
|
+
if (typeof entry?.content !== 'string' || entry.content === '')
|
|
1508
|
+
continue;
|
|
1509
|
+
const status = entry.status === 'completed' || entry.status === 'in_progress' ? entry.status : 'pending';
|
|
1510
|
+
clean.push({ content: entry.content.slice(0, 160), status });
|
|
1511
|
+
if (clean.length >= 40)
|
|
1512
|
+
break;
|
|
1513
|
+
}
|
|
1514
|
+
taskTodos.set(key, clean);
|
|
1515
|
+
queueChromePatch({ op: 'task.todos', key, todos: clean });
|
|
1516
|
+
}
|
|
1517
|
+
/**
|
|
1518
|
+
* Hand every known plan to the visible surface. The orb is always on screen, so
|
|
1519
|
+
* a surface that has just become visible has never seen any of them.
|
|
1520
|
+
*/
|
|
1521
|
+
function pushCachedTaskTodos() {
|
|
1522
|
+
for (const [key, todos] of taskTodos)
|
|
1523
|
+
queueChromePatch({ op: 'task.todos', key, todos });
|
|
1524
|
+
}
|
|
1525
|
+
function pushCachedTaskThumbnails() {
|
|
1526
|
+
for (const [key, dataUrl] of taskThumbnails) {
|
|
1527
|
+
queueChromePatch({ op: 'task.thumbnail', key, version: taskThumbnailVersions.get(key) ?? 0, dataUrl });
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1428
1530
|
function scheduleVisibleTaskThumbnail(taskKey, delayMs = 360) {
|
|
1429
1531
|
if (taskKey !== visibleTaskKey)
|
|
1430
1532
|
return;
|
|
@@ -1525,6 +1627,10 @@ function switchVisibleTask(taskKey) {
|
|
|
1525
1627
|
// changed with it. Queued after the bootstrap: resetChromeDelivery() inside
|
|
1526
1628
|
// pushVisibleChromeState() drops anything queued before it.
|
|
1527
1629
|
queueTabsSet();
|
|
1630
|
+
// 新露出来的那个表面此前没收到过别的任务的图像(补丁只发给可见表面),所以把
|
|
1631
|
+
// 宿主手里已有的图都补给它 —— 否则切过去之后面板里除了当前任务全是占位符。
|
|
1632
|
+
pushCachedTaskThumbnails();
|
|
1633
|
+
pushCachedTaskTodos();
|
|
1528
1634
|
// 必须在 pushVisibleChromeState() **之后**排队:它里面的 resetChromeDelivery() 会把
|
|
1529
1635
|
// 先排的东西丢掉(切标签就是走这条路)。导航走 applyPageChrome,不会播。
|
|
1530
1636
|
queueChromePatch({ op: 'reveal' });
|
|
@@ -1609,6 +1715,10 @@ function applyPageChrome(view, viewId) {
|
|
|
1609
1715
|
// (whose own chrome is not on screen but whose title belongs in the strip).
|
|
1610
1716
|
queueTabsSet();
|
|
1611
1717
|
scheduleVisibleTaskThumbnail(taskKey, 550);
|
|
1718
|
+
// 每一次真导航都会重跑整个 chrome 脚本,页面侧那份 todo 缓存跟着归零。
|
|
1719
|
+
// 缩略图有 scheduleVisibleTaskThumbnail 兜住,计划没有对应物 —— 漏了它,
|
|
1720
|
+
// Agent 一导航球上就只剩状态圆点(这是本轮功能的卖点场景,必现)。
|
|
1721
|
+
pushCachedTaskTodos();
|
|
1612
1722
|
}
|
|
1613
1723
|
}
|
|
1614
1724
|
/** Install human browser chrome without creating or reparenting a child view. */
|
|
@@ -1706,6 +1816,7 @@ async function handle(op, msg) {
|
|
|
1706
1816
|
if (trail !== undefined)
|
|
1707
1817
|
operations.push({ op: 'trail.append', taskKey: entryView.taskKey, entry: trail });
|
|
1708
1818
|
scheduleVisibleTaskThumbnail(entryView.taskKey);
|
|
1819
|
+
pushCachedTaskTodos();
|
|
1709
1820
|
}
|
|
1710
1821
|
if (operations.length > 0)
|
|
1711
1822
|
queueChromePatch(...operations);
|
|
@@ -2015,6 +2126,7 @@ async function handle(op, msg) {
|
|
|
2015
2126
|
clearTimeout(thumbnailTimer);
|
|
2016
2127
|
thumbnailTimers.delete(entry.taskKey);
|
|
2017
2128
|
taskThumbnails.delete(entry.taskKey);
|
|
2129
|
+
taskTodos.delete(entry.taskKey);
|
|
2018
2130
|
taskThumbnailVersions.delete(entry.taskKey);
|
|
2019
2131
|
thumbnailDirty.delete(entry.taskKey);
|
|
2020
2132
|
thumbnailLastCapturedAt.delete(entry.taskKey);
|
|
@@ -2093,8 +2205,16 @@ async function handle(op, msg) {
|
|
|
2093
2205
|
frame.webContents.debugger.attach('1.3');
|
|
2094
2206
|
}
|
|
2095
2207
|
catch { /* already attached */ }
|
|
2208
|
+
// 隔离模式(默认)下,帧里的 chrome 也住在它自己的世界 —— 读它必须带上下文,
|
|
2209
|
+
// 否则读的是帧的主世界(那里什么都没有)。
|
|
2210
|
+
const frameContextId = CHROME_WORLD === 'isolated' ? await ensureChromeContext(frame) : undefined;
|
|
2096
2211
|
frame.webContents.debugger
|
|
2097
|
-
.sendCommand('Runtime.evaluate', {
|
|
2212
|
+
.sendCommand('Runtime.evaluate', {
|
|
2213
|
+
expression,
|
|
2214
|
+
returnByValue: true,
|
|
2215
|
+
awaitPromise: true,
|
|
2216
|
+
...frameContextId === undefined ? {} : { contextId: frameContextId },
|
|
2217
|
+
})
|
|
2098
2218
|
.then((result) => {
|
|
2099
2219
|
// The client resolves with `msg.result`, so the value goes in there.
|
|
2100
2220
|
const value = result?.result?.value;
|
|
@@ -2126,6 +2246,31 @@ async function handle(op, msg) {
|
|
|
2126
2246
|
reply(msg.id, { ok: true });
|
|
2127
2247
|
return;
|
|
2128
2248
|
}
|
|
2249
|
+
case 'focusWindow': {
|
|
2250
|
+
const viewId = msg.viewId;
|
|
2251
|
+
if (viewId === undefined)
|
|
2252
|
+
throw new Error('focusWindow missing viewId');
|
|
2253
|
+
if (!views.has(viewId))
|
|
2254
|
+
throw new Error(`focusWindow: unknown view ${viewId}`);
|
|
2255
|
+
const win = ensureWindow();
|
|
2256
|
+
// The same three steps capture uses: a minimized window has to be
|
|
2257
|
+
// restored, and a window behind another one only comes forward on focus.
|
|
2258
|
+
try {
|
|
2259
|
+
if (!win.isVisible())
|
|
2260
|
+
win.show();
|
|
2261
|
+
}
|
|
2262
|
+
catch { /* closing */ }
|
|
2263
|
+
try {
|
|
2264
|
+
win.restore();
|
|
2265
|
+
}
|
|
2266
|
+
catch { /* not minimized */ }
|
|
2267
|
+
try {
|
|
2268
|
+
win.focus();
|
|
2269
|
+
}
|
|
2270
|
+
catch { /* closing */ }
|
|
2271
|
+
reply(msg.id, { ok: true });
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2129
2274
|
case 'label': {
|
|
2130
2275
|
const viewId = msg.viewId;
|
|
2131
2276
|
const label = msg.label;
|
|
@@ -2173,6 +2318,14 @@ async function handle(op, msg) {
|
|
|
2173
2318
|
reply(msg.id, { ok: true, result: { task: task ?? null } });
|
|
2174
2319
|
return;
|
|
2175
2320
|
}
|
|
2321
|
+
case 'setTaskTodos': {
|
|
2322
|
+
const key = msg.key;
|
|
2323
|
+
if (typeof key !== 'string' || msg.todos === undefined)
|
|
2324
|
+
throw new Error('setTaskTodos missing key or todos');
|
|
2325
|
+
updateTaskTodos(key, msg.todos);
|
|
2326
|
+
reply(msg.id, { ok: true, result: {} });
|
|
2327
|
+
return;
|
|
2328
|
+
}
|
|
2176
2329
|
case 'updateTask': {
|
|
2177
2330
|
const key = msg.key;
|
|
2178
2331
|
if (typeof key !== 'string' || msg.task === undefined)
|
|
@@ -2198,6 +2351,31 @@ async function handle(op, msg) {
|
|
|
2198
2351
|
reply(msg.id, { ok: true, result });
|
|
2199
2352
|
return;
|
|
2200
2353
|
}
|
|
2354
|
+
case 'chromeWorldEval': {
|
|
2355
|
+
// Run a snippet in the world the *page's* injected chrome lives in.
|
|
2356
|
+
//
|
|
2357
|
+
// In isolated mode the chrome's globals are not reachable from the page
|
|
2358
|
+
// world, and creating a world with the same name from outside yields a
|
|
2359
|
+
// different, empty context — so the host, which owns the real one, has to
|
|
2360
|
+
// resolve it here. In main mode this is a plain page-world evaluate.
|
|
2361
|
+
const viewId = msg.viewId;
|
|
2362
|
+
if (viewId === undefined)
|
|
2363
|
+
throw new Error('chromeWorldEval missing viewId');
|
|
2364
|
+
const entry = views.get(viewId);
|
|
2365
|
+
if (entry === undefined)
|
|
2366
|
+
throw new Error(`chromeWorldEval: unknown view ${viewId}`);
|
|
2367
|
+
if (typeof msg.expression !== 'string')
|
|
2368
|
+
throw new Error('chromeWorldEval missing expression');
|
|
2369
|
+
const contextId = CHROME_WORLD === 'isolated' ? await ensureChromeContext(entry.webContentsView) : undefined;
|
|
2370
|
+
const result = await entry.webContentsView.webContents.debugger.sendCommand('Runtime.evaluate', {
|
|
2371
|
+
expression: msg.expression,
|
|
2372
|
+
returnByValue: true,
|
|
2373
|
+
awaitPromise: true,
|
|
2374
|
+
...contextId === undefined ? {} : { contextId },
|
|
2375
|
+
});
|
|
2376
|
+
reply(msg.id, { ok: true, result });
|
|
2377
|
+
return;
|
|
2378
|
+
}
|
|
2201
2379
|
case 'capture': {
|
|
2202
2380
|
const viewId = msg.viewId;
|
|
2203
2381
|
if (viewId === undefined)
|
|
@@ -2289,6 +2467,25 @@ async function handle(op, msg) {
|
|
|
2289
2467
|
reply(msg.id, { ok: true, result: { base64, width: 0, height: 0 } });
|
|
2290
2468
|
return;
|
|
2291
2469
|
}
|
|
2470
|
+
case 'printToPdf': {
|
|
2471
|
+
const viewId = msg.viewId;
|
|
2472
|
+
if (viewId === undefined)
|
|
2473
|
+
throw new Error('printToPdf missing viewId');
|
|
2474
|
+
const entry = views.get(viewId);
|
|
2475
|
+
if (entry === undefined)
|
|
2476
|
+
throw new Error(`printToPdf: unknown view ${viewId}`);
|
|
2477
|
+
// Electron's debugger does not implement CDP `Page.printToPDF`; this
|
|
2478
|
+
// native path is the one that actually produces a document.
|
|
2479
|
+
const options = (msg.options ?? {});
|
|
2480
|
+
// `printToPDF` and its option type are Electron's; Electron is an optional
|
|
2481
|
+
// dependency (CI installs with --omit=optional, so neither the type nor the
|
|
2482
|
+
// `Electron` namespace exists there). Name the shape locally instead of
|
|
2483
|
+
// reaching for the namespace, and let this child — which only ever runs
|
|
2484
|
+
// inside Electron — be the one that calls it.
|
|
2485
|
+
const data = await entry.webContentsView.webContents.printToPDF(options);
|
|
2486
|
+
reply(msg.id, { ok: true, result: { base64: data.toString('base64') } });
|
|
2487
|
+
return;
|
|
2488
|
+
}
|
|
2292
2489
|
case 'download': {
|
|
2293
2490
|
const viewId = msg.viewId;
|
|
2294
2491
|
const url = msg.url;
|