leetcode-capture-helper 0.0.0-stage → 0.1.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 (48) hide show
  1. package/cli.js +92 -0
  2. package/config.js +25 -0
  3. package/framing.js +57 -0
  4. package/host.js +346 -0
  5. package/install.js +290 -0
  6. package/package.json +31 -4
  7. package/runtime/bundle.json +42 -0
  8. package/runtime/companion/capture-log.js +83 -0
  9. package/runtime/companion/codex-backend.js +337 -0
  10. package/runtime/companion/companion.js +2193 -0
  11. package/runtime/companion/mouse-input.js +154 -0
  12. package/runtime/companion/package.json +4 -0
  13. package/runtime/companion/panel-bridge.js +273 -0
  14. package/runtime/companion/settings-rpc.js +228 -0
  15. package/runtime/companion/settings.js +464 -0
  16. package/runtime/companion/terminal-format.js +330 -0
  17. package/runtime/companion/vault-summary.js +169 -0
  18. package/runtime/companion/virtual-terminal.js +138 -0
  19. package/runtime/extension/shared/anthropic-backend.js +193 -0
  20. package/runtime/extension/shared/catalog.js +415 -0
  21. package/runtime/extension/shared/code-ledger.js +333 -0
  22. package/runtime/extension/shared/context-budget.js +259 -0
  23. package/runtime/extension/shared/context-summary.js +271 -0
  24. package/runtime/extension/shared/conversation.js +81 -0
  25. package/runtime/extension/shared/event-hub.js +126 -0
  26. package/runtime/extension/shared/extension-settings.js +445 -0
  27. package/runtime/extension/shared/helper-launcher.js +248 -0
  28. package/runtime/extension/shared/local-backend.js +706 -0
  29. package/runtime/extension/shared/local-server.js +149 -0
  30. package/runtime/extension/shared/package.json +4 -0
  31. package/runtime/extension/shared/practice.js +228 -0
  32. package/runtime/extension/shared/problem-index.js +4128 -0
  33. package/runtime/extension/shared/profiles.js +106 -0
  34. package/runtime/extension/shared/providers.js +58 -0
  35. package/runtime/extension/shared/reply-guard.js +864 -0
  36. package/runtime/extension/shared/roadmap.js +1344 -0
  37. package/runtime/extension/shared/sanitize.js +68 -0
  38. package/runtime/extension/shared/solved.js +277 -0
  39. package/runtime/extension/shared/tutor-engine.js +369 -0
  40. package/runtime/extension/shared/tutor-prompt.js +108 -0
  41. package/runtime/extension/shared/tutor-service.js +520 -0
  42. package/runtime/relay-server/package.json +4 -0
  43. package/runtime/relay-server/panel-broker.js +797 -0
  44. package/runtime/relay-server/server.js +241 -0
  45. package/runtime/vault-tool/log-session.js +230 -0
  46. package/runtime/vault-tool/package.json +4 -0
  47. package/runtime/vault-tool/vault-notes.js +206 -0
  48. package/README.md +0 -3
@@ -0,0 +1,520 @@
1
+ // extension/shared/tutor-service.js - everything the background worker does
2
+ // for the extension runtime, with the browser APIs passed in so it runs (and
3
+ // is tested) under Node too. background.js only binds chrome.* to it.
4
+ //
5
+ // - Keeps the extension's settings and API keys in storage (two records,
6
+ // see extension-settings.js) and builds the backend from them.
7
+ // - Runs the tutor (tutor-engine.js) and its event hub (event-hub.js).
8
+ // - Speaks the panel's port protocol (extension/panel.js, "extension"
9
+ // transport). Panel to worker:
10
+ // { type: 'subscribe', after, epoch } replay, then follow live
11
+ // { type: 'message', id, text, problemSlug, snapshot }
12
+ // { type: 'switch-profile', reqId, id } the model switcher picked a profile
13
+ // { type: 'profile-switched', id } the helper's companion switched to it
14
+ // { type: 'ping' } keeps the worker awake
15
+ // Worker to panel:
16
+ // { type: 'runtime', runtime, chosen } on connect and on every change
17
+ // { type: 'profiles', profiles, active } the switcher's entries (no secrets)
18
+ // { type: 'event', event } one hub event
19
+ // { type: 'reply', id, ok, error? } answer to a message
20
+ // { type: 'switch-result', reqId, ok, runtime?, message? }
21
+ // - Answers runtime messages: a capture from content.js
22
+ // ('lc-tutor:capture'), and the options page's settings calls
23
+ // ('lc-tutor:settings-get|save|test|models'), which only the extension's
24
+ // own pages may make. No answer to a content script ever carries a key.
25
+ //
26
+ // Pure JavaScript with no Node API (extension/shared.test.js checks that).
27
+
28
+ import { PROVIDERS } from './providers.js';
29
+ import { createEventHub } from './event-hub.js';
30
+ import { createTutorEngine } from './tutor-engine.js';
31
+ import { CONNECTION_TEST_PROMPT } from './tutor-prompt.js';
32
+ import { listAnthropicModels } from './anthropic-backend.js';
33
+ import { findProfile } from './profiles.js';
34
+ import {
35
+ SettingsError,
36
+ applyPatch,
37
+ backendKey,
38
+ buildBackend,
39
+ explainError,
40
+ missingRequirement,
41
+ normalizeSecrets,
42
+ normalizeSettings,
43
+ originPatternFor,
44
+ profileEntry,
45
+ profileProblem,
46
+ publicView,
47
+ runtimeFor,
48
+ serverUrlFor,
49
+ statusSummary,
50
+ withProfile,
51
+ } from './extension-settings.js';
52
+
53
+ export const SETTINGS_KEY = 'tutorSettings';
54
+ export const SECRETS_KEY = 'tutorSecrets';
55
+ export const TEST_TIMEOUT_MS = 90_000;
56
+ export const MODELS_TIMEOUT_MS = 6000;
57
+ const MAX_MODELS = 200;
58
+
59
+ // Replaces every occurrence of a secret in a message with ***.
60
+ function scrub(message, secrets) {
61
+ let out = String(message);
62
+ for (const secret of secrets) {
63
+ if (secret && secret.length >= 6) out = out.split(secret).join('***');
64
+ }
65
+ return out;
66
+ }
67
+
68
+ function withTimeout(promise, ms, what) {
69
+ let timer;
70
+ const timeout = new Promise((_, reject) => {
71
+ timer = setTimeout(() => reject(new Error(`${what} did not answer within ${Math.round(ms / 1000)} seconds`)), ms);
72
+ });
73
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
74
+ }
75
+
76
+ // storage: { get(keys) -> Promise<object>, set(object) -> Promise }
77
+ // permissions: { contains({ origins }) -> Promise<boolean> }
78
+ // extensionOrigin: 'chrome-extension://<id>' (or moz-extension://<uuid>)
79
+ // onRuntime(runtime): called once the settings are read and after every save,
80
+ // with 'extension' or 'helper' (the background's helper launcher follows it).
81
+ // helperProblem(currentRuntime): why a profile that runs through the helper
82
+ // cannot be switched to right now, as a short line, or null (async allowed).
83
+ export function createTutorService({
84
+ storage,
85
+ permissions,
86
+ extensionOrigin,
87
+ log = () => {},
88
+ buildBackendImpl = buildBackend,
89
+ onRuntime = () => {},
90
+ helperProblem = async () => null,
91
+ }) {
92
+ const hub = createEventHub();
93
+ const engine = createTutorEngine({ hub, log });
94
+ const ports = new Set();
95
+ let settings = normalizeSettings(null);
96
+ let secrets = normalizeSecrets(null);
97
+ let configuredKey = null;
98
+
99
+ const ready = (async () => {
100
+ try {
101
+ const stored = await storage.get([SETTINGS_KEY, SECRETS_KEY]);
102
+ settings = normalizeSettings(stored && stored[SETTINGS_KEY]);
103
+ secrets = normalizeSecrets(stored && stored[SECRETS_KEY]);
104
+ } catch (err) {
105
+ log(`could not read settings: ${err.message}`);
106
+ }
107
+ await applyToEngine();
108
+ onRuntime(runtimeFor(settings));
109
+ })();
110
+
111
+ async function permissionFor(current) {
112
+ const pattern = originPatternFor(serverUrlFor(current));
113
+ if (!pattern) return { pattern: null, granted: true };
114
+ try {
115
+ return { pattern, granted: Boolean(await permissions.contains({ origins: [pattern] })) };
116
+ } catch {
117
+ return { pattern, granted: false };
118
+ }
119
+ }
120
+
121
+ function permissionMessage(current) {
122
+ return (
123
+ `The extension isn't allowed to reach ${serverUrlFor(current)} yet. ` +
124
+ 'Open the extension options and press Save (or Test connection) to allow it.'
125
+ );
126
+ }
127
+
128
+ // Hands the engine the backend for the current settings, or the reason
129
+ // there is none. A configuration whose backend key is unchanged keeps the
130
+ // running conversation.
131
+ async function applyToEngine() {
132
+ const config = statusSummary(settings);
133
+ const label = config ? config.label : 'tutor';
134
+ const options = { autoClearContext: settings.options.autoClearContext };
135
+ if (runtimeFor(settings) !== 'extension') {
136
+ configuredKey = null;
137
+ await engine.configure({ backend: null, label, options, config, error: 'The tutor runs through the helper app for this provider.' });
138
+ return;
139
+ }
140
+ const problem = missingRequirement(settings, secrets);
141
+ if (problem) {
142
+ configuredKey = null;
143
+ await engine.configure({ backend: null, label, options, config, error: `${problem} Open the extension options to finish setting it up.` });
144
+ return;
145
+ }
146
+ const permission = await permissionFor(settings);
147
+ if (!permission.granted) {
148
+ configuredKey = null;
149
+ await engine.configure({ backend: null, label, options, config, error: permissionMessage(settings) });
150
+ return;
151
+ }
152
+ const key = backendKey(settings, secrets);
153
+ const explain = makeExplainer(settings, secrets);
154
+ if (key === configuredKey) {
155
+ await engine.configure({ keep: true, key, label, options, config, explainError: explain });
156
+ return;
157
+ }
158
+ let backend;
159
+ try {
160
+ backend = buildBackendImpl(settings, secrets);
161
+ } catch (err) {
162
+ configuredKey = null;
163
+ await engine.configure({ backend: null, label, options, config, error: scrub(err.message, [secrets[settings.provider]]) });
164
+ return;
165
+ }
166
+ configuredKey = key;
167
+ await engine.configure({ backend, key, label, options, config, explainError: explain });
168
+ }
169
+
170
+ function makeExplainer(current, keys) {
171
+ const provider = current.provider;
172
+ const serverUrl = serverUrlFor(current);
173
+ const secret = keys[provider];
174
+ return (message) => explainError(scrub(message, [secret]), { provider, serverUrl, extensionOrigin });
175
+ }
176
+
177
+ // `chosen`: whether a provider is chosen at all, so a panel with no helper
178
+ // running can say to pick one rather than to start the helper.
179
+ function runtimeMessage() {
180
+ return { type: 'runtime', runtime: runtimeFor(settings), chosen: Boolean(settings.provider) };
181
+ }
182
+
183
+ function broadcast(message) {
184
+ for (const port of ports) {
185
+ try {
186
+ port.postMessage(message);
187
+ } catch {
188
+ // gone; its disconnect handler removes it
189
+ }
190
+ }
191
+ }
192
+
193
+ function broadcastRuntime() {
194
+ broadcast(runtimeMessage());
195
+ }
196
+
197
+ // ---- the panel's model switcher ------------------------------------------
198
+
199
+ // Every saved profile with whether it can be switched to now and, if not,
200
+ // a one-line reason. Only non-secret fields.
201
+ async function profilesMessage() {
202
+ const current = runtimeFor(settings);
203
+ const entries = [];
204
+ for (const profile of settings.profiles) {
205
+ const entry = profileEntry(settings, profile);
206
+ let reason = null;
207
+ if (entry.runtime === 'extension') {
208
+ const draft = withProfile(settings, profile);
209
+ reason = profileProblem(draft, secrets);
210
+ if (!reason) {
211
+ const permission = await permissionFor(draft);
212
+ if (!permission.granted) reason = `Not allowed to reach ${permission.pattern.replace(/\/\*$/, '')} yet`;
213
+ }
214
+ } else {
215
+ try {
216
+ reason = (await helperProblem(current)) || null;
217
+ } catch {
218
+ reason = null;
219
+ }
220
+ }
221
+ entries.push({ ...entry, available: !reason, reason });
222
+ }
223
+ return { type: 'profiles', profiles: entries, active: settings.activeProfile };
224
+ }
225
+
226
+ async function broadcastProfiles() {
227
+ if (ports.size === 0) return;
228
+ broadcast(await profilesMessage());
229
+ }
230
+
231
+ // Every change to the settings (a switch, a helper's switch noted, an
232
+ // options-page save) reads the current settings, checks, writes storage and
233
+ // takes the result in one step on this queue, so none of them works from a
234
+ // copy another one replaced while it awaited (a permission check, the
235
+ // storage write) and overwrites it. Only that step is queued: the engine
236
+ // swap after it, which may wait for a running reply, is not, so a save is
237
+ // never held up behind a reply.
238
+ let commitChain = Promise.resolve();
239
+ function commit(step) {
240
+ const run = commitChain.then(step);
241
+ commitChain = run.catch(() => {});
242
+ return run;
243
+ }
244
+
245
+ // Switches to a saved profile: only the active profile changes (never a
246
+ // key, a URL or anything else), saved like any other setting, and the new
247
+ // backend goes in through the engine's turn queue, so a reply already
248
+ // running finishes first and the chat, statement and code move over.
249
+ // Resolves once the swap has happened. One switch at a time.
250
+ let switchChain = Promise.resolve();
251
+ function switchProfile(id) {
252
+ const run = switchChain.then(() => doSwitchProfile(id));
253
+ switchChain = run.catch(() => {});
254
+ return run;
255
+ }
256
+
257
+ async function doSwitchProfile(id) {
258
+ const result = await commit(async () => {
259
+ const profile = findProfile(settings.profiles, id);
260
+ if (!profile) return { ok: false, code: 'unknown', message: 'That model is no longer saved. Open Manage models to add it again.' };
261
+ if (settings.activeProfile === id) return { ok: true, runtime: runtimeFor(settings), unchanged: true };
262
+ const next = applyPatch(settings, secrets, { activeProfile: id });
263
+ const runtime = runtimeFor(next.settings);
264
+ if (runtime === 'extension') {
265
+ const problem = profileProblem(next.settings, next.secrets);
266
+ if (problem) return { ok: false, code: 'unavailable', message: `${problem}. Open Manage models to finish setting it up.` };
267
+ const permission = await permissionFor(next.settings);
268
+ if (!permission.granted) return { ok: false, code: 'unavailable', message: permissionMessage(next.settings) };
269
+ try {
270
+ buildBackend(next.settings, next.secrets, { probe: async () => null });
271
+ } catch (err) {
272
+ return { ok: false, code: 'unavailable', message: scrub(err.message, [next.secrets[next.settings.provider]]) };
273
+ }
274
+ }
275
+ await storage.set({ [SETTINGS_KEY]: next.settings });
276
+ settings = next.settings;
277
+ return { ok: true, runtime };
278
+ });
279
+ if (!result.ok) return result;
280
+ if (result.unchanged) return { ok: true, runtime: result.runtime };
281
+ await applyToEngine();
282
+ broadcastRuntime();
283
+ onRuntime(runtimeFor(settings));
284
+ await broadcastProfiles();
285
+ return { ok: true, runtime: result.runtime };
286
+ }
287
+
288
+ // The helper's companion switched to a profile (the panel asked it through
289
+ // the relay); the extension remembers which one is active. Nothing else
290
+ // changes: the tutor keeps running through the helper.
291
+ async function noteHelperSwitch(id) {
292
+ const changed = await commit(async () => {
293
+ const profile = findProfile(settings.profiles, id);
294
+ if (!profile || settings.activeProfile === id || runtimeFor(settings) !== 'helper') return false;
295
+ const next = applyPatch(settings, secrets, { activeProfile: id });
296
+ if (runtimeFor(next.settings) !== 'helper') return false;
297
+ await storage.set({ [SETTINGS_KEY]: next.settings });
298
+ settings = next.settings;
299
+ return true;
300
+ });
301
+ if (!changed) return;
302
+ await applyToEngine();
303
+ await broadcastProfiles();
304
+ }
305
+
306
+ // ---- the options page -------------------------------------------------
307
+
308
+ async function view(extra = {}) {
309
+ const permission = await permissionFor(settings);
310
+ return { settings: publicView(settings, secrets), active: statusSummary(settings), permission, catalog: PROVIDERS, ...extra };
311
+ }
312
+
313
+ async function save({ patch }) {
314
+ await commit(async () => {
315
+ const next = applyPatch(settings, secrets, patch);
316
+ if (runtimeFor(next.settings) === 'extension') {
317
+ const problem = missingRequirement(next.settings, next.secrets);
318
+ if (problem) throw new SettingsError(problem, 'provider');
319
+ try {
320
+ // Only checks that a backend can be built; the probe stays off.
321
+ buildBackend(next.settings, next.secrets, { probe: async () => null });
322
+ } catch (err) {
323
+ throw new SettingsError(scrub(err.message, [next.secrets[next.settings.provider]]), err.field || 'provider');
324
+ }
325
+ }
326
+ await storage.set({ [SETTINGS_KEY]: next.settings, [SECRETS_KEY]: next.secrets });
327
+ settings = next.settings;
328
+ secrets = next.secrets;
329
+ });
330
+ await applyToEngine();
331
+ broadcastRuntime();
332
+ onRuntime(runtimeFor(settings));
333
+ await broadcastProfiles();
334
+ return view({ saved: true });
335
+ }
336
+
337
+ async function test({ patch }) {
338
+ let draft;
339
+ try {
340
+ draft = applyPatch(settings, secrets, patch || {});
341
+ } catch (err) {
342
+ return { ok: false, message: err.message };
343
+ }
344
+ const secret = draft.secrets[draft.settings.provider];
345
+ const problem = missingRequirement(draft.settings, draft.secrets);
346
+ if (problem) return { ok: false, message: problem };
347
+ if (runtimeFor(draft.settings) !== 'extension') return { ok: false, message: 'This provider is tested through the helper app.' };
348
+ const permission = await permissionFor(draft.settings);
349
+ if (!permission.granted) return { ok: false, message: permissionMessage(draft.settings), permission };
350
+ const started = Date.now();
351
+ try {
352
+ const backend = buildBackendImpl(draft.settings, draft.secrets);
353
+ const reply = await withTimeout(backend.sendMessage(CONNECTION_TEST_PROMPT, {}), TEST_TIMEOUT_MS, PROVIDERS[draft.settings.provider].label);
354
+ if (!reply || !String(reply).trim()) return { ok: false, message: 'The model answered with nothing.' };
355
+ return { ok: true, message: `Connected in ${((Date.now() - started) / 1000).toFixed(1)}s`, reply: scrub(String(reply).trim().slice(0, 120), [secret]) };
356
+ } catch (err) {
357
+ return { ok: false, message: makeExplainer(draft.settings, draft.secrets)(err.message) };
358
+ }
359
+ }
360
+
361
+ async function models({ provider, baseUrl, apiKey }) {
362
+ const id = provider === 'anthropic' || provider === 'openai' ? provider : 'local';
363
+ const patch = { provider: id, providers: { [id]: {} } };
364
+ if (baseUrl && id !== 'anthropic') patch.providers[id].baseUrl = baseUrl;
365
+ if (typeof apiKey === 'string' && apiKey) patch.providers[id].apiKey = apiKey;
366
+ let draft;
367
+ try {
368
+ draft = applyPatch(settings, secrets, patch);
369
+ } catch (err) {
370
+ return { models: [], message: err.message };
371
+ }
372
+ const key = draft.secrets[id];
373
+ const permission = await permissionFor(draft.settings);
374
+ if (!permission.granted) return { models: [], message: permissionMessage(draft.settings), permission };
375
+ const signal = AbortSignal.timeout(MODELS_TIMEOUT_MS);
376
+ try {
377
+ let names = [];
378
+ if (id === 'anthropic') {
379
+ if (!key) return { models: [], message: 'Enter the API key first.' };
380
+ names = await listAnthropicModels({ apiKey: key, signal });
381
+ } else {
382
+ const base = draft.settings.providers[id].baseUrl.replace(/\/+$/, '');
383
+ const headers = key ? { Authorization: `Bearer ${key}` } : {};
384
+ const ask = async (url) => {
385
+ const res = await fetch(url, { headers, signal });
386
+ if (!res.ok) throw new Error(`${url} answered ${res.status}`);
387
+ return res.json();
388
+ };
389
+ if (id === 'local' && /\/v1$/.test(base)) {
390
+ try {
391
+ const tags = await ask(`${base.replace(/\/v1$/, '')}/api/tags`);
392
+ names = (tags.models || []).map((m) => m.name);
393
+ } catch {
394
+ names = [];
395
+ }
396
+ }
397
+ if (names.length === 0) {
398
+ const list = await ask(`${base}/models`);
399
+ names = (list.data || []).map((m) => m.id);
400
+ }
401
+ }
402
+ return { models: [...new Set(names.filter((n) => typeof n === 'string' && n))].sort().slice(0, MAX_MODELS) };
403
+ } catch (err) {
404
+ const message = err && err.name === 'TimeoutError' ? 'The server did not answer in time.' : err.message;
405
+ return { models: [], message: makeExplainer(draft.settings, draft.secrets)(scrub(message, [key])) };
406
+ }
407
+ }
408
+
409
+ const SETTINGS_METHODS = {
410
+ 'lc-tutor:settings-get': () => view(),
411
+ 'lc-tutor:settings-save': save,
412
+ 'lc-tutor:settings-test': test,
413
+ 'lc-tutor:settings-models': models,
414
+ };
415
+
416
+ function isExtensionPage(sender) {
417
+ return Boolean(sender && typeof sender.url === 'string' && sender.url.startsWith(`${extensionOrigin}/`));
418
+ }
419
+
420
+ return {
421
+ ready,
422
+ hub,
423
+ engine,
424
+
425
+ // A runtime message. Resolves to the answer, or undefined when the
426
+ // message is not for this service.
427
+ async handleMessage(message, sender) {
428
+ if (!message || typeof message !== 'object') return undefined;
429
+ await ready;
430
+ if (message.type === 'lc-tutor:capture') {
431
+ if (runtimeFor(settings) !== 'extension') return { handled: false };
432
+ engine.handleCapture(message.capture);
433
+ return { handled: true };
434
+ }
435
+ if (message.type === 'lc-tutor:runtime') return { runtime: runtimeFor(settings) };
436
+ const method = Object.prototype.hasOwnProperty.call(SETTINGS_METHODS, message.type) ? SETTINGS_METHODS[message.type] : null;
437
+ if (!method) return undefined;
438
+ // Only the extension's own pages (the options page) may read or change
439
+ // settings, never a content script on leetcode.com.
440
+ if (!isExtensionPage(sender)) return { ok: false, error: { message: 'not allowed' } };
441
+ try {
442
+ return { ok: true, result: await method(message.params && typeof message.params === 'object' ? message.params : {}) };
443
+ } catch (err) {
444
+ if (err instanceof SettingsError) return { ok: false, error: { message: err.message, field: err.field } };
445
+ return { ok: false, error: { message: scrub(err.message, Object.values(secrets)) } };
446
+ }
447
+ },
448
+
449
+ // A Tutor panel's port.
450
+ connectPort(port) {
451
+ ports.add(port);
452
+ let unsubscribe = null;
453
+ const post = (message) => {
454
+ try {
455
+ port.postMessage(message);
456
+ } catch {
457
+ // disconnected
458
+ }
459
+ };
460
+ ready.then(async () => {
461
+ post(runtimeMessage());
462
+ post(await profilesMessage());
463
+ });
464
+ port.onMessage.addListener((message) => {
465
+ if (!message || typeof message !== 'object') return;
466
+ ready.then(() => {
467
+ if (message.type === 'subscribe') {
468
+ if (unsubscribe) unsubscribe();
469
+ unsubscribe = hub.subscribe((event) => post({ type: 'event', event }), {
470
+ after: Number(message.after) || 0,
471
+ epoch: typeof message.epoch === 'string' ? message.epoch : null,
472
+ });
473
+ } else if (message.type === 'unsubscribe') {
474
+ if (unsubscribe) unsubscribe();
475
+ unsubscribe = null;
476
+ } else if (message.type === 'message') {
477
+ const answer =
478
+ runtimeFor(settings) === 'extension'
479
+ ? engine.handleMessage(message)
480
+ : { ok: false, error: 'helper' };
481
+ post({ type: 'reply', id: message.id, ...answer });
482
+ } else if (message.type === 'switch-profile') {
483
+ const reqId = message.reqId;
484
+ switchProfile(typeof message.id === 'string' ? message.id : '').then(
485
+ (result) => post({ type: 'switch-result', reqId, ...result }),
486
+ (err) => post({ type: 'switch-result', reqId, ok: false, code: 'failed', message: scrub(err.message, Object.values(secrets)) })
487
+ );
488
+ } else if (message.type === 'profile-switched' && typeof message.id === 'string') {
489
+ noteHelperSwitch(message.id).catch((err) => log(`could not note a model switch: ${err.message}`));
490
+ }
491
+ // 'ping' needs no answer: receiving it is what keeps the worker up.
492
+ });
493
+ });
494
+ port.onDisconnect.addListener(() => {
495
+ ports.delete(port);
496
+ if (unsubscribe) unsubscribe();
497
+ });
498
+ },
499
+
500
+ // Re-checks what the current settings need (call when host permissions
501
+ // change).
502
+ refresh() {
503
+ return ready.then(async () => {
504
+ await applyToEngine();
505
+ await broadcastProfiles();
506
+ });
507
+ },
508
+
509
+ // Sends every panel the switcher's entries again (call when what the
510
+ // helper can do changed).
511
+ refreshProfiles() {
512
+ return ready.then(() => broadcastProfiles());
513
+ },
514
+
515
+ // For tests: the same as a panel's 'switch-profile'.
516
+ switchProfile(id) {
517
+ return ready.then(() => switchProfile(id));
518
+ },
519
+ };
520
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "private": true,
3
+ "type": "commonjs"
4
+ }