codex-ringside 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.
package/src/card.js ADDED
@@ -0,0 +1,302 @@
1
+ // Runs inside the Codex renderer. Must stay self-contained: cdp.cjs injects this file as text.
2
+ function installCodexRingsideCard() {
3
+ const VERSION = 13;
4
+ const KEY = '__codexRingsideCard';
5
+ const STALE_AFTER_MS = 150000;
6
+ const TICKS = 30;
7
+ const TICK_COLORS = {
8
+ normal: [[34, 211, 238], [52, 224, 130]],
9
+ warning: [[250, 204, 21], [249, 115, 22]],
10
+ danger: [[251, 146, 60], [239, 68, 68]]
11
+ };
12
+ if (window[KEY]?.version === VERSION) {
13
+ window[KEY].place();
14
+ return;
15
+ }
16
+ window[KEY]?.destroy?.();
17
+
18
+ const card = document.createElement('div');
19
+ card.id = 'codex-ringside-card';
20
+ card.tabIndex = 0;
21
+ card.setAttribute('role', 'group');
22
+ card.setAttribute('aria-label', 'Codex 剩余额度');
23
+ card.setAttribute('aria-describedby', 'codex-ringside-tooltip');
24
+ card.setAttribute('aria-haspopup', 'dialog');
25
+
26
+ const tooltip = document.createElement('div');
27
+ tooltip.id = 'codex-ringside-tooltip';
28
+ tooltip.setAttribute('role', 'tooltip');
29
+ tooltip.hidden = true;
30
+
31
+ const style = document.createElement('style');
32
+ style.id = 'codex-ringside-style';
33
+ const dark = `
34
+ --ringside-track: #ffffff26; --ringside-text: #f2f4f2; --ringside-label: #b4b9b5;
35
+ `;
36
+ style.textContent = `
37
+ #codex-ringside-card {
38
+ --ringside-track: #dfe3e9; --ringside-text: #25313a; --ringside-label: #65737c;
39
+ --ringside-hover: color-mix(in srgb, var(--ringside-text) 12%, transparent);
40
+ box-sizing: border-box; width: 30px; flex: 0 0 auto; align-self: center; margin: 0 0 8px; padding: 0;
41
+ display: flex; flex-direction: column; align-items: center; gap: 8px;
42
+ border-radius: 4px; outline: none;
43
+ color: var(--ringside-text); background: transparent; cursor: pointer; user-select: none;
44
+ -webkit-app-region: no-drag; font-family: inherit; font-variant-numeric: tabular-nums; line-height: 1;
45
+ transition: background-color 120ms ease, box-shadow 120ms ease, transform 120ms ease;
46
+ }
47
+ /* The spread also grows the corners, so the outer radius is 4 + 3 = 7px; the width matches the rail buttons' 36px. */
48
+ #codex-ringside-card:hover { background: var(--ringside-hover); box-shadow: 0 0 0 3px var(--ringside-hover); }
49
+ #codex-ringside-card:active { transform: scale(.96); }
50
+ @media (prefers-reduced-motion: reduce) { #codex-ringside-card { transition: none; } }
51
+ html.dark #codex-ringside-card, html[data-theme="dark"] #codex-ringside-card { ${dark} }
52
+ @media (prefers-color-scheme: dark) {
53
+ html:not(.light):not([data-theme="light"]) #codex-ringside-card { ${dark} }
54
+ }
55
+ #codex-ringside-card[hidden], #codex-ringside-tooltip[hidden] { display: none !important; }
56
+ #codex-ringside-card:focus-visible { outline: 1px solid var(--ringside-text); outline-offset: 2px; }
57
+ #codex-ringside-card .ringside-meter { position: relative; flex: 0 0 auto; width: 30px; height: 30px; }
58
+ #codex-ringside-card .ringside-meter[data-tone="normal"] { --ringside-glow: #34e08280; }
59
+ #codex-ringside-card .ringside-meter[data-tone="warning"] { --ringside-glow: #f9731680; }
60
+ #codex-ringside-card .ringside-meter[data-tone="danger"] { --ringside-glow: #ef444480; }
61
+ #codex-ringside-card svg { display: block; width: 100%; height: 100%; overflow: visible; }
62
+ #codex-ringside-card .ringside-track { fill: none; stroke: var(--ringside-track); stroke-width: 3.6; }
63
+ #codex-ringside-card .ringside-ticks { stroke-width: 2.5; filter: drop-shadow(0 0 1.5px var(--ringside-glow, transparent)); }
64
+ #codex-ringside-card .ringside-center {
65
+ position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center;
66
+ }
67
+ #codex-ringside-card .ringside-number { font-size: 8px; line-height: 9px; font-weight: 600; letter-spacing: -.3px; white-space: nowrap; }
68
+ #codex-ringside-card .ringside-label { font-size: 6.5px; line-height: 8px; color: var(--ringside-label); white-space: nowrap; }
69
+ #codex-ringside-tooltip {
70
+ box-sizing: border-box; position: fixed; z-index: 2147483000; min-width: 170px; max-width: min(320px, calc(100vw - 16px));
71
+ padding: 12px 14px; border: 1px solid color-mix(in srgb, currentColor 12%, transparent); border-radius: 10px;
72
+ background: var(--color-surface-elevated-secondary, #f8f8f7); color: var(--color-text-primary, #303630);
73
+ box-shadow: 0 4px 18px #0002; pointer-events: none; -webkit-app-region: no-drag;
74
+ font: 12px/1.5 -apple-system, BlinkMacSystemFont, sans-serif;
75
+ }
76
+ html.dark #codex-ringside-tooltip, html[data-theme="dark"] #codex-ringside-tooltip {
77
+ background: var(--color-surface-elevated-secondary, #2c2e2c); color: var(--color-text-primary, #edf0ed);
78
+ }
79
+ @media (prefers-color-scheme: dark) {
80
+ html:not(.light):not([data-theme="light"]) #codex-ringside-tooltip {
81
+ background: var(--color-surface-elevated-secondary, #2c2e2c); color: var(--color-text-primary, #edf0ed);
82
+ }
83
+ }
84
+ #codex-ringside-tooltip .ringside-title { font-weight: 600; }
85
+ #codex-ringside-tooltip .ringside-sections { display: flex; gap: 20px; margin-top: 8px; }
86
+ #codex-ringside-tooltip .ringside-section-percent { margin: 2px 0; font-size: 22px; line-height: 26px; font-weight: 700; font-variant-numeric: tabular-nums; }
87
+ #codex-ringside-tooltip .ringside-dim { opacity: .68; font-size: 11px; white-space: nowrap; }
88
+ #codex-ringside-tooltip .ringside-footer { margin-top: 10px; padding-top: 8px; border-top: 1px solid color-mix(in srgb, currentColor 10%, transparent); }
89
+ `;
90
+
91
+ let value = { rings: [], message: '正在读取 Codex 剩余额度', note: null, stale: false, updatedAt: null };
92
+ let rail = null;
93
+ let placementTimer = null;
94
+ let hoverTimer = null;
95
+ let disposed = false;
96
+
97
+ const element = (className, text) => {
98
+ const node = document.createElement('div');
99
+ node.className = className;
100
+ if (text != null) node.textContent = text;
101
+ return node;
102
+ };
103
+ const visible = node => {
104
+ if (!node?.isConnected) return false;
105
+ const rect = node.getBoundingClientRect();
106
+ return rect.width > 0 && rect.height > 0 && getComputedStyle(node).visibility !== 'hidden';
107
+ };
108
+ const percentText = percent => (Number.isFinite(percent) ? `${percent}%` : '—');
109
+
110
+ // Filled ticks run clockwise from 12 o'clock; the unused part of the circle stays a solid track.
111
+ function ringSvg(percent, tone) {
112
+ const filled = Number.isFinite(percent) && percent > 0 ? Math.max(1, Math.round(percent / 100 * TICKS)) : 0;
113
+ const [from, to] = TICK_COLORS[tone] ?? TICK_COLORS.normal;
114
+ const ticks = Array.from({ length: filled }, (_, index) => {
115
+ const angle = ((index + 0.5) / TICKS) * 2 * Math.PI - Math.PI / 2;
116
+ const point = radius => [Math.cos(angle), Math.sin(angle)].map(axis => (22 + radius * axis).toFixed(2));
117
+ const [x1, y1] = point(17);
118
+ const [x2, y2] = point(21.9);
119
+ const progress = filled > 1 ? index / (filled - 1) : 0;
120
+ const color = from.map((channel, i) => Math.round(channel + (to[i] - channel) * progress));
121
+ return `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="rgb(${color.join(',')})"/>`;
122
+ }).join('');
123
+ const used = (filled / TICKS) * 100;
124
+ return '<svg viewBox="0 0 44 44" aria-hidden="true">'
125
+ + `<circle class="ringside-track" cx="22" cy="22" r="19.5" pathLength="100" stroke-dasharray="${100 - used} 100"`
126
+ + ` stroke-dashoffset="${-used}" transform="rotate(-90 22 22)"/><g class="ringside-ticks">${ticks}</g></svg>`;
127
+ }
128
+
129
+ function buildMeter(ring) {
130
+ const meter = element('ringside-meter');
131
+ meter.dataset.tone = ring.tone;
132
+ meter.innerHTML = ringSvg(ring.percent, ring.tone);
133
+ const center = element('ringside-center');
134
+ // "100%" is too wide for the ring's inner circle; the tooltip still shows the full text.
135
+ const number = ring.percent === 100 ? '100' : percentText(ring.percent);
136
+ center.append(element('ringside-number', number), element('ringside-label', ring.label));
137
+ meter.append(center);
138
+ meter.setAttribute('role', 'meter');
139
+ meter.setAttribute('aria-label', ring.name ?? ring.label);
140
+ meter.setAttribute('aria-valuemin', '0');
141
+ meter.setAttribute('aria-valuemax', '100');
142
+ if (Number.isFinite(ring.percent)) meter.setAttribute('aria-valuenow', String(ring.percent));
143
+ return meter;
144
+ }
145
+
146
+ function renderTooltip() {
147
+ const sections = element('ringside-sections');
148
+ for (const ring of value.rings ?? []) {
149
+ const section = element('ringside-section');
150
+ section.append(element('ringside-dim', ring.name ?? ring.label), element('ringside-section-percent', percentText(ring.percent)));
151
+ if (ring.resetsAt) section.append(element('ringside-dim', `${ring.resetsAt} 重置`));
152
+ sections.append(section);
153
+ }
154
+ const lines = [element('ringside-title', 'Codex 剩余额度')];
155
+ if (sections.childElementCount) lines.push(sections);
156
+ if (value.message) lines.push(element('ringside-dim', value.message));
157
+ if (value.note) {
158
+ const footer = element('ringside-dim ringside-footer', value.note);
159
+ lines.push(footer);
160
+ }
161
+ tooltip.replaceChildren(...lines);
162
+ }
163
+
164
+ function render() {
165
+ const rings = value.rings?.length ? value.rings : [{ label: '额度', name: '额度', percent: null, tone: 'muted' }];
166
+ card.replaceChildren(...rings.map(buildMeter));
167
+ renderTooltip();
168
+ if (!tooltip.hidden) positionTooltip();
169
+ }
170
+
171
+ // A dead agent must not leave a healthy-looking number on screen.
172
+ function expire() {
173
+ if (disposed || value.stale || !Number.isFinite(value.updatedAt) || Date.now() - value.updatedAt < STALE_AFTER_MS) return;
174
+ value = {
175
+ ...value,
176
+ stale: true,
177
+ message: '额度数据已过期,正在等待重新连接',
178
+ rings: (value.rings ?? []).map(ring => ({ ...ring, percent: null, tone: 'muted', resetsAt: null }))
179
+ };
180
+ render();
181
+ }
182
+ const freshnessTimer = setInterval(expire, 1000);
183
+
184
+ const panelOpen = () => document.getElementById('codex-ringside-panel')?.hidden === false;
185
+ function togglePanel() {
186
+ hideTooltip();
187
+ window.__codexRingsideBackground?.togglePanel(card);
188
+ }
189
+
190
+ function positionTooltip() {
191
+ const rect = card.getBoundingClientRect();
192
+ tooltip.style.left = `${Math.max(8, Math.min(rect.right + 12, innerWidth - tooltip.offsetWidth - 8))}px`;
193
+ const top = rect.top + rect.height / 2 - tooltip.offsetHeight / 2;
194
+ tooltip.style.top = `${Math.max(8, Math.min(top, innerHeight - tooltip.offsetHeight - 8))}px`;
195
+ }
196
+ function hideTooltip() {
197
+ clearTimeout(hoverTimer);
198
+ hoverTimer = null;
199
+ tooltip.hidden = true;
200
+ }
201
+ function showTooltip() {
202
+ clearTimeout(hoverTimer);
203
+ if (disposed || document.hidden || !visible(card) || panelOpen()) return;
204
+ tooltip.hidden = false;
205
+ positionTooltip();
206
+ }
207
+ function scheduleTooltip() {
208
+ clearTimeout(hoverTimer);
209
+ hoverTimer = setTimeout(showTooltip, 350);
210
+ }
211
+
212
+ function place() {
213
+ if (disposed || !document.body) return;
214
+ if (!style.isConnected) (document.head ?? document.documentElement).appendChild(style);
215
+ if (!tooltip.isConnected) document.body.appendChild(tooltip);
216
+ const nextRail = [...document.querySelectorAll('nav[data-app-navigation-rail]')].find(visible) ?? null;
217
+ if (rail !== nextRail) {
218
+ if (rail) resizeObserver.unobserve(rail);
219
+ rail = nextRail;
220
+ if (rail) resizeObserver.observe(rail);
221
+ }
222
+ // Own a sibling just before the rail's footer; never move React-owned nodes.
223
+ const footer = rail && [...rail.children]
224
+ .filter(child => child !== card && visible(child) && getComputedStyle(child).position !== 'absolute')
225
+ .at(-1);
226
+ if (!footer) {
227
+ card.hidden = true;
228
+ hideTooltip();
229
+ return;
230
+ }
231
+ card.hidden = false;
232
+ if (card.parentElement !== rail || card.nextElementSibling !== footer) rail.insertBefore(card, footer);
233
+ if (!tooltip.hidden) positionTooltip();
234
+ }
235
+ function schedulePlacement() {
236
+ if (disposed || placementTimer !== null) return;
237
+ placementTimer = setTimeout(() => {
238
+ placementTimer = null;
239
+ place();
240
+ }, 100);
241
+ }
242
+
243
+ const ownNode = node => card.contains(node) || tooltip.contains(node) || node === style;
244
+ const observer = new MutationObserver(records => {
245
+ if (records.some(record => !ownNode(record.target))) schedulePlacement();
246
+ });
247
+ const resizeObserver = new ResizeObserver(schedulePlacement);
248
+ const onKeyDown = event => {
249
+ if (event.key === 'Escape') hideTooltip();
250
+ if (event.key !== 'Enter' && event.key !== ' ') return;
251
+ event.preventDefault();
252
+ togglePanel();
253
+ };
254
+
255
+ observer.observe(document.documentElement, { childList: true, subtree: true });
256
+ card.addEventListener('mouseenter', scheduleTooltip);
257
+ card.addEventListener('mouseleave', hideTooltip);
258
+ card.addEventListener('focus', showTooltip);
259
+ card.addEventListener('blur', hideTooltip);
260
+ card.addEventListener('keydown', onKeyDown);
261
+ card.addEventListener('click', togglePanel);
262
+ window.addEventListener('resize', schedulePlacement);
263
+ document.addEventListener('visibilitychange', hideTooltip);
264
+
265
+ window[KEY] = {
266
+ version: VERSION,
267
+ place,
268
+ update(next) {
269
+ value = { ...value, ...next };
270
+ expire();
271
+ render();
272
+ place();
273
+ },
274
+ status() {
275
+ return {
276
+ version: VERSION,
277
+ placed: card.parentElement === rail && !card.hidden && visible(card),
278
+ rings: value.rings,
279
+ message: value.message,
280
+ stale: value.stale ?? false,
281
+ updatedAt: value.updatedAt ?? null,
282
+ cardCount: document.querySelectorAll('#codex-ringside-card').length
283
+ };
284
+ },
285
+ destroy() {
286
+ disposed = true;
287
+ observer.disconnect();
288
+ resizeObserver.disconnect();
289
+ clearTimeout(placementTimer);
290
+ clearInterval(freshnessTimer);
291
+ hideTooltip();
292
+ window.removeEventListener('resize', schedulePlacement);
293
+ document.removeEventListener('visibilitychange', hideTooltip);
294
+ card.remove();
295
+ tooltip.remove();
296
+ style.remove();
297
+ delete window[KEY];
298
+ }
299
+ };
300
+ render();
301
+ place();
302
+ }
package/src/cdp.cjs ADDED
@@ -0,0 +1,227 @@
1
+ 'use strict';
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+
5
+ const CDP_PORT = 39222;
6
+ const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '[::1]']);
7
+ const PANEL_BINDING = '__codexRingsideSend';
8
+
9
+ // The endpoint can run code inside Codex, so never follow a target to anywhere but this machine.
10
+ function assertLoopback(url, port = CDP_PORT) {
11
+ const parsed = new URL(url);
12
+ if (parsed.protocol !== 'ws:' || !LOOPBACK_HOSTS.has(parsed.hostname) || Number(parsed.port) !== port) {
13
+ throw new Error(`CDP must use a loopback WebSocket on port ${port}`);
14
+ }
15
+ return parsed;
16
+ }
17
+
18
+ function isMainWindow(target) {
19
+ if (target?.type !== 'page' || typeof target.webSocketDebuggerUrl !== 'string') return false;
20
+ try {
21
+ const url = new URL(target.url || '');
22
+ return url.protocol === 'app:' && !/overlay|detached/i.test(decodeURIComponent(url.pathname + url.search + url.hash));
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+
28
+ function buildBootstrapScript() {
29
+ const [card, background] = ['card.js', 'background.js'].map(name => fs.readFileSync(path.join(__dirname, name), 'utf8'));
30
+ return `(() => {\nwindow.__codexPlusCard?.destroy?.();\nwindow.__codexPlusBackground?.destroy?.();\n${card}\n${background}\ninstallCodexRingsideCard();\ninstallCodexRingsideBackground();\n})()`;
31
+ }
32
+
33
+ const updateExpression = value => `window.__codexRingsideCard?.update(${JSON.stringify(value)})`;
34
+ const backgroundCall = (method, ...args) => `window.__codexRingsideBackground?.${method}(${args.map(arg => JSON.stringify(arg)).join(', ')})`;
35
+
36
+ async function listTargets(port = CDP_PORT, timeoutMs = 3000) {
37
+ const response = await fetch(`http://127.0.0.1:${port}/json/list`, { signal: AbortSignal.timeout(timeoutMs) });
38
+ if (!response.ok) throw new Error('CDP unavailable');
39
+ return response.json();
40
+ }
41
+
42
+ class CdpSession {
43
+ constructor(target, port = CDP_PORT) {
44
+ this.target = target;
45
+ this.port = port;
46
+ this.pending = new Map();
47
+ this.serial = 0;
48
+ this.socket = null;
49
+ this.onBinding = null;
50
+ }
51
+
52
+ async connect() {
53
+ const ws = new WebSocket(assertLoopback(this.target.webSocketDebuggerUrl, this.port));
54
+ this.socket = ws;
55
+ ws.addEventListener('message', event => {
56
+ let message;
57
+ try { message = JSON.parse(event.data); } catch { return; }
58
+ if (message?.method === 'Runtime.bindingCalled') this.onBinding?.(message.params);
59
+ const request = this.pending.get(message?.id);
60
+ if (!request) return;
61
+ this.pending.delete(message.id);
62
+ clearTimeout(request.timer);
63
+ if (message.error || message.result?.exceptionDetails) request.reject(new Error('CDP evaluation failed'));
64
+ else request.resolve(message.result);
65
+ });
66
+ ws.addEventListener('close', () => this.rejectPending());
67
+ ws.addEventListener('error', () => this.rejectPending());
68
+ await new Promise((resolve, reject) => {
69
+ const timer = setTimeout(() => {
70
+ ws.close();
71
+ reject(new Error('CDP handshake timed out'));
72
+ }, 4000);
73
+ const finish = fn => {
74
+ clearTimeout(timer);
75
+ fn();
76
+ };
77
+ ws.addEventListener('open', () => finish(resolve), { once: true });
78
+ ws.addEventListener('error', () => finish(() => reject(new Error('CDP unavailable'))), { once: true });
79
+ ws.addEventListener('close', () => finish(() => reject(new Error('CDP closed'))), { once: true });
80
+ });
81
+ await this.request('Runtime.enable');
82
+ }
83
+
84
+ request(method, params = {}) {
85
+ if (this.socket?.readyState !== WebSocket.OPEN) return Promise.reject(new Error('CDP disconnected'));
86
+ const id = ++this.serial;
87
+ return new Promise((resolve, reject) => {
88
+ const timer = setTimeout(() => {
89
+ this.pending.delete(id);
90
+ reject(new Error('CDP request timed out'));
91
+ }, 5000);
92
+ this.pending.set(id, { resolve, reject, timer });
93
+ try {
94
+ this.socket.send(JSON.stringify({ id, method, params }));
95
+ } catch (error) {
96
+ this.pending.delete(id);
97
+ clearTimeout(timer);
98
+ reject(error);
99
+ }
100
+ });
101
+ }
102
+
103
+ evaluate(expression) {
104
+ return this.request('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true });
105
+ }
106
+
107
+ rejectPending() {
108
+ for (const request of this.pending.values()) {
109
+ clearTimeout(request.timer);
110
+ request.reject(new Error('CDP disconnected'));
111
+ }
112
+ this.pending.clear();
113
+ }
114
+
115
+ close() {
116
+ this.rejectPending();
117
+ this.socket?.close();
118
+ this.socket = null;
119
+ }
120
+ }
121
+
122
+ // Keeps one session per main window; a scan installs the card and replays the latest value.
123
+ class RendererInjector {
124
+ constructor({ port = CDP_PORT } = {}) {
125
+ this.port = port;
126
+ this.sessions = new Map();
127
+ this.currentValue = null;
128
+ this.background = null;
129
+ this.onPanelMessage = null;
130
+ this.scanning = false;
131
+ this.stopped = false;
132
+ }
133
+
134
+ async connectSession(session) {
135
+ session.onBinding = ({ name, payload }) => {
136
+ if (name === PANEL_BINDING) this.onPanelMessage?.(payload);
137
+ };
138
+ await session.connect();
139
+ await session.request('Runtime.addBinding', { name: PANEL_BINDING });
140
+ }
141
+
142
+ // Images travel once: a window that already has one only needs the new settings.
143
+ async syncBackground(session) {
144
+ if (!this.background) return;
145
+ const { payload, files } = this.background;
146
+ const { result } = await session.evaluate(backgroundCall('state'));
147
+ const state = result.value;
148
+ if (!state || state.key === payload.key) return;
149
+ for (const [id, file] of Object.entries(files)) {
150
+ if (!state.images.includes(id)) await session.evaluate(backgroundCall('addImage', id, fs.readFileSync(file, 'base64')));
151
+ }
152
+ await session.evaluate(backgroundCall('apply', payload));
153
+ }
154
+
155
+ async scan() {
156
+ if (this.scanning || this.stopped) return;
157
+ this.scanning = true;
158
+ try {
159
+ const pages = (await listTargets(this.port)).filter(isMainWindow);
160
+ const live = new Set(pages.map(target => target.id));
161
+ for (const [id, session] of this.sessions) {
162
+ if (!live.has(id)) {
163
+ session.close();
164
+ this.sessions.delete(id);
165
+ }
166
+ }
167
+ const script = buildBootstrapScript();
168
+ await Promise.all(pages.map(async target => {
169
+ const existing = this.sessions.get(target.id);
170
+ const session = existing ?? new CdpSession(target, this.port);
171
+ try {
172
+ if (!existing) await this.connectSession(session);
173
+ await session.evaluate(script);
174
+ if (this.currentValue) await session.evaluate(updateExpression(this.currentValue));
175
+ await this.syncBackground(session);
176
+ if (this.stopped) {
177
+ session.close();
178
+ return;
179
+ }
180
+ this.sessions.set(target.id, session);
181
+ } catch {
182
+ session.close();
183
+ this.sessions.delete(target.id);
184
+ }
185
+ }));
186
+ } finally {
187
+ this.scanning = false;
188
+ }
189
+ }
190
+
191
+ clearSessions() {
192
+ for (const session of this.sessions.values()) session.close();
193
+ this.sessions.clear();
194
+ }
195
+
196
+ async eachSession(action) {
197
+ await Promise.all([...this.sessions].map(async ([id, session]) => {
198
+ try {
199
+ await action(session);
200
+ } catch {
201
+ session.close();
202
+ this.sessions.delete(id);
203
+ }
204
+ }));
205
+ }
206
+
207
+ async update(value) {
208
+ this.currentValue = value;
209
+ await this.eachSession(session => session.evaluate(updateExpression(value)));
210
+ }
211
+
212
+ async setBackground(background) {
213
+ this.background = background;
214
+ await this.eachSession(session => this.syncBackground(session));
215
+ }
216
+
217
+ async notifyBackground(message) {
218
+ await this.eachSession(session => session.evaluate(backgroundCall('notify', message)));
219
+ }
220
+
221
+ stop() {
222
+ this.stopped = true;
223
+ this.clearSessions();
224
+ }
225
+ }
226
+
227
+ module.exports = { CDP_PORT, CdpSession, RendererInjector, assertLoopback, buildBootstrapScript, isMainWindow, listTargets };