@flrande/bak-extension 0.2.5 → 0.3.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/dist/.bak-e2e-build-stamp +1 -1
- package/dist/background.global.js +1052 -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 +706 -0
|
@@ -30,11 +30,560 @@
|
|
|
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
|
+
return this.inspectWorkspace(workspaceId);
|
|
47
|
+
}
|
|
48
|
+
async ensureWorkspace(options = {}) {
|
|
49
|
+
const workspaceId = this.normalizeWorkspaceId(options.workspaceId);
|
|
50
|
+
const repairActions = [];
|
|
51
|
+
const initialUrl = options.initialUrl ?? DEFAULT_WORKSPACE_URL;
|
|
52
|
+
const persisted = await this.storage.load();
|
|
53
|
+
const created = !persisted;
|
|
54
|
+
let state = this.normalizeState(persisted, workspaceId);
|
|
55
|
+
let window = state.windowId !== null ? await this.waitForWindow(state.windowId) : null;
|
|
56
|
+
let tabs = [];
|
|
57
|
+
if (!window) {
|
|
58
|
+
const createdWindow = await this.browser.createWindow({
|
|
59
|
+
url: initialUrl,
|
|
60
|
+
focused: options.focus === true
|
|
61
|
+
});
|
|
62
|
+
state.windowId = createdWindow.id;
|
|
63
|
+
state.groupId = null;
|
|
64
|
+
state.tabIds = [];
|
|
65
|
+
state.activeTabId = null;
|
|
66
|
+
state.primaryTabId = null;
|
|
67
|
+
window = createdWindow;
|
|
68
|
+
tabs = await this.waitForWindowTabs(createdWindow.id);
|
|
69
|
+
state.tabIds = tabs.map((tab) => tab.id);
|
|
70
|
+
if (state.primaryTabId === null) {
|
|
71
|
+
state.primaryTabId = tabs[0]?.id ?? null;
|
|
72
|
+
}
|
|
73
|
+
if (state.activeTabId === null) {
|
|
74
|
+
state.activeTabId = tabs.find((tab) => tab.active)?.id ?? tabs[0]?.id ?? null;
|
|
75
|
+
}
|
|
76
|
+
repairActions.push(created ? "created-window" : "recreated-window");
|
|
77
|
+
}
|
|
78
|
+
tabs = tabs.length > 0 ? tabs : await this.readTrackedTabs(state.tabIds, state.windowId);
|
|
79
|
+
const recoveredTabs = await this.recoverWorkspaceTabs(state, tabs);
|
|
80
|
+
if (recoveredTabs.length > tabs.length) {
|
|
81
|
+
tabs = recoveredTabs;
|
|
82
|
+
repairActions.push("recovered-tracked-tabs");
|
|
83
|
+
}
|
|
84
|
+
if (tabs.length !== state.tabIds.length) {
|
|
85
|
+
repairActions.push("pruned-missing-tabs");
|
|
86
|
+
}
|
|
87
|
+
state.tabIds = tabs.map((tab) => tab.id);
|
|
88
|
+
if (tabs.length === 0) {
|
|
89
|
+
const primary = await this.createWorkspaceTab({
|
|
90
|
+
windowId: state.windowId,
|
|
91
|
+
url: initialUrl,
|
|
92
|
+
active: true
|
|
93
|
+
});
|
|
94
|
+
tabs = [primary];
|
|
95
|
+
state.tabIds = [primary.id];
|
|
96
|
+
state.primaryTabId = primary.id;
|
|
97
|
+
state.activeTabId = primary.id;
|
|
98
|
+
repairActions.push("created-primary-tab");
|
|
99
|
+
}
|
|
100
|
+
if (state.primaryTabId === null || !tabs.some((tab) => tab.id === state.primaryTabId)) {
|
|
101
|
+
state.primaryTabId = tabs[0]?.id ?? null;
|
|
102
|
+
repairActions.push("reassigned-primary-tab");
|
|
103
|
+
}
|
|
104
|
+
if (state.activeTabId === null || !tabs.some((tab) => tab.id === state.activeTabId)) {
|
|
105
|
+
state.activeTabId = state.primaryTabId ?? tabs[0]?.id ?? null;
|
|
106
|
+
repairActions.push("reassigned-active-tab");
|
|
107
|
+
}
|
|
108
|
+
let group = state.groupId !== null ? await this.browser.getGroup(state.groupId) : null;
|
|
109
|
+
if (!group || group.windowId !== state.windowId) {
|
|
110
|
+
const groupId = await this.browser.groupTabs(tabs.map((tab) => tab.id));
|
|
111
|
+
group = await this.browser.updateGroup(groupId, {
|
|
112
|
+
title: state.label,
|
|
113
|
+
color: state.color,
|
|
114
|
+
collapsed: false
|
|
115
|
+
});
|
|
116
|
+
state.groupId = group.id;
|
|
117
|
+
repairActions.push("recreated-group");
|
|
118
|
+
} else {
|
|
119
|
+
await this.browser.updateGroup(group.id, {
|
|
120
|
+
title: state.label,
|
|
121
|
+
color: state.color,
|
|
122
|
+
collapsed: false
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
const ungroupedIds = tabs.filter((tab) => tab.groupId !== state.groupId).map((tab) => tab.id);
|
|
126
|
+
if (ungroupedIds.length > 0) {
|
|
127
|
+
await this.browser.groupTabs(ungroupedIds, state.groupId ?? void 0);
|
|
128
|
+
repairActions.push("regrouped-tabs");
|
|
129
|
+
}
|
|
130
|
+
tabs = await this.readTrackedTabs(state.tabIds, state.windowId);
|
|
131
|
+
tabs = await this.recoverWorkspaceTabs(state, tabs);
|
|
132
|
+
const activeTab = state.activeTabId !== null ? await this.waitForTrackedTab(state.activeTabId, state.windowId) : null;
|
|
133
|
+
if (activeTab && !tabs.some((tab) => tab.id === activeTab.id)) {
|
|
134
|
+
tabs = [...tabs, activeTab];
|
|
135
|
+
}
|
|
136
|
+
if (tabs.length === 0 && state.primaryTabId !== null) {
|
|
137
|
+
const primaryTab = await this.waitForTrackedTab(state.primaryTabId, state.windowId);
|
|
138
|
+
if (primaryTab) {
|
|
139
|
+
tabs = [primaryTab];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
state.tabIds = [...new Set(tabs.map((tab) => tab.id))];
|
|
143
|
+
if (options.focus === true && state.activeTabId !== null) {
|
|
144
|
+
await this.browser.updateTab(state.activeTabId, { active: true });
|
|
145
|
+
window = await this.browser.updateWindow(state.windowId, { focused: true });
|
|
146
|
+
void window;
|
|
147
|
+
repairActions.push("focused-window");
|
|
148
|
+
}
|
|
149
|
+
await this.storage.save(state);
|
|
150
|
+
return {
|
|
151
|
+
workspace: {
|
|
152
|
+
...state,
|
|
153
|
+
tabs
|
|
154
|
+
},
|
|
155
|
+
created,
|
|
156
|
+
repaired: repairActions.length > 0,
|
|
157
|
+
repairActions
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
async openTab(options = {}) {
|
|
161
|
+
const ensured = await this.ensureWorkspace({
|
|
162
|
+
workspaceId: options.workspaceId,
|
|
163
|
+
focus: false,
|
|
164
|
+
initialUrl: options.url ?? DEFAULT_WORKSPACE_URL
|
|
165
|
+
});
|
|
166
|
+
let state = { ...ensured.workspace };
|
|
167
|
+
const active = options.active === true;
|
|
168
|
+
const desiredUrl = options.url ?? DEFAULT_WORKSPACE_URL;
|
|
169
|
+
let reusablePrimaryTab = await this.resolveReusablePrimaryTab(
|
|
170
|
+
state,
|
|
171
|
+
ensured.created || ensured.repairActions.includes("recreated-window") || ensured.repairActions.includes("created-primary-tab")
|
|
172
|
+
);
|
|
173
|
+
let createdTab;
|
|
174
|
+
try {
|
|
175
|
+
createdTab = reusablePrimaryTab ? await this.browser.updateTab(reusablePrimaryTab.id, {
|
|
176
|
+
url: desiredUrl,
|
|
177
|
+
active
|
|
178
|
+
}) : await this.createWorkspaceTab({
|
|
179
|
+
windowId: state.windowId,
|
|
180
|
+
url: desiredUrl,
|
|
181
|
+
active
|
|
182
|
+
});
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (!this.isMissingWindowError(error)) {
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
const repaired = await this.ensureWorkspace({
|
|
188
|
+
workspaceId: options.workspaceId,
|
|
189
|
+
focus: false,
|
|
190
|
+
initialUrl: desiredUrl
|
|
191
|
+
});
|
|
192
|
+
state = { ...repaired.workspace };
|
|
193
|
+
reusablePrimaryTab = await this.resolveReusablePrimaryTab(state, true);
|
|
194
|
+
createdTab = reusablePrimaryTab ? await this.browser.updateTab(reusablePrimaryTab.id, {
|
|
195
|
+
url: desiredUrl,
|
|
196
|
+
active
|
|
197
|
+
}) : await this.createWorkspaceTab({
|
|
198
|
+
windowId: state.windowId,
|
|
199
|
+
url: desiredUrl,
|
|
200
|
+
active
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
const nextTabIds = [.../* @__PURE__ */ new Set([...state.tabIds, createdTab.id])];
|
|
204
|
+
const groupId = await this.browser.groupTabs([createdTab.id], state.groupId ?? void 0);
|
|
205
|
+
await this.browser.updateGroup(groupId, {
|
|
206
|
+
title: state.label,
|
|
207
|
+
color: state.color,
|
|
208
|
+
collapsed: false
|
|
209
|
+
});
|
|
210
|
+
const nextState = {
|
|
211
|
+
id: state.id,
|
|
212
|
+
label: state.label,
|
|
213
|
+
color: state.color,
|
|
214
|
+
windowId: state.windowId,
|
|
215
|
+
groupId,
|
|
216
|
+
tabIds: nextTabIds,
|
|
217
|
+
activeTabId: createdTab.id,
|
|
218
|
+
primaryTabId: state.primaryTabId ?? createdTab.id
|
|
219
|
+
};
|
|
220
|
+
if (options.focus === true) {
|
|
221
|
+
await this.browser.updateTab(createdTab.id, { active: true });
|
|
222
|
+
await this.browser.updateWindow(state.windowId, { focused: true });
|
|
223
|
+
}
|
|
224
|
+
await this.storage.save(nextState);
|
|
225
|
+
const tabs = await this.readTrackedTabs(nextState.tabIds, nextState.windowId);
|
|
226
|
+
const tab = tabs.find((item) => item.id === createdTab.id) ?? createdTab;
|
|
227
|
+
return {
|
|
228
|
+
workspace: {
|
|
229
|
+
...nextState,
|
|
230
|
+
tabs
|
|
231
|
+
},
|
|
232
|
+
tab
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
async listTabs(workspaceId = DEFAULT_WORKSPACE_ID) {
|
|
236
|
+
const ensured = await this.inspectWorkspace(workspaceId);
|
|
237
|
+
if (!ensured) {
|
|
238
|
+
throw new Error(`Workspace ${workspaceId} does not exist`);
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
workspace: ensured,
|
|
242
|
+
tabs: ensured.tabs
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
async getActiveTab(workspaceId = DEFAULT_WORKSPACE_ID) {
|
|
246
|
+
const ensured = await this.inspectWorkspace(workspaceId);
|
|
247
|
+
if (!ensured) {
|
|
248
|
+
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
249
|
+
return {
|
|
250
|
+
workspace: {
|
|
251
|
+
...this.normalizeState(null, normalizedWorkspaceId),
|
|
252
|
+
tabs: []
|
|
253
|
+
},
|
|
254
|
+
tab: null
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
workspace: ensured,
|
|
259
|
+
tab: ensured.tabs.find((tab) => tab.id === ensured.activeTabId) ?? null
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
async setActiveTab(tabId, workspaceId = DEFAULT_WORKSPACE_ID) {
|
|
263
|
+
const ensured = await this.ensureWorkspace({ workspaceId });
|
|
264
|
+
if (!ensured.workspace.tabIds.includes(tabId)) {
|
|
265
|
+
throw new Error(`Tab ${tabId} does not belong to workspace ${workspaceId}`);
|
|
266
|
+
}
|
|
267
|
+
const nextState = {
|
|
268
|
+
id: ensured.workspace.id,
|
|
269
|
+
label: ensured.workspace.label,
|
|
270
|
+
color: ensured.workspace.color,
|
|
271
|
+
windowId: ensured.workspace.windowId,
|
|
272
|
+
groupId: ensured.workspace.groupId,
|
|
273
|
+
tabIds: [...ensured.workspace.tabIds],
|
|
274
|
+
activeTabId: tabId,
|
|
275
|
+
primaryTabId: ensured.workspace.primaryTabId ?? tabId
|
|
276
|
+
};
|
|
277
|
+
await this.storage.save(nextState);
|
|
278
|
+
const tabs = await this.readTrackedTabs(nextState.tabIds, nextState.windowId);
|
|
279
|
+
const tab = tabs.find((item) => item.id === tabId);
|
|
280
|
+
if (!tab) {
|
|
281
|
+
throw new Error(`Tab ${tabId} is missing from workspace ${workspaceId}`);
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
workspace: {
|
|
285
|
+
...nextState,
|
|
286
|
+
tabs
|
|
287
|
+
},
|
|
288
|
+
tab
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
async focus(workspaceId = DEFAULT_WORKSPACE_ID) {
|
|
292
|
+
const ensured = await this.ensureWorkspace({ workspaceId, focus: false });
|
|
293
|
+
if (ensured.workspace.activeTabId !== null) {
|
|
294
|
+
await this.browser.updateTab(ensured.workspace.activeTabId, { active: true });
|
|
295
|
+
}
|
|
296
|
+
if (ensured.workspace.windowId !== null) {
|
|
297
|
+
await this.browser.updateWindow(ensured.workspace.windowId, { focused: true });
|
|
298
|
+
}
|
|
299
|
+
const refreshed = await this.ensureWorkspace({ workspaceId, focus: false });
|
|
300
|
+
return { ok: true, workspace: refreshed.workspace };
|
|
301
|
+
}
|
|
302
|
+
async reset(options = {}) {
|
|
303
|
+
await this.close(options.workspaceId);
|
|
304
|
+
return this.ensureWorkspace(options);
|
|
305
|
+
}
|
|
306
|
+
async close(workspaceId = DEFAULT_WORKSPACE_ID) {
|
|
307
|
+
const state = await this.storage.load();
|
|
308
|
+
if (!state || state.id !== workspaceId) {
|
|
309
|
+
await this.storage.save(null);
|
|
310
|
+
return { ok: true };
|
|
311
|
+
}
|
|
312
|
+
if (state.windowId !== null) {
|
|
313
|
+
const existingWindow = await this.browser.getWindow(state.windowId);
|
|
314
|
+
if (existingWindow) {
|
|
315
|
+
await this.browser.closeWindow(state.windowId);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
await this.storage.save(null);
|
|
319
|
+
return { ok: true };
|
|
320
|
+
}
|
|
321
|
+
async resolveTarget(options = {}) {
|
|
322
|
+
if (typeof options.tabId === "number") {
|
|
323
|
+
const explicitTab = await this.browser.getTab(options.tabId);
|
|
324
|
+
if (!explicitTab) {
|
|
325
|
+
throw new Error(`No tab with id ${options.tabId}`);
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
tab: explicitTab,
|
|
329
|
+
workspace: null,
|
|
330
|
+
resolution: "explicit-tab",
|
|
331
|
+
createdWorkspace: false,
|
|
332
|
+
repaired: false,
|
|
333
|
+
repairActions: []
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
const explicitWorkspaceId = typeof options.workspaceId === "string" ? this.normalizeWorkspaceId(options.workspaceId) : void 0;
|
|
337
|
+
if (explicitWorkspaceId) {
|
|
338
|
+
const ensured2 = await this.ensureWorkspace({
|
|
339
|
+
workspaceId: explicitWorkspaceId,
|
|
340
|
+
focus: false
|
|
341
|
+
});
|
|
342
|
+
return this.buildWorkspaceResolution(ensured2, "explicit-workspace");
|
|
343
|
+
}
|
|
344
|
+
const existingWorkspace = await this.loadWorkspaceRecord(DEFAULT_WORKSPACE_ID);
|
|
345
|
+
if (existingWorkspace) {
|
|
346
|
+
const ensured2 = await this.ensureWorkspace({
|
|
347
|
+
workspaceId: existingWorkspace.id,
|
|
348
|
+
focus: false
|
|
349
|
+
});
|
|
350
|
+
return this.buildWorkspaceResolution(ensured2, "default-workspace");
|
|
351
|
+
}
|
|
352
|
+
if (options.createIfMissing !== true) {
|
|
353
|
+
const activeTab = await this.browser.getActiveTab();
|
|
354
|
+
if (!activeTab) {
|
|
355
|
+
throw new Error("No active tab");
|
|
356
|
+
}
|
|
357
|
+
return {
|
|
358
|
+
tab: activeTab,
|
|
359
|
+
workspace: null,
|
|
360
|
+
resolution: "browser-active",
|
|
361
|
+
createdWorkspace: false,
|
|
362
|
+
repaired: false,
|
|
363
|
+
repairActions: []
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
const ensured = await this.ensureWorkspace({
|
|
367
|
+
workspaceId: DEFAULT_WORKSPACE_ID,
|
|
368
|
+
focus: false
|
|
369
|
+
});
|
|
370
|
+
return this.buildWorkspaceResolution(ensured, "default-workspace");
|
|
371
|
+
}
|
|
372
|
+
normalizeWorkspaceId(workspaceId) {
|
|
373
|
+
const candidate = workspaceId?.trim();
|
|
374
|
+
if (!candidate) {
|
|
375
|
+
return DEFAULT_WORKSPACE_ID;
|
|
376
|
+
}
|
|
377
|
+
if (candidate !== DEFAULT_WORKSPACE_ID) {
|
|
378
|
+
throw new Error(`Unsupported workspace id: ${candidate}`);
|
|
379
|
+
}
|
|
380
|
+
return candidate;
|
|
381
|
+
}
|
|
382
|
+
normalizeState(state, workspaceId) {
|
|
383
|
+
return {
|
|
384
|
+
id: workspaceId,
|
|
385
|
+
label: state?.label ?? DEFAULT_WORKSPACE_LABEL,
|
|
386
|
+
color: state?.color ?? DEFAULT_WORKSPACE_COLOR,
|
|
387
|
+
windowId: state?.windowId ?? null,
|
|
388
|
+
groupId: state?.groupId ?? null,
|
|
389
|
+
tabIds: state?.tabIds ?? [],
|
|
390
|
+
activeTabId: state?.activeTabId ?? null,
|
|
391
|
+
primaryTabId: state?.primaryTabId ?? null
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
async loadWorkspaceRecord(workspaceId = DEFAULT_WORKSPACE_ID) {
|
|
395
|
+
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
396
|
+
const state = await this.storage.load();
|
|
397
|
+
if (!state || state.id !== normalizedWorkspaceId) {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
return this.normalizeState(state, normalizedWorkspaceId);
|
|
401
|
+
}
|
|
402
|
+
async buildWorkspaceResolution(ensured, resolution) {
|
|
403
|
+
const tab = ensured.workspace.tabs.find((item) => item.id === ensured.workspace.activeTabId) ?? ensured.workspace.tabs[0] ?? null;
|
|
404
|
+
if (tab) {
|
|
405
|
+
return {
|
|
406
|
+
tab,
|
|
407
|
+
workspace: ensured.workspace,
|
|
408
|
+
resolution,
|
|
409
|
+
createdWorkspace: ensured.created,
|
|
410
|
+
repaired: ensured.repaired,
|
|
411
|
+
repairActions: ensured.repairActions
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
if (ensured.workspace.activeTabId !== null) {
|
|
415
|
+
const activeWorkspaceTab = await this.waitForTrackedTab(ensured.workspace.activeTabId, ensured.workspace.windowId);
|
|
416
|
+
if (activeWorkspaceTab) {
|
|
417
|
+
return {
|
|
418
|
+
tab: activeWorkspaceTab,
|
|
419
|
+
workspace: ensured.workspace,
|
|
420
|
+
resolution,
|
|
421
|
+
createdWorkspace: ensured.created,
|
|
422
|
+
repaired: ensured.repaired,
|
|
423
|
+
repairActions: ensured.repairActions
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
const activeTab = await this.browser.getActiveTab();
|
|
428
|
+
if (!activeTab) {
|
|
429
|
+
throw new Error("No active tab");
|
|
430
|
+
}
|
|
431
|
+
return {
|
|
432
|
+
tab: activeTab,
|
|
433
|
+
workspace: null,
|
|
434
|
+
resolution: "browser-active",
|
|
435
|
+
createdWorkspace: ensured.created,
|
|
436
|
+
repaired: ensured.repaired,
|
|
437
|
+
repairActions: ensured.repairActions
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
async readTrackedTabs(tabIds, windowId) {
|
|
441
|
+
const tabs = (await Promise.all(
|
|
442
|
+
tabIds.map(async (tabId) => {
|
|
443
|
+
const tab = await this.browser.getTab(tabId);
|
|
444
|
+
if (!tab) {
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
if (windowId !== null && tab.windowId !== windowId) {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
return tab;
|
|
451
|
+
})
|
|
452
|
+
)).filter((tab) => tab !== null);
|
|
453
|
+
return tabs;
|
|
454
|
+
}
|
|
455
|
+
async recoverWorkspaceTabs(state, existingTabs) {
|
|
456
|
+
if (state.windowId === null) {
|
|
457
|
+
return existingTabs;
|
|
458
|
+
}
|
|
459
|
+
const candidates = await this.waitForWindowTabs(state.windowId, 500);
|
|
460
|
+
if (candidates.length === 0) {
|
|
461
|
+
return existingTabs;
|
|
462
|
+
}
|
|
463
|
+
const trackedIds = new Set(state.tabIds);
|
|
464
|
+
const trackedTabs = candidates.filter((tab) => trackedIds.has(tab.id));
|
|
465
|
+
if (trackedTabs.length > existingTabs.length) {
|
|
466
|
+
return trackedTabs;
|
|
467
|
+
}
|
|
468
|
+
if (state.groupId !== null) {
|
|
469
|
+
const groupedTabs = candidates.filter((tab) => tab.groupId === state.groupId);
|
|
470
|
+
if (groupedTabs.length > 0) {
|
|
471
|
+
return groupedTabs;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
const preferredIds = new Set([state.activeTabId, state.primaryTabId].filter((value) => typeof value === "number"));
|
|
475
|
+
const preferredTabs = candidates.filter((tab) => preferredIds.has(tab.id));
|
|
476
|
+
if (preferredTabs.length > existingTabs.length) {
|
|
477
|
+
return preferredTabs;
|
|
478
|
+
}
|
|
479
|
+
return existingTabs;
|
|
480
|
+
}
|
|
481
|
+
async createWorkspaceTab(options) {
|
|
482
|
+
if (options.windowId === null) {
|
|
483
|
+
throw new Error("Workspace window is unavailable");
|
|
484
|
+
}
|
|
485
|
+
const deadline = Date.now() + 1500;
|
|
486
|
+
let lastError2 = null;
|
|
487
|
+
while (Date.now() < deadline) {
|
|
488
|
+
try {
|
|
489
|
+
return await this.browser.createTab({
|
|
490
|
+
windowId: options.windowId,
|
|
491
|
+
url: options.url,
|
|
492
|
+
active: options.active
|
|
493
|
+
});
|
|
494
|
+
} catch (error) {
|
|
495
|
+
if (!this.isMissingWindowError(error)) {
|
|
496
|
+
throw error;
|
|
497
|
+
}
|
|
498
|
+
lastError2 = error instanceof Error ? error : new Error(String(error));
|
|
499
|
+
await this.delay(50);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
throw lastError2 ?? new Error(`No window with id: ${options.windowId}.`);
|
|
503
|
+
}
|
|
504
|
+
async inspectWorkspace(workspaceId = DEFAULT_WORKSPACE_ID) {
|
|
505
|
+
const state = await this.loadWorkspaceRecord(workspaceId);
|
|
506
|
+
if (!state) {
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
let tabs = await this.readTrackedTabs(state.tabIds, state.windowId);
|
|
510
|
+
const activeTab = state.activeTabId !== null ? await this.waitForTrackedTab(state.activeTabId, state.windowId, 300) : null;
|
|
511
|
+
if (activeTab && !tabs.some((tab) => tab.id === activeTab.id)) {
|
|
512
|
+
tabs = [...tabs, activeTab];
|
|
513
|
+
}
|
|
514
|
+
if (tabs.length === 0 && state.primaryTabId !== null) {
|
|
515
|
+
const primaryTab = await this.waitForTrackedTab(state.primaryTabId, state.windowId, 300);
|
|
516
|
+
if (primaryTab) {
|
|
517
|
+
tabs = [primaryTab];
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return {
|
|
521
|
+
...state,
|
|
522
|
+
tabIds: [...new Set(state.tabIds.concat(tabs.map((tab) => tab.id)))],
|
|
523
|
+
tabs
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
async resolveReusablePrimaryTab(workspace, allowReuse) {
|
|
527
|
+
if (!allowReuse || workspace.windowId === null) {
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
if (workspace.primaryTabId !== null) {
|
|
531
|
+
const trackedPrimary = workspace.tabs.find((tab) => tab.id === workspace.primaryTabId) ?? await this.waitForTrackedTab(workspace.primaryTabId, workspace.windowId);
|
|
532
|
+
if (trackedPrimary) {
|
|
533
|
+
return trackedPrimary;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
const windowTabs = await this.waitForWindowTabs(workspace.windowId, 750);
|
|
537
|
+
return windowTabs.length === 1 ? windowTabs[0] : null;
|
|
538
|
+
}
|
|
539
|
+
async waitForWindow(windowId, timeoutMs = 750) {
|
|
540
|
+
const deadline = Date.now() + timeoutMs;
|
|
541
|
+
while (Date.now() < deadline) {
|
|
542
|
+
const window = await this.browser.getWindow(windowId);
|
|
543
|
+
if (window) {
|
|
544
|
+
return window;
|
|
545
|
+
}
|
|
546
|
+
await this.delay(50);
|
|
547
|
+
}
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
async waitForTrackedTab(tabId, windowId, timeoutMs = 1e3) {
|
|
551
|
+
const deadline = Date.now() + timeoutMs;
|
|
552
|
+
while (Date.now() < deadline) {
|
|
553
|
+
const tab = await this.browser.getTab(tabId);
|
|
554
|
+
if (tab && (windowId === null || tab.windowId === windowId)) {
|
|
555
|
+
return tab;
|
|
556
|
+
}
|
|
557
|
+
await this.delay(50);
|
|
558
|
+
}
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
async waitForWindowTabs(windowId, timeoutMs = 1e3) {
|
|
562
|
+
const deadline = Date.now() + timeoutMs;
|
|
563
|
+
while (Date.now() < deadline) {
|
|
564
|
+
const tabs = await this.browser.listTabs({ windowId });
|
|
565
|
+
if (tabs.length > 0) {
|
|
566
|
+
return tabs;
|
|
567
|
+
}
|
|
568
|
+
await this.delay(50);
|
|
569
|
+
}
|
|
570
|
+
return [];
|
|
571
|
+
}
|
|
572
|
+
async delay(ms) {
|
|
573
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
574
|
+
}
|
|
575
|
+
isMissingWindowError(error) {
|
|
576
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
577
|
+
return message.toLowerCase().includes("no window with id");
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
|
|
33
581
|
// src/background.ts
|
|
34
582
|
var DEFAULT_PORT = 17373;
|
|
35
583
|
var STORAGE_KEY_TOKEN = "pairToken";
|
|
36
584
|
var STORAGE_KEY_PORT = "cliPort";
|
|
37
585
|
var STORAGE_KEY_DEBUG_RICH_TEXT = "debugRichText";
|
|
586
|
+
var STORAGE_KEY_WORKSPACE = "agentWorkspace";
|
|
38
587
|
var DEFAULT_TAB_LOAD_TIMEOUT_MS = 4e4;
|
|
39
588
|
var ws = null;
|
|
40
589
|
var reconnectTimer = null;
|
|
@@ -104,6 +653,171 @@
|
|
|
104
653
|
}
|
|
105
654
|
return toError("E_INTERNAL", message);
|
|
106
655
|
}
|
|
656
|
+
function toTabInfo(tab) {
|
|
657
|
+
if (typeof tab.id !== "number" || typeof tab.windowId !== "number") {
|
|
658
|
+
throw new Error("Tab is missing runtime identifiers");
|
|
659
|
+
}
|
|
660
|
+
return {
|
|
661
|
+
id: tab.id,
|
|
662
|
+
title: tab.title ?? "",
|
|
663
|
+
url: tab.url ?? "",
|
|
664
|
+
active: Boolean(tab.active),
|
|
665
|
+
windowId: tab.windowId,
|
|
666
|
+
groupId: typeof tab.groupId === "number" && tab.groupId >= 0 ? tab.groupId : null
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
async function loadWorkspaceState() {
|
|
670
|
+
const stored = await chrome.storage.local.get(STORAGE_KEY_WORKSPACE);
|
|
671
|
+
const state = stored[STORAGE_KEY_WORKSPACE];
|
|
672
|
+
if (!state || typeof state !== "object") {
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
return state;
|
|
676
|
+
}
|
|
677
|
+
async function saveWorkspaceState(state) {
|
|
678
|
+
if (state === null) {
|
|
679
|
+
await chrome.storage.local.remove(STORAGE_KEY_WORKSPACE);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
await chrome.storage.local.set({ [STORAGE_KEY_WORKSPACE]: state });
|
|
683
|
+
}
|
|
684
|
+
var workspaceBrowser = {
|
|
685
|
+
async getTab(tabId) {
|
|
686
|
+
try {
|
|
687
|
+
return toTabInfo(await chrome.tabs.get(tabId));
|
|
688
|
+
} catch {
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
},
|
|
692
|
+
async getActiveTab() {
|
|
693
|
+
const tabs = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
|
|
694
|
+
const tab = tabs[0];
|
|
695
|
+
if (!tab) {
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
return toTabInfo(tab);
|
|
699
|
+
},
|
|
700
|
+
async listTabs(filter) {
|
|
701
|
+
const tabs = await chrome.tabs.query(filter?.windowId ? { windowId: filter.windowId } : {});
|
|
702
|
+
return tabs.filter((tab) => typeof tab.id === "number" && typeof tab.windowId === "number").map((tab) => toTabInfo(tab));
|
|
703
|
+
},
|
|
704
|
+
async createTab(options) {
|
|
705
|
+
const createdTab = await chrome.tabs.create({
|
|
706
|
+
windowId: options.windowId,
|
|
707
|
+
url: options.url ?? "about:blank",
|
|
708
|
+
active: options.active
|
|
709
|
+
});
|
|
710
|
+
if (!createdTab) {
|
|
711
|
+
throw new Error("Tab creation returned no tab");
|
|
712
|
+
}
|
|
713
|
+
return toTabInfo(createdTab);
|
|
714
|
+
},
|
|
715
|
+
async updateTab(tabId, options) {
|
|
716
|
+
const updatedTab = await chrome.tabs.update(tabId, {
|
|
717
|
+
active: options.active,
|
|
718
|
+
url: options.url
|
|
719
|
+
});
|
|
720
|
+
if (!updatedTab) {
|
|
721
|
+
throw new Error(`Tab update returned no tab for ${tabId}`);
|
|
722
|
+
}
|
|
723
|
+
return toTabInfo(updatedTab);
|
|
724
|
+
},
|
|
725
|
+
async closeTab(tabId) {
|
|
726
|
+
await chrome.tabs.remove(tabId);
|
|
727
|
+
},
|
|
728
|
+
async getWindow(windowId) {
|
|
729
|
+
try {
|
|
730
|
+
const window = await chrome.windows.get(windowId);
|
|
731
|
+
return {
|
|
732
|
+
id: window.id,
|
|
733
|
+
focused: Boolean(window.focused)
|
|
734
|
+
};
|
|
735
|
+
} catch {
|
|
736
|
+
return null;
|
|
737
|
+
}
|
|
738
|
+
},
|
|
739
|
+
async createWindow(options) {
|
|
740
|
+
const previouslyFocusedWindow = options.focused === true ? null : (await chrome.windows.getAll()).find((window) => window.focused === true && typeof window.id === "number") ?? null;
|
|
741
|
+
const previouslyFocusedTab = previouslyFocusedWindow?.id !== void 0 ? (await chrome.tabs.query({ windowId: previouslyFocusedWindow.id, active: true })).find((tab) => typeof tab.id === "number") ?? null : null;
|
|
742
|
+
const created = await chrome.windows.create({
|
|
743
|
+
url: options.url ?? "about:blank",
|
|
744
|
+
focused: true
|
|
745
|
+
});
|
|
746
|
+
if (!created || typeof created.id !== "number") {
|
|
747
|
+
throw new Error("Window missing id");
|
|
748
|
+
}
|
|
749
|
+
if (options.focused !== true && previouslyFocusedWindow?.id && previouslyFocusedWindow.id !== created.id) {
|
|
750
|
+
await chrome.windows.update(previouslyFocusedWindow.id, { focused: true });
|
|
751
|
+
if (typeof previouslyFocusedTab?.id === "number") {
|
|
752
|
+
await chrome.tabs.update(previouslyFocusedTab.id, { active: true });
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
const finalWindow = await chrome.windows.get(created.id);
|
|
756
|
+
return {
|
|
757
|
+
id: finalWindow.id,
|
|
758
|
+
focused: Boolean(finalWindow.focused)
|
|
759
|
+
};
|
|
760
|
+
},
|
|
761
|
+
async updateWindow(windowId, options) {
|
|
762
|
+
const updated = await chrome.windows.update(windowId, {
|
|
763
|
+
focused: options.focused
|
|
764
|
+
});
|
|
765
|
+
if (!updated || typeof updated.id !== "number") {
|
|
766
|
+
throw new Error("Window missing id");
|
|
767
|
+
}
|
|
768
|
+
return {
|
|
769
|
+
id: updated.id,
|
|
770
|
+
focused: Boolean(updated.focused)
|
|
771
|
+
};
|
|
772
|
+
},
|
|
773
|
+
async closeWindow(windowId) {
|
|
774
|
+
await chrome.windows.remove(windowId);
|
|
775
|
+
},
|
|
776
|
+
async getGroup(groupId) {
|
|
777
|
+
try {
|
|
778
|
+
const group = await chrome.tabGroups.get(groupId);
|
|
779
|
+
return {
|
|
780
|
+
id: group.id,
|
|
781
|
+
windowId: group.windowId,
|
|
782
|
+
title: group.title ?? "",
|
|
783
|
+
color: group.color,
|
|
784
|
+
collapsed: Boolean(group.collapsed)
|
|
785
|
+
};
|
|
786
|
+
} catch {
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
789
|
+
},
|
|
790
|
+
async groupTabs(tabIds, groupId) {
|
|
791
|
+
return await chrome.tabs.group({
|
|
792
|
+
tabIds,
|
|
793
|
+
groupId
|
|
794
|
+
});
|
|
795
|
+
},
|
|
796
|
+
async updateGroup(groupId, options) {
|
|
797
|
+
const updated = await chrome.tabGroups.update(groupId, {
|
|
798
|
+
title: options.title,
|
|
799
|
+
color: options.color,
|
|
800
|
+
collapsed: options.collapsed
|
|
801
|
+
});
|
|
802
|
+
if (!updated) {
|
|
803
|
+
throw new Error(`Tab group update returned no group for ${groupId}`);
|
|
804
|
+
}
|
|
805
|
+
return {
|
|
806
|
+
id: updated.id,
|
|
807
|
+
windowId: updated.windowId,
|
|
808
|
+
title: updated.title ?? "",
|
|
809
|
+
color: updated.color,
|
|
810
|
+
collapsed: Boolean(updated.collapsed)
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
};
|
|
814
|
+
var workspaceManager = new WorkspaceManager(
|
|
815
|
+
{
|
|
816
|
+
load: loadWorkspaceState,
|
|
817
|
+
save: saveWorkspaceState
|
|
818
|
+
},
|
|
819
|
+
workspaceBrowser
|
|
820
|
+
);
|
|
107
821
|
async function waitForTabComplete(tabId, timeoutMs = DEFAULT_TAB_LOAD_TIMEOUT_MS) {
|
|
108
822
|
try {
|
|
109
823
|
const current = await chrome.tabs.get(tabId);
|
|
@@ -161,28 +875,46 @@
|
|
|
161
875
|
probeStatus();
|
|
162
876
|
});
|
|
163
877
|
}
|
|
164
|
-
async function
|
|
878
|
+
async function waitForTabUrl(tabId, expectedUrl, timeoutMs = 1e4) {
|
|
879
|
+
const deadline = Date.now() + timeoutMs;
|
|
880
|
+
while (Date.now() < deadline) {
|
|
881
|
+
try {
|
|
882
|
+
const tab = await chrome.tabs.get(tabId);
|
|
883
|
+
const currentUrl = tab.url ?? "";
|
|
884
|
+
const pendingUrl = "pendingUrl" in tab && typeof tab.pendingUrl === "string" ? tab.pendingUrl : "";
|
|
885
|
+
if (currentUrl === expectedUrl || pendingUrl === expectedUrl) {
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
} catch {
|
|
889
|
+
}
|
|
890
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
891
|
+
}
|
|
892
|
+
throw new Error(`tab url timeout: ${tabId} -> ${expectedUrl}`);
|
|
893
|
+
}
|
|
894
|
+
async function withTab(target = {}, options = {}) {
|
|
165
895
|
const requireSupportedAutomationUrl = options.requireSupportedAutomationUrl !== false;
|
|
166
896
|
const validate = (tab2) => {
|
|
167
897
|
if (!tab2.id) {
|
|
168
898
|
throw toError("E_NOT_FOUND", "Tab missing id");
|
|
169
899
|
}
|
|
170
|
-
|
|
900
|
+
const pendingUrl = "pendingUrl" in tab2 && typeof tab2.pendingUrl === "string" ? tab2.pendingUrl : "";
|
|
901
|
+
if (requireSupportedAutomationUrl && !isSupportedAutomationUrl(tab2.url) && !isSupportedAutomationUrl(pendingUrl)) {
|
|
171
902
|
throw toError("E_PERMISSION", "Unsupported tab URL: only http/https pages can be automated", {
|
|
172
|
-
url: tab2.url ?? ""
|
|
903
|
+
url: tab2.url ?? pendingUrl ?? ""
|
|
173
904
|
});
|
|
174
905
|
}
|
|
175
906
|
return tab2;
|
|
176
907
|
};
|
|
177
|
-
if (typeof tabId === "number") {
|
|
178
|
-
const tab2 = await chrome.tabs.get(tabId);
|
|
908
|
+
if (typeof target.tabId === "number") {
|
|
909
|
+
const tab2 = await chrome.tabs.get(target.tabId);
|
|
179
910
|
return validate(tab2);
|
|
180
911
|
}
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
912
|
+
const resolved = await workspaceManager.resolveTarget({
|
|
913
|
+
tabId: target.tabId,
|
|
914
|
+
workspaceId: typeof target.workspaceId === "string" ? target.workspaceId : void 0,
|
|
915
|
+
createIfMissing: false
|
|
916
|
+
});
|
|
917
|
+
const tab = await chrome.tabs.get(resolved.tab.id);
|
|
186
918
|
return validate(tab);
|
|
187
919
|
}
|
|
188
920
|
async function captureAlignedTabScreenshot(tab) {
|
|
@@ -208,7 +940,7 @@
|
|
|
208
940
|
}
|
|
209
941
|
}
|
|
210
942
|
async function sendToContent(tabId, message) {
|
|
211
|
-
const maxAttempts =
|
|
943
|
+
const maxAttempts = 10;
|
|
212
944
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
213
945
|
try {
|
|
214
946
|
const response = await chrome.tabs.sendMessage(tabId, message);
|
|
@@ -222,11 +954,44 @@
|
|
|
222
954
|
if (!retriable || attempt >= maxAttempts) {
|
|
223
955
|
throw toError("E_NOT_READY", "Content script unavailable", { detail });
|
|
224
956
|
}
|
|
225
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
957
|
+
await new Promise((resolve) => setTimeout(resolve, 100 * attempt));
|
|
226
958
|
}
|
|
227
959
|
}
|
|
228
960
|
throw toError("E_NOT_READY", "Content script unavailable");
|
|
229
961
|
}
|
|
962
|
+
async function captureFocusContext() {
|
|
963
|
+
const activeTabs = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
|
|
964
|
+
const activeTab = activeTabs.find((tab) => typeof tab.id === "number" && typeof tab.windowId === "number") ?? null;
|
|
965
|
+
return {
|
|
966
|
+
windowId: activeTab?.windowId ?? null,
|
|
967
|
+
tabId: activeTab?.id ?? null
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
async function restoreFocusContext(context) {
|
|
971
|
+
if (context.windowId !== null) {
|
|
972
|
+
try {
|
|
973
|
+
await chrome.windows.update(context.windowId, { focused: true });
|
|
974
|
+
} catch {
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
if (context.tabId !== null) {
|
|
978
|
+
try {
|
|
979
|
+
await chrome.tabs.update(context.tabId, { active: true });
|
|
980
|
+
} catch {
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
async function preserveHumanFocus(enabled, action) {
|
|
985
|
+
if (!enabled) {
|
|
986
|
+
return action();
|
|
987
|
+
}
|
|
988
|
+
const focusContext = await captureFocusContext();
|
|
989
|
+
try {
|
|
990
|
+
return await action();
|
|
991
|
+
} finally {
|
|
992
|
+
await restoreFocusContext(focusContext);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
230
995
|
function requireRpcEnvelope(method, value) {
|
|
231
996
|
if (typeof value !== "object" || value === null || typeof value.ok !== "boolean") {
|
|
232
997
|
throw toError("E_NOT_READY", `Content script returned malformed response for ${method}`);
|
|
@@ -247,6 +1012,10 @@
|
|
|
247
1012
|
}
|
|
248
1013
|
async function handleRequest(request) {
|
|
249
1014
|
const params = request.params ?? {};
|
|
1015
|
+
const target = {
|
|
1016
|
+
tabId: typeof params.tabId === "number" ? params.tabId : void 0,
|
|
1017
|
+
workspaceId: typeof params.workspaceId === "string" ? params.workspaceId : void 0
|
|
1018
|
+
};
|
|
250
1019
|
const rpcForwardMethods = /* @__PURE__ */ new Set([
|
|
251
1020
|
"page.title",
|
|
252
1021
|
"page.url",
|
|
@@ -291,12 +1060,7 @@
|
|
|
291
1060
|
case "tabs.list": {
|
|
292
1061
|
const tabs = await chrome.tabs.query({});
|
|
293
1062
|
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
|
-
}))
|
|
1063
|
+
tabs: tabs.filter((tab) => typeof tab.id === "number" && typeof tab.windowId === "number").map((tab) => toTabInfo(tab))
|
|
300
1064
|
};
|
|
301
1065
|
}
|
|
302
1066
|
case "tabs.getActive": {
|
|
@@ -306,12 +1070,7 @@
|
|
|
306
1070
|
return { tab: null };
|
|
307
1071
|
}
|
|
308
1072
|
return {
|
|
309
|
-
tab:
|
|
310
|
-
id: tab.id,
|
|
311
|
-
title: tab.title ?? "",
|
|
312
|
-
url: tab.url ?? "",
|
|
313
|
-
active: Boolean(tab.active)
|
|
314
|
-
}
|
|
1073
|
+
tab: toTabInfo(tab)
|
|
315
1074
|
};
|
|
316
1075
|
}
|
|
317
1076
|
case "tabs.get": {
|
|
@@ -321,12 +1080,7 @@
|
|
|
321
1080
|
throw toError("E_NOT_FOUND", "Tab missing id");
|
|
322
1081
|
}
|
|
323
1082
|
return {
|
|
324
|
-
tab:
|
|
325
|
-
id: tab.id,
|
|
326
|
-
title: tab.title ?? "",
|
|
327
|
-
url: tab.url ?? "",
|
|
328
|
-
active: Boolean(tab.active)
|
|
329
|
-
}
|
|
1083
|
+
tab: toTabInfo(tab)
|
|
330
1084
|
};
|
|
331
1085
|
}
|
|
332
1086
|
case "tabs.focus": {
|
|
@@ -335,164 +1089,275 @@
|
|
|
335
1089
|
return { ok: true };
|
|
336
1090
|
}
|
|
337
1091
|
case "tabs.new": {
|
|
338
|
-
|
|
339
|
-
|
|
1092
|
+
if (typeof params.workspaceId === "string" || params.windowId === void 0) {
|
|
1093
|
+
const opened = await preserveHumanFocus(true, async () => {
|
|
1094
|
+
return await workspaceManager.openTab({
|
|
1095
|
+
workspaceId: typeof params.workspaceId === "string" ? params.workspaceId : DEFAULT_WORKSPACE_ID,
|
|
1096
|
+
url: params.url ?? "about:blank",
|
|
1097
|
+
active: params.active === true,
|
|
1098
|
+
focus: false
|
|
1099
|
+
});
|
|
1100
|
+
});
|
|
1101
|
+
return {
|
|
1102
|
+
tabId: opened.tab.id,
|
|
1103
|
+
windowId: opened.tab.windowId,
|
|
1104
|
+
groupId: opened.workspace.groupId,
|
|
1105
|
+
workspaceId: opened.workspace.id
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
const tab = await chrome.tabs.create({
|
|
1109
|
+
url: params.url ?? "about:blank",
|
|
1110
|
+
windowId: typeof params.windowId === "number" ? params.windowId : void 0,
|
|
1111
|
+
active: params.active === true
|
|
1112
|
+
});
|
|
1113
|
+
if (params.addToGroup === true && typeof tab.id === "number") {
|
|
1114
|
+
const groupId = await chrome.tabs.group({ tabIds: [tab.id] });
|
|
1115
|
+
return {
|
|
1116
|
+
tabId: tab.id,
|
|
1117
|
+
windowId: tab.windowId,
|
|
1118
|
+
groupId
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
return {
|
|
1122
|
+
tabId: tab.id,
|
|
1123
|
+
windowId: tab.windowId
|
|
1124
|
+
};
|
|
340
1125
|
}
|
|
341
1126
|
case "tabs.close": {
|
|
342
1127
|
const tabId = Number(params.tabId);
|
|
343
1128
|
await chrome.tabs.remove(tabId);
|
|
344
1129
|
return { ok: true };
|
|
345
1130
|
}
|
|
1131
|
+
case "workspace.ensure": {
|
|
1132
|
+
return preserveHumanFocus(params.focus !== true, async () => {
|
|
1133
|
+
return await workspaceManager.ensureWorkspace({
|
|
1134
|
+
workspaceId: typeof params.workspaceId === "string" ? params.workspaceId : void 0,
|
|
1135
|
+
focus: params.focus === true,
|
|
1136
|
+
initialUrl: typeof params.url === "string" ? params.url : void 0
|
|
1137
|
+
});
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
case "workspace.info": {
|
|
1141
|
+
return {
|
|
1142
|
+
workspace: await workspaceManager.getWorkspaceInfo(typeof params.workspaceId === "string" ? params.workspaceId : void 0)
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
case "workspace.openTab": {
|
|
1146
|
+
return await preserveHumanFocus(params.focus !== true, async () => {
|
|
1147
|
+
return await workspaceManager.openTab({
|
|
1148
|
+
workspaceId: typeof params.workspaceId === "string" ? params.workspaceId : void 0,
|
|
1149
|
+
url: typeof params.url === "string" ? params.url : void 0,
|
|
1150
|
+
active: params.active === true,
|
|
1151
|
+
focus: params.focus === true
|
|
1152
|
+
});
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
case "workspace.listTabs": {
|
|
1156
|
+
return await workspaceManager.listTabs(typeof params.workspaceId === "string" ? params.workspaceId : void 0);
|
|
1157
|
+
}
|
|
1158
|
+
case "workspace.getActiveTab": {
|
|
1159
|
+
return await workspaceManager.getActiveTab(typeof params.workspaceId === "string" ? params.workspaceId : void 0);
|
|
1160
|
+
}
|
|
1161
|
+
case "workspace.setActiveTab": {
|
|
1162
|
+
return await workspaceManager.setActiveTab(Number(params.tabId), typeof params.workspaceId === "string" ? params.workspaceId : void 0);
|
|
1163
|
+
}
|
|
1164
|
+
case "workspace.focus": {
|
|
1165
|
+
return await workspaceManager.focus(typeof params.workspaceId === "string" ? params.workspaceId : void 0);
|
|
1166
|
+
}
|
|
1167
|
+
case "workspace.reset": {
|
|
1168
|
+
return await preserveHumanFocus(params.focus !== true, async () => {
|
|
1169
|
+
return await workspaceManager.reset({
|
|
1170
|
+
workspaceId: typeof params.workspaceId === "string" ? params.workspaceId : void 0,
|
|
1171
|
+
focus: params.focus === true,
|
|
1172
|
+
initialUrl: typeof params.url === "string" ? params.url : void 0
|
|
1173
|
+
});
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
case "workspace.close": {
|
|
1177
|
+
return await workspaceManager.close(typeof params.workspaceId === "string" ? params.workspaceId : void 0);
|
|
1178
|
+
}
|
|
346
1179
|
case "page.goto": {
|
|
347
|
-
|
|
348
|
-
|
|
1180
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
1181
|
+
const tab = await withTab(target, {
|
|
1182
|
+
requireSupportedAutomationUrl: false
|
|
1183
|
+
});
|
|
1184
|
+
const url = String(params.url ?? "about:blank");
|
|
1185
|
+
await chrome.tabs.update(tab.id, { url });
|
|
1186
|
+
await waitForTabUrl(tab.id, url);
|
|
1187
|
+
await forwardContentRpc(tab.id, "page.url", { tabId: tab.id }).catch(() => void 0);
|
|
1188
|
+
await waitForTabComplete(tab.id, 5e3).catch(() => void 0);
|
|
1189
|
+
return { ok: true };
|
|
349
1190
|
});
|
|
350
|
-
await chrome.tabs.update(tab.id, { url: String(params.url ?? "about:blank") });
|
|
351
|
-
await waitForTabComplete(tab.id);
|
|
352
|
-
return { ok: true };
|
|
353
1191
|
}
|
|
354
1192
|
case "page.back": {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
1193
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
1194
|
+
const tab = await withTab(target);
|
|
1195
|
+
await chrome.tabs.goBack(tab.id);
|
|
1196
|
+
await waitForTabComplete(tab.id);
|
|
1197
|
+
return { ok: true };
|
|
1198
|
+
});
|
|
359
1199
|
}
|
|
360
1200
|
case "page.forward": {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
1201
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
1202
|
+
const tab = await withTab(target);
|
|
1203
|
+
await chrome.tabs.goForward(tab.id);
|
|
1204
|
+
await waitForTabComplete(tab.id);
|
|
1205
|
+
return { ok: true };
|
|
1206
|
+
});
|
|
365
1207
|
}
|
|
366
1208
|
case "page.reload": {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
1209
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
1210
|
+
const tab = await withTab(target);
|
|
1211
|
+
await chrome.tabs.reload(tab.id);
|
|
1212
|
+
await waitForTabComplete(tab.id);
|
|
1213
|
+
return { ok: true };
|
|
1214
|
+
});
|
|
371
1215
|
}
|
|
372
1216
|
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
|
|
1217
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
1218
|
+
const tab = await withTab(target, {
|
|
1219
|
+
requireSupportedAutomationUrl: false
|
|
385
1220
|
});
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
width
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
1221
|
+
if (typeof tab.windowId !== "number") {
|
|
1222
|
+
throw toError("E_NOT_FOUND", "Tab window unavailable");
|
|
1223
|
+
}
|
|
1224
|
+
const width = typeof params.width === "number" ? Math.max(320, Math.floor(params.width)) : void 0;
|
|
1225
|
+
const height = typeof params.height === "number" ? Math.max(320, Math.floor(params.height)) : void 0;
|
|
1226
|
+
if (width || height) {
|
|
1227
|
+
await chrome.windows.update(tab.windowId, {
|
|
1228
|
+
width,
|
|
1229
|
+
height
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
const viewport = await forwardContentRpc(tab.id, "page.viewport", {});
|
|
1233
|
+
const viewWidth = typeof width === "number" ? width : viewport.width ?? tab.width ?? 0;
|
|
1234
|
+
const viewHeight = typeof height === "number" ? height : viewport.height ?? tab.height ?? 0;
|
|
1235
|
+
return {
|
|
1236
|
+
width: viewWidth,
|
|
1237
|
+
height: viewHeight,
|
|
1238
|
+
devicePixelRatio: viewport.devicePixelRatio
|
|
1239
|
+
};
|
|
1240
|
+
});
|
|
395
1241
|
}
|
|
396
1242
|
case "page.snapshot": {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
1243
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
1244
|
+
const tab = await withTab(target);
|
|
1245
|
+
if (typeof tab.id !== "number" || typeof tab.windowId !== "number") {
|
|
1246
|
+
throw toError("E_NOT_FOUND", "Tab missing id");
|
|
1247
|
+
}
|
|
1248
|
+
const includeBase64 = params.includeBase64 !== false;
|
|
1249
|
+
const config = await getConfig();
|
|
1250
|
+
const elements = await sendToContent(tab.id, {
|
|
1251
|
+
type: "bak.collectElements",
|
|
1252
|
+
debugRichText: config.debugRichText
|
|
1253
|
+
});
|
|
1254
|
+
const imageData = await captureAlignedTabScreenshot(tab);
|
|
1255
|
+
return {
|
|
1256
|
+
imageBase64: includeBase64 ? imageData.replace(/^data:image\/png;base64,/, "") : "",
|
|
1257
|
+
elements: elements.elements,
|
|
1258
|
+
tabId: tab.id,
|
|
1259
|
+
url: tab.url ?? ""
|
|
1260
|
+
};
|
|
406
1261
|
});
|
|
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
1262
|
}
|
|
415
1263
|
case "element.click": {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
1264
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
1265
|
+
const tab = await withTab(target);
|
|
1266
|
+
const response = await sendToContent(tab.id, {
|
|
1267
|
+
type: "bak.performAction",
|
|
1268
|
+
action: "click",
|
|
1269
|
+
locator: params.locator,
|
|
1270
|
+
requiresConfirm: params.requiresConfirm === true
|
|
1271
|
+
});
|
|
1272
|
+
if (!response.ok) {
|
|
1273
|
+
throw response.error ?? toError("E_INTERNAL", "element.click failed");
|
|
1274
|
+
}
|
|
1275
|
+
return { ok: true };
|
|
422
1276
|
});
|
|
423
|
-
if (!response.ok) {
|
|
424
|
-
throw response.error ?? toError("E_INTERNAL", "element.click failed");
|
|
425
|
-
}
|
|
426
|
-
return { ok: true };
|
|
427
1277
|
}
|
|
428
1278
|
case "element.type": {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
1279
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
1280
|
+
const tab = await withTab(target);
|
|
1281
|
+
const response = await sendToContent(tab.id, {
|
|
1282
|
+
type: "bak.performAction",
|
|
1283
|
+
action: "type",
|
|
1284
|
+
locator: params.locator,
|
|
1285
|
+
text: String(params.text ?? ""),
|
|
1286
|
+
clear: Boolean(params.clear),
|
|
1287
|
+
requiresConfirm: params.requiresConfirm === true
|
|
1288
|
+
});
|
|
1289
|
+
if (!response.ok) {
|
|
1290
|
+
throw response.error ?? toError("E_INTERNAL", "element.type failed");
|
|
1291
|
+
}
|
|
1292
|
+
return { ok: true };
|
|
437
1293
|
});
|
|
438
|
-
if (!response.ok) {
|
|
439
|
-
throw response.error ?? toError("E_INTERNAL", "element.type failed");
|
|
440
|
-
}
|
|
441
|
-
return { ok: true };
|
|
442
1294
|
}
|
|
443
1295
|
case "element.scroll": {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
1296
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
1297
|
+
const tab = await withTab(target);
|
|
1298
|
+
const response = await sendToContent(tab.id, {
|
|
1299
|
+
type: "bak.performAction",
|
|
1300
|
+
action: "scroll",
|
|
1301
|
+
locator: params.locator,
|
|
1302
|
+
dx: Number(params.dx ?? 0),
|
|
1303
|
+
dy: Number(params.dy ?? 320)
|
|
1304
|
+
});
|
|
1305
|
+
if (!response.ok) {
|
|
1306
|
+
throw response.error ?? toError("E_INTERNAL", "element.scroll failed");
|
|
1307
|
+
}
|
|
1308
|
+
return { ok: true };
|
|
451
1309
|
});
|
|
452
|
-
if (!response.ok) {
|
|
453
|
-
throw response.error ?? toError("E_INTERNAL", "element.scroll failed");
|
|
454
|
-
}
|
|
455
|
-
return { ok: true };
|
|
456
1310
|
}
|
|
457
1311
|
case "page.wait": {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
1312
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
1313
|
+
const tab = await withTab(target);
|
|
1314
|
+
const response = await sendToContent(tab.id, {
|
|
1315
|
+
type: "bak.waitFor",
|
|
1316
|
+
mode: String(params.mode ?? "selector"),
|
|
1317
|
+
value: String(params.value ?? ""),
|
|
1318
|
+
timeoutMs: Number(params.timeoutMs ?? 5e3)
|
|
1319
|
+
});
|
|
1320
|
+
if (!response.ok) {
|
|
1321
|
+
throw response.error ?? toError("E_TIMEOUT", "page.wait failed");
|
|
1322
|
+
}
|
|
1323
|
+
return { ok: true };
|
|
464
1324
|
});
|
|
465
|
-
if (!response.ok) {
|
|
466
|
-
throw response.error ?? toError("E_TIMEOUT", "page.wait failed");
|
|
467
|
-
}
|
|
468
|
-
return { ok: true };
|
|
469
1325
|
}
|
|
470
1326
|
case "debug.getConsole": {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
1327
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
1328
|
+
const tab = await withTab(target);
|
|
1329
|
+
const response = await sendToContent(tab.id, {
|
|
1330
|
+
type: "bak.getConsole",
|
|
1331
|
+
limit: Number(params.limit ?? 50)
|
|
1332
|
+
});
|
|
1333
|
+
return { entries: response.entries };
|
|
475
1334
|
});
|
|
476
|
-
return { entries: response.entries };
|
|
477
1335
|
}
|
|
478
1336
|
case "ui.selectCandidate": {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
1337
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
1338
|
+
const tab = await withTab(target);
|
|
1339
|
+
const response = await sendToContent(
|
|
1340
|
+
tab.id,
|
|
1341
|
+
{
|
|
1342
|
+
type: "bak.selectCandidate",
|
|
1343
|
+
candidates: params.candidates
|
|
1344
|
+
}
|
|
1345
|
+
);
|
|
1346
|
+
if (!response.ok || !response.selectedEid) {
|
|
1347
|
+
throw response.error ?? toError("E_NEED_USER_CONFIRM", "User did not confirm candidate");
|
|
485
1348
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
throw response.error ?? toError("E_NEED_USER_CONFIRM", "User did not confirm candidate");
|
|
489
|
-
}
|
|
490
|
-
return { selectedEid: response.selectedEid };
|
|
1349
|
+
return { selectedEid: response.selectedEid };
|
|
1350
|
+
});
|
|
491
1351
|
}
|
|
492
1352
|
default:
|
|
493
1353
|
if (rpcForwardMethods.has(request.method)) {
|
|
494
|
-
|
|
495
|
-
|
|
1354
|
+
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
1355
|
+
const tab = await withTab(target);
|
|
1356
|
+
return await forwardContentRpc(tab.id, request.method, {
|
|
1357
|
+
...params,
|
|
1358
|
+
tabId: tab.id
|
|
1359
|
+
});
|
|
1360
|
+
});
|
|
496
1361
|
}
|
|
497
1362
|
throw toError("E_NOT_FOUND", `Unsupported method from CLI bridge: ${request.method}`);
|
|
498
1363
|
}
|
|
@@ -572,6 +1437,46 @@
|
|
|
572
1437
|
ws?.close();
|
|
573
1438
|
});
|
|
574
1439
|
}
|
|
1440
|
+
chrome.tabs.onRemoved.addListener((tabId) => {
|
|
1441
|
+
void loadWorkspaceState().then(async (state) => {
|
|
1442
|
+
if (!state || !state.tabIds.includes(tabId)) {
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
const nextTabIds = state.tabIds.filter((id) => id !== tabId);
|
|
1446
|
+
await saveWorkspaceState({
|
|
1447
|
+
...state,
|
|
1448
|
+
tabIds: nextTabIds,
|
|
1449
|
+
activeTabId: state.activeTabId === tabId ? null : state.activeTabId,
|
|
1450
|
+
primaryTabId: state.primaryTabId === tabId ? null : state.primaryTabId
|
|
1451
|
+
});
|
|
1452
|
+
});
|
|
1453
|
+
});
|
|
1454
|
+
chrome.tabs.onActivated.addListener((activeInfo) => {
|
|
1455
|
+
void loadWorkspaceState().then(async (state) => {
|
|
1456
|
+
if (!state || state.windowId !== activeInfo.windowId || !state.tabIds.includes(activeInfo.tabId)) {
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
await saveWorkspaceState({
|
|
1460
|
+
...state,
|
|
1461
|
+
activeTabId: activeInfo.tabId
|
|
1462
|
+
});
|
|
1463
|
+
});
|
|
1464
|
+
});
|
|
1465
|
+
chrome.windows.onRemoved.addListener((windowId) => {
|
|
1466
|
+
void loadWorkspaceState().then(async (state) => {
|
|
1467
|
+
if (!state || state.windowId !== windowId) {
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
await saveWorkspaceState({
|
|
1471
|
+
...state,
|
|
1472
|
+
windowId: null,
|
|
1473
|
+
groupId: null,
|
|
1474
|
+
tabIds: [],
|
|
1475
|
+
activeTabId: null,
|
|
1476
|
+
primaryTabId: null
|
|
1477
|
+
});
|
|
1478
|
+
});
|
|
1479
|
+
});
|
|
575
1480
|
chrome.runtime.onInstalled.addListener(() => {
|
|
576
1481
|
void setConfig({ port: DEFAULT_PORT, debugRichText: false });
|
|
577
1482
|
});
|