@rmrdeveloper/sideroom-pi 8.11.0 → 8.13.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 +49 -5
- package/assets/artifacts/GUIDELINES_TEMPLATE.md +3 -3
- package/assets/visual-context/browser/INSTALL.md +69 -0
- package/assets/visual-context/browser/background.js +578 -0
- package/assets/visual-context/browser/content.js +198 -0
- package/assets/visual-context/browser/icons/README.md +9 -0
- package/assets/visual-context/browser/icons/icon-128.png +0 -0
- package/assets/visual-context/browser/icons/icon-16.png +0 -0
- package/assets/visual-context/browser/icons/icon-32.png +0 -0
- package/assets/visual-context/browser/icons/icon-48.png +0 -0
- package/assets/visual-context/browser/manifest.json +20 -0
- package/assets/visual-context/browser/popup.css +19 -0
- package/assets/visual-context/browser/popup.html +27 -0
- package/assets/visual-context/browser/popup.js +141 -0
- package/extensions/done/guard.ts +42 -37
- package/extensions/done/index.ts +3 -2
- package/extensions/done/model.ts +1 -5
- package/extensions/explain/guard.ts +13 -11
- package/extensions/explain/model.ts +2 -25
- package/extensions/guidelines/review.ts +31 -13
- package/extensions/jev/client.ts +22 -1
- package/extensions/jev/context.ts +36 -3
- package/extensions/jev/guard.ts +113 -34
- package/extensions/jev/model.ts +90 -12
- package/extensions/jev/workflow.ts +188 -0
- package/extensions/persona/execute.ts +4 -6
- package/extensions/persona/index.ts +3 -2
- package/extensions/persona/model.ts +1 -31
- package/extensions/persona/prompt.ts +3 -1
- package/extensions/shared/settle.ts +63 -13
- package/extensions/visual-context/bridge.ts +240 -0
- package/extensions/visual-context/broker.ts +292 -0
- package/extensions/visual-context/envelope.ts +34 -0
- package/extensions/visual-context/index.ts +328 -0
- package/extensions/visual-context/literal.ts +34 -0
- package/extensions/visual-context/markup.ts +104 -0
- package/extensions/visual-context/match.ts +222 -0
- package/extensions/visual-context/model.ts +180 -0
- package/extensions/visual-context/pairing.ts +114 -0
- package/extensions/visual-context/parse-astro.ts +166 -0
- package/extensions/visual-context/parse-html.ts +72 -0
- package/extensions/visual-context/parse-jsx.ts +157 -0
- package/extensions/visual-context/parse-svelte.ts +128 -0
- package/extensions/visual-context/parse-vue.ts +133 -0
- package/extensions/visual-context/session.ts +192 -0
- package/extensions/visual-context/source.ts +197 -0
- package/extensions/visual-context/ui.ts +57 -0
- package/package.json +16 -7
- package/skills/sideroom-architecture/SKILL.md +5 -3
- package/skills/sideroom-codebase-design/LICENSE.txt +21 -0
- package/skills/sideroom-codebase-design/SKILL.md +151 -0
- package/skills/sideroom-codebase-design/references/growth.md +62 -0
- package/skills/sideroom-domain-modeling/SKILL.md +1 -1
- package/skills/sideroom-domain-scaffold/SKILL.md +6 -4
- package/skills/sideroom-grill/SKILL.md +6 -4
- package/skills/sideroom-guidelines/SKILL.md +9 -7
- package/skills/sideroom-guidelines/references/languages/go.md +3 -3
- package/skills/sideroom-guidelines/references/languages/java.md +3 -3
- package/skills/sideroom-guidelines/references/languages/javascript.md +25 -3
- package/skills/sideroom-guidelines/references/languages/php-laravel.md +3 -3
- package/skills/sideroom-guidelines/references/languages/python.md +3 -3
- package/skills/sideroom-guidelines/references/languages/rust.md +3 -3
- package/skills/sideroom-guidelines/references/languages/typescript.md +27 -3
- package/skills/sideroom-persona/SKILL.md +15 -4
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
const PROTOCOL_VERSION = 2;
|
|
2
|
+
const BRIDGE_PATH = '/sideroom-visual-context';
|
|
3
|
+
const HEARTBEAT_MS = 20_000;
|
|
4
|
+
const RESPONSE_TIMEOUT_MS = 10_000;
|
|
5
|
+
const RETRY_MS = 2_000;
|
|
6
|
+
const RETRY_ALARM = 'sideroom-reconnect';
|
|
7
|
+
const MAX_INSTALLATIONS = 4;
|
|
8
|
+
const INSPECTOR_COMMAND = 'toggle-inspector';
|
|
9
|
+
const IDLE_TITLE = 'Select an element for Pi';
|
|
10
|
+
const ACTIVE_TITLE =
|
|
11
|
+
'Selecting elements for Pi. Press Escape or the selection shortcut to stop.';
|
|
12
|
+
let inspectorCommands = Promise.resolve();
|
|
13
|
+
const MESSAGE = {
|
|
14
|
+
authenticate: 'authenticate',
|
|
15
|
+
ready: 'ready',
|
|
16
|
+
sessions: 'sessions',
|
|
17
|
+
selection: 'selection',
|
|
18
|
+
received: 'received',
|
|
19
|
+
error: 'error',
|
|
20
|
+
ping: 'ping',
|
|
21
|
+
};
|
|
22
|
+
const WAITING_NOTICE = 'Waiting for Pi. Reconnection is automatic.';
|
|
23
|
+
const PAUSED_NOTICE = 'Disconnected. Your authorizations are remembered.';
|
|
24
|
+
const activeTabs = new Map();
|
|
25
|
+
const receipts = new Map();
|
|
26
|
+
const connections = new Map();
|
|
27
|
+
let paused = false;
|
|
28
|
+
let sessions = [];
|
|
29
|
+
let selected;
|
|
30
|
+
let preferredId;
|
|
31
|
+
let automaticSelection = false;
|
|
32
|
+
let lastAddress = '';
|
|
33
|
+
let lastNotice = 'Pair once with each Pi installation to select elements.';
|
|
34
|
+
const isConnected = () =>
|
|
35
|
+
[...connections.values()].some((entry) => entry.connected);
|
|
36
|
+
const availableSessions = () =>
|
|
37
|
+
[...connections.values()].flatMap((entry) => entry.sessions);
|
|
38
|
+
|
|
39
|
+
function parseUrl(address, failureMessage) {
|
|
40
|
+
try {
|
|
41
|
+
return new URL(address);
|
|
42
|
+
} catch (cause) {
|
|
43
|
+
throw new Error(failureMessage, { cause });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parsePairingAddress(address) {
|
|
48
|
+
const uri = parseUrl(
|
|
49
|
+
address.trim(),
|
|
50
|
+
'Paste the local pairing address shown by Pi (F7).',
|
|
51
|
+
);
|
|
52
|
+
if (
|
|
53
|
+
uri.protocol !== 'ws:' ||
|
|
54
|
+
!['127.0.0.1', 'localhost'].includes(uri.hostname) ||
|
|
55
|
+
!uri.port ||
|
|
56
|
+
uri.pathname !== BRIDGE_PATH ||
|
|
57
|
+
uri.search ||
|
|
58
|
+
uri.username ||
|
|
59
|
+
uri.password ||
|
|
60
|
+
!/^#[a-f0-9]{64}$/u.test(uri.hash)
|
|
61
|
+
) {
|
|
62
|
+
throw new Error('Paste the local pairing address shown by Pi (F7).');
|
|
63
|
+
}
|
|
64
|
+
const token = uri.hash.slice(1);
|
|
65
|
+
uri.hash = '';
|
|
66
|
+
return { endpoint: uri.href, token };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function sendToTab(tabId, message) {
|
|
70
|
+
try {
|
|
71
|
+
await chrome.tabs.sendMessage(tabId, message);
|
|
72
|
+
} catch (cause) {
|
|
73
|
+
lastNotice = `Inspector stopped: ${cause.message}`;
|
|
74
|
+
activeTabs.delete(tabId);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function showInspectorIndicator(tabId, { text, title }) {
|
|
79
|
+
await chrome.action.setBadgeBackgroundColor({ tabId, color: '#ffb340' });
|
|
80
|
+
await chrome.action.setBadgeText({ tabId, text });
|
|
81
|
+
await chrome.action.setTitle({ tabId, title });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function stopInspector(tabId) {
|
|
85
|
+
activeTabs.delete(tabId);
|
|
86
|
+
await sendToTab(tabId, { action: 'stop' });
|
|
87
|
+
await showInspectorIndicator(tabId, { text: '', title: IDLE_TITLE });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function stopInspectors() {
|
|
91
|
+
const tabIds = [...activeTabs.keys()];
|
|
92
|
+
activeTabs.clear();
|
|
93
|
+
receipts.clear();
|
|
94
|
+
for (const tabId of tabIds) {
|
|
95
|
+
await stopInspector(tabId);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function startInspector(tab) {
|
|
100
|
+
if (!selected || !isConnected()) {
|
|
101
|
+
throw new Error('Choose an available Pi session first.');
|
|
102
|
+
}
|
|
103
|
+
if (!tab?.id || !/^https?:\/\//u.test(tab.url ?? '')) {
|
|
104
|
+
throw new Error('Open the web page you want to inspect.');
|
|
105
|
+
}
|
|
106
|
+
const target = selected;
|
|
107
|
+
await chrome.scripting.executeScript({
|
|
108
|
+
target: { tabId: tab.id },
|
|
109
|
+
files: ['content.js'],
|
|
110
|
+
});
|
|
111
|
+
const commands = await chrome.commands.getAll();
|
|
112
|
+
const shortcut = commands.find(
|
|
113
|
+
(command) => command.name === INSPECTOR_COMMAND,
|
|
114
|
+
)?.shortcut;
|
|
115
|
+
if (selected !== target) {
|
|
116
|
+
throw new Error('The destination changed. Start the inspector again.');
|
|
117
|
+
}
|
|
118
|
+
activeTabs.set(tab.id, target);
|
|
119
|
+
try {
|
|
120
|
+
await chrome.tabs.sendMessage(tab.id, { action: 'start', shortcut });
|
|
121
|
+
await showInspectorIndicator(tab.id, { text: 'ON', title: ACTIVE_TITLE });
|
|
122
|
+
} catch (cause) {
|
|
123
|
+
await stopInspector(tab.id);
|
|
124
|
+
throw cause;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function toggleInspector(commandTab) {
|
|
129
|
+
let tab = commandTab;
|
|
130
|
+
try {
|
|
131
|
+
await initialized;
|
|
132
|
+
if (!tab) {
|
|
133
|
+
[tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
134
|
+
}
|
|
135
|
+
if (tab?.id && activeTabs.has(tab.id)) {
|
|
136
|
+
await stopInspector(tab.id);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
await startInspector(tab);
|
|
140
|
+
} catch (cause) {
|
|
141
|
+
lastNotice = `${cause.message} Open the Sideroom extension for details.`;
|
|
142
|
+
await showInspectorIndicator(tab?.id, { text: '!', title: lastNotice });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function updateSessions(available) {
|
|
147
|
+
const previous = selected;
|
|
148
|
+
sessions = available;
|
|
149
|
+
selected = sessions.find(
|
|
150
|
+
(session) => session.id === (preferredId ?? previous?.id),
|
|
151
|
+
);
|
|
152
|
+
if (automaticSelection && sessions.length > 1) {
|
|
153
|
+
selected = undefined;
|
|
154
|
+
automaticSelection = false;
|
|
155
|
+
}
|
|
156
|
+
if (!selected && !preferredId && sessions.length === 1) {
|
|
157
|
+
selected = sessions[0];
|
|
158
|
+
automaticSelection = true;
|
|
159
|
+
}
|
|
160
|
+
if (
|
|
161
|
+
previous?.id !== selected?.id ||
|
|
162
|
+
previous?.revision !== selected?.revision
|
|
163
|
+
) {
|
|
164
|
+
void stopInspectors().catch((cause) => {
|
|
165
|
+
console.error('Sideroom could not stop its inspectors:', cause);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
lastNotice = selected
|
|
169
|
+
? `Connected to ${selected.name}.`
|
|
170
|
+
: sessions.length
|
|
171
|
+
? 'Choose which Pi session receives your selections.'
|
|
172
|
+
: WAITING_NOTICE;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function closeConnection(entry) {
|
|
176
|
+
const previous = entry.socket;
|
|
177
|
+
entry.socket = undefined;
|
|
178
|
+
entry.connected = false;
|
|
179
|
+
entry.sessions = [];
|
|
180
|
+
clearInterval(entry.heartbeat);
|
|
181
|
+
clearTimeout(entry.retry);
|
|
182
|
+
entry.cancel?.(new Error('The connection was closed.'));
|
|
183
|
+
entry.cancel = undefined;
|
|
184
|
+
previous?.close();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function disconnect() {
|
|
188
|
+
for (const entry of connections.values()) {
|
|
189
|
+
closeConnection(entry);
|
|
190
|
+
}
|
|
191
|
+
sessions = [];
|
|
192
|
+
selected = undefined;
|
|
193
|
+
preferredId = undefined;
|
|
194
|
+
await stopInspectors();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function scheduleReconnect(entry) {
|
|
198
|
+
if (paused || entry.blocked || connections.get(entry.endpoint) !== entry) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
clearTimeout(entry.retry);
|
|
202
|
+
entry.retry = setTimeout(() => {
|
|
203
|
+
void reconnect(entry);
|
|
204
|
+
}, RETRY_MS);
|
|
205
|
+
// Alarms wake the worker after it has been suspended while Pi is closed.
|
|
206
|
+
void chrome.alarms.create(RETRY_ALARM, { periodInMinutes: 1 });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function openConnection(entry) {
|
|
210
|
+
closeConnection(entry);
|
|
211
|
+
const socket = new WebSocket(entry.endpoint);
|
|
212
|
+
entry.socket = socket;
|
|
213
|
+
await new Promise((resolve, reject) => {
|
|
214
|
+
entry.cancel = reject;
|
|
215
|
+
const timeout = setTimeout(() => {
|
|
216
|
+
socket.close();
|
|
217
|
+
reject(new Error(WAITING_NOTICE));
|
|
218
|
+
}, RESPONSE_TIMEOUT_MS);
|
|
219
|
+
socket.onopen = () =>
|
|
220
|
+
socket.send(
|
|
221
|
+
JSON.stringify({
|
|
222
|
+
type: MESSAGE.authenticate,
|
|
223
|
+
role: 'browser',
|
|
224
|
+
version: PROTOCOL_VERSION,
|
|
225
|
+
token: entry.token,
|
|
226
|
+
}),
|
|
227
|
+
);
|
|
228
|
+
socket.onmessage = async (event) => {
|
|
229
|
+
if (entry.socket !== socket) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
const message = JSON.parse(event.data);
|
|
234
|
+
if (message.type === MESSAGE.ready) {
|
|
235
|
+
if (message.version !== PROTOCOL_VERSION) {
|
|
236
|
+
throw new Error('Reload the updated Sideroom browser extension.');
|
|
237
|
+
}
|
|
238
|
+
clearTimeout(timeout);
|
|
239
|
+
entry.connected = true;
|
|
240
|
+
entry.cancel = undefined;
|
|
241
|
+
entry.sessions = message.sessions;
|
|
242
|
+
updateSessions(availableSessions());
|
|
243
|
+
if (
|
|
244
|
+
[...connections.values()].every(
|
|
245
|
+
(connection) => connection.connected,
|
|
246
|
+
)
|
|
247
|
+
) {
|
|
248
|
+
await chrome.alarms.clear(RETRY_ALARM);
|
|
249
|
+
}
|
|
250
|
+
entry.heartbeat = setInterval(() => {
|
|
251
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
252
|
+
socket.send(JSON.stringify({ type: MESSAGE.ping }));
|
|
253
|
+
}
|
|
254
|
+
}, HEARTBEAT_MS);
|
|
255
|
+
resolve();
|
|
256
|
+
}
|
|
257
|
+
if (message.type === MESSAGE.sessions) {
|
|
258
|
+
entry.sessions = message.sessions;
|
|
259
|
+
updateSessions(availableSessions());
|
|
260
|
+
}
|
|
261
|
+
if (message.type === MESSAGE.received) {
|
|
262
|
+
lastNotice = `${String(message.candidates)} source candidates. Review in Pi (F7).`;
|
|
263
|
+
const receipt = receipts.get(message.id);
|
|
264
|
+
receipts.delete(message.id);
|
|
265
|
+
if (receipt?.endpoint === entry.endpoint) {
|
|
266
|
+
await sendToTab(receipt.tabId, {
|
|
267
|
+
action: 'receipt',
|
|
268
|
+
message: lastNotice,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (message.type === MESSAGE.error) {
|
|
273
|
+
lastNotice = message.message;
|
|
274
|
+
for (const [selectionId, receipt] of receipts) {
|
|
275
|
+
if (receipt.endpoint === entry.endpoint) {
|
|
276
|
+
receipts.delete(selectionId);
|
|
277
|
+
await sendToTab(receipt.tabId, {
|
|
278
|
+
action: 'receipt',
|
|
279
|
+
message: lastNotice,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
} catch (cause) {
|
|
285
|
+
lastNotice = `Invalid bridge response: ${cause.message}`;
|
|
286
|
+
clearTimeout(timeout);
|
|
287
|
+
socket.close();
|
|
288
|
+
reject(new Error(lastNotice));
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
socket.onerror = () => {
|
|
292
|
+
clearTimeout(timeout);
|
|
293
|
+
reject(
|
|
294
|
+
new Error(
|
|
295
|
+
'Waiting for Pi. Windows must be able to reach WSL localhost when using Pi in WSL.',
|
|
296
|
+
),
|
|
297
|
+
);
|
|
298
|
+
};
|
|
299
|
+
socket.onclose = (event) => {
|
|
300
|
+
clearTimeout(timeout);
|
|
301
|
+
if (entry.socket !== socket) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
entry.socket = undefined;
|
|
305
|
+
entry.connected = false;
|
|
306
|
+
entry.sessions = [];
|
|
307
|
+
entry.blocked = event?.code === 1008;
|
|
308
|
+
clearInterval(entry.heartbeat);
|
|
309
|
+
for (const [selectionId, receipt] of receipts) {
|
|
310
|
+
if (receipt.endpoint === entry.endpoint) {
|
|
311
|
+
receipts.delete(selectionId);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
updateSessions(availableSessions());
|
|
315
|
+
reject(
|
|
316
|
+
new Error(
|
|
317
|
+
entry.blocked
|
|
318
|
+
? 'Authorization rejected. Pair that installation again from F7.'
|
|
319
|
+
: WAITING_NOTICE,
|
|
320
|
+
),
|
|
321
|
+
);
|
|
322
|
+
scheduleReconnect(entry);
|
|
323
|
+
};
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function reconnect(entry) {
|
|
328
|
+
if (paused || entry.blocked || entry.connected) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (!entry.attempt) {
|
|
332
|
+
entry.attempt = openConnection(entry)
|
|
333
|
+
.catch((cause) => {
|
|
334
|
+
if (!selected) {
|
|
335
|
+
lastNotice = cause.message;
|
|
336
|
+
}
|
|
337
|
+
scheduleReconnect(entry);
|
|
338
|
+
})
|
|
339
|
+
.finally(() => {
|
|
340
|
+
entry.attempt = undefined;
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
await entry.attempt;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function remember(address) {
|
|
347
|
+
const pairing = parsePairingAddress(address);
|
|
348
|
+
const existing = connections.get(pairing.endpoint);
|
|
349
|
+
if (existing && existing.token === pairing.token) {
|
|
350
|
+
existing.blocked = false;
|
|
351
|
+
return existing;
|
|
352
|
+
}
|
|
353
|
+
if (!existing && connections.size >= MAX_INSTALLATIONS) {
|
|
354
|
+
throw new Error(
|
|
355
|
+
'Four installations are already authorized. Forget them before adding another.',
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
if (existing) {
|
|
359
|
+
closeConnection(existing);
|
|
360
|
+
}
|
|
361
|
+
const entry = {
|
|
362
|
+
...pairing,
|
|
363
|
+
address: address.trim(),
|
|
364
|
+
connected: false,
|
|
365
|
+
sessions: [],
|
|
366
|
+
blocked: false,
|
|
367
|
+
};
|
|
368
|
+
connections.set(entry.endpoint, entry);
|
|
369
|
+
return entry;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const initialized = chrome.storage.local
|
|
373
|
+
.setAccessLevel({ accessLevel: 'TRUSTED_CONTEXTS' })
|
|
374
|
+
.then(async () => {
|
|
375
|
+
const saved = await chrome.storage.local.get([
|
|
376
|
+
'pairingAddresses',
|
|
377
|
+
'pairingAddress',
|
|
378
|
+
'paused',
|
|
379
|
+
]);
|
|
380
|
+
const addresses =
|
|
381
|
+
saved.pairingAddresses ??
|
|
382
|
+
(saved.pairingAddress ? [saved.pairingAddress] : []);
|
|
383
|
+
paused = saved.paused === true;
|
|
384
|
+
for (const address of addresses) {
|
|
385
|
+
const entry = remember(address);
|
|
386
|
+
lastAddress = address;
|
|
387
|
+
void reconnect(entry);
|
|
388
|
+
}
|
|
389
|
+
if (paused) {
|
|
390
|
+
lastNotice = PAUSED_NOTICE;
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
async function handleMessage(message, sender) {
|
|
395
|
+
if (sender.id !== chrome.runtime.id) {
|
|
396
|
+
throw new Error('Unknown inspector sender.');
|
|
397
|
+
}
|
|
398
|
+
await initialized;
|
|
399
|
+
const fromPopup = sender.url === chrome.runtime.getURL('popup.html');
|
|
400
|
+
if (!fromPopup) {
|
|
401
|
+
const target = activeTabs.get(sender.tab?.id);
|
|
402
|
+
if (message.action === 'stopped') {
|
|
403
|
+
if (sender.tab?.id) {
|
|
404
|
+
activeTabs.delete(sender.tab.id);
|
|
405
|
+
await showInspectorIndicator(sender.tab.id, {
|
|
406
|
+
text: '',
|
|
407
|
+
title: IDLE_TITLE,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
return { ok: true };
|
|
411
|
+
}
|
|
412
|
+
if (message.action !== 'selection' || !target || !isConnected()) {
|
|
413
|
+
throw new Error('Connect to Pi and start the inspector first.');
|
|
414
|
+
}
|
|
415
|
+
if (target.id !== selected?.id || target.revision !== selected?.revision) {
|
|
416
|
+
throw new Error('The destination changed. Start the inspector again.');
|
|
417
|
+
}
|
|
418
|
+
const entry = [...connections.values()].find(
|
|
419
|
+
(connection) =>
|
|
420
|
+
connection.connected &&
|
|
421
|
+
connection.sessions.some(
|
|
422
|
+
(session) =>
|
|
423
|
+
session.id === target.id && session.revision === target.revision,
|
|
424
|
+
),
|
|
425
|
+
);
|
|
426
|
+
if (!entry || entry.socket?.readyState !== WebSocket.OPEN) {
|
|
427
|
+
throw new Error('The destination is no longer available.');
|
|
428
|
+
}
|
|
429
|
+
if (receipts.size >= 8) {
|
|
430
|
+
throw new Error('Pi is still processing selections. Try again shortly.');
|
|
431
|
+
}
|
|
432
|
+
const page = parseUrl(
|
|
433
|
+
sender.tab.url,
|
|
434
|
+
'The selected page has an invalid address.',
|
|
435
|
+
);
|
|
436
|
+
const selection = {
|
|
437
|
+
...message.selection,
|
|
438
|
+
page: `${page.origin}${page.pathname}`,
|
|
439
|
+
};
|
|
440
|
+
receipts.set(selection.id, {
|
|
441
|
+
tabId: sender.tab.id,
|
|
442
|
+
endpoint: entry.endpoint,
|
|
443
|
+
});
|
|
444
|
+
entry.socket.send(
|
|
445
|
+
JSON.stringify({ type: MESSAGE.selection, selection, target }),
|
|
446
|
+
);
|
|
447
|
+
return { ok: true };
|
|
448
|
+
}
|
|
449
|
+
if (message.action === 'status') {
|
|
450
|
+
for (const entry of connections.values()) {
|
|
451
|
+
void reconnect(entry);
|
|
452
|
+
}
|
|
453
|
+
return {
|
|
454
|
+
ok: true,
|
|
455
|
+
connected: isConnected(),
|
|
456
|
+
notice: lastNotice,
|
|
457
|
+
address: lastAddress,
|
|
458
|
+
sessions,
|
|
459
|
+
selectedId: selected?.id ?? '',
|
|
460
|
+
paused,
|
|
461
|
+
installations: connections.size,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
if (message.action === 'connect') {
|
|
465
|
+
const entry = remember(message.address);
|
|
466
|
+
lastAddress = entry.address;
|
|
467
|
+
paused = false;
|
|
468
|
+
await chrome.storage.local.set({
|
|
469
|
+
pairingAddresses: [...connections.values()].map(
|
|
470
|
+
(connection) => connection.address,
|
|
471
|
+
),
|
|
472
|
+
paused,
|
|
473
|
+
});
|
|
474
|
+
await chrome.storage.local.remove('pairingAddress');
|
|
475
|
+
await reconnect(entry);
|
|
476
|
+
for (const connection of connections.values()) {
|
|
477
|
+
if (connection !== entry) {
|
|
478
|
+
void reconnect(connection);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return {
|
|
482
|
+
ok: true,
|
|
483
|
+
connected: isConnected(),
|
|
484
|
+
notice: lastNotice,
|
|
485
|
+
sessions,
|
|
486
|
+
selectedId: selected?.id ?? '',
|
|
487
|
+
address: lastAddress,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
if (message.action === 'disconnect' || message.action === 'forget') {
|
|
491
|
+
paused = true;
|
|
492
|
+
await disconnect();
|
|
493
|
+
await chrome.alarms.clear(RETRY_ALARM);
|
|
494
|
+
if (message.action === 'forget') {
|
|
495
|
+
connections.clear();
|
|
496
|
+
lastAddress = '';
|
|
497
|
+
await chrome.storage.local.remove(['pairingAddress', 'pairingAddresses']);
|
|
498
|
+
}
|
|
499
|
+
await chrome.storage.local.set({ paused });
|
|
500
|
+
lastNotice = lastAddress
|
|
501
|
+
? PAUSED_NOTICE
|
|
502
|
+
: 'Authorizations forgotten. Pair again from F7.';
|
|
503
|
+
return {
|
|
504
|
+
ok: true,
|
|
505
|
+
connected: false,
|
|
506
|
+
notice: lastNotice,
|
|
507
|
+
address: lastAddress,
|
|
508
|
+
sessions: [],
|
|
509
|
+
selectedId: '',
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
if (message.action === 'choose') {
|
|
513
|
+
const destination = sessions.find((session) => session.id === message.id);
|
|
514
|
+
if (!destination) {
|
|
515
|
+
throw new Error('That Pi session is no longer available.');
|
|
516
|
+
}
|
|
517
|
+
await stopInspectors();
|
|
518
|
+
selected = destination;
|
|
519
|
+
preferredId = destination.id;
|
|
520
|
+
automaticSelection = false;
|
|
521
|
+
lastNotice = `Connected to ${selected.name}.`;
|
|
522
|
+
return {
|
|
523
|
+
ok: true,
|
|
524
|
+
connected: isConnected(),
|
|
525
|
+
notice: lastNotice,
|
|
526
|
+
sessions,
|
|
527
|
+
selectedId: selected.id,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
if (message.action === 'inspect') {
|
|
531
|
+
const [tab] = await chrome.tabs.query({
|
|
532
|
+
active: true,
|
|
533
|
+
currentWindow: true,
|
|
534
|
+
});
|
|
535
|
+
await startInspector(tab);
|
|
536
|
+
return { ok: true };
|
|
537
|
+
}
|
|
538
|
+
throw new Error('Unknown inspector action.');
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
chrome.runtime.onMessage.addListener((message, sender, respond) => {
|
|
542
|
+
handleMessage(message, sender).then(respond, (cause) => {
|
|
543
|
+
lastNotice = cause.message;
|
|
544
|
+
respond({ ok: false, message: lastNotice });
|
|
545
|
+
});
|
|
546
|
+
return true;
|
|
547
|
+
});
|
|
548
|
+
chrome.commands.onCommand.addListener((command, tab) => {
|
|
549
|
+
if (command !== INSPECTOR_COMMAND) {
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
inspectorCommands = inspectorCommands
|
|
553
|
+
.then(() => toggleInspector(tab))
|
|
554
|
+
.catch((cause) => {
|
|
555
|
+
console.error('Sideroom could not update its inspector:', cause);
|
|
556
|
+
});
|
|
557
|
+
return inspectorCommands;
|
|
558
|
+
});
|
|
559
|
+
chrome.tabs.onRemoved.addListener((tabId) => activeTabs.delete(tabId));
|
|
560
|
+
chrome.tabs.onUpdated.addListener((tabId, change) => {
|
|
561
|
+
if (change.status === 'loading' && activeTabs.has(tabId)) {
|
|
562
|
+
void stopInspector(tabId).catch((cause) => {
|
|
563
|
+
console.error(
|
|
564
|
+
'Sideroom could not clear its inspector after navigation:',
|
|
565
|
+
cause,
|
|
566
|
+
);
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
chrome.alarms.onAlarm.addListener((alarm) => {
|
|
571
|
+
if (alarm.name === RETRY_ALARM) {
|
|
572
|
+
void initialized
|
|
573
|
+
.then(() => Promise.all([...connections.values()].map(reconnect)))
|
|
574
|
+
.catch((cause) => {
|
|
575
|
+
lastNotice = cause.message;
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
});
|