@rmrdeveloper/sideroom-pi 8.13.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.
Files changed (38) hide show
  1. package/README.md +19 -9
  2. package/assets/artifacts/GUIDELINES_TEMPLATE.md +33 -2
  3. package/assets/visual-context/browser/INSTALL.md +11 -2
  4. package/assets/visual-context/browser/background.js +72 -60
  5. package/assets/visual-context/browser/content.js +45 -7
  6. package/assets/visual-context/browser/destinations.js +40 -0
  7. package/assets/visual-context/browser/manifest.json +1 -1
  8. package/extensions/guidelines/catalog.ts +9 -0
  9. package/extensions/guidelines/execute.ts +26 -0
  10. package/extensions/guidelines/guard.ts +46 -14
  11. package/extensions/guidelines/index.ts +31 -0
  12. package/extensions/guidelines/prompt.ts +2 -2
  13. package/extensions/guidelines/review.ts +1 -1
  14. package/extensions/jev/model.ts +13 -0
  15. package/extensions/visual-context/broker.ts +14 -6
  16. package/extensions/visual-context/compiler-config.ts +103 -0
  17. package/extensions/visual-context/configure.ts +421 -0
  18. package/extensions/visual-context/dialogs.ts +95 -0
  19. package/extensions/visual-context/files.ts +139 -0
  20. package/extensions/visual-context/index.ts +48 -56
  21. package/extensions/visual-context/match.ts +48 -16
  22. package/extensions/visual-context/model.ts +29 -3
  23. package/extensions/visual-context/projects.ts +226 -0
  24. package/extensions/visual-context/session.ts +150 -17
  25. package/extensions/visual-context/setup.ts +146 -0
  26. package/extensions/visual-context/source.ts +203 -100
  27. package/extensions/visual-context/ui.ts +5 -1
  28. package/package.json +5 -5
  29. package/skills/sideroom-codebase-design/SKILL.md +28 -1
  30. package/skills/sideroom-guidelines/SKILL.md +8 -5
  31. package/skills/sideroom-guidelines/references/languages/go.md +35 -1
  32. package/skills/sideroom-guidelines/references/languages/java.md +34 -1
  33. package/skills/sideroom-guidelines/references/languages/javascript.md +34 -1
  34. package/skills/sideroom-guidelines/references/languages/php-laravel.md +34 -1
  35. package/skills/sideroom-guidelines/references/languages/python.md +30 -1
  36. package/skills/sideroom-guidelines/references/languages/rust.md +34 -1
  37. package/skills/sideroom-guidelines/references/languages/typescript.md +38 -1
  38. 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 19 canonical rules without dumping them into
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. Source ranges are real, but the
153
- DOM-to-source relationship is inferred and stays labelled as a candidate.
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`, a guard requires a
194
- full read of the packaged skill and, for supported targets, the one complete
195
- language guide under `skills/sideroom-guidelines/references/languages/`.
196
- Each guide mirrors all 19 rules in the canonical seed with idiomatic examples.
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 only when the supplied
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. Ordered by how often they're violated and how cheap they are to check — mechanical rules first, judgment calls last.
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 (structural) are catchable by targeted lint rules or code review checklists in most languages. Rules 11–19 (architectural) require judgment and stay as review guidance.
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, never proven mappings.
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
- Los archivos de origen son candidatos inferidos, no correspondencias demostradas.
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
- const PROTOCOL_VERSION = 2;
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
- let sessions = [];
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
- if (!selected || !isConnected()) {
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 (selected !== target) {
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 = 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
- }
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
- : sessions.length
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
- sessions = [];
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
- 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
+ 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
- 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
- }
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 (!selected) {
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 >= 8) {
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
- entry.socket.send(
445
- JSON.stringify({ type: MESSAGE.selection, selection, target }),
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: selected?.id ?? '',
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: selected?.id ?? '',
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
- const destination = sessions.find((session) => session.id === message.id);
514
- if (!destination) {
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
- selected = destination;
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: selected.id,
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.1.0",
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
+ }