@flrande/bak-extension 0.2.5 → 0.3.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/dist/.bak-e2e-build-stamp +1 -1
- package/dist/background.global.js +973 -147
- package/dist/manifest.json +1 -1
- package/package.json +2 -2
- package/public/manifest.json +1 -1
- package/src/background.ts +550 -153
- package/src/workspace.ts +626 -0
|
@@ -30,11 +30,481 @@
|
|
|
30
30
|
return Math.min(maxDelayMs, baseDelayMs * 2 ** safeAttempt);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// src/workspace.ts
|
|
34
|
+
var DEFAULT_WORKSPACE_ID = "default";
|
|
35
|
+
var DEFAULT_WORKSPACE_LABEL = "bak agent";
|
|
36
|
+
var DEFAULT_WORKSPACE_COLOR = "blue";
|
|
37
|
+
var DEFAULT_WORKSPACE_URL = "about:blank";
|
|
38
|
+
var WorkspaceManager = class {
|
|
39
|
+
storage;
|
|
40
|
+
browser;
|
|
41
|
+
constructor(storage, browser) {
|
|
42
|
+
this.storage = storage;
|
|
43
|
+
this.browser = browser;
|
|
44
|
+
}
|
|
45
|
+
async getWorkspaceInfo(workspaceId = DEFAULT_WORKSPACE_ID) {
|
|
46
|
+
const state = await this.loadWorkspaceRecord(workspaceId);
|
|
47
|
+
if (!state) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const repaired = await this.ensureWorkspace({ workspaceId, focus: false, initialUrl: DEFAULT_WORKSPACE_URL });
|
|
51
|
+
return repaired.workspace;
|
|
52
|
+
}
|
|
53
|
+
async ensureWorkspace(options = {}) {
|
|
54
|
+
const workspaceId = this.normalizeWorkspaceId(options.workspaceId);
|
|
55
|
+
const repairActions = [];
|
|
56
|
+
const initialUrl = options.initialUrl ?? DEFAULT_WORKSPACE_URL;
|
|
57
|
+
const persisted = await this.storage.load();
|
|
58
|
+
const created = !persisted;
|
|
59
|
+
let state = this.normalizeState(persisted, workspaceId);
|
|
60
|
+
let window = state.windowId !== null ? await this.browser.getWindow(state.windowId) : null;
|
|
61
|
+
let tabs = [];
|
|
62
|
+
if (!window) {
|
|
63
|
+
const createdWindow = await this.browser.createWindow({
|
|
64
|
+
url: initialUrl,
|
|
65
|
+
focused: options.focus === true
|
|
66
|
+
});
|
|
67
|
+
state.windowId = createdWindow.id;
|
|
68
|
+
state.groupId = null;
|
|
69
|
+
state.tabIds = [];
|
|
70
|
+
state.activeTabId = null;
|
|
71
|
+
state.primaryTabId = null;
|
|
72
|
+
window = createdWindow;
|
|
73
|
+
tabs = await this.waitForWindowTabs(createdWindow.id);
|
|
74
|
+
state.tabIds = tabs.map((tab) => tab.id);
|
|
75
|
+
if (state.primaryTabId === null) {
|
|
76
|
+
state.primaryTabId = tabs[0]?.id ?? null;
|
|
77
|
+
}
|
|
78
|
+
if (state.activeTabId === null) {
|
|
79
|
+
state.activeTabId = tabs.find((tab) => tab.active)?.id ?? tabs[0]?.id ?? null;
|
|
80
|
+
}
|
|
81
|
+
repairActions.push(created ? "created-window" : "recreated-window");
|
|
82
|
+
}
|
|
83
|
+
tabs = tabs.length > 0 ? tabs : await this.readTrackedTabs(state.tabIds, state.windowId);
|
|
84
|
+
const recoveredTabs = await this.recoverWorkspaceTabs(state, tabs);
|
|
85
|
+
if (recoveredTabs.length > tabs.length) {
|
|
86
|
+
tabs = recoveredTabs;
|
|
87
|
+
repairActions.push("recovered-tracked-tabs");
|
|
88
|
+
}
|
|
89
|
+
if (tabs.length !== state.tabIds.length) {
|
|
90
|
+
repairActions.push("pruned-missing-tabs");
|
|
91
|
+
}
|
|
92
|
+
state.tabIds = tabs.map((tab) => tab.id);
|
|
93
|
+
if (tabs.length === 0) {
|
|
94
|
+
const primary = await this.createWorkspaceTab({
|
|
95
|
+
windowId: state.windowId,
|
|
96
|
+
url: initialUrl,
|
|
97
|
+
active: true
|
|
98
|
+
});
|
|
99
|
+
tabs = [primary];
|
|
100
|
+
state.tabIds = [primary.id];
|
|
101
|
+
state.primaryTabId = primary.id;
|
|
102
|
+
state.activeTabId = primary.id;
|
|
103
|
+
repairActions.push("created-primary-tab");
|
|
104
|
+
}
|
|
105
|
+
if (state.primaryTabId === null || !tabs.some((tab) => tab.id === state.primaryTabId)) {
|
|
106
|
+
state.primaryTabId = tabs[0]?.id ?? null;
|
|
107
|
+
repairActions.push("reassigned-primary-tab");
|
|
108
|
+
}
|
|
109
|
+
if (state.activeTabId === null || !tabs.some((tab) => tab.id === state.activeTabId)) {
|
|
110
|
+
state.activeTabId = state.primaryTabId ?? tabs[0]?.id ?? null;
|
|
111
|
+
repairActions.push("reassigned-active-tab");
|
|
112
|
+
}
|
|
113
|
+
let group = state.groupId !== null ? await this.browser.getGroup(state.groupId) : null;
|
|
114
|
+
if (!group || group.windowId !== state.windowId) {
|
|
115
|
+
const groupId = await this.browser.groupTabs(tabs.map((tab) => tab.id));
|
|
116
|
+
group = await this.browser.updateGroup(groupId, {
|
|
117
|
+
title: state.label,
|
|
118
|
+
color: state.color,
|
|
119
|
+
collapsed: false
|
|
120
|
+
});
|
|
121
|
+
state.groupId = group.id;
|
|
122
|
+
repairActions.push("recreated-group");
|
|
123
|
+
} else {
|
|
124
|
+
await this.browser.updateGroup(group.id, {
|
|
125
|
+
title: state.label,
|
|
126
|
+
color: state.color,
|
|
127
|
+
collapsed: false
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const ungroupedIds = tabs.filter((tab) => tab.groupId !== state.groupId).map((tab) => tab.id);
|
|
131
|
+
if (ungroupedIds.length > 0) {
|
|
132
|
+
await this.browser.groupTabs(ungroupedIds, state.groupId ?? void 0);
|
|
133
|
+
repairActions.push("regrouped-tabs");
|
|
134
|
+
}
|
|
135
|
+
tabs = await this.readTrackedTabs(state.tabIds, state.windowId);
|
|
136
|
+
tabs = await this.recoverWorkspaceTabs(state, tabs);
|
|
137
|
+
const activeTab = state.activeTabId !== null ? await this.waitForTrackedTab(state.activeTabId, state.windowId) : null;
|
|
138
|
+
if (activeTab && !tabs.some((tab) => tab.id === activeTab.id)) {
|
|
139
|
+
tabs = [...tabs, activeTab];
|
|
140
|
+
}
|
|
141
|
+
state.tabIds = [...new Set(tabs.map((tab) => tab.id))];
|
|
142
|
+
if (options.focus === true && state.activeTabId !== null) {
|
|
143
|
+
await this.browser.updateTab(state.activeTabId, { active: true });
|
|
144
|
+
window = await this.browser.updateWindow(state.windowId, { focused: true });
|
|
145
|
+
void window;
|
|
146
|
+
repairActions.push("focused-window");
|
|
147
|
+
}
|
|
148
|
+
await this.storage.save(state);
|
|
149
|
+
return {
|
|
150
|
+
workspace: {
|
|
151
|
+
...state,
|
|
152
|
+
tabs
|
|
153
|
+
},
|
|
154
|
+
created,
|
|
155
|
+
repaired: repairActions.length > 0,
|
|
156
|
+
repairActions
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
async openTab(options = {}) {
|
|
160
|
+
const ensured = await this.ensureWorkspace({
|
|
161
|
+
workspaceId: options.workspaceId,
|
|
162
|
+
focus: false,
|
|
163
|
+
initialUrl: options.url ?? DEFAULT_WORKSPACE_URL
|
|
164
|
+
});
|
|
165
|
+
const state = { ...ensured.workspace };
|
|
166
|
+
const active = options.active === true;
|
|
167
|
+
const desiredUrl = options.url ?? DEFAULT_WORKSPACE_URL;
|
|
168
|
+
const reusablePrimaryTab = (ensured.created || ensured.repairActions.includes("recreated-window") || ensured.repairActions.includes("created-primary-tab")) && state.tabs.length === 1 && state.primaryTabId !== null ? state.tabs.find((tab2) => tab2.id === state.primaryTabId) ?? null : null;
|
|
169
|
+
const createdTab = reusablePrimaryTab ? await this.browser.updateTab(reusablePrimaryTab.id, {
|
|
170
|
+
url: desiredUrl,
|
|
171
|
+
active
|
|
172
|
+
}) : await this.createWorkspaceTab({
|
|
173
|
+
windowId: state.windowId,
|
|
174
|
+
url: desiredUrl,
|
|
175
|
+
active
|
|
176
|
+
});
|
|
177
|
+
const nextTabIds = [.../* @__PURE__ */ new Set([...state.tabIds, createdTab.id])];
|
|
178
|
+
const groupId = await this.browser.groupTabs([createdTab.id], state.groupId ?? void 0);
|
|
179
|
+
await this.browser.updateGroup(groupId, {
|
|
180
|
+
title: state.label,
|
|
181
|
+
color: state.color,
|
|
182
|
+
collapsed: false
|
|
183
|
+
});
|
|
184
|
+
const nextState = {
|
|
185
|
+
id: state.id,
|
|
186
|
+
label: state.label,
|
|
187
|
+
color: state.color,
|
|
188
|
+
windowId: state.windowId,
|
|
189
|
+
groupId,
|
|
190
|
+
tabIds: nextTabIds,
|
|
191
|
+
activeTabId: createdTab.id,
|
|
192
|
+
primaryTabId: state.primaryTabId ?? createdTab.id
|
|
193
|
+
};
|
|
194
|
+
if (options.focus === true) {
|
|
195
|
+
await this.browser.updateTab(createdTab.id, { active: true });
|
|
196
|
+
await this.browser.updateWindow(state.windowId, { focused: true });
|
|
197
|
+
}
|
|
198
|
+
await this.storage.save(nextState);
|
|
199
|
+
const tabs = await this.readTrackedTabs(nextState.tabIds, nextState.windowId);
|
|
200
|
+
const tab = tabs.find((item) => item.id === createdTab.id) ?? createdTab;
|
|
201
|
+
return {
|
|
202
|
+
workspace: {
|
|
203
|
+
...nextState,
|
|
204
|
+
tabs
|
|
205
|
+
},
|
|
206
|
+
tab
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
async listTabs(workspaceId = DEFAULT_WORKSPACE_ID) {
|
|
210
|
+
const ensured = await this.ensureWorkspace({ workspaceId });
|
|
211
|
+
return {
|
|
212
|
+
workspace: ensured.workspace,
|
|
213
|
+
tabs: ensured.workspace.tabs
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
async getActiveTab(workspaceId = DEFAULT_WORKSPACE_ID) {
|
|
217
|
+
const ensured = await this.ensureWorkspace({ workspaceId });
|
|
218
|
+
return {
|
|
219
|
+
workspace: ensured.workspace,
|
|
220
|
+
tab: ensured.workspace.tabs.find((tab) => tab.id === ensured.workspace.activeTabId) ?? null
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
async setActiveTab(tabId, workspaceId = DEFAULT_WORKSPACE_ID) {
|
|
224
|
+
const ensured = await this.ensureWorkspace({ workspaceId });
|
|
225
|
+
if (!ensured.workspace.tabIds.includes(tabId)) {
|
|
226
|
+
throw new Error(`Tab ${tabId} does not belong to workspace ${workspaceId}`);
|
|
227
|
+
}
|
|
228
|
+
const nextState = {
|
|
229
|
+
id: ensured.workspace.id,
|
|
230
|
+
label: ensured.workspace.label,
|
|
231
|
+
color: ensured.workspace.color,
|
|
232
|
+
windowId: ensured.workspace.windowId,
|
|
233
|
+
groupId: ensured.workspace.groupId,
|
|
234
|
+
tabIds: [...ensured.workspace.tabIds],
|
|
235
|
+
activeTabId: tabId,
|
|
236
|
+
primaryTabId: ensured.workspace.primaryTabId ?? tabId
|
|
237
|
+
};
|
|
238
|
+
await this.storage.save(nextState);
|
|
239
|
+
const tabs = await this.readTrackedTabs(nextState.tabIds, nextState.windowId);
|
|
240
|
+
const tab = tabs.find((item) => item.id === tabId);
|
|
241
|
+
if (!tab) {
|
|
242
|
+
throw new Error(`Tab ${tabId} is missing from workspace ${workspaceId}`);
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
workspace: {
|
|
246
|
+
...nextState,
|
|
247
|
+
tabs
|
|
248
|
+
},
|
|
249
|
+
tab
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
async focus(workspaceId = DEFAULT_WORKSPACE_ID) {
|
|
253
|
+
const ensured = await this.ensureWorkspace({ workspaceId, focus: false });
|
|
254
|
+
if (ensured.workspace.activeTabId !== null) {
|
|
255
|
+
await this.browser.updateTab(ensured.workspace.activeTabId, { active: true });
|
|
256
|
+
}
|
|
257
|
+
if (ensured.workspace.windowId !== null) {
|
|
258
|
+
await this.browser.updateWindow(ensured.workspace.windowId, { focused: true });
|
|
259
|
+
}
|
|
260
|
+
const refreshed = await this.ensureWorkspace({ workspaceId, focus: false });
|
|
261
|
+
return { ok: true, workspace: refreshed.workspace };
|
|
262
|
+
}
|
|
263
|
+
async reset(options = {}) {
|
|
264
|
+
await this.close(options.workspaceId);
|
|
265
|
+
return this.ensureWorkspace(options);
|
|
266
|
+
}
|
|
267
|
+
async close(workspaceId = DEFAULT_WORKSPACE_ID) {
|
|
268
|
+
const state = await this.storage.load();
|
|
269
|
+
if (!state || state.id !== workspaceId) {
|
|
270
|
+
await this.storage.save(null);
|
|
271
|
+
return { ok: true };
|
|
272
|
+
}
|
|
273
|
+
if (state.windowId !== null) {
|
|
274
|
+
const existingWindow = await this.browser.getWindow(state.windowId);
|
|
275
|
+
if (existingWindow) {
|
|
276
|
+
await this.browser.closeWindow(state.windowId);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
await this.storage.save(null);
|
|
280
|
+
return { ok: true };
|
|
281
|
+
}
|
|
282
|
+
async resolveTarget(options = {}) {
|
|
283
|
+
if (typeof options.tabId === "number") {
|
|
284
|
+
const explicitTab = await this.browser.getTab(options.tabId);
|
|
285
|
+
if (!explicitTab) {
|
|
286
|
+
throw new Error(`No tab with id ${options.tabId}`);
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
tab: explicitTab,
|
|
290
|
+
workspace: null,
|
|
291
|
+
resolution: "explicit-tab",
|
|
292
|
+
createdWorkspace: false,
|
|
293
|
+
repaired: false,
|
|
294
|
+
repairActions: []
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
const explicitWorkspaceId = typeof options.workspaceId === "string" ? this.normalizeWorkspaceId(options.workspaceId) : void 0;
|
|
298
|
+
if (explicitWorkspaceId) {
|
|
299
|
+
const ensured2 = await this.ensureWorkspace({
|
|
300
|
+
workspaceId: explicitWorkspaceId,
|
|
301
|
+
focus: false
|
|
302
|
+
});
|
|
303
|
+
return this.buildWorkspaceResolution(ensured2, "explicit-workspace");
|
|
304
|
+
}
|
|
305
|
+
const existingWorkspace = await this.loadWorkspaceRecord(DEFAULT_WORKSPACE_ID);
|
|
306
|
+
if (existingWorkspace) {
|
|
307
|
+
const ensured2 = await this.ensureWorkspace({
|
|
308
|
+
workspaceId: existingWorkspace.id,
|
|
309
|
+
focus: false
|
|
310
|
+
});
|
|
311
|
+
return this.buildWorkspaceResolution(ensured2, "default-workspace");
|
|
312
|
+
}
|
|
313
|
+
if (options.createIfMissing !== true) {
|
|
314
|
+
const activeTab = await this.browser.getActiveTab();
|
|
315
|
+
if (!activeTab) {
|
|
316
|
+
throw new Error("No active tab");
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
tab: activeTab,
|
|
320
|
+
workspace: null,
|
|
321
|
+
resolution: "browser-active",
|
|
322
|
+
createdWorkspace: false,
|
|
323
|
+
repaired: false,
|
|
324
|
+
repairActions: []
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
const ensured = await this.ensureWorkspace({
|
|
328
|
+
workspaceId: DEFAULT_WORKSPACE_ID,
|
|
329
|
+
focus: false
|
|
330
|
+
});
|
|
331
|
+
return this.buildWorkspaceResolution(ensured, "default-workspace");
|
|
332
|
+
}
|
|
333
|
+
normalizeWorkspaceId(workspaceId) {
|
|
334
|
+
const candidate = workspaceId?.trim();
|
|
335
|
+
if (!candidate) {
|
|
336
|
+
return DEFAULT_WORKSPACE_ID;
|
|
337
|
+
}
|
|
338
|
+
if (candidate !== DEFAULT_WORKSPACE_ID) {
|
|
339
|
+
throw new Error(`Unsupported workspace id: ${candidate}`);
|
|
340
|
+
}
|
|
341
|
+
return candidate;
|
|
342
|
+
}
|
|
343
|
+
normalizeState(state, workspaceId) {
|
|
344
|
+
return {
|
|
345
|
+
id: workspaceId,
|
|
346
|
+
label: state?.label ?? DEFAULT_WORKSPACE_LABEL,
|
|
347
|
+
color: state?.color ?? DEFAULT_WORKSPACE_COLOR,
|
|
348
|
+
windowId: state?.windowId ?? null,
|
|
349
|
+
groupId: state?.groupId ?? null,
|
|
350
|
+
tabIds: state?.tabIds ?? [],
|
|
351
|
+
activeTabId: state?.activeTabId ?? null,
|
|
352
|
+
primaryTabId: state?.primaryTabId ?? null
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
async loadWorkspaceRecord(workspaceId = DEFAULT_WORKSPACE_ID) {
|
|
356
|
+
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
357
|
+
const state = await this.storage.load();
|
|
358
|
+
if (!state || state.id !== normalizedWorkspaceId) {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
return this.normalizeState(state, normalizedWorkspaceId);
|
|
362
|
+
}
|
|
363
|
+
async buildWorkspaceResolution(ensured, resolution) {
|
|
364
|
+
const tab = ensured.workspace.tabs.find((item) => item.id === ensured.workspace.activeTabId) ?? ensured.workspace.tabs[0] ?? null;
|
|
365
|
+
if (tab) {
|
|
366
|
+
return {
|
|
367
|
+
tab,
|
|
368
|
+
workspace: ensured.workspace,
|
|
369
|
+
resolution,
|
|
370
|
+
createdWorkspace: ensured.created,
|
|
371
|
+
repaired: ensured.repaired,
|
|
372
|
+
repairActions: ensured.repairActions
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
if (ensured.workspace.activeTabId !== null) {
|
|
376
|
+
const activeWorkspaceTab = await this.waitForTrackedTab(ensured.workspace.activeTabId, ensured.workspace.windowId);
|
|
377
|
+
if (activeWorkspaceTab) {
|
|
378
|
+
return {
|
|
379
|
+
tab: activeWorkspaceTab,
|
|
380
|
+
workspace: ensured.workspace,
|
|
381
|
+
resolution,
|
|
382
|
+
createdWorkspace: ensured.created,
|
|
383
|
+
repaired: ensured.repaired,
|
|
384
|
+
repairActions: ensured.repairActions
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const activeTab = await this.browser.getActiveTab();
|
|
389
|
+
if (!activeTab) {
|
|
390
|
+
throw new Error("No active tab");
|
|
391
|
+
}
|
|
392
|
+
return {
|
|
393
|
+
tab: activeTab,
|
|
394
|
+
workspace: null,
|
|
395
|
+
resolution: "browser-active",
|
|
396
|
+
createdWorkspace: ensured.created,
|
|
397
|
+
repaired: ensured.repaired,
|
|
398
|
+
repairActions: ensured.repairActions
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
async readTrackedTabs(tabIds, windowId) {
|
|
402
|
+
const tabs = (await Promise.all(
|
|
403
|
+
tabIds.map(async (tabId) => {
|
|
404
|
+
const tab = await this.browser.getTab(tabId);
|
|
405
|
+
if (!tab) {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
if (windowId !== null && tab.windowId !== windowId) {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
return tab;
|
|
412
|
+
})
|
|
413
|
+
)).filter((tab) => tab !== null);
|
|
414
|
+
return tabs;
|
|
415
|
+
}
|
|
416
|
+
async recoverWorkspaceTabs(state, existingTabs) {
|
|
417
|
+
if (state.windowId === null) {
|
|
418
|
+
return existingTabs;
|
|
419
|
+
}
|
|
420
|
+
const candidates = await this.waitForWindowTabs(state.windowId, 500);
|
|
421
|
+
if (candidates.length === 0) {
|
|
422
|
+
return existingTabs;
|
|
423
|
+
}
|
|
424
|
+
const trackedIds = new Set(state.tabIds);
|
|
425
|
+
const trackedTabs = candidates.filter((tab) => trackedIds.has(tab.id));
|
|
426
|
+
if (trackedTabs.length > existingTabs.length) {
|
|
427
|
+
return trackedTabs;
|
|
428
|
+
}
|
|
429
|
+
if (state.groupId !== null) {
|
|
430
|
+
const groupedTabs = candidates.filter((tab) => tab.groupId === state.groupId);
|
|
431
|
+
if (groupedTabs.length > 0) {
|
|
432
|
+
return groupedTabs;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
const preferredIds = new Set([state.activeTabId, state.primaryTabId].filter((value) => typeof value === "number"));
|
|
436
|
+
const preferredTabs = candidates.filter((tab) => preferredIds.has(tab.id));
|
|
437
|
+
if (preferredTabs.length > existingTabs.length) {
|
|
438
|
+
return preferredTabs;
|
|
439
|
+
}
|
|
440
|
+
return existingTabs;
|
|
441
|
+
}
|
|
442
|
+
async createWorkspaceTab(options) {
|
|
443
|
+
if (options.windowId === null) {
|
|
444
|
+
throw new Error("Workspace window is unavailable");
|
|
445
|
+
}
|
|
446
|
+
const deadline = Date.now() + 1500;
|
|
447
|
+
let lastError2 = null;
|
|
448
|
+
while (Date.now() < deadline) {
|
|
449
|
+
const window = await this.browser.getWindow(options.windowId);
|
|
450
|
+
if (!window) {
|
|
451
|
+
lastError2 = new Error(`No window with id: ${options.windowId}.`);
|
|
452
|
+
await this.delay(50);
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
try {
|
|
456
|
+
return await this.browser.createTab({
|
|
457
|
+
windowId: options.windowId,
|
|
458
|
+
url: options.url,
|
|
459
|
+
active: options.active
|
|
460
|
+
});
|
|
461
|
+
} catch (error) {
|
|
462
|
+
if (!this.isMissingWindowError(error)) {
|
|
463
|
+
throw error;
|
|
464
|
+
}
|
|
465
|
+
lastError2 = error instanceof Error ? error : new Error(String(error));
|
|
466
|
+
await this.delay(50);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
throw lastError2 ?? new Error(`No window with id: ${options.windowId}.`);
|
|
470
|
+
}
|
|
471
|
+
async waitForTrackedTab(tabId, windowId, timeoutMs = 1e3) {
|
|
472
|
+
const deadline = Date.now() + timeoutMs;
|
|
473
|
+
while (Date.now() < deadline) {
|
|
474
|
+
const tab = await this.browser.getTab(tabId);
|
|
475
|
+
if (tab && (windowId === null || tab.windowId === windowId)) {
|
|
476
|
+
return tab;
|
|
477
|
+
}
|
|
478
|
+
await this.delay(50);
|
|
479
|
+
}
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
async waitForWindowTabs(windowId, timeoutMs = 1e3) {
|
|
483
|
+
const deadline = Date.now() + timeoutMs;
|
|
484
|
+
while (Date.now() < deadline) {
|
|
485
|
+
const tabs = await this.browser.listTabs({ windowId });
|
|
486
|
+
if (tabs.length > 0) {
|
|
487
|
+
return tabs;
|
|
488
|
+
}
|
|
489
|
+
await this.delay(50);
|
|
490
|
+
}
|
|
491
|
+
return [];
|
|
492
|
+
}
|
|
493
|
+
async delay(ms) {
|
|
494
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
495
|
+
}
|
|
496
|
+
isMissingWindowError(error) {
|
|
497
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
498
|
+
return message.toLowerCase().includes("no window with id");
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
|
|
33
502
|
// src/background.ts
|
|
34
503
|
var DEFAULT_PORT = 17373;
|
|
35
504
|
var STORAGE_KEY_TOKEN = "pairToken";
|
|
36
505
|
var STORAGE_KEY_PORT = "cliPort";
|
|
37
506
|
var STORAGE_KEY_DEBUG_RICH_TEXT = "debugRichText";
|
|
507
|
+
var STORAGE_KEY_WORKSPACE = "agentWorkspace";
|
|
38
508
|
var DEFAULT_TAB_LOAD_TIMEOUT_MS = 4e4;
|
|
39
509
|
var ws = null;
|
|
40
510
|
var reconnectTimer = null;
|
|
@@ -104,6 +574,171 @@
|
|
|
104
574
|
}
|
|
105
575
|
return toError("E_INTERNAL", message);
|
|
106
576
|
}
|
|
577
|
+
function toTabInfo(tab) {
|
|
578
|
+
if (typeof tab.id !== "number" || typeof tab.windowId !== "number") {
|
|
579
|
+
throw new Error("Tab is missing runtime identifiers");
|
|
580
|
+
}
|
|
581
|
+
return {
|
|
582
|
+
id: tab.id,
|
|
583
|
+
title: tab.title ?? "",
|
|
584
|
+
url: tab.url ?? "",
|
|
585
|
+
active: Boolean(tab.active),
|
|
586
|
+
windowId: tab.windowId,
|
|
587
|
+
groupId: typeof tab.groupId === "number" && tab.groupId >= 0 ? tab.groupId : null
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
async function loadWorkspaceState() {
|
|
591
|
+
const stored = await chrome.storage.local.get(STORAGE_KEY_WORKSPACE);
|
|
592
|
+
const state = stored[STORAGE_KEY_WORKSPACE];
|
|
593
|
+
if (!state || typeof state !== "object") {
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
return state;
|
|
597
|
+
}
|
|
598
|
+
async function saveWorkspaceState(state) {
|
|
599
|
+
if (state === null) {
|
|
600
|
+
await chrome.storage.local.remove(STORAGE_KEY_WORKSPACE);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
await chrome.storage.local.set({ [STORAGE_KEY_WORKSPACE]: state });
|
|
604
|
+
}
|
|
605
|
+
var workspaceBrowser = {
|
|
606
|
+
async getTab(tabId) {
|
|
607
|
+
try {
|
|
608
|
+
return toTabInfo(await chrome.tabs.get(tabId));
|
|
609
|
+
} catch {
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
async getActiveTab() {
|
|
614
|
+
const tabs = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
|
|
615
|
+
const tab = tabs[0];
|
|
616
|
+
if (!tab) {
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
return toTabInfo(tab);
|
|
620
|
+
},
|
|
621
|
+
async listTabs(filter) {
|
|
622
|
+
const tabs = await chrome.tabs.query(filter?.windowId ? { windowId: filter.windowId } : {});
|
|
623
|
+
return tabs.filter((tab) => typeof tab.id === "number" && typeof tab.windowId === "number").map((tab) => toTabInfo(tab));
|
|
624
|
+
},
|
|
625
|
+
async createTab(options) {
|
|
626
|
+
const createdTab = await chrome.tabs.create({
|
|
627
|
+
windowId: options.windowId,
|
|
628
|
+
url: options.url ?? "about:blank",
|
|
629
|
+
active: options.active
|
|
630
|
+
});
|
|
631
|
+
if (!createdTab) {
|
|
632
|
+
throw new Error("Tab creation returned no tab");
|
|
633
|
+
}
|
|
634
|
+
return toTabInfo(createdTab);
|
|
635
|
+
},
|
|
636
|
+
async updateTab(tabId, options) {
|
|
637
|
+
const updatedTab = await chrome.tabs.update(tabId, {
|
|
638
|
+
active: options.active,
|
|
639
|
+
url: options.url
|
|
640
|
+
});
|
|
641
|
+
if (!updatedTab) {
|
|
642
|
+
throw new Error(`Tab update returned no tab for ${tabId}`);
|
|
643
|
+
}
|
|
644
|
+
return toTabInfo(updatedTab);
|
|
645
|
+
},
|
|
646
|
+
async closeTab(tabId) {
|
|
647
|
+
await chrome.tabs.remove(tabId);
|
|
648
|
+
},
|
|
649
|
+
async getWindow(windowId) {
|
|
650
|
+
try {
|
|
651
|
+
const window = await chrome.windows.get(windowId);
|
|
652
|
+
return {
|
|
653
|
+
id: window.id,
|
|
654
|
+
focused: Boolean(window.focused)
|
|
655
|
+
};
|
|
656
|
+
} catch {
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
},
|
|
660
|
+
async createWindow(options) {
|
|
661
|
+
const previouslyFocusedWindow = options.focused === true ? null : (await chrome.windows.getAll()).find((window) => window.focused === true && typeof window.id === "number") ?? null;
|
|
662
|
+
const previouslyFocusedTab = previouslyFocusedWindow?.id !== void 0 ? (await chrome.tabs.query({ windowId: previouslyFocusedWindow.id, active: true })).find((tab) => typeof tab.id === "number") ?? null : null;
|
|
663
|
+
const created = await chrome.windows.create({
|
|
664
|
+
url: options.url ?? "about:blank",
|
|
665
|
+
focused: true
|
|
666
|
+
});
|
|
667
|
+
if (!created || typeof created.id !== "number") {
|
|
668
|
+
throw new Error("Window missing id");
|
|
669
|
+
}
|
|
670
|
+
if (options.focused !== true && previouslyFocusedWindow?.id && previouslyFocusedWindow.id !== created.id) {
|
|
671
|
+
await chrome.windows.update(previouslyFocusedWindow.id, { focused: true });
|
|
672
|
+
if (typeof previouslyFocusedTab?.id === "number") {
|
|
673
|
+
await chrome.tabs.update(previouslyFocusedTab.id, { active: true });
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
const finalWindow = await chrome.windows.get(created.id);
|
|
677
|
+
return {
|
|
678
|
+
id: finalWindow.id,
|
|
679
|
+
focused: Boolean(finalWindow.focused)
|
|
680
|
+
};
|
|
681
|
+
},
|
|
682
|
+
async updateWindow(windowId, options) {
|
|
683
|
+
const updated = await chrome.windows.update(windowId, {
|
|
684
|
+
focused: options.focused
|
|
685
|
+
});
|
|
686
|
+
if (!updated || typeof updated.id !== "number") {
|
|
687
|
+
throw new Error("Window missing id");
|
|
688
|
+
}
|
|
689
|
+
return {
|
|
690
|
+
id: updated.id,
|
|
691
|
+
focused: Boolean(updated.focused)
|
|
692
|
+
};
|
|
693
|
+
},
|
|
694
|
+
async closeWindow(windowId) {
|
|
695
|
+
await chrome.windows.remove(windowId);
|
|
696
|
+
},
|
|
697
|
+
async getGroup(groupId) {
|
|
698
|
+
try {
|
|
699
|
+
const group = await chrome.tabGroups.get(groupId);
|
|
700
|
+
return {
|
|
701
|
+
id: group.id,
|
|
702
|
+
windowId: group.windowId,
|
|
703
|
+
title: group.title ?? "",
|
|
704
|
+
color: group.color,
|
|
705
|
+
collapsed: Boolean(group.collapsed)
|
|
706
|
+
};
|
|
707
|
+
} catch {
|
|
708
|
+
return null;
|
|
709
|
+
}
|
|
710
|
+
},
|
|
711
|
+
async groupTabs(tabIds, groupId) {
|
|
712
|
+
return await chrome.tabs.group({
|
|
713
|
+
tabIds,
|
|
714
|
+
groupId
|
|
715
|
+
});
|
|
716
|
+
},
|
|
717
|
+
async updateGroup(groupId, options) {
|
|
718
|
+
const updated = await chrome.tabGroups.update(groupId, {
|
|
719
|
+
title: options.title,
|
|
720
|
+
color: options.color,
|
|
721
|
+
collapsed: options.collapsed
|
|
722
|
+
});
|
|
723
|
+
if (!updated) {
|
|
724
|
+
throw new Error(`Tab group update returned no group for ${groupId}`);
|
|
725
|
+
}
|
|
726
|
+
return {
|
|
727
|
+
id: updated.id,
|
|
728
|
+
windowId: updated.windowId,
|
|
729
|
+
title: updated.title ?? "",
|
|
730
|
+
color: updated.color,
|
|
731
|
+
collapsed: Boolean(updated.collapsed)
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
var workspaceManager = new WorkspaceManager(
|
|
736
|
+
{
|
|
737
|
+
load: loadWorkspaceState,
|
|
738
|
+
save: saveWorkspaceState
|
|
739
|
+
},
|
|
740
|
+
workspaceBrowser
|
|
741
|
+
);
|
|
107
742
|
async function waitForTabComplete(tabId, timeoutMs = DEFAULT_TAB_LOAD_TIMEOUT_MS) {
|
|
108
743
|
try {
|
|
109
744
|
const current = await chrome.tabs.get(tabId);
|
|
@@ -161,28 +796,46 @@
|
|
|
161
796
|
probeStatus();
|
|
162
797
|
});
|
|
163
798
|
}
|
|
164
|
-
async function
|
|
799
|
+
async function waitForTabUrl(tabId, expectedUrl, timeoutMs = 1e4) {
|
|
800
|
+
const deadline = Date.now() + timeoutMs;
|
|
801
|
+
while (Date.now() < deadline) {
|
|
802
|
+
try {
|
|
803
|
+
const tab = await chrome.tabs.get(tabId);
|
|
804
|
+
const currentUrl = tab.url ?? "";
|
|
805
|
+
const pendingUrl = "pendingUrl" in tab && typeof tab.pendingUrl === "string" ? tab.pendingUrl : "";
|
|
806
|
+
if (currentUrl === expectedUrl || pendingUrl === expectedUrl) {
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
} catch {
|
|
810
|
+
}
|
|
811
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
812
|
+
}
|
|
813
|
+
throw new Error(`tab url timeout: ${tabId} -> ${expectedUrl}`);
|
|
814
|
+
}
|
|
815
|
+
async function withTab(target = {}, options = {}) {
|
|
165
816
|
const requireSupportedAutomationUrl = options.requireSupportedAutomationUrl !== false;
|
|
166
817
|
const validate = (tab2) => {
|
|
167
818
|
if (!tab2.id) {
|
|
168
819
|
throw toError("E_NOT_FOUND", "Tab missing id");
|
|
169
820
|
}
|
|
170
|
-
|
|
821
|
+
const pendingUrl = "pendingUrl" in tab2 && typeof tab2.pendingUrl === "string" ? tab2.pendingUrl : "";
|
|
822
|
+
if (requireSupportedAutomationUrl && !isSupportedAutomationUrl(tab2.url) && !isSupportedAutomationUrl(pendingUrl)) {
|
|
171
823
|
throw toError("E_PERMISSION", "Unsupported tab URL: only http/https pages can be automated", {
|
|
172
|
-
url: tab2.url ?? ""
|
|
824
|
+
url: tab2.url ?? pendingUrl ?? ""
|
|
173
825
|
});
|
|
174
826
|
}
|
|
175
827
|
return tab2;
|
|
176
828
|
};
|
|
177
|
-
if (typeof tabId === "number") {
|
|
178
|
-
const tab2 = await chrome.tabs.get(tabId);
|
|
829
|
+
if (typeof target.tabId === "number") {
|
|
830
|
+
const tab2 = await chrome.tabs.get(target.tabId);
|
|
179
831
|
return validate(tab2);
|
|
180
832
|
}
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
833
|
+
const resolved = await workspaceManager.resolveTarget({
|
|
834
|
+
tabId: target.tabId,
|
|
835
|
+
workspaceId: typeof target.workspaceId === "string" ? target.workspaceId : void 0,
|
|
836
|
+
createIfMissing: false
|
|
837
|
+
});
|
|
838
|
+
const tab = await chrome.tabs.get(resolved.tab.id);
|
|
186
839
|
return validate(tab);
|
|
187
840
|
}
|
|
188
841
|
async function captureAlignedTabScreenshot(tab) {
|
|
@@ -208,7 +861,7 @@
|
|
|
208
861
|
}
|
|
209
862
|
}
|
|
210
863
|
async function sendToContent(tabId, message) {
|
|
211
|
-
const maxAttempts =
|
|
864
|
+
const maxAttempts = 10;
|
|
212
865
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
213
866
|
try {
|
|
214
867
|
const response = await chrome.tabs.sendMessage(tabId, message);
|
|
@@ -222,11 +875,44 @@
|
|
|
222
875
|
if (!retriable || attempt >= maxAttempts) {
|
|
223
876
|
throw toError("E_NOT_READY", "Content script unavailable", { detail });
|
|
224
877
|
}
|
|
225
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
878
|
+
await new Promise((resolve) => setTimeout(resolve, 100 * attempt));
|
|
226
879
|
}
|
|
227
880
|
}
|
|
228
881
|
throw toError("E_NOT_READY", "Content script unavailable");
|
|
229
882
|
}
|
|
883
|
+
async function captureFocusContext() {
|
|
884
|
+
const activeTabs = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
|
|
885
|
+
const activeTab = activeTabs.find((tab) => typeof tab.id === "number" && typeof tab.windowId === "number") ?? null;
|
|
886
|
+
return {
|
|
887
|
+
windowId: activeTab?.windowId ?? null,
|
|
888
|
+
tabId: activeTab?.id ?? null
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
async function restoreFocusContext(context) {
|
|
892
|
+
if (context.windowId !== null) {
|
|
893
|
+
try {
|
|
894
|
+
await chrome.windows.update(context.windowId, { focused: true });
|
|
895
|
+
} catch {
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
if (context.tabId !== null) {
|
|
899
|
+
try {
|
|
900
|
+
await chrome.tabs.update(context.tabId, { active: true });
|
|
901
|
+
} catch {
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
async function preserveHumanFocus(enabled, action) {
|
|
906
|
+
if (!enabled) {
|
|
907
|
+
return action();
|
|
908
|
+
}
|
|
909
|
+
const focusContext = await captureFocusContext();
|
|
910
|
+
try {
|
|
911
|
+
return await action();
|
|
912
|
+
} finally {
|
|
913
|
+
await restoreFocusContext(focusContext);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
230
916
|
function requireRpcEnvelope(method, value) {
|
|
231
917
|
if (typeof value !== "object" || value === null || typeof value.ok !== "boolean") {
|
|
232
918
|
throw toError("E_NOT_READY", `Content script returned malformed response for ${method}`);
|
|
@@ -247,6 +933,10 @@
|
|
|
247
933
|
}
|
|
248
934
|
async function handleRequest(request) {
|
|
249
935
|
const params = request.params ?? {};
|
|
936
|
+
const target = {
|
|
937
|
+
tabId: typeof params.tabId === "number" ? params.tabId : void 0,
|
|
938
|
+
workspaceId: typeof params.workspaceId === "string" ? params.workspaceId : void 0
|
|
939
|
+
};
|
|
250
940
|
const rpcForwardMethods = /* @__PURE__ */ new Set([
|
|
251
941
|
"page.title",
|
|
252
942
|
"page.url",
|
|
@@ -291,12 +981,7 @@
|
|
|
291
981
|
case "tabs.list": {
|
|
292
982
|
const tabs = await chrome.tabs.query({});
|
|
293
983
|
return {
|
|
294
|
-
tabs: tabs.filter((tab) => typeof tab.id === "number").map((tab) => (
|
|
295
|
-
id: tab.id,
|
|
296
|
-
title: tab.title ?? "",
|
|
297
|
-
url: tab.url ?? "",
|
|
298
|
-
active: tab.active
|
|
299
|
-
}))
|
|
984
|
+
tabs: tabs.filter((tab) => typeof tab.id === "number" && typeof tab.windowId === "number").map((tab) => toTabInfo(tab))
|
|
300
985
|
};
|
|
301
986
|
}
|
|
302
987
|
case "tabs.getActive": {
|
|
@@ -306,12 +991,7 @@
|
|
|
306
991
|
return { tab: null };
|
|
307
992
|
}
|
|
308
993
|
return {
|
|
309
|
-
tab:
|
|
310
|
-
id: tab.id,
|
|
311
|
-
title: tab.title ?? "",
|
|
312
|
-
url: tab.url ?? "",
|
|
313
|
-
active: Boolean(tab.active)
|
|
314
|
-
}
|
|
994
|
+
tab: toTabInfo(tab)
|
|
315
995
|
};
|
|
316
996
|
}
|
|
317
997
|
case "tabs.get": {
|
|
@@ -321,12 +1001,7 @@
|
|
|
321
1001
|
throw toError("E_NOT_FOUND", "Tab missing id");
|
|
322
1002
|
}
|
|
323
1003
|
return {
|
|
324
|
-
tab:
|
|
325
|
-
id: tab.id,
|
|
326
|
-
title: tab.title ?? "",
|
|
327
|
-
url: tab.url ?? "",
|
|
328
|
-
active: Boolean(tab.active)
|
|
329
|
-
}
|
|
1004
|
+
tab: toTabInfo(tab)
|
|
330
1005
|
};
|
|
331
1006
|
}
|
|
332
1007
|
case "tabs.focus": {
|
|
@@ -335,164 +1010,275 @@
|
|
|
335
1010
|
return { ok: true };
|
|
336
1011
|
}
|
|
337
1012
|
case "tabs.new": {
|
|
338
|
-
|
|
339
|
-
|
|
1013
|
+
if (typeof params.workspaceId === "string" || params.windowId === void 0) {
|
|
1014
|
+
const opened = await preserveHumanFocus(true, async () => {
|
|
1015
|
+
return await workspaceManager.openTab({
|
|
1016
|
+
workspaceId: typeof params.workspaceId === "string" ? params.workspaceId : DEFAULT_WORKSPACE_ID,
|
|
1017
|
+
url: params.url ?? "about:blank",
|
|
1018
|
+
active: params.active === true,
|
|
1019
|
+
focus: false
|
|
1020
|
+
});
|
|
1021
|
+
});
|
|
1022
|
+
return {
|
|
1023
|
+
tabId: opened.tab.id,
|
|
1024
|
+
windowId: opened.tab.windowId,
|
|
1025
|
+
groupId: opened.workspace.groupId,
|
|
1026
|
+
workspaceId: opened.workspace.id
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
const tab = await chrome.tabs.create({
|
|
1030
|
+
url: params.url ?? "about:blank",
|
|
1031
|
+
windowId: typeof params.windowId === "number" ? params.windowId : void 0,
|
|
1032
|
+
active: params.active === true
|
|
1033
|
+
});
|
|
1034
|
+
if (params.addToGroup === true && typeof tab.id === "number") {
|
|
1035
|
+
const groupId = await chrome.tabs.group({ tabIds: [tab.id] });
|
|
1036
|
+
return {
|
|
1037
|
+
tabId: tab.id,
|
|
1038
|
+
windowId: tab.windowId,
|
|
1039
|
+
groupId
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
return {
|
|
1043
|
+
tabId: tab.id,
|
|
1044
|
+
windowId: tab.windowId
|
|
1045
|
+
};
|
|
340
1046
|
}
|
|
341
1047
|
case "tabs.close": {
|
|
342
1048
|
const tabId = Number(params.tabId);
|
|
343
1049
|
await chrome.tabs.remove(tabId);
|
|
344
1050
|
return { ok: true };
|
|
345
1051
|
}
|
|
1052
|
+
case "workspace.ensure": {
|
|
1053
|
+
return preserveHumanFocus(params.focus !== true, async () => {
|
|
1054
|
+
return await workspaceManager.ensureWorkspace({
|
|
1055
|
+
workspaceId: typeof params.workspaceId === "string" ? params.workspaceId : void 0,
|
|
1056
|
+
focus: params.focus === true,
|
|
1057
|
+
initialUrl: typeof params.url === "string" ? params.url : void 0
|
|
1058
|
+
});
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
case "workspace.info": {
|
|
1062
|
+
return {
|
|
1063
|
+
workspace: await workspaceManager.getWorkspaceInfo(typeof params.workspaceId === "string" ? params.workspaceId : void 0)
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
case "workspace.openTab": {
|
|
1067
|
+
return await preserveHumanFocus(params.focus !== true, async () => {
|
|
1068
|
+
return await workspaceManager.openTab({
|
|
1069
|
+
workspaceId: typeof params.workspaceId === "string" ? params.workspaceId : void 0,
|
|
1070
|
+
url: typeof params.url === "string" ? params.url : void 0,
|
|
1071
|
+
active: params.active === true,
|
|
1072
|
+
focus: params.focus === true
|
|
1073
|
+
});
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
case "workspace.listTabs": {
|
|
1077
|
+
return await workspaceManager.listTabs(typeof params.workspaceId === "string" ? params.workspaceId : void 0);
|
|
1078
|
+
}
|
|
1079
|
+
case "workspace.getActiveTab": {
|
|
1080
|
+
return await workspaceManager.getActiveTab(typeof params.workspaceId === "string" ? params.workspaceId : void 0);
|
|
1081
|
+
}
|
|
1082
|
+
case "workspace.setActiveTab": {
|
|
1083
|
+
return await workspaceManager.setActiveTab(Number(params.tabId), typeof params.workspaceId === "string" ? params.workspaceId : void 0);
|
|
1084
|
+
}
|
|
1085
|
+
case "workspace.focus": {
|
|
1086
|
+
return await workspaceManager.focus(typeof params.workspaceId === "string" ? params.workspaceId : void 0);
|
|
1087
|
+
}
|
|
1088
|
+
case "workspace.reset": {
|
|
1089
|
+
return await preserveHumanFocus(params.focus !== true, async () => {
|
|
1090
|
+
return await workspaceManager.reset({
|
|
1091
|
+
workspaceId: typeof params.workspaceId === "string" ? params.workspaceId : void 0,
|
|
1092
|
+
focus: params.focus === true,
|
|
1093
|
+
initialUrl: typeof params.url === "string" ? params.url : void 0
|
|
1094
|
+
});
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
case "workspace.close": {
|
|
1098
|
+
return await workspaceManager.close(typeof params.workspaceId === "string" ? params.workspaceId : void 0);
|
|
1099
|
+
}
|
|
346
1100
|
case "page.goto": {
|
|
347
|
-
|
|
348
|
-
|
|
1101
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
1102
|
+
const tab = await withTab(target, {
|
|
1103
|
+
requireSupportedAutomationUrl: false
|
|
1104
|
+
});
|
|
1105
|
+
const url = String(params.url ?? "about:blank");
|
|
1106
|
+
await chrome.tabs.update(tab.id, { url });
|
|
1107
|
+
await waitForTabUrl(tab.id, url);
|
|
1108
|
+
await forwardContentRpc(tab.id, "page.url", { tabId: tab.id }).catch(() => void 0);
|
|
1109
|
+
await waitForTabComplete(tab.id, 5e3).catch(() => void 0);
|
|
1110
|
+
return { ok: true };
|
|
349
1111
|
});
|
|
350
|
-
await chrome.tabs.update(tab.id, { url: String(params.url ?? "about:blank") });
|
|
351
|
-
await waitForTabComplete(tab.id);
|
|
352
|
-
return { ok: true };
|
|
353
1112
|
}
|
|
354
1113
|
case "page.back": {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
1114
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
1115
|
+
const tab = await withTab(target);
|
|
1116
|
+
await chrome.tabs.goBack(tab.id);
|
|
1117
|
+
await waitForTabComplete(tab.id);
|
|
1118
|
+
return { ok: true };
|
|
1119
|
+
});
|
|
359
1120
|
}
|
|
360
1121
|
case "page.forward": {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
1122
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
1123
|
+
const tab = await withTab(target);
|
|
1124
|
+
await chrome.tabs.goForward(tab.id);
|
|
1125
|
+
await waitForTabComplete(tab.id);
|
|
1126
|
+
return { ok: true };
|
|
1127
|
+
});
|
|
365
1128
|
}
|
|
366
1129
|
case "page.reload": {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
1130
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
1131
|
+
const tab = await withTab(target);
|
|
1132
|
+
await chrome.tabs.reload(tab.id);
|
|
1133
|
+
await waitForTabComplete(tab.id);
|
|
1134
|
+
return { ok: true };
|
|
1135
|
+
});
|
|
371
1136
|
}
|
|
372
1137
|
case "page.viewport": {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
if (typeof tab.windowId !== "number") {
|
|
377
|
-
throw toError("E_NOT_FOUND", "Tab window unavailable");
|
|
378
|
-
}
|
|
379
|
-
const width = typeof params.width === "number" ? Math.max(320, Math.floor(params.width)) : void 0;
|
|
380
|
-
const height = typeof params.height === "number" ? Math.max(320, Math.floor(params.height)) : void 0;
|
|
381
|
-
if (width || height) {
|
|
382
|
-
await chrome.windows.update(tab.windowId, {
|
|
383
|
-
width,
|
|
384
|
-
height
|
|
1138
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
1139
|
+
const tab = await withTab(target, {
|
|
1140
|
+
requireSupportedAutomationUrl: false
|
|
385
1141
|
});
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
width
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
1142
|
+
if (typeof tab.windowId !== "number") {
|
|
1143
|
+
throw toError("E_NOT_FOUND", "Tab window unavailable");
|
|
1144
|
+
}
|
|
1145
|
+
const width = typeof params.width === "number" ? Math.max(320, Math.floor(params.width)) : void 0;
|
|
1146
|
+
const height = typeof params.height === "number" ? Math.max(320, Math.floor(params.height)) : void 0;
|
|
1147
|
+
if (width || height) {
|
|
1148
|
+
await chrome.windows.update(tab.windowId, {
|
|
1149
|
+
width,
|
|
1150
|
+
height
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
const viewport = await forwardContentRpc(tab.id, "page.viewport", {});
|
|
1154
|
+
const viewWidth = typeof width === "number" ? width : viewport.width ?? tab.width ?? 0;
|
|
1155
|
+
const viewHeight = typeof height === "number" ? height : viewport.height ?? tab.height ?? 0;
|
|
1156
|
+
return {
|
|
1157
|
+
width: viewWidth,
|
|
1158
|
+
height: viewHeight,
|
|
1159
|
+
devicePixelRatio: viewport.devicePixelRatio
|
|
1160
|
+
};
|
|
1161
|
+
});
|
|
395
1162
|
}
|
|
396
1163
|
case "page.snapshot": {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
1164
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
1165
|
+
const tab = await withTab(target);
|
|
1166
|
+
if (typeof tab.id !== "number" || typeof tab.windowId !== "number") {
|
|
1167
|
+
throw toError("E_NOT_FOUND", "Tab missing id");
|
|
1168
|
+
}
|
|
1169
|
+
const includeBase64 = params.includeBase64 !== false;
|
|
1170
|
+
const config = await getConfig();
|
|
1171
|
+
const elements = await sendToContent(tab.id, {
|
|
1172
|
+
type: "bak.collectElements",
|
|
1173
|
+
debugRichText: config.debugRichText
|
|
1174
|
+
});
|
|
1175
|
+
const imageData = await captureAlignedTabScreenshot(tab);
|
|
1176
|
+
return {
|
|
1177
|
+
imageBase64: includeBase64 ? imageData.replace(/^data:image\/png;base64,/, "") : "",
|
|
1178
|
+
elements: elements.elements,
|
|
1179
|
+
tabId: tab.id,
|
|
1180
|
+
url: tab.url ?? ""
|
|
1181
|
+
};
|
|
406
1182
|
});
|
|
407
|
-
const imageData = await captureAlignedTabScreenshot(tab);
|
|
408
|
-
return {
|
|
409
|
-
imageBase64: includeBase64 ? imageData.replace(/^data:image\/png;base64,/, "") : "",
|
|
410
|
-
elements: elements.elements,
|
|
411
|
-
tabId: tab.id,
|
|
412
|
-
url: tab.url ?? ""
|
|
413
|
-
};
|
|
414
1183
|
}
|
|
415
1184
|
case "element.click": {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
1185
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
1186
|
+
const tab = await withTab(target);
|
|
1187
|
+
const response = await sendToContent(tab.id, {
|
|
1188
|
+
type: "bak.performAction",
|
|
1189
|
+
action: "click",
|
|
1190
|
+
locator: params.locator,
|
|
1191
|
+
requiresConfirm: params.requiresConfirm === true
|
|
1192
|
+
});
|
|
1193
|
+
if (!response.ok) {
|
|
1194
|
+
throw response.error ?? toError("E_INTERNAL", "element.click failed");
|
|
1195
|
+
}
|
|
1196
|
+
return { ok: true };
|
|
422
1197
|
});
|
|
423
|
-
if (!response.ok) {
|
|
424
|
-
throw response.error ?? toError("E_INTERNAL", "element.click failed");
|
|
425
|
-
}
|
|
426
|
-
return { ok: true };
|
|
427
1198
|
}
|
|
428
1199
|
case "element.type": {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
1200
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
1201
|
+
const tab = await withTab(target);
|
|
1202
|
+
const response = await sendToContent(tab.id, {
|
|
1203
|
+
type: "bak.performAction",
|
|
1204
|
+
action: "type",
|
|
1205
|
+
locator: params.locator,
|
|
1206
|
+
text: String(params.text ?? ""),
|
|
1207
|
+
clear: Boolean(params.clear),
|
|
1208
|
+
requiresConfirm: params.requiresConfirm === true
|
|
1209
|
+
});
|
|
1210
|
+
if (!response.ok) {
|
|
1211
|
+
throw response.error ?? toError("E_INTERNAL", "element.type failed");
|
|
1212
|
+
}
|
|
1213
|
+
return { ok: true };
|
|
437
1214
|
});
|
|
438
|
-
if (!response.ok) {
|
|
439
|
-
throw response.error ?? toError("E_INTERNAL", "element.type failed");
|
|
440
|
-
}
|
|
441
|
-
return { ok: true };
|
|
442
1215
|
}
|
|
443
1216
|
case "element.scroll": {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
1217
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
1218
|
+
const tab = await withTab(target);
|
|
1219
|
+
const response = await sendToContent(tab.id, {
|
|
1220
|
+
type: "bak.performAction",
|
|
1221
|
+
action: "scroll",
|
|
1222
|
+
locator: params.locator,
|
|
1223
|
+
dx: Number(params.dx ?? 0),
|
|
1224
|
+
dy: Number(params.dy ?? 320)
|
|
1225
|
+
});
|
|
1226
|
+
if (!response.ok) {
|
|
1227
|
+
throw response.error ?? toError("E_INTERNAL", "element.scroll failed");
|
|
1228
|
+
}
|
|
1229
|
+
return { ok: true };
|
|
451
1230
|
});
|
|
452
|
-
if (!response.ok) {
|
|
453
|
-
throw response.error ?? toError("E_INTERNAL", "element.scroll failed");
|
|
454
|
-
}
|
|
455
|
-
return { ok: true };
|
|
456
1231
|
}
|
|
457
1232
|
case "page.wait": {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
1233
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
1234
|
+
const tab = await withTab(target);
|
|
1235
|
+
const response = await sendToContent(tab.id, {
|
|
1236
|
+
type: "bak.waitFor",
|
|
1237
|
+
mode: String(params.mode ?? "selector"),
|
|
1238
|
+
value: String(params.value ?? ""),
|
|
1239
|
+
timeoutMs: Number(params.timeoutMs ?? 5e3)
|
|
1240
|
+
});
|
|
1241
|
+
if (!response.ok) {
|
|
1242
|
+
throw response.error ?? toError("E_TIMEOUT", "page.wait failed");
|
|
1243
|
+
}
|
|
1244
|
+
return { ok: true };
|
|
464
1245
|
});
|
|
465
|
-
if (!response.ok) {
|
|
466
|
-
throw response.error ?? toError("E_TIMEOUT", "page.wait failed");
|
|
467
|
-
}
|
|
468
|
-
return { ok: true };
|
|
469
1246
|
}
|
|
470
1247
|
case "debug.getConsole": {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
1248
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
1249
|
+
const tab = await withTab(target);
|
|
1250
|
+
const response = await sendToContent(tab.id, {
|
|
1251
|
+
type: "bak.getConsole",
|
|
1252
|
+
limit: Number(params.limit ?? 50)
|
|
1253
|
+
});
|
|
1254
|
+
return { entries: response.entries };
|
|
475
1255
|
});
|
|
476
|
-
return { entries: response.entries };
|
|
477
1256
|
}
|
|
478
1257
|
case "ui.selectCandidate": {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
1258
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
1259
|
+
const tab = await withTab(target);
|
|
1260
|
+
const response = await sendToContent(
|
|
1261
|
+
tab.id,
|
|
1262
|
+
{
|
|
1263
|
+
type: "bak.selectCandidate",
|
|
1264
|
+
candidates: params.candidates
|
|
1265
|
+
}
|
|
1266
|
+
);
|
|
1267
|
+
if (!response.ok || !response.selectedEid) {
|
|
1268
|
+
throw response.error ?? toError("E_NEED_USER_CONFIRM", "User did not confirm candidate");
|
|
485
1269
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
throw response.error ?? toError("E_NEED_USER_CONFIRM", "User did not confirm candidate");
|
|
489
|
-
}
|
|
490
|
-
return { selectedEid: response.selectedEid };
|
|
1270
|
+
return { selectedEid: response.selectedEid };
|
|
1271
|
+
});
|
|
491
1272
|
}
|
|
492
1273
|
default:
|
|
493
1274
|
if (rpcForwardMethods.has(request.method)) {
|
|
494
|
-
|
|
495
|
-
|
|
1275
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
1276
|
+
const tab = await withTab(target);
|
|
1277
|
+
return await forwardContentRpc(tab.id, request.method, {
|
|
1278
|
+
...params,
|
|
1279
|
+
tabId: tab.id
|
|
1280
|
+
});
|
|
1281
|
+
});
|
|
496
1282
|
}
|
|
497
1283
|
throw toError("E_NOT_FOUND", `Unsupported method from CLI bridge: ${request.method}`);
|
|
498
1284
|
}
|
|
@@ -572,6 +1358,46 @@
|
|
|
572
1358
|
ws?.close();
|
|
573
1359
|
});
|
|
574
1360
|
}
|
|
1361
|
+
chrome.tabs.onRemoved.addListener((tabId) => {
|
|
1362
|
+
void loadWorkspaceState().then(async (state) => {
|
|
1363
|
+
if (!state || !state.tabIds.includes(tabId)) {
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
const nextTabIds = state.tabIds.filter((id) => id !== tabId);
|
|
1367
|
+
await saveWorkspaceState({
|
|
1368
|
+
...state,
|
|
1369
|
+
tabIds: nextTabIds,
|
|
1370
|
+
activeTabId: state.activeTabId === tabId ? null : state.activeTabId,
|
|
1371
|
+
primaryTabId: state.primaryTabId === tabId ? null : state.primaryTabId
|
|
1372
|
+
});
|
|
1373
|
+
});
|
|
1374
|
+
});
|
|
1375
|
+
chrome.tabs.onActivated.addListener((activeInfo) => {
|
|
1376
|
+
void loadWorkspaceState().then(async (state) => {
|
|
1377
|
+
if (!state || state.windowId !== activeInfo.windowId || !state.tabIds.includes(activeInfo.tabId)) {
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
await saveWorkspaceState({
|
|
1381
|
+
...state,
|
|
1382
|
+
activeTabId: activeInfo.tabId
|
|
1383
|
+
});
|
|
1384
|
+
});
|
|
1385
|
+
});
|
|
1386
|
+
chrome.windows.onRemoved.addListener((windowId) => {
|
|
1387
|
+
void loadWorkspaceState().then(async (state) => {
|
|
1388
|
+
if (!state || state.windowId !== windowId) {
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
await saveWorkspaceState({
|
|
1392
|
+
...state,
|
|
1393
|
+
windowId: null,
|
|
1394
|
+
groupId: null,
|
|
1395
|
+
tabIds: [],
|
|
1396
|
+
activeTabId: null,
|
|
1397
|
+
primaryTabId: null
|
|
1398
|
+
});
|
|
1399
|
+
});
|
|
1400
|
+
});
|
|
575
1401
|
chrome.runtime.onInstalled.addListener(() => {
|
|
576
1402
|
void setConfig({ port: DEFAULT_PORT, debugRichText: false });
|
|
577
1403
|
});
|