@rmrdeveloper/sideroom-pi 8.14.0 → 9.0.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 +19 -9
- package/assets/artifacts/GUIDELINES_TEMPLATE.md +33 -2
- package/assets/visual-context/browser/INSTALL.md +11 -2
- package/assets/visual-context/browser/background.js +72 -60
- package/assets/visual-context/browser/content.js +45 -7
- package/assets/visual-context/browser/destinations.js +40 -0
- package/assets/visual-context/browser/manifest.json +1 -1
- package/extensions/guidelines/catalog.ts +9 -0
- package/extensions/guidelines/execute.ts +26 -0
- package/extensions/guidelines/guard.ts +46 -14
- package/extensions/guidelines/index.ts +31 -0
- package/extensions/guidelines/prompt.ts +2 -2
- package/extensions/guidelines/review.ts +1 -1
- package/extensions/jev/model.ts +13 -0
- package/extensions/visual-context/broker.ts +14 -6
- package/extensions/visual-context/compiler-config.ts +103 -0
- package/extensions/visual-context/configure.ts +421 -0
- package/extensions/visual-context/dialogs.ts +95 -0
- package/extensions/visual-context/files.ts +139 -0
- package/extensions/visual-context/index.ts +48 -56
- package/extensions/visual-context/match.ts +48 -16
- package/extensions/visual-context/model.ts +29 -3
- package/extensions/visual-context/projects.ts +226 -0
- package/extensions/visual-context/session.ts +150 -17
- package/extensions/visual-context/setup.ts +146 -0
- package/extensions/visual-context/source.ts +203 -100
- package/extensions/visual-context/ui.ts +5 -1
- package/package.json +5 -5
- package/skills/sideroom-guidelines/SKILL.md +8 -5
- package/skills/sideroom-guidelines/references/languages/go.md +35 -1
- package/skills/sideroom-guidelines/references/languages/java.md +34 -1
- package/skills/sideroom-guidelines/references/languages/javascript.md +34 -1
- package/skills/sideroom-guidelines/references/languages/php-laravel.md +34 -1
- package/skills/sideroom-guidelines/references/languages/python.md +30 -1
- package/skills/sideroom-guidelines/references/languages/rust.md +34 -1
- package/skills/sideroom-guidelines/references/languages/typescript.md +38 -1
- package/skills/sideroom-visual-setup/SKILL.md +49 -0
package/README.md
CHANGED
|
@@ -91,7 +91,7 @@ wall of tool calls, it:
|
|
|
91
91
|
each agent run, the agent must read the shared contract and the one complete
|
|
92
92
|
guide matching the target language. Premature `write`/`edit` calls are
|
|
93
93
|
blocked. The guides cover guard clauses, braced conditionals, fail-fast
|
|
94
|
-
errors, focused units, and all
|
|
94
|
+
errors, focused units, and all 20 canonical rules without dumping them into
|
|
95
95
|
the system prompt.
|
|
96
96
|
- **Finds the skills below the working directory.** In a trusted monorepo, Pi
|
|
97
97
|
normally ignores project skills stored under child folders. Sideroom scans
|
|
@@ -149,8 +149,11 @@ a destination if several Pi sessions are open. Press `Alt+Shift+S` to select
|
|
|
149
149
|
several elements without opening the popup; Escape or the shortcut exits.
|
|
150
150
|
Review their local source candidates in Pi and send your
|
|
151
151
|
instruction. The click prepares context; it never starts the agent. No project
|
|
152
|
-
configuration or framework plugin is needed
|
|
153
|
-
|
|
152
|
+
configuration or framework plugin is needed for inferred source candidates.
|
|
153
|
+
For more precise locations, ask Pi to enable visual compiler context in one
|
|
154
|
+
project. It prepares the development dependency and configuration, including
|
|
155
|
+
projects below your working folder. Compiler locations are checked against local
|
|
156
|
+
markup; missing or mismatched locations stay unresolved.
|
|
154
157
|
Download [the companion ZIP](https://github.com/RMRdeveloper/sideroom-pi/releases/latest/download/sideroom-browser-extension.zip)
|
|
155
158
|
from GitHub Releases, extract it, and load its folder in your browser's developer mode.
|
|
156
159
|
No extension store is required; installation and updates are manual. The same
|
|
@@ -190,15 +193,21 @@ scope and preserves behavior and compatibility. See
|
|
|
190
193
|
### Guidelines — a gate, not a novel
|
|
191
194
|
|
|
192
195
|
A short system-prompt contract points at the packaged
|
|
193
|
-
`sideroom-guidelines` skill. Before `write` or `edit`,
|
|
194
|
-
|
|
195
|
-
language guide
|
|
196
|
-
|
|
196
|
+
`sideroom-guidelines` skill. Before `write` or `edit`, call
|
|
197
|
+
`sideroom_guidelines` with the target file's `path` to load the complete skill
|
|
198
|
+
and matching language guide from the packaged catalog. Unsupported languages
|
|
199
|
+
load only the general skill. Full `read` calls of the packaged documents also
|
|
200
|
+
satisfy the same guard; failed or incomplete loads do not. Reuse loaded guides
|
|
201
|
+
in the same run and reload after compaction or a new run. Requires Pi and
|
|
202
|
+
pi-tui 1.2.0 or later; call the tool directly, not through another tool.
|
|
203
|
+
Each guide mirrors all 20 rules in the canonical seed with idiomatic examples.
|
|
197
204
|
The seed, `assets/artifacts/GUIDELINES_TEMPLATE.md`, is never pasted into the
|
|
198
205
|
system prompt. After any successful mutation, one hidden review steer fires when
|
|
199
206
|
the turn settles and asks the agent to re-check every changed file against the
|
|
200
207
|
loaded guide and run the relevant formatter, linter, type checks, and tests —
|
|
201
|
-
so the contract is applied, not just read.
|
|
208
|
+
so the contract is applied, not just read. Rule 20, Decomposition by intent,
|
|
209
|
+
asks for coherent named operations that clarify the caller's flow, even with
|
|
210
|
+
one caller. It keeps tightly coupled steps together and imposes no line limit.
|
|
202
211
|
|
|
203
212
|
### Monorepo skills — child skills are still project skills
|
|
204
213
|
|
|
@@ -231,7 +240,8 @@ to TypeSafe's decision model about the changed file and, when available, up to
|
|
|
231
240
|
four directly related source files. It checks guard clauses, fail fast,
|
|
232
241
|
command/query separation, null handling, immutability, repeated validation,
|
|
233
242
|
responsibility, dependency direction, comments, testability, redundant
|
|
234
|
-
abstractions, consumer compatibility and test quality
|
|
243
|
+
abstractions, decomposition by intent, consumer compatibility and test quality
|
|
244
|
+
only when the supplied
|
|
235
245
|
files support the question. An answer above the cutoff travels with the
|
|
236
246
|
end-of-run guidelines review instead of interrupting each edit. READMEs, JSON,
|
|
237
247
|
lockfiles, deletion-only edits, and files outside the project are never sent.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Coding Guidelines
|
|
2
2
|
|
|
3
|
-
Rules every change in this repo must follow.
|
|
3
|
+
Rules every change in this repo must follow. The index identifies mechanical checks and rules that need review judgment.
|
|
4
4
|
|
|
5
5
|
**Adapting this template to a new language:** keep `RULE` and `WHY` text as-is unless the language genuinely doesn't support the concept (e.g. Python has no braces — see rule 1). Swap the code fence language tag and rewrite the bad/good snippet idiomatically. Do not add prose. Do not merge two rules into one section.
|
|
6
6
|
|
|
@@ -27,6 +27,7 @@ Rules every change in this repo must follow. Ordered by how often they're violat
|
|
|
27
27
|
| 17 | [Dependency direction](#17-dependency-direction) | architectural |
|
|
28
28
|
| 18 | [Testability](#18-testability-as-a-design-constraint) | architectural |
|
|
29
29
|
| 19 | [Comments](#19-comments) | architectural |
|
|
30
|
+
| 20 | [Decomposition by intent](#20-decomposition-by-intent) | structural |
|
|
30
31
|
|
|
31
32
|
---
|
|
32
33
|
|
|
@@ -265,8 +266,38 @@ counter++;
|
|
|
265
266
|
setInterval(checkPaymentStatus, 5000);
|
|
266
267
|
```
|
|
267
268
|
|
|
269
|
+
### 20. Decomposition by intent
|
|
270
|
+
RULE: Extract a block into a named function when it represents a coherent operation and its name and contract let the caller understand the flow without reading its implementation. Keep orchestration separate from implementation details. Do not split solely to reduce line count; keep tightly coupled steps together when separating them would introduce shared mutable state, excessive parameters, or require reading both functions to understand either.
|
|
271
|
+
WHY: Good decomposition reduces how much a reader must understand at once. Function length is a review signal; extraction must improve comprehension rather than merely move code elsewhere.
|
|
272
|
+
```js
|
|
273
|
+
// bad
|
|
274
|
+
function createPackingSlip(items, destination) {
|
|
275
|
+
let unitCount = 0;
|
|
276
|
+
let weightGrams = 0;
|
|
277
|
+
for (const item of items) {
|
|
278
|
+
unitCount += item.quantity;
|
|
279
|
+
weightGrams += item.quantity * item.unitWeightGrams;
|
|
280
|
+
}
|
|
281
|
+
return { destination, totals: { unitCount, weightGrams } };
|
|
282
|
+
}
|
|
283
|
+
// good
|
|
284
|
+
function createPackingSlip(items, destination) {
|
|
285
|
+
return { destination, totals: calculatePackingTotals(items) };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function calculatePackingTotals(items) {
|
|
289
|
+
let unitCount = 0;
|
|
290
|
+
let weightGrams = 0;
|
|
291
|
+
for (const item of items) {
|
|
292
|
+
unitCount += item.quantity;
|
|
293
|
+
weightGrams += item.quantity * item.unitWeightGrams;
|
|
294
|
+
}
|
|
295
|
+
return { unitCount, weightGrams };
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
268
299
|
---
|
|
269
300
|
|
|
270
301
|
**When in doubt:** fail fast, keep it flat, keep it small.
|
|
271
302
|
|
|
272
|
-
**Enforcement note:** rules 1–2 (mechanical) should be backed by a linter/formatter in the target language, not left to this document alone — text-only compliance from an agent caps out well below what a lint rule enforces. Rules 3–10
|
|
303
|
+
**Enforcement note:** rules 1–2 (mechanical) should be backed by a linter/formatter in the target language, not left to this document alone — text-only compliance from an agent caps out well below what a lint rule enforces. Rules 3–10 and 20 are structural; rules 11–19 are architectural. Rule 20 requires semantic review, not a line-count limit or mechanical length detector. Rules 11–20 require judgment and stay as review guidance.
|
|
@@ -29,7 +29,11 @@ replace the files in the same folder, and reload the extension from the browser'
|
|
|
29
29
|
extensions page. Do not remove and reinstall it unless you want to pair again.
|
|
30
30
|
The extension connects only to authorized local Pi installations; it captures
|
|
31
31
|
DOM descriptions, not form values or editable text. Source locations are inferred
|
|
32
|
-
candidates
|
|
32
|
+
candidates by default. Ask Pi to enable compiler context in a selected project
|
|
33
|
+
for locations checked against local markup. Pi can run in a parent folder;
|
|
34
|
+
F7 can associate a page with a child project. After setup, restart the development
|
|
35
|
+
server and refresh the page. This companion requires visual protocol version 3;
|
|
36
|
+
upgrade/reload Pi and the companion together without changing the pairing address.
|
|
33
37
|
|
|
34
38
|
## Español
|
|
35
39
|
|
|
@@ -63,7 +67,12 @@ extensión desde la página de extensiones del navegador. No la elimines y
|
|
|
63
67
|
reinstales salvo que quieras volver a emparejarla.
|
|
64
68
|
La extensión solo se conecta a instalaciones locales de Pi que hayas autorizado;
|
|
65
69
|
recoge descripciones de los elementos, no valores de formularios ni texto editable.
|
|
66
|
-
|
|
70
|
+
Por defecto, los archivos de origen son candidatos inferidos. Pide a Pi que active
|
|
71
|
+
el contexto del compilador en un proyecto elegido para obtener ubicaciones
|
|
72
|
+
comprobadas contra el código local. Pi puede estar en la carpeta padre; F7 permite
|
|
73
|
+
asociar una página con un proyecto hijo. Tras configurarlo, reinicia el servidor
|
|
74
|
+
de desarrollo y actualiza la página. Esta versión requiere el protocolo visual 3:
|
|
75
|
+
actualiza y recarga Pi y la extensión juntos, conservando el emparejamiento.
|
|
67
76
|
|
|
68
77
|
Full guide / Guía completa:
|
|
69
78
|
https://github.com/RMRdeveloper/sideroom-pi/blob/main/docs/extensions/visual-context.md
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
importScripts('destinations.js');
|
|
2
|
+
|
|
3
|
+
const PROTOCOL_VERSION = 3;
|
|
2
4
|
const BRIDGE_PATH = '/sideroom-visual-context';
|
|
3
5
|
const HEARTBEAT_MS = 20_000;
|
|
4
6
|
const RESPONSE_TIMEOUT_MS = 10_000;
|
|
5
7
|
const RETRY_MS = 2_000;
|
|
6
8
|
const RETRY_ALARM = 'sideroom-reconnect';
|
|
7
9
|
const MAX_INSTALLATIONS = 4;
|
|
10
|
+
const MAX_PENDING_RECEIPTS = 8;
|
|
11
|
+
const MAX_MESSAGE_BYTES = 8_192;
|
|
8
12
|
const INSPECTOR_COMMAND = 'toggle-inspector';
|
|
9
13
|
const IDLE_TITLE = 'Select an element for Pi';
|
|
10
14
|
const ACTIVE_TITLE =
|
|
@@ -25,10 +29,7 @@ const activeTabs = new Map();
|
|
|
25
29
|
const receipts = new Map();
|
|
26
30
|
const connections = new Map();
|
|
27
31
|
let paused = false;
|
|
28
|
-
|
|
29
|
-
let selected;
|
|
30
|
-
let preferredId;
|
|
31
|
-
let automaticSelection = false;
|
|
32
|
+
const destinations = createDestinations();
|
|
32
33
|
let lastAddress = '';
|
|
33
34
|
let lastNotice = 'Pair once with each Pi installation to select elements.';
|
|
34
35
|
const isConnected = () =>
|
|
@@ -75,6 +76,25 @@ async function sendToTab(tabId, message) {
|
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
function encodeSelectionFrame(selection, target) {
|
|
80
|
+
const frame = JSON.stringify({ type: MESSAGE.selection, selection, target });
|
|
81
|
+
if (new TextEncoder().encode(frame).byteLength > MAX_MESSAGE_BYTES) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
'The selected element exceeds the message limit. Select a smaller element.',
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return frame;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function notifyReceipt({ endpoint, selectionId, notice }) {
|
|
90
|
+
const receipt = receipts.get(selectionId);
|
|
91
|
+
if (!receipt || receipt.endpoint !== endpoint) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
receipts.delete(selectionId);
|
|
95
|
+
await sendToTab(receipt.tabId, { action: 'receipt', message: notice });
|
|
96
|
+
}
|
|
97
|
+
|
|
78
98
|
async function showInspectorIndicator(tabId, { text, title }) {
|
|
79
99
|
await chrome.action.setBadgeBackgroundColor({ tabId, color: '#ffb340' });
|
|
80
100
|
await chrome.action.setBadgeText({ tabId, text });
|
|
@@ -97,13 +117,13 @@ async function stopInspectors() {
|
|
|
97
117
|
}
|
|
98
118
|
|
|
99
119
|
async function startInspector(tab) {
|
|
100
|
-
|
|
120
|
+
const target = destinations.current();
|
|
121
|
+
if (!target || !isConnected()) {
|
|
101
122
|
throw new Error('Choose an available Pi session first.');
|
|
102
123
|
}
|
|
103
124
|
if (!tab?.id || !/^https?:\/\//u.test(tab.url ?? '')) {
|
|
104
125
|
throw new Error('Open the web page you want to inspect.');
|
|
105
126
|
}
|
|
106
|
-
const target = selected;
|
|
107
127
|
await chrome.scripting.executeScript({
|
|
108
128
|
target: { tabId: tab.id },
|
|
109
129
|
files: ['content.js'],
|
|
@@ -112,7 +132,7 @@ async function startInspector(tab) {
|
|
|
112
132
|
const shortcut = commands.find(
|
|
113
133
|
(command) => command.name === INSPECTOR_COMMAND,
|
|
114
134
|
)?.shortcut;
|
|
115
|
-
if (
|
|
135
|
+
if (destinations.current() !== target) {
|
|
116
136
|
throw new Error('The destination changed. Start the inspector again.');
|
|
117
137
|
}
|
|
118
138
|
activeTabs.set(tab.id, target);
|
|
@@ -144,19 +164,9 @@ async function toggleInspector(commandTab) {
|
|
|
144
164
|
}
|
|
145
165
|
|
|
146
166
|
function updateSessions(available) {
|
|
147
|
-
const previous =
|
|
148
|
-
|
|
149
|
-
selected =
|
|
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
|
-
}
|
|
167
|
+
const previous = destinations.current();
|
|
168
|
+
destinations.update(available);
|
|
169
|
+
const selected = destinations.current();
|
|
160
170
|
if (
|
|
161
171
|
previous?.id !== selected?.id ||
|
|
162
172
|
previous?.revision !== selected?.revision
|
|
@@ -167,7 +177,7 @@ function updateSessions(available) {
|
|
|
167
177
|
}
|
|
168
178
|
lastNotice = selected
|
|
169
179
|
? `Connected to ${selected.name}.`
|
|
170
|
-
:
|
|
180
|
+
: destinations.all().length
|
|
171
181
|
? 'Choose which Pi session receives your selections.'
|
|
172
182
|
: WAITING_NOTICE;
|
|
173
183
|
}
|
|
@@ -188,9 +198,7 @@ async function disconnect() {
|
|
|
188
198
|
for (const entry of connections.values()) {
|
|
189
199
|
closeConnection(entry);
|
|
190
200
|
}
|
|
191
|
-
|
|
192
|
-
selected = undefined;
|
|
193
|
-
preferredId = undefined;
|
|
201
|
+
destinations.clear();
|
|
194
202
|
await stopInspectors();
|
|
195
203
|
}
|
|
196
204
|
|
|
@@ -260,25 +268,29 @@ async function openConnection(entry) {
|
|
|
260
268
|
}
|
|
261
269
|
if (message.type === MESSAGE.received) {
|
|
262
270
|
lastNotice = `${String(message.candidates)} source candidates. Review in Pi (F7).`;
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
message: lastNotice,
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
+
await notifyReceipt({
|
|
272
|
+
endpoint: entry.endpoint,
|
|
273
|
+
selectionId: message.id,
|
|
274
|
+
notice: lastNotice,
|
|
275
|
+
});
|
|
271
276
|
}
|
|
272
277
|
if (message.type === MESSAGE.error) {
|
|
273
278
|
lastNotice = message.message;
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
279
|
+
if (typeof message.id === 'string') {
|
|
280
|
+
await notifyReceipt({
|
|
281
|
+
endpoint: entry.endpoint,
|
|
282
|
+
selectionId: message.id,
|
|
283
|
+
notice: lastNotice,
|
|
284
|
+
});
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
// Older brokers and connection failures can only identify the installation.
|
|
288
|
+
for (const selectionId of [...receipts.keys()]) {
|
|
289
|
+
await notifyReceipt({
|
|
290
|
+
endpoint: entry.endpoint,
|
|
291
|
+
selectionId,
|
|
292
|
+
notice: lastNotice,
|
|
293
|
+
});
|
|
282
294
|
}
|
|
283
295
|
}
|
|
284
296
|
} catch (cause) {
|
|
@@ -331,7 +343,7 @@ async function reconnect(entry) {
|
|
|
331
343
|
if (!entry.attempt) {
|
|
332
344
|
entry.attempt = openConnection(entry)
|
|
333
345
|
.catch((cause) => {
|
|
334
|
-
if (!
|
|
346
|
+
if (!destinations.current()) {
|
|
335
347
|
lastNotice = cause.message;
|
|
336
348
|
}
|
|
337
349
|
scheduleReconnect(entry);
|
|
@@ -412,6 +424,7 @@ async function handleMessage(message, sender) {
|
|
|
412
424
|
if (message.action !== 'selection' || !target || !isConnected()) {
|
|
413
425
|
throw new Error('Connect to Pi and start the inspector first.');
|
|
414
426
|
}
|
|
427
|
+
const selected = destinations.current();
|
|
415
428
|
if (target.id !== selected?.id || target.revision !== selected?.revision) {
|
|
416
429
|
throw new Error('The destination changed. Start the inspector again.');
|
|
417
430
|
}
|
|
@@ -426,7 +439,7 @@ async function handleMessage(message, sender) {
|
|
|
426
439
|
if (!entry || entry.socket?.readyState !== WebSocket.OPEN) {
|
|
427
440
|
throw new Error('The destination is no longer available.');
|
|
428
441
|
}
|
|
429
|
-
if (receipts.size >=
|
|
442
|
+
if (receipts.size >= MAX_PENDING_RECEIPTS) {
|
|
430
443
|
throw new Error('Pi is still processing selections. Try again shortly.');
|
|
431
444
|
}
|
|
432
445
|
const page = parseUrl(
|
|
@@ -437,13 +450,17 @@ async function handleMessage(message, sender) {
|
|
|
437
450
|
...message.selection,
|
|
438
451
|
page: `${page.origin}${page.pathname}`,
|
|
439
452
|
};
|
|
453
|
+
const frame = encodeSelectionFrame(selection, target);
|
|
440
454
|
receipts.set(selection.id, {
|
|
441
455
|
tabId: sender.tab.id,
|
|
442
456
|
endpoint: entry.endpoint,
|
|
443
457
|
});
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
)
|
|
458
|
+
try {
|
|
459
|
+
entry.socket.send(frame);
|
|
460
|
+
} catch (cause) {
|
|
461
|
+
receipts.delete(selection.id);
|
|
462
|
+
throw new Error('Cannot send the selected element to Pi.', { cause });
|
|
463
|
+
}
|
|
447
464
|
return { ok: true };
|
|
448
465
|
}
|
|
449
466
|
if (message.action === 'status') {
|
|
@@ -455,8 +472,8 @@ async function handleMessage(message, sender) {
|
|
|
455
472
|
connected: isConnected(),
|
|
456
473
|
notice: lastNotice,
|
|
457
474
|
address: lastAddress,
|
|
458
|
-
sessions,
|
|
459
|
-
selectedId:
|
|
475
|
+
sessions: destinations.all(),
|
|
476
|
+
selectedId: destinations.current()?.id ?? '',
|
|
460
477
|
paused,
|
|
461
478
|
installations: connections.size,
|
|
462
479
|
};
|
|
@@ -482,8 +499,8 @@ async function handleMessage(message, sender) {
|
|
|
482
499
|
ok: true,
|
|
483
500
|
connected: isConnected(),
|
|
484
501
|
notice: lastNotice,
|
|
485
|
-
sessions,
|
|
486
|
-
selectedId:
|
|
502
|
+
sessions: destinations.all(),
|
|
503
|
+
selectedId: destinations.current()?.id ?? '',
|
|
487
504
|
address: lastAddress,
|
|
488
505
|
};
|
|
489
506
|
}
|
|
@@ -510,21 +527,16 @@ async function handleMessage(message, sender) {
|
|
|
510
527
|
};
|
|
511
528
|
}
|
|
512
529
|
if (message.action === 'choose') {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
throw new Error('That Pi session is no longer available.');
|
|
516
|
-
}
|
|
530
|
+
destinations.choose(message.id);
|
|
531
|
+
const destination = destinations.current();
|
|
517
532
|
await stopInspectors();
|
|
518
|
-
|
|
519
|
-
preferredId = destination.id;
|
|
520
|
-
automaticSelection = false;
|
|
521
|
-
lastNotice = `Connected to ${selected.name}.`;
|
|
533
|
+
lastNotice = `Connected to ${destination.name}.`;
|
|
522
534
|
return {
|
|
523
535
|
ok: true,
|
|
524
536
|
connected: isConnected(),
|
|
525
537
|
notice: lastNotice,
|
|
526
|
-
sessions,
|
|
527
|
-
selectedId:
|
|
538
|
+
sessions: destinations.all(),
|
|
539
|
+
selectedId: destination.id,
|
|
528
540
|
};
|
|
529
541
|
}
|
|
530
542
|
if (message.action === 'inspect') {
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
const MAX_TEXT = 160;
|
|
7
7
|
const MAX_CLASSES = 32;
|
|
8
8
|
const MAX_ANCESTORS = 4;
|
|
9
|
+
const SOURCE_ATTRIBUTE = 'data-insp-path';
|
|
10
|
+
const MAX_SOURCE_PATH = 2_048;
|
|
9
11
|
const BORDER = '#ffb340';
|
|
10
12
|
let active = false;
|
|
11
13
|
let root;
|
|
@@ -35,6 +37,41 @@
|
|
|
35
37
|
};
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
function sourceLocation(element) {
|
|
41
|
+
const marker = element.getAttribute(SOURCE_ATTRIBUTE);
|
|
42
|
+
if (!marker) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
const parts = marker.match(/^(.*):(\d+):(\d+):([^:]+)$/u);
|
|
46
|
+
if (
|
|
47
|
+
!parts?.[1] ||
|
|
48
|
+
parts[1].length > MAX_SOURCE_PATH ||
|
|
49
|
+
[...parts[1]].some(
|
|
50
|
+
(character) =>
|
|
51
|
+
character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127,
|
|
52
|
+
)
|
|
53
|
+
) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
'Invalid compiler location. Refresh the development page.',
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
const line = Number(parts[2]);
|
|
59
|
+
const column = Number(parts[3]);
|
|
60
|
+
if (
|
|
61
|
+
!Number.isSafeInteger(line) ||
|
|
62
|
+
!Number.isSafeInteger(column) ||
|
|
63
|
+
line < 1 ||
|
|
64
|
+
column < 1 ||
|
|
65
|
+
line > 1_000_000 ||
|
|
66
|
+
column > 1_000_000
|
|
67
|
+
) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
'Invalid compiler position. Refresh the development page.',
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return { path: parts[1], line, column };
|
|
73
|
+
}
|
|
74
|
+
|
|
38
75
|
function visibleText(element) {
|
|
39
76
|
if (element.closest('input,textarea,select,[contenteditable]')) {
|
|
40
77
|
return '';
|
|
@@ -91,15 +128,16 @@
|
|
|
91
128
|
ancestors.push(describe(parent));
|
|
92
129
|
parent = parent.parentElement;
|
|
93
130
|
}
|
|
94
|
-
const selection = {
|
|
95
|
-
id: crypto.randomUUID(),
|
|
96
|
-
page: `${location.origin}${location.pathname}`,
|
|
97
|
-
element: describe(element),
|
|
98
|
-
text: visibleText(element),
|
|
99
|
-
ancestors,
|
|
100
|
-
};
|
|
101
131
|
caption.textContent = 'Sending selection to Pi…';
|
|
102
132
|
try {
|
|
133
|
+
const selection = {
|
|
134
|
+
id: crypto.randomUUID(),
|
|
135
|
+
page: `${location.origin}${location.pathname}`,
|
|
136
|
+
element: describe(element),
|
|
137
|
+
text: visibleText(element),
|
|
138
|
+
ancestors,
|
|
139
|
+
source: sourceLocation(element),
|
|
140
|
+
};
|
|
103
141
|
const response = await chrome.runtime.sendMessage({
|
|
104
142
|
action: 'selection',
|
|
105
143
|
selection,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
globalThis.createDestinations = function createDestinations() {
|
|
2
|
+
let sessions = [];
|
|
3
|
+
let selected;
|
|
4
|
+
let preferredId;
|
|
5
|
+
let automaticSelection = false;
|
|
6
|
+
|
|
7
|
+
return {
|
|
8
|
+
update(available) {
|
|
9
|
+
sessions = [...available];
|
|
10
|
+
selected = sessions.find(
|
|
11
|
+
(session) => session.id === (preferredId ?? selected?.id),
|
|
12
|
+
);
|
|
13
|
+
if (automaticSelection && sessions.length > 1) {
|
|
14
|
+
selected = undefined;
|
|
15
|
+
automaticSelection = false;
|
|
16
|
+
}
|
|
17
|
+
if (!selected && !preferredId && sessions.length === 1) {
|
|
18
|
+
selected = sessions[0];
|
|
19
|
+
automaticSelection = true;
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
choose(id) {
|
|
23
|
+
const destination = sessions.find((session) => session.id === id);
|
|
24
|
+
if (!destination) {
|
|
25
|
+
throw new Error('That Pi session is no longer available.');
|
|
26
|
+
}
|
|
27
|
+
selected = destination;
|
|
28
|
+
preferredId = destination.id;
|
|
29
|
+
automaticSelection = false;
|
|
30
|
+
},
|
|
31
|
+
clear() {
|
|
32
|
+
sessions = [];
|
|
33
|
+
selected = undefined;
|
|
34
|
+
preferredId = undefined;
|
|
35
|
+
automaticSelection = false;
|
|
36
|
+
},
|
|
37
|
+
current: () => selected,
|
|
38
|
+
all: () => [...sessions],
|
|
39
|
+
};
|
|
40
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "Sideroom Visual Context",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.2.0",
|
|
5
5
|
"description": "Select a page element and attach local source candidates to your Pi session.",
|
|
6
6
|
"minimum_chrome_version": "116",
|
|
7
7
|
"permissions": ["activeTab", "scripting", "storage", "alarms"],
|
|
@@ -7,6 +7,8 @@ const LANGUAGE_GUIDE_DIRECTORY = join(
|
|
|
7
7
|
'skills/sideroom-guidelines/references/languages',
|
|
8
8
|
);
|
|
9
9
|
|
|
10
|
+
export const GUIDELINES_TOOL_NAME = 'sideroom_guidelines';
|
|
11
|
+
|
|
10
12
|
export const GUIDELINE_SKILL_PATH = join(
|
|
11
13
|
PACKAGE_ROOT,
|
|
12
14
|
'skills/sideroom-guidelines/SKILL.md',
|
|
@@ -36,6 +38,13 @@ export const LANGUAGE_TARGET_LABELS: readonly string[] = LANGUAGE_GUIDES.map(
|
|
|
36
38
|
({ extensions }) => extensions.join('/'),
|
|
37
39
|
);
|
|
38
40
|
|
|
41
|
+
export function languageGuideFor(path: string): string | undefined {
|
|
42
|
+
const normalizedPath = path.replaceAll('\\', '/').toLowerCase();
|
|
43
|
+
return LANGUAGE_GUIDES.find(({ extensions }) =>
|
|
44
|
+
extensions.some((extension) => normalizedPath.endsWith(extension)),
|
|
45
|
+
)?.path;
|
|
46
|
+
}
|
|
47
|
+
|
|
39
48
|
function languageGuide(
|
|
40
49
|
fileName: string,
|
|
41
50
|
extensions: readonly string[],
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { GUIDELINE_SKILL_PATH, languageGuideFor } from './catalog.ts';
|
|
3
|
+
|
|
4
|
+
export interface GuidelinesToolResult {
|
|
5
|
+
readonly content: { readonly type: 'text'; readonly text: string }[];
|
|
6
|
+
readonly details: { readonly paths: readonly string[] };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function executeGuidelines(
|
|
10
|
+
path: string,
|
|
11
|
+
readDocument: (path: string) => string = (path) => readFileSync(path, 'utf8'),
|
|
12
|
+
): GuidelinesToolResult {
|
|
13
|
+
const guidePath = languageGuideFor(path);
|
|
14
|
+
const paths = [
|
|
15
|
+
GUIDELINE_SKILL_PATH,
|
|
16
|
+
...(guidePath === undefined ? [] : [guidePath]),
|
|
17
|
+
];
|
|
18
|
+
const content = paths.map((documentPath) => {
|
|
19
|
+
const text = readDocument(documentPath);
|
|
20
|
+
if (text.trim().length === 0) {
|
|
21
|
+
throw new Error(`Empty guidelines document: ${documentPath}`);
|
|
22
|
+
}
|
|
23
|
+
return { type: 'text' as const, text };
|
|
24
|
+
});
|
|
25
|
+
return { content, details: { paths } };
|
|
26
|
+
}
|