tab-agent 0.2.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/README.md +62 -0
- package/bin/tab-agent.js +40 -0
- package/cli/detect-extension.js +131 -0
- package/cli/setup.js +133 -0
- package/cli/start.js +19 -0
- package/cli/status.js +70 -0
- package/extension/content-script.js +510 -0
- package/extension/icons/icon128.png +0 -0
- package/extension/icons/icon16.png +0 -0
- package/extension/icons/icon48.png +0 -0
- package/extension/manifest.json +40 -0
- package/extension/popup/popup.html +142 -0
- package/extension/popup/popup.js +104 -0
- package/extension/service-worker.js +471 -0
- package/extension/snapshot.js +194 -0
- package/package.json +25 -0
- package/relay/install-native-host.sh +57 -0
- package/relay/native-host-wrapper.cmd +3 -0
- package/relay/native-host-wrapper.sh +29 -0
- package/relay/native-host.js +128 -0
- package/relay/node_modules/.package-lock.json +29 -0
- package/relay/node_modules/ws/LICENSE +20 -0
- package/relay/node_modules/ws/README.md +548 -0
- package/relay/node_modules/ws/browser.js +8 -0
- package/relay/node_modules/ws/index.js +13 -0
- package/relay/node_modules/ws/lib/buffer-util.js +131 -0
- package/relay/node_modules/ws/lib/constants.js +19 -0
- package/relay/node_modules/ws/lib/event-target.js +292 -0
- package/relay/node_modules/ws/lib/extension.js +203 -0
- package/relay/node_modules/ws/lib/limiter.js +55 -0
- package/relay/node_modules/ws/lib/permessage-deflate.js +528 -0
- package/relay/node_modules/ws/lib/receiver.js +706 -0
- package/relay/node_modules/ws/lib/sender.js +602 -0
- package/relay/node_modules/ws/lib/stream.js +161 -0
- package/relay/node_modules/ws/lib/subprotocol.js +62 -0
- package/relay/node_modules/ws/lib/validation.js +152 -0
- package/relay/node_modules/ws/lib/websocket-server.js +554 -0
- package/relay/node_modules/ws/lib/websocket.js +1393 -0
- package/relay/node_modules/ws/package.json +69 -0
- package/relay/node_modules/ws/wrapper.mjs +8 -0
- package/relay/package-lock.json +36 -0
- package/relay/package.json +12 -0
- package/relay/server.js +114 -0
- package/skills/claude-code/tab-agent.md +53 -0
- package/skills/codex/tab-agent.md +40 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// popup.js
|
|
2
|
+
|
|
3
|
+
let currentTabId = null;
|
|
4
|
+
let activatedTabs = [];
|
|
5
|
+
|
|
6
|
+
async function init() {
|
|
7
|
+
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
8
|
+
currentTabId = tab.id;
|
|
9
|
+
|
|
10
|
+
document.getElementById('currentUrl').textContent = tab.url;
|
|
11
|
+
|
|
12
|
+
await refreshActivatedTabs();
|
|
13
|
+
updateActivateButton();
|
|
14
|
+
checkConnection();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function refreshActivatedTabs() {
|
|
18
|
+
const response = await chrome.runtime.sendMessage({ action: 'getTabs' });
|
|
19
|
+
if (response && response.ok) {
|
|
20
|
+
activatedTabs = response.tabs;
|
|
21
|
+
renderTabList();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function updateActivateButton() {
|
|
26
|
+
const btn = document.getElementById('activateBtn');
|
|
27
|
+
const isActivated = activatedTabs.some(t => t.tabId === currentTabId);
|
|
28
|
+
|
|
29
|
+
if (isActivated) {
|
|
30
|
+
btn.textContent = 'Deactivate Control';
|
|
31
|
+
btn.className = 'btn btn-danger';
|
|
32
|
+
} else {
|
|
33
|
+
btn.textContent = 'Activate Control';
|
|
34
|
+
btn.className = 'btn btn-primary';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
btn.disabled = false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function renderTabList() {
|
|
41
|
+
const container = document.getElementById('tabList');
|
|
42
|
+
|
|
43
|
+
if (activatedTabs.length === 0) {
|
|
44
|
+
container.innerHTML = '<div style="color: #666; font-size: 12px;">No tabs activated</div>';
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
container.innerHTML = activatedTabs.map(tab => `
|
|
49
|
+
<div class="tab-item" data-tab-id="${tab.tabId}">
|
|
50
|
+
<span class="title" title="${tab.url}">${tab.title || tab.url}</span>
|
|
51
|
+
<button class="deactivate" title="Deactivate">✕</button>
|
|
52
|
+
</div>
|
|
53
|
+
`).join('');
|
|
54
|
+
|
|
55
|
+
container.querySelectorAll('.deactivate').forEach(btn => {
|
|
56
|
+
btn.addEventListener('click', async (e) => {
|
|
57
|
+
const tabId = parseInt(e.target.closest('.tab-item').dataset.tabId);
|
|
58
|
+
await chrome.runtime.sendMessage({ action: 'deactivate', tabId });
|
|
59
|
+
await refreshActivatedTabs();
|
|
60
|
+
updateActivateButton();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function checkConnection() {
|
|
66
|
+
const statusEl = document.getElementById('status');
|
|
67
|
+
try {
|
|
68
|
+
const response = await chrome.runtime.sendMessage({ action: 'ping' });
|
|
69
|
+
if (response && response.ok) {
|
|
70
|
+
if (response.nativeConnected) {
|
|
71
|
+
statusEl.className = 'status connected';
|
|
72
|
+
statusEl.textContent = 'Connected - Ready for commands';
|
|
73
|
+
} else {
|
|
74
|
+
statusEl.className = 'status disconnected';
|
|
75
|
+
const errorMsg = response.lastNativeError || 'Native host not connected';
|
|
76
|
+
statusEl.textContent = errorMsg;
|
|
77
|
+
statusEl.title = errorMsg;
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
throw new Error('No response');
|
|
81
|
+
}
|
|
82
|
+
} catch (error) {
|
|
83
|
+
statusEl.className = 'status disconnected';
|
|
84
|
+
statusEl.textContent = 'Extension error';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
document.getElementById('activateBtn').addEventListener('click', async () => {
|
|
89
|
+
const btn = document.getElementById('activateBtn');
|
|
90
|
+
btn.disabled = true;
|
|
91
|
+
|
|
92
|
+
const isActivated = activatedTabs.some(t => t.tabId === currentTabId);
|
|
93
|
+
|
|
94
|
+
if (isActivated) {
|
|
95
|
+
await chrome.runtime.sendMessage({ action: 'deactivate', tabId: currentTabId });
|
|
96
|
+
} else {
|
|
97
|
+
await chrome.runtime.sendMessage({ action: 'activate', tabId: currentTabId });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
await refreshActivatedTabs();
|
|
101
|
+
updateActivateButton();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
init();
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
// service-worker.js
|
|
2
|
+
// Tab Agent - Service Worker
|
|
3
|
+
// Manages activated tabs and routes commands to content scripts
|
|
4
|
+
|
|
5
|
+
const state = {
|
|
6
|
+
activatedTabs: new Map(), // tabId -> { url, title, activatedAt }
|
|
7
|
+
auditLog: [],
|
|
8
|
+
nativeConnected: false,
|
|
9
|
+
lastNativeError: null,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// Dialog handling with chrome.debugger
|
|
13
|
+
const pendingDialogs = new Map();
|
|
14
|
+
const attachedDebuggerTabs = new Set();
|
|
15
|
+
|
|
16
|
+
chrome.debugger.onEvent.addListener((source, method, params) => {
|
|
17
|
+
if (method === 'Page.javascriptDialogOpening') {
|
|
18
|
+
pendingDialogs.set(source.tabId, {
|
|
19
|
+
type: params.type,
|
|
20
|
+
message: params.message,
|
|
21
|
+
defaultPrompt: params.defaultPrompt
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
async function handleDialog(tabId, accept, promptText = '') {
|
|
27
|
+
if (!attachedDebuggerTabs.has(tabId)) {
|
|
28
|
+
try {
|
|
29
|
+
await chrome.debugger.attach({ tabId }, '1.3');
|
|
30
|
+
await chrome.debugger.sendCommand({ tabId }, 'Page.enable');
|
|
31
|
+
attachedDebuggerTabs.add(tabId);
|
|
32
|
+
} catch (e) {
|
|
33
|
+
return { ok: false, error: `Failed to attach debugger: ${e.message}` };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const pending = pendingDialogs.get(tabId);
|
|
38
|
+
if (!pending) {
|
|
39
|
+
return { ok: false, error: 'No dialog present' };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
await chrome.debugger.sendCommand({ tabId }, 'Page.handleJavaScriptDialog', {
|
|
44
|
+
accept,
|
|
45
|
+
promptText
|
|
46
|
+
});
|
|
47
|
+
pendingDialogs.delete(tabId);
|
|
48
|
+
return { ok: true, accepted: accept, dialogType: pending.type };
|
|
49
|
+
} catch (e) {
|
|
50
|
+
return { ok: false, error: e.message };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Update badge for a tab
|
|
55
|
+
function updateBadge(tabId) {
|
|
56
|
+
const isActive = state.activatedTabs.has(tabId);
|
|
57
|
+
chrome.action.setBadgeText({ tabId, text: isActive ? 'ON' : '' });
|
|
58
|
+
chrome.action.setBadgeBackgroundColor({ tabId, color: isActive ? '#22c55e' : '#666' });
|
|
59
|
+
chrome.action.setTitle({
|
|
60
|
+
tabId,
|
|
61
|
+
title: isActive ? 'Tab Agent - Active (click to deactivate)' : 'Tab Agent - Click to activate'
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Log all actions for audit trail
|
|
66
|
+
function audit(action, data, result) {
|
|
67
|
+
const entry = {
|
|
68
|
+
timestamp: new Date().toISOString(),
|
|
69
|
+
action,
|
|
70
|
+
data,
|
|
71
|
+
result,
|
|
72
|
+
};
|
|
73
|
+
state.auditLog.push(entry);
|
|
74
|
+
// Keep last 1000 entries
|
|
75
|
+
if (state.auditLog.length > 1000) {
|
|
76
|
+
state.auditLog.shift();
|
|
77
|
+
}
|
|
78
|
+
// Persist to storage
|
|
79
|
+
chrome.storage.local.set({ auditLog: state.auditLog });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Check if tab is activated
|
|
83
|
+
function isTabActivated(tabId) {
|
|
84
|
+
return state.activatedTabs.has(tabId);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Ensure content script is ready in the tab
|
|
88
|
+
async function ensureContentScript(tabId) {
|
|
89
|
+
try {
|
|
90
|
+
await chrome.tabs.sendMessage(tabId, { action: '__ping' });
|
|
91
|
+
return { ok: true, alreadyLoaded: true };
|
|
92
|
+
} catch (error) {
|
|
93
|
+
try {
|
|
94
|
+
await chrome.scripting.executeScript({
|
|
95
|
+
target: { tabId },
|
|
96
|
+
files: ['content-script.js']
|
|
97
|
+
});
|
|
98
|
+
return { ok: true, injected: true };
|
|
99
|
+
} catch (injectError) {
|
|
100
|
+
return { ok: false, error: injectError.message || 'Failed to inject content script' };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Activate a tab for control
|
|
106
|
+
async function activateTab(tabId) {
|
|
107
|
+
try {
|
|
108
|
+
const tab = await chrome.tabs.get(tabId);
|
|
109
|
+
const injectResult = await ensureContentScript(tabId);
|
|
110
|
+
if (!injectResult.ok) {
|
|
111
|
+
const result = { ok: false, error: injectResult.error };
|
|
112
|
+
audit('activate', { tabId, url: tab.url }, result);
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
state.activatedTabs.set(tabId, {
|
|
117
|
+
url: tab.url,
|
|
118
|
+
title: tab.title,
|
|
119
|
+
activatedAt: new Date().toISOString(),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Update badge
|
|
123
|
+
updateBadge(tabId);
|
|
124
|
+
|
|
125
|
+
audit('activate', { tabId, url: tab.url }, { ok: true });
|
|
126
|
+
return { ok: true, tabId, url: tab.url, title: tab.title };
|
|
127
|
+
} catch (error) {
|
|
128
|
+
const result = { ok: false, error: error.message };
|
|
129
|
+
audit('activate', { tabId }, result);
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Deactivate a tab
|
|
135
|
+
function deactivateTab(tabId) {
|
|
136
|
+
state.activatedTabs.delete(tabId);
|
|
137
|
+
updateBadge(tabId);
|
|
138
|
+
audit('deactivate', { tabId }, { ok: true });
|
|
139
|
+
return { ok: true };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// List all activated tabs
|
|
143
|
+
function listActivatedTabs() {
|
|
144
|
+
const tabs = [];
|
|
145
|
+
for (const [tabId, info] of state.activatedTabs) {
|
|
146
|
+
tabs.push({ tabId, ...info });
|
|
147
|
+
}
|
|
148
|
+
return { ok: true, tabs };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Route command to content script
|
|
152
|
+
async function routeCommand(tabId, command) {
|
|
153
|
+
if (!isTabActivated(tabId)) {
|
|
154
|
+
return { ok: false, error: 'Tab not activated' };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
// Handle evaluate - must use world: MAIN for page context access
|
|
159
|
+
if (command.action === 'evaluate') {
|
|
160
|
+
try {
|
|
161
|
+
const results = await chrome.scripting.executeScript({
|
|
162
|
+
target: { tabId },
|
|
163
|
+
world: 'MAIN',
|
|
164
|
+
func: (code) => {
|
|
165
|
+
try {
|
|
166
|
+
const result = eval(code);
|
|
167
|
+
if (typeof result === 'function') return { ok: true, result: '[Function]' };
|
|
168
|
+
if (result instanceof Node) return { ok: true, result: '[DOM Node]' };
|
|
169
|
+
return { ok: true, result };
|
|
170
|
+
} catch (e) {
|
|
171
|
+
return { ok: false, error: e.message };
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
args: [command.script]
|
|
175
|
+
});
|
|
176
|
+
const evalResult = results[0]?.result || { ok: false, error: 'No result' };
|
|
177
|
+
audit('evaluate', { tabId, script: command.script }, evalResult);
|
|
178
|
+
return evalResult;
|
|
179
|
+
} catch (error) {
|
|
180
|
+
const result = { ok: false, error: error.message };
|
|
181
|
+
audit('evaluate', { tabId, script: command.script }, result);
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Handle screenshot specially - must be done in service worker
|
|
187
|
+
if (command.action === 'screenshot') {
|
|
188
|
+
const { fullPage = false } = command;
|
|
189
|
+
const tab = await chrome.tabs.get(tabId);
|
|
190
|
+
|
|
191
|
+
// Full page screenshot using chrome.debugger
|
|
192
|
+
if (fullPage) {
|
|
193
|
+
try {
|
|
194
|
+
await chrome.debugger.attach({ tabId }, '1.3');
|
|
195
|
+
|
|
196
|
+
const { result: layout } = await chrome.debugger.sendCommand(
|
|
197
|
+
{ tabId },
|
|
198
|
+
'Page.getLayoutMetrics'
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const { data } = await chrome.debugger.sendCommand(
|
|
202
|
+
{ tabId },
|
|
203
|
+
'Page.captureScreenshot',
|
|
204
|
+
{
|
|
205
|
+
format: 'png',
|
|
206
|
+
captureBeyondViewport: true,
|
|
207
|
+
clip: {
|
|
208
|
+
x: 0,
|
|
209
|
+
y: 0,
|
|
210
|
+
width: layout.contentSize.width,
|
|
211
|
+
height: layout.contentSize.height,
|
|
212
|
+
scale: 1
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
await chrome.debugger.detach({ tabId });
|
|
218
|
+
audit('screenshot', { tabId, fullPage: true }, { ok: true });
|
|
219
|
+
return { ok: true, screenshot: 'data:image/png;base64,' + data, format: 'png' };
|
|
220
|
+
} catch (error) {
|
|
221
|
+
try { await chrome.debugger.detach({ tabId }); } catch {}
|
|
222
|
+
const result = { ok: false, error: error.message };
|
|
223
|
+
audit('screenshot', { tabId, fullPage: true }, result);
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Viewport screenshot (existing behavior)
|
|
229
|
+
let previousActiveTabId = null;
|
|
230
|
+
let dataUrl = null;
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
if (!tab.active) {
|
|
234
|
+
const [activeTab] = await chrome.tabs.query({ active: true, windowId: tab.windowId });
|
|
235
|
+
if (activeTab && activeTab.id !== tabId) {
|
|
236
|
+
previousActiveTabId = activeTab.id;
|
|
237
|
+
}
|
|
238
|
+
await chrome.tabs.update(tabId, { active: true });
|
|
239
|
+
await new Promise(r => setTimeout(r, 150));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, {
|
|
243
|
+
format: 'png',
|
|
244
|
+
quality: 90
|
|
245
|
+
});
|
|
246
|
+
} finally {
|
|
247
|
+
if (previousActiveTabId && previousActiveTabId !== tabId) {
|
|
248
|
+
try {
|
|
249
|
+
await chrome.tabs.update(previousActiveTabId, { active: true });
|
|
250
|
+
} catch (restoreError) {
|
|
251
|
+
console.warn('Failed to restore active tab after screenshot:', restoreError);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
audit('screenshot', { tabId }, { ok: true });
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
ok: true,
|
|
260
|
+
screenshot: dataUrl,
|
|
261
|
+
format: 'png',
|
|
262
|
+
encoding: 'base64'
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const injectResult = await ensureContentScript(tabId);
|
|
267
|
+
if (!injectResult.ok) {
|
|
268
|
+
const result = { ok: false, error: injectResult.error };
|
|
269
|
+
audit(command.action, { tabId, ...command }, result);
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const response = await chrome.tabs.sendMessage(tabId, command);
|
|
274
|
+
audit(command.action, { tabId, ...command }, response);
|
|
275
|
+
return response;
|
|
276
|
+
|
|
277
|
+
} catch (error) {
|
|
278
|
+
const result = { ok: false, error: error.message };
|
|
279
|
+
audit(command.action, { tabId, ...command }, result);
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Handle internal messages (from popup)
|
|
285
|
+
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
286
|
+
const { action, tabId, ...params } = message;
|
|
287
|
+
|
|
288
|
+
(async () => {
|
|
289
|
+
let result;
|
|
290
|
+
|
|
291
|
+
switch (action) {
|
|
292
|
+
case 'ping':
|
|
293
|
+
result = {
|
|
294
|
+
ok: true,
|
|
295
|
+
message: 'pong',
|
|
296
|
+
version: '0.1.0',
|
|
297
|
+
nativeConnected: state.nativeConnected,
|
|
298
|
+
lastNativeError: state.lastNativeError
|
|
299
|
+
};
|
|
300
|
+
break;
|
|
301
|
+
|
|
302
|
+
case 'activate':
|
|
303
|
+
result = await activateTab(tabId);
|
|
304
|
+
break;
|
|
305
|
+
|
|
306
|
+
case 'deactivate':
|
|
307
|
+
result = deactivateTab(tabId);
|
|
308
|
+
break;
|
|
309
|
+
|
|
310
|
+
case 'getTabs':
|
|
311
|
+
result = listActivatedTabs();
|
|
312
|
+
break;
|
|
313
|
+
|
|
314
|
+
default:
|
|
315
|
+
result = { ok: false, error: `Unknown action: ${action}` };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
sendResponse(result);
|
|
319
|
+
})();
|
|
320
|
+
|
|
321
|
+
return true;
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// Handle extension icon click - toggle activation
|
|
325
|
+
chrome.action.onClicked.addListener(async (tab) => {
|
|
326
|
+
if (state.activatedTabs.has(tab.id)) {
|
|
327
|
+
deactivateTab(tab.id);
|
|
328
|
+
} else {
|
|
329
|
+
await activateTab(tab.id);
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// Update badge when switching tabs
|
|
334
|
+
chrome.tabs.onActivated.addListener(({ tabId }) => {
|
|
335
|
+
updateBadge(tabId);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// Clean up when tabs are closed
|
|
339
|
+
chrome.tabs.onRemoved.addListener((tabId) => {
|
|
340
|
+
// Clean up debugger state
|
|
341
|
+
if (attachedDebuggerTabs.has(tabId)) {
|
|
342
|
+
chrome.debugger.detach({ tabId }).catch(() => {});
|
|
343
|
+
attachedDebuggerTabs.delete(tabId);
|
|
344
|
+
}
|
|
345
|
+
pendingDialogs.delete(tabId);
|
|
346
|
+
|
|
347
|
+
if (state.activatedTabs.has(tabId)) {
|
|
348
|
+
state.activatedTabs.delete(tabId);
|
|
349
|
+
audit('tab_closed', { tabId }, { ok: true });
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// Clean up when tabs navigate away
|
|
354
|
+
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
|
355
|
+
if (changeInfo.url && state.activatedTabs.has(tabId)) {
|
|
356
|
+
const info = state.activatedTabs.get(tabId);
|
|
357
|
+
info.url = changeInfo.url;
|
|
358
|
+
info.title = tab.title;
|
|
359
|
+
}
|
|
360
|
+
if (changeInfo.status === 'complete' && state.activatedTabs.has(tabId)) {
|
|
361
|
+
ensureContentScript(tabId);
|
|
362
|
+
updateBadge(tabId);
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// Native messaging connection
|
|
367
|
+
let nativePort = null;
|
|
368
|
+
|
|
369
|
+
function connectNativeHost() {
|
|
370
|
+
console.log('Attempting to connect to native host...');
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
nativePort = chrome.runtime.connectNative('com.tabagent.relay');
|
|
374
|
+
console.log('connectNative called, port created');
|
|
375
|
+
|
|
376
|
+
nativePort.onMessage.addListener(async (message) => {
|
|
377
|
+
console.log('Received from native host:', message);
|
|
378
|
+
|
|
379
|
+
if (message.type === 'connected') {
|
|
380
|
+
console.log('Native host connected to relay server');
|
|
381
|
+
state.nativeConnected = true;
|
|
382
|
+
state.lastNativeError = null;
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (message.type === 'error') {
|
|
387
|
+
console.error('Native host error:', message.error);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (message.type === 'command') {
|
|
392
|
+
const { id, action, tabId, ...params } = message;
|
|
393
|
+
console.log(`Processing command: ${action}`, { tabId, params });
|
|
394
|
+
|
|
395
|
+
let result;
|
|
396
|
+
|
|
397
|
+
switch (action) {
|
|
398
|
+
case 'ping':
|
|
399
|
+
result = { ok: true, message: 'pong', version: '0.1.0' };
|
|
400
|
+
break;
|
|
401
|
+
|
|
402
|
+
case 'activate':
|
|
403
|
+
result = await activateTab(tabId);
|
|
404
|
+
break;
|
|
405
|
+
|
|
406
|
+
case 'deactivate':
|
|
407
|
+
result = deactivateTab(tabId);
|
|
408
|
+
break;
|
|
409
|
+
|
|
410
|
+
case 'tabs':
|
|
411
|
+
result = listActivatedTabs();
|
|
412
|
+
break;
|
|
413
|
+
|
|
414
|
+
case 'audit':
|
|
415
|
+
result = { ok: true, log: state.auditLog.slice(-100) };
|
|
416
|
+
break;
|
|
417
|
+
|
|
418
|
+
case 'dialog':
|
|
419
|
+
result = await handleDialog(tabId, params.accept, params.promptText);
|
|
420
|
+
break;
|
|
421
|
+
|
|
422
|
+
case 'snapshot':
|
|
423
|
+
case 'screenshot':
|
|
424
|
+
case 'click':
|
|
425
|
+
case 'type':
|
|
426
|
+
case 'fill':
|
|
427
|
+
case 'press':
|
|
428
|
+
case 'select':
|
|
429
|
+
case 'hover':
|
|
430
|
+
case 'scroll':
|
|
431
|
+
case 'navigate':
|
|
432
|
+
case 'evaluate':
|
|
433
|
+
case 'wait':
|
|
434
|
+
case 'scrollintoview':
|
|
435
|
+
case 'batchfill':
|
|
436
|
+
result = await routeCommand(tabId, { action, ...params });
|
|
437
|
+
break;
|
|
438
|
+
|
|
439
|
+
default:
|
|
440
|
+
result = { ok: false, error: `Unknown action: ${action}` };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
console.log(`Command ${action} result:`, result);
|
|
444
|
+
nativePort.postMessage({ type: 'response', id, ...result });
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
nativePort.onDisconnect.addListener(() => {
|
|
449
|
+
const error = chrome.runtime.lastError;
|
|
450
|
+
const errorMsg = error ? error.message : null;
|
|
451
|
+
console.log('Native host disconnected', errorMsg ? `Error: ${errorMsg}` : '');
|
|
452
|
+
state.nativeConnected = false;
|
|
453
|
+
state.lastNativeError = errorMsg;
|
|
454
|
+
nativePort = null;
|
|
455
|
+
console.log('Will retry connection in 5 seconds...');
|
|
456
|
+
setTimeout(connectNativeHost, 5000);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
console.log('Native messaging listeners registered');
|
|
460
|
+
|
|
461
|
+
} catch (error) {
|
|
462
|
+
console.error('Failed to connect to native host:', error);
|
|
463
|
+
setTimeout(connectNativeHost, 5000);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Start native messaging connection
|
|
468
|
+
console.log('Starting native messaging connection...');
|
|
469
|
+
connectNativeHost();
|
|
470
|
+
|
|
471
|
+
console.log('Tab Agent service worker started');
|