opensteer 0.9.0 → 0.9.1

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/dist/chunk-4LP7QP2O.js +4336 -0
  2. package/dist/chunk-4LP7QP2O.js.map +1 -0
  3. package/dist/{chunk-656MQUSM.js → chunk-6PGXWW3X.js} +4787 -9519
  4. package/dist/chunk-6PGXWW3X.js.map +1 -0
  5. package/dist/chunk-BMPUL66S.js +1170 -0
  6. package/dist/chunk-BMPUL66S.js.map +1 -0
  7. package/dist/{chunk-OIKLSFXA.js → chunk-L4FWHBQJ.js} +4 -3
  8. package/dist/chunk-L4FWHBQJ.js.map +1 -0
  9. package/dist/chunk-Z53HNZ7Z.js +1800 -0
  10. package/dist/chunk-Z53HNZ7Z.js.map +1 -0
  11. package/dist/cli/bin.cjs +3050 -281
  12. package/dist/cli/bin.cjs.map +1 -1
  13. package/dist/cli/bin.js +124 -7
  14. package/dist/cli/bin.js.map +1 -1
  15. package/dist/index.cjs +918 -263
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +1 -0
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.js +4 -2
  20. package/dist/local-view/public/assets/app.css +770 -0
  21. package/dist/local-view/public/assets/app.js +2053 -0
  22. package/dist/local-view/public/index.html +235 -0
  23. package/dist/local-view/serve-entry.cjs +7436 -0
  24. package/dist/local-view/serve-entry.cjs.map +1 -0
  25. package/dist/local-view/serve-entry.d.cts +1 -0
  26. package/dist/local-view/serve-entry.d.ts +1 -0
  27. package/dist/local-view/serve-entry.js +23 -0
  28. package/dist/local-view/serve-entry.js.map +1 -0
  29. package/dist/opensteer-KZCRP425.js +6 -0
  30. package/dist/{opensteer-LKX3233A.js.map → opensteer-KZCRP425.js.map} +1 -1
  31. package/dist/session-control-VGBFOH3Y.js +39 -0
  32. package/dist/session-control-VGBFOH3Y.js.map +1 -0
  33. package/package.json +8 -8
  34. package/skills/README.md +3 -0
  35. package/skills/opensteer/SKILL.md +229 -48
  36. package/dist/chunk-656MQUSM.js.map +0 -1
  37. package/dist/chunk-OIKLSFXA.js.map +0 -1
  38. package/dist/opensteer-LKX3233A.js +0 -4
@@ -0,0 +1,1800 @@
1
+ import { writeLocalViewServiceState, CURRENT_PROCESS_OWNER, OPENSTEER_LOCAL_VIEW_SERVICE_VERSION, OPENSTEER_LOCAL_VIEW_SERVICE_LAYOUT, clearLocalViewServiceState, readLocalViewSessionManifest, listLocalViewSessionManifests, deleteLocalViewSessionManifest, isProcessRunning, pathExists, readPersistedLocalBrowserSessionRecord, inspectCdpEndpoint } from './chunk-BMPUL66S.js';
2
+ import { randomBytes } from 'crypto';
3
+ import { createServer } from 'http';
4
+ import { readFile } from 'fs/promises';
5
+ import { existsSync } from 'fs';
6
+ import { once } from 'events';
7
+ import path2 from 'path';
8
+ import { fileURLToPath } from 'url';
9
+ import * as WebSocket2 from 'ws';
10
+ import WebSocket2__default from 'ws';
11
+
12
+ // src/local-view/resolve-browser-websocket.ts
13
+ async function resolveBrowserWebSocketUrl(record) {
14
+ if (record.engine === "playwright") {
15
+ if (!record.endpoint) {
16
+ throw new Error("Local Playwright session is missing a browser WebSocket endpoint.");
17
+ }
18
+ return record.endpoint;
19
+ }
20
+ if (!record.remoteDebuggingUrl) {
21
+ throw new Error("Local ABP session is missing a remote debugging URL.");
22
+ }
23
+ const inspected = await inspectCdpEndpoint({
24
+ endpoint: record.remoteDebuggingUrl,
25
+ timeoutMs: 5e3
26
+ });
27
+ return inspected.endpoint;
28
+ }
29
+
30
+ // src/local-view/discovery.ts
31
+ async function listResolvedLocalViewSessions() {
32
+ const manifests = await listLocalViewSessionManifests();
33
+ const resolved = await Promise.all(manifests.map((manifest) => resolveSessionSummary(manifest)));
34
+ return resolved.filter((session) => session !== void 0).sort(
35
+ (left, right) => right.startedAt - left.startedAt || left.label.localeCompare(right.label)
36
+ );
37
+ }
38
+ async function resolveLocalViewSession(sessionId) {
39
+ const manifest = await readLocalViewSessionManifest(sessionId);
40
+ if (!manifest) {
41
+ return void 0;
42
+ }
43
+ return readResolvedLocalViewSession(manifest);
44
+ }
45
+ async function resolveSessionSummary(manifest) {
46
+ const record = await readLiveRecord(manifest);
47
+ if (!record) {
48
+ await deleteLocalViewSessionManifest(manifest.sessionId);
49
+ return void 0;
50
+ }
51
+ const browserName = record.executablePath ? path2.basename(record.executablePath).replace(/\.[A-Za-z0-9]+$/u, "") : void 0;
52
+ return {
53
+ sessionId: manifest.sessionId,
54
+ label: manifest.workspace ?? (path2.basename(manifest.rootPath) || manifest.sessionId),
55
+ status: isProcessRunning(record.pid) ? "live" : "stale",
56
+ ...manifest.workspace === void 0 ? {} : { workspace: manifest.workspace },
57
+ rootPath: manifest.rootPath,
58
+ engine: record.engine,
59
+ ownership: manifest.ownership,
60
+ pid: record.pid,
61
+ startedAt: record.startedAt,
62
+ ...browserName === void 0 ? {} : { browserName }
63
+ };
64
+ }
65
+ async function readResolvedLocalViewSession(manifest) {
66
+ const record = await readLiveRecord(manifest);
67
+ if (!record) {
68
+ await deleteLocalViewSessionManifest(manifest.sessionId);
69
+ return void 0;
70
+ }
71
+ const browserWebSocketUrl = await resolveBrowserWebSocketUrl(record).catch(() => void 0);
72
+ if (!browserWebSocketUrl) {
73
+ return void 0;
74
+ }
75
+ return {
76
+ manifest,
77
+ record,
78
+ browserWebSocketUrl
79
+ };
80
+ }
81
+ async function readLiveRecord(manifest) {
82
+ if (!await pathExists(manifest.rootPath)) {
83
+ return void 0;
84
+ }
85
+ const record = await readPersistedLocalBrowserSessionRecord(manifest.rootPath);
86
+ if (!record) {
87
+ return void 0;
88
+ }
89
+ if (record.pid !== manifest.pid || record.startedAt !== manifest.startedAt || !isProcessRunning(record.pid)) {
90
+ return void 0;
91
+ }
92
+ return record;
93
+ }
94
+
95
+ // src/local-view/runtime-state.ts
96
+ var LocalViewRuntimeState = class {
97
+ activationIntentBySessionId = /* @__PURE__ */ new Map();
98
+ setPageActivationIntent(sessionId, targetId) {
99
+ this.activationIntentBySessionId.set(sessionId, {
100
+ targetId,
101
+ ts: Date.now()
102
+ });
103
+ }
104
+ getPageActivationIntent(sessionId) {
105
+ return this.activationIntentBySessionId.get(sessionId);
106
+ }
107
+ clearPageActivationIntent(sessionId, targetId) {
108
+ const current = this.activationIntentBySessionId.get(sessionId);
109
+ if (!current) {
110
+ return;
111
+ }
112
+ if (targetId !== void 0 && current.targetId !== targetId) {
113
+ return;
114
+ }
115
+ this.activationIntentBySessionId.delete(sessionId);
116
+ }
117
+ };
118
+ var LocalViewWebSocketServer = WebSocket2.WebSocketServer;
119
+
120
+ // src/local-view/cdp-proxy.ts
121
+ var DEFAULT_MAX_PENDING_CLIENT_BUFFER_BYTES = 1e6;
122
+ var DEFAULT_UPSTREAM_OPEN_TIMEOUT_MS = 1e4;
123
+ var LocalViewCdpProxy = class {
124
+ constructor(deps) {
125
+ this.deps = deps;
126
+ this.wss = new LocalViewWebSocketServer({ noServer: true });
127
+ this.createUpstreamSocket = deps.createUpstreamSocket ?? ((url) => new WebSocket2__default(url));
128
+ this.maxPendingClientBufferBytes = deps.maxPendingClientBufferBytes ?? DEFAULT_MAX_PENDING_CLIENT_BUFFER_BYTES;
129
+ this.upstreamOpenTimeoutMs = deps.upstreamOpenTimeoutMs ?? DEFAULT_UPSTREAM_OPEN_TIMEOUT_MS;
130
+ }
131
+ wss;
132
+ createUpstreamSocket;
133
+ maxPendingClientBufferBytes;
134
+ upstreamOpenTimeoutMs;
135
+ handleUpgrade(req, socket, head) {
136
+ const url = new URL(req.url || "/", "http://localhost");
137
+ const parts = url.pathname.split("/").filter(Boolean);
138
+ const isCdpPath = parts.length === 3 && parts[0] === "ws" && parts[1] === "cdp";
139
+ if (!isCdpPath) {
140
+ socket.destroy();
141
+ return;
142
+ }
143
+ const sessionId = parts[2];
144
+ this.wss.handleUpgrade(req, socket, head, (clientSocket) => {
145
+ void this.bindProxy(clientSocket, sessionId).catch(() => {
146
+ safeCloseSocket(clientSocket);
147
+ });
148
+ });
149
+ }
150
+ close() {
151
+ for (const client of this.wss.clients) {
152
+ safeCloseSocket(client);
153
+ }
154
+ this.wss.close();
155
+ }
156
+ async bindProxy(clientSocket, sessionId) {
157
+ const resolved = await resolveLocalViewSession(sessionId);
158
+ if (!resolved) {
159
+ safeCloseSocket(clientSocket);
160
+ return;
161
+ }
162
+ const upstream = this.createUpstreamSocket(resolved.browserWebSocketUrl);
163
+ const pendingCreateTargetCommandIds = /* @__PURE__ */ new Set();
164
+ const pendingAttachTargetCommandTargetIds = /* @__PURE__ */ new Map();
165
+ const targetIdByAttachedSessionId = /* @__PURE__ */ new Map();
166
+ const pendingClientMessages = [];
167
+ let pendingClientBufferBytes = 0;
168
+ let closed = false;
169
+ let upstreamOpenTimeout = null;
170
+ const closeConnection = () => {
171
+ if (closed) {
172
+ return;
173
+ }
174
+ closed = true;
175
+ if (upstreamOpenTimeout) {
176
+ clearTimeout(upstreamOpenTimeout);
177
+ upstreamOpenTimeout = null;
178
+ }
179
+ pendingClientMessages.length = 0;
180
+ pendingClientBufferBytes = 0;
181
+ safeCloseSocket(upstream);
182
+ safeCloseSocket(clientSocket);
183
+ };
184
+ upstreamOpenTimeout = setTimeout(() => {
185
+ if (upstream.readyState === WebSocket2__default.OPEN) {
186
+ return;
187
+ }
188
+ closeConnection();
189
+ }, this.upstreamOpenTimeoutMs);
190
+ clientSocket.on("message", (data, isBinary) => {
191
+ const outboundData = data;
192
+ if (!isBinary) {
193
+ const message = parseCdpProtocolMessage(data);
194
+ if (message) {
195
+ const activatedTargetId = readActivateTargetCommandTargetId(message);
196
+ if (activatedTargetId) {
197
+ this.deps.runtimeState.setPageActivationIntent(sessionId, activatedTargetId);
198
+ }
199
+ const createTargetCommandId = readCreateTargetCommandId(message);
200
+ if (createTargetCommandId !== null) {
201
+ pendingCreateTargetCommandIds.add(createTargetCommandId);
202
+ }
203
+ const attachTargetCommand = readAttachTargetCommand(message);
204
+ if (attachTargetCommand) {
205
+ pendingAttachTargetCommandTargetIds.set(
206
+ attachTargetCommand.id,
207
+ attachTargetCommand.targetId
208
+ );
209
+ }
210
+ const interactionTargetId = readInteractionTargetId(message, targetIdByAttachedSessionId);
211
+ if (interactionTargetId) {
212
+ this.deps.runtimeState.setPageActivationIntent(sessionId, interactionTargetId);
213
+ }
214
+ }
215
+ }
216
+ if (upstream.readyState === WebSocket2__default.OPEN) {
217
+ upstream.send(outboundData, { binary: isBinary });
218
+ return;
219
+ }
220
+ if (upstream.readyState !== WebSocket2__default.CONNECTING) {
221
+ closeConnection();
222
+ return;
223
+ }
224
+ const sizeBytes = rawDataSizeBytes(outboundData);
225
+ if (pendingClientBufferBytes + sizeBytes > this.maxPendingClientBufferBytes) {
226
+ closeConnection();
227
+ return;
228
+ }
229
+ pendingClientMessages.push({ data: outboundData, isBinary });
230
+ pendingClientBufferBytes += sizeBytes;
231
+ });
232
+ upstream.on("open", () => {
233
+ if (upstreamOpenTimeout) {
234
+ clearTimeout(upstreamOpenTimeout);
235
+ upstreamOpenTimeout = null;
236
+ }
237
+ for (const pendingMessage of pendingClientMessages.splice(0)) {
238
+ upstream.send(pendingMessage.data, { binary: pendingMessage.isBinary });
239
+ }
240
+ pendingClientBufferBytes = 0;
241
+ });
242
+ upstream.on("message", (data, isBinary) => {
243
+ if (!isBinary) {
244
+ const message = parseCdpProtocolMessage(data);
245
+ if (message) {
246
+ const createdTargetId = readCreateTargetResultTargetId(
247
+ message,
248
+ pendingCreateTargetCommandIds
249
+ );
250
+ if (createdTargetId) {
251
+ this.deps.runtimeState.setPageActivationIntent(sessionId, createdTargetId);
252
+ }
253
+ const attachedTarget = readAttachTargetResult(
254
+ message,
255
+ pendingAttachTargetCommandTargetIds
256
+ );
257
+ if (attachedTarget) {
258
+ targetIdByAttachedSessionId.set(attachedTarget.sessionId, attachedTarget.targetId);
259
+ }
260
+ const detachedSessionId = readDetachedTargetSessionId(message);
261
+ if (detachedSessionId) {
262
+ targetIdByAttachedSessionId.delete(detachedSessionId);
263
+ }
264
+ }
265
+ }
266
+ if (clientSocket.readyState === WebSocket2__default.OPEN) {
267
+ clientSocket.send(data, { binary: isBinary });
268
+ }
269
+ });
270
+ clientSocket.on("close", closeConnection);
271
+ clientSocket.on("error", closeConnection);
272
+ upstream.on("close", closeConnection);
273
+ upstream.on("error", closeConnection);
274
+ }
275
+ };
276
+ function parseCdpProtocolMessage(data) {
277
+ try {
278
+ const parsed = JSON.parse(rawDataToString(data));
279
+ return parsed && typeof parsed === "object" ? parsed : null;
280
+ } catch {
281
+ return null;
282
+ }
283
+ }
284
+ function readCreateTargetCommandId(message) {
285
+ return message.method === "Target.createTarget" && typeof message.id === "number" ? message.id : null;
286
+ }
287
+ function readCreateTargetResultTargetId(message, pendingCommandIds) {
288
+ if (typeof message.id !== "number" || !pendingCommandIds.has(message.id)) {
289
+ return null;
290
+ }
291
+ pendingCommandIds.delete(message.id);
292
+ const targetId = message.result?.targetId;
293
+ return typeof targetId === "string" && targetId.length > 0 ? targetId : null;
294
+ }
295
+ function readActivateTargetCommandTargetId(message) {
296
+ const targetId = message.method === "Target.activateTarget" ? message.params?.targetId : void 0;
297
+ return typeof targetId === "string" && targetId.length > 0 ? targetId : null;
298
+ }
299
+ function readAttachTargetCommand(message) {
300
+ if (message.method !== "Target.attachToTarget" || typeof message.id !== "number") {
301
+ return null;
302
+ }
303
+ const targetId = message.params?.targetId;
304
+ if (typeof targetId !== "string" || targetId.length === 0) {
305
+ return null;
306
+ }
307
+ return {
308
+ id: message.id,
309
+ targetId
310
+ };
311
+ }
312
+ function readAttachTargetResult(message, pendingTargetIds) {
313
+ if (typeof message.id !== "number") {
314
+ return null;
315
+ }
316
+ const targetId = pendingTargetIds.get(message.id);
317
+ if (!targetId) {
318
+ return null;
319
+ }
320
+ pendingTargetIds.delete(message.id);
321
+ const sessionId = message.result?.sessionId;
322
+ if (typeof sessionId !== "string" || sessionId.length === 0) {
323
+ return null;
324
+ }
325
+ return {
326
+ sessionId,
327
+ targetId
328
+ };
329
+ }
330
+ function readInteractionTargetId(message, targetIdByAttachedSessionId) {
331
+ const sessionId = message.sessionId;
332
+ if (typeof sessionId !== "string" || sessionId.length === 0) {
333
+ return null;
334
+ }
335
+ if (!message.method || !message.method.startsWith("Input.") && !message.method.startsWith("Page.")) {
336
+ return null;
337
+ }
338
+ return targetIdByAttachedSessionId.get(sessionId) ?? null;
339
+ }
340
+ function readDetachedTargetSessionId(message) {
341
+ if (message.method !== "Target.detachedFromTarget") {
342
+ return null;
343
+ }
344
+ const sessionId = message.params?.sessionId;
345
+ return typeof sessionId === "string" && sessionId.length > 0 ? sessionId : null;
346
+ }
347
+ function rawDataToString(data) {
348
+ if (typeof data === "string") {
349
+ return data;
350
+ }
351
+ if (data instanceof ArrayBuffer) {
352
+ return Buffer.from(data).toString("utf8");
353
+ }
354
+ if (Array.isArray(data)) {
355
+ return Buffer.concat(data).toString("utf8");
356
+ }
357
+ return data.toString("utf8");
358
+ }
359
+ function rawDataSizeBytes(data) {
360
+ if (typeof data === "string") {
361
+ return Buffer.byteLength(data);
362
+ }
363
+ if (data instanceof ArrayBuffer) {
364
+ return data.byteLength;
365
+ }
366
+ if (Array.isArray(data)) {
367
+ return data.reduce((total, entry) => total + entry.byteLength, 0);
368
+ }
369
+ return data.byteLength;
370
+ }
371
+ function safeCloseSocket(socket) {
372
+ try {
373
+ socket.close();
374
+ } catch {
375
+ }
376
+ }
377
+
378
+ // src/local-view/tab-state-tracker.ts
379
+ var ACTIVATION_INTENT_DISCOVERY_GRACE_MS = 2e3;
380
+ var TabStateTracker = class {
381
+ deps;
382
+ timer = null;
383
+ running = false;
384
+ lastActivePage = null;
385
+ lastTabsSignature = "";
386
+ tickInFlight = false;
387
+ metadataByPage = /* @__PURE__ */ new Map();
388
+ targetIdByPage = /* @__PURE__ */ new WeakMap();
389
+ pageCleanupByPage = /* @__PURE__ */ new Map();
390
+ boundContextCleanup = null;
391
+ constructor(deps) {
392
+ this.deps = deps;
393
+ }
394
+ start() {
395
+ if (this.running) {
396
+ return;
397
+ }
398
+ this.running = true;
399
+ this.bindContextEvents();
400
+ void this.reconcile({
401
+ includeFocus: true,
402
+ refreshMetadata: true
403
+ });
404
+ }
405
+ stop() {
406
+ this.running = false;
407
+ if (this.timer) {
408
+ clearInterval(this.timer);
409
+ this.timer = null;
410
+ }
411
+ this.boundContextCleanup?.();
412
+ this.boundContextCleanup = null;
413
+ for (const cleanup of this.pageCleanupByPage.values()) {
414
+ cleanup();
415
+ }
416
+ this.pageCleanupByPage.clear();
417
+ this.metadataByPage.clear();
418
+ }
419
+ bindContextEvents() {
420
+ if (this.boundContextCleanup) {
421
+ this.syncTrackedPages(this.deps.browserContext.pages());
422
+ this.updatePolling(this.deps.browserContext.pages().length);
423
+ return;
424
+ }
425
+ const onPage = (page) => {
426
+ this.syncTrackedPages(this.deps.browserContext.pages());
427
+ this.updatePolling(this.deps.browserContext.pages().length);
428
+ this.attachPageListeners(page);
429
+ void this.reconcile({
430
+ includeFocus: true,
431
+ refreshMetadata: true
432
+ });
433
+ };
434
+ this.deps.browserContext.on("page", onPage);
435
+ this.boundContextCleanup = () => {
436
+ this.deps.browserContext.off("page", onPage);
437
+ };
438
+ this.syncTrackedPages(this.deps.browserContext.pages());
439
+ this.updatePolling(this.deps.browserContext.pages().length);
440
+ }
441
+ syncTrackedPages(pages) {
442
+ const nextPages = new Set(pages);
443
+ for (const [page, cleanup] of this.pageCleanupByPage.entries()) {
444
+ if (nextPages.has(page)) {
445
+ continue;
446
+ }
447
+ cleanup();
448
+ this.pageCleanupByPage.delete(page);
449
+ this.metadataByPage.delete(page);
450
+ }
451
+ for (const page of pages) {
452
+ this.attachPageListeners(page);
453
+ }
454
+ }
455
+ attachPageListeners(page) {
456
+ if (this.pageCleanupByPage.has(page)) {
457
+ return;
458
+ }
459
+ const refreshMetadata = () => {
460
+ void this.reconcile({
461
+ includeFocus: false,
462
+ refreshMetadata: true
463
+ });
464
+ };
465
+ const handleClose = () => {
466
+ this.pageCleanupByPage.get(page)?.();
467
+ this.pageCleanupByPage.delete(page);
468
+ this.metadataByPage.delete(page);
469
+ void this.reconcile({
470
+ includeFocus: true,
471
+ refreshMetadata: true
472
+ });
473
+ };
474
+ page.on("close", handleClose);
475
+ page.on("domcontentloaded", refreshMetadata);
476
+ page.on("load", refreshMetadata);
477
+ page.on("framenavigated", refreshMetadata);
478
+ this.pageCleanupByPage.set(page, () => {
479
+ page.off("close", handleClose);
480
+ page.off("domcontentloaded", refreshMetadata);
481
+ page.off("load", refreshMetadata);
482
+ page.off("framenavigated", refreshMetadata);
483
+ });
484
+ }
485
+ updatePolling(pageCount) {
486
+ const shouldPoll = this.running && pageCount > 0;
487
+ if (!shouldPoll) {
488
+ if (this.timer) {
489
+ clearInterval(this.timer);
490
+ this.timer = null;
491
+ }
492
+ return;
493
+ }
494
+ if (this.timer) {
495
+ return;
496
+ }
497
+ this.timer = setInterval(() => {
498
+ const trackedPageCount = this.deps.browserContext.pages().length;
499
+ void this.reconcile({
500
+ includeFocus: trackedPageCount > 1,
501
+ refreshMetadata: true
502
+ });
503
+ }, this.deps.pollMs);
504
+ }
505
+ async reconcile(args) {
506
+ if (!this.running || this.tickInFlight) {
507
+ return;
508
+ }
509
+ this.tickInFlight = true;
510
+ try {
511
+ this.bindContextEvents();
512
+ const pages = this.deps.browserContext.pages();
513
+ this.syncTrackedPages(pages);
514
+ this.updatePolling(pages.length);
515
+ const preferredActivePage = this.lastActivePage ?? pages[0] ?? null;
516
+ const pageStates = await Promise.all(
517
+ pages.map(async (page, index) => {
518
+ const metadata = await this.readPageMetadata(page, {
519
+ refresh: args.refreshMetadata
520
+ });
521
+ const focusState = args.includeFocus ? await this.readFocusState(page) : {
522
+ isVisible: page === preferredActivePage,
523
+ hasFocus: page === preferredActivePage
524
+ };
525
+ return {
526
+ page,
527
+ index,
528
+ targetId: metadata.targetId,
529
+ url: page.url(),
530
+ title: metadata.title,
531
+ isVisible: focusState.isVisible,
532
+ hasFocus: focusState.hasFocus
533
+ };
534
+ })
535
+ );
536
+ const activePage = this.pickActivePage(
537
+ pageStates,
538
+ this.lastActivePage,
539
+ preferredActivePage,
540
+ this.resolveIntentPage(pageStates)
541
+ );
542
+ if (activePage && activePage !== this.lastActivePage) {
543
+ this.lastActivePage = activePage;
544
+ this.deps.onActivePageChanged(activePage);
545
+ }
546
+ const tabs = pageStates.map((state) => ({
547
+ index: state.index,
548
+ ...state.targetId === void 0 ? {} : { targetId: state.targetId },
549
+ url: state.url,
550
+ title: state.title,
551
+ active: activePage ? state.page === activePage : false
552
+ }));
553
+ const activeTabIndex = tabs.findIndex((tab) => tab.active);
554
+ const signature = JSON.stringify({
555
+ activeTabIndex,
556
+ tabs: tabs.map((tab) => ({
557
+ index: tab.index,
558
+ targetId: tab.targetId,
559
+ url: tab.url,
560
+ title: tab.title,
561
+ active: tab.active
562
+ }))
563
+ });
564
+ if (signature !== this.lastTabsSignature) {
565
+ this.lastTabsSignature = signature;
566
+ this.deps.onTabsChanged({
567
+ tabs,
568
+ activeTabIndex
569
+ });
570
+ }
571
+ } finally {
572
+ this.tickInFlight = false;
573
+ }
574
+ }
575
+ async readPageMetadata(page, options) {
576
+ const cached = this.metadataByPage.get(page);
577
+ if (cached && !options.refresh) {
578
+ return cached;
579
+ }
580
+ const [title, targetId] = await Promise.all([
581
+ page.title().catch(() => cached?.title ?? ""),
582
+ this.resolveTargetId(page).catch(() => cached?.targetId)
583
+ ]);
584
+ const nextMetadata = {
585
+ title,
586
+ targetId: targetId ?? void 0
587
+ };
588
+ this.metadataByPage.set(page, nextMetadata);
589
+ return nextMetadata;
590
+ }
591
+ async resolveTargetId(page) {
592
+ const cached = this.targetIdByPage.get(page);
593
+ if (cached) {
594
+ return cached;
595
+ }
596
+ const cdp = await page.context().newCDPSession(page);
597
+ try {
598
+ const result = await cdp.send("Target.getTargetInfo");
599
+ const targetId = result?.targetInfo?.targetId;
600
+ if (typeof targetId === "string" && targetId.length > 0) {
601
+ this.targetIdByPage.set(page, targetId);
602
+ return targetId;
603
+ }
604
+ return null;
605
+ } finally {
606
+ await cdp.detach().catch(() => void 0);
607
+ }
608
+ }
609
+ async readFocusState(page) {
610
+ try {
611
+ const result = await page.evaluate(() => ({
612
+ visibilityState: globalThis.document?.visibilityState,
613
+ hasFocus: globalThis.document?.hasFocus?.() ?? false
614
+ }));
615
+ return {
616
+ isVisible: result.visibilityState === "visible",
617
+ hasFocus: result.hasFocus === true
618
+ };
619
+ } catch {
620
+ return {
621
+ isVisible: false,
622
+ hasFocus: false
623
+ };
624
+ }
625
+ }
626
+ pickActivePage(pageStates, lastActivePage, fallbackPage, intent) {
627
+ if (intent) {
628
+ return intent.page;
629
+ }
630
+ const focusedVisiblePages = pageStates.filter((state) => state.isVisible && state.hasFocus);
631
+ if (focusedVisiblePages.length === 1) {
632
+ return focusedVisiblePages[0]?.page ?? null;
633
+ }
634
+ const visiblePages = pageStates.filter((state) => state.isVisible);
635
+ if (visiblePages.length === 1) {
636
+ return visiblePages[0]?.page ?? null;
637
+ }
638
+ const lastActivePageState = lastActivePage ? pageStates.find((state) => state.page === lastActivePage) ?? null : null;
639
+ if (lastActivePageState && (focusedVisiblePages.length > 1 && lastActivePageState.isVisible && lastActivePageState.hasFocus || visiblePages.length > 1 && lastActivePageState.isVisible || visiblePages.length === 0)) {
640
+ return lastActivePage;
641
+ }
642
+ const fallbackPageState = fallbackPage ? pageStates.find((state) => state.page === fallbackPage) ?? null : null;
643
+ if (fallbackPageState && (focusedVisiblePages.length > 1 && fallbackPageState.isVisible && fallbackPageState.hasFocus || visiblePages.length > 1 && fallbackPageState.isVisible)) {
644
+ return fallbackPage;
645
+ }
646
+ if (focusedVisiblePages.length > 0) {
647
+ return focusedVisiblePages[0]?.page ?? null;
648
+ }
649
+ if (visiblePages.length > 0) {
650
+ return visiblePages[0]?.page ?? null;
651
+ }
652
+ if (lastActivePageState) {
653
+ return lastActivePageState.page;
654
+ }
655
+ if (fallbackPageState) {
656
+ return fallbackPageState.page;
657
+ }
658
+ return pageStates[0]?.page ?? null;
659
+ }
660
+ resolveIntentPage(pageStates) {
661
+ const intent = this.deps.runtimeState.getPageActivationIntent(this.deps.sessionId);
662
+ if (!intent) {
663
+ return null;
664
+ }
665
+ const matched = pageStates.find((state) => state.targetId === intent.targetId);
666
+ if (!matched) {
667
+ if (Date.now() - intent.ts > ACTIVATION_INTENT_DISCOVERY_GRACE_MS) {
668
+ this.deps.runtimeState.clearPageActivationIntent(this.deps.sessionId, intent.targetId);
669
+ }
670
+ return null;
671
+ }
672
+ this.deps.runtimeState.clearPageActivationIntent(this.deps.sessionId, intent.targetId);
673
+ return { page: matched.page };
674
+ }
675
+ };
676
+
677
+ // src/local-view/view-stream-capture-policy.ts
678
+ var MIN_CAPTURE_DIMENSION_PX = 100;
679
+ var MAX_CAPTURE_DIMENSION_PX = 8192;
680
+ var CAPTURE_BUCKET_PX = 64;
681
+ function selectScreencastSize(args) {
682
+ const viewport = normalizeViewport(args.viewport);
683
+ if (!viewport) {
684
+ return null;
685
+ }
686
+ let maxRequestedWidth = 0;
687
+ let maxRequestedHeight = 0;
688
+ for (const requestedSize of args.requestedSizes) {
689
+ const normalized = normalizeRequestedSize(requestedSize);
690
+ if (!normalized) {
691
+ return null;
692
+ }
693
+ maxRequestedWidth = Math.max(maxRequestedWidth, normalized.width);
694
+ maxRequestedHeight = Math.max(maxRequestedHeight, normalized.height);
695
+ }
696
+ if (maxRequestedWidth < MIN_CAPTURE_DIMENSION_PX || maxRequestedHeight < MIN_CAPTURE_DIMENSION_PX) {
697
+ return null;
698
+ }
699
+ const desiredScale = Math.max(
700
+ maxRequestedWidth / viewport.width,
701
+ maxRequestedHeight / viewport.height
702
+ );
703
+ if (desiredScale >= 1) {
704
+ return viewport;
705
+ }
706
+ const landscape = viewport.width >= viewport.height;
707
+ const sourcePrimary = landscape ? viewport.width : viewport.height;
708
+ const sourceSecondary = landscape ? viewport.height : viewport.width;
709
+ const nextPrimary = bucketDimension(sourcePrimary * desiredScale);
710
+ const nextSecondary = clampDimension(Math.round(nextPrimary / sourcePrimary * sourceSecondary));
711
+ if (!nextSecondary) {
712
+ return null;
713
+ }
714
+ return landscape ? { width: nextPrimary, height: nextSecondary } : { width: nextSecondary, height: nextPrimary };
715
+ }
716
+ function normalizeViewport(viewport) {
717
+ const width = clampDimension(viewport.width);
718
+ const height = clampDimension(viewport.height);
719
+ return width && height ? { width, height } : null;
720
+ }
721
+ function normalizeRequestedSize(requestedSize) {
722
+ const width = clampDimension(requestedSize.width);
723
+ const height = clampDimension(requestedSize.height);
724
+ return width && height ? { width, height } : null;
725
+ }
726
+ function clampDimension(value) {
727
+ if (!Number.isFinite(value)) {
728
+ return null;
729
+ }
730
+ const normalized = Math.floor(value);
731
+ if (normalized < MIN_CAPTURE_DIMENSION_PX) {
732
+ return null;
733
+ }
734
+ return Math.min(MAX_CAPTURE_DIMENSION_PX, normalized);
735
+ }
736
+ function bucketDimension(value) {
737
+ const bucketed = Math.ceil(Math.max(MIN_CAPTURE_DIMENSION_PX, value) / CAPTURE_BUCKET_PX) * CAPTURE_BUCKET_PX;
738
+ return Math.min(MAX_CAPTURE_DIMENSION_PX, bucketed);
739
+ }
740
+ function buildHelloMessage(args) {
741
+ return {
742
+ type: "hello",
743
+ sessionId: args.sessionId,
744
+ ts: Date.now(),
745
+ mimeType: "image/jpeg",
746
+ fps: args.fps,
747
+ quality: args.quality,
748
+ viewport: args.viewport
749
+ };
750
+ }
751
+ function buildTabsMessage(args) {
752
+ return {
753
+ type: "tabs",
754
+ sessionId: args.sessionId,
755
+ ts: Date.now(),
756
+ tabs: args.tabs,
757
+ activeTabIndex: args.activeTabIndex
758
+ };
759
+ }
760
+ function buildStatusMessage(args) {
761
+ return {
762
+ type: "status",
763
+ sessionId: args.sessionId,
764
+ ts: Date.now(),
765
+ status: args.status
766
+ };
767
+ }
768
+ function buildErrorMessage(args) {
769
+ return {
770
+ type: "error",
771
+ sessionId: args.sessionId,
772
+ ts: Date.now(),
773
+ error: args.error
774
+ };
775
+ }
776
+ function sendControlMessage(ws, message) {
777
+ if (ws.readyState !== WebSocket2__default.OPEN) {
778
+ return;
779
+ }
780
+ try {
781
+ ws.send(JSON.stringify(message), { binary: false });
782
+ } catch {
783
+ }
784
+ }
785
+ function parseViewClientMessage(raw) {
786
+ try {
787
+ const parsed = JSON.parse(raw);
788
+ if (parsed?.type !== "stream-config") {
789
+ return null;
790
+ }
791
+ const renderWidth = normalizeRenderDimension(parsed.renderWidth);
792
+ const renderHeight = normalizeRenderDimension(parsed.renderHeight);
793
+ if (renderWidth === null || renderHeight === null) {
794
+ return null;
795
+ }
796
+ return {
797
+ type: "stream-config",
798
+ renderWidth,
799
+ renderHeight
800
+ };
801
+ } catch {
802
+ return null;
803
+ }
804
+ }
805
+ function normalizeRenderDimension(value) {
806
+ if (typeof value !== "number" || !Number.isFinite(value)) {
807
+ return null;
808
+ }
809
+ const normalized = Math.floor(value);
810
+ if (normalized < 100) {
811
+ return null;
812
+ }
813
+ return Math.min(8192, normalized);
814
+ }
815
+
816
+ // src/local-view/view-stream.ts
817
+ var INITIAL_FRAME_CAPTURE_ATTEMPTS = 3;
818
+ var INITIAL_FRAME_CAPTURE_RETRY_DELAY_MS = 150;
819
+ var TAB_STATE_POLL_MS = 1e3;
820
+ var CLIENT_FRAME_FLUSH_RETRY_MS = 16;
821
+ var LocalViewStreamHub = class {
822
+ deps;
823
+ producers = /* @__PURE__ */ new Map();
824
+ constructor(deps) {
825
+ this.deps = deps;
826
+ }
827
+ attachClient(sessionId, ws) {
828
+ let producer = this.producers.get(sessionId);
829
+ if (!producer) {
830
+ producer = new SessionViewStreamProducer({
831
+ sessionId,
832
+ runtimeState: this.deps.runtimeState,
833
+ maxFps: this.deps.maxFps,
834
+ quality: this.deps.quality,
835
+ maxClientBufferBytes: this.deps.maxClientBufferBytes,
836
+ onDrained: () => {
837
+ this.producers.delete(sessionId);
838
+ }
839
+ });
840
+ this.producers.set(sessionId, producer);
841
+ }
842
+ producer.addClient(ws);
843
+ }
844
+ };
845
+ var SessionViewStreamProducer = class {
846
+ deps;
847
+ clients = /* @__PURE__ */ new Set();
848
+ clientStateBySocket = /* @__PURE__ */ new Map();
849
+ frameIntervalMs;
850
+ tracker = null;
851
+ browser = null;
852
+ browserDisconnectedHandler = null;
853
+ context = null;
854
+ cdpSession = null;
855
+ screencastHandler = null;
856
+ pageLifecycleCleanup = null;
857
+ activePage = null;
858
+ activeViewport = null;
859
+ activeScreencastSizeKey = null;
860
+ pendingFrameAckTimer = null;
861
+ starting = null;
862
+ started = false;
863
+ rebinding = Promise.resolve();
864
+ stopped = false;
865
+ lastFrameSentAt = 0;
866
+ lastFrameBuffer = null;
867
+ lastTabsPayload = null;
868
+ constructor(deps) {
869
+ this.deps = deps;
870
+ this.frameIntervalMs = Math.max(1, Math.floor(1e3 / Math.max(1, deps.maxFps)));
871
+ }
872
+ addClient(ws) {
873
+ if (this.stopped) {
874
+ ws.close(1011, "View stream is unavailable.");
875
+ return;
876
+ }
877
+ this.clients.add(ws);
878
+ this.clientStateBySocket.set(ws, {
879
+ requestedRenderSize: null,
880
+ frameSendInFlight: false,
881
+ pendingFrameBuffer: null,
882
+ pendingFlushTimer: null
883
+ });
884
+ if (this.activeViewport) {
885
+ sendControlMessage(
886
+ ws,
887
+ buildHelloMessage({
888
+ sessionId: this.deps.sessionId,
889
+ fps: this.deps.maxFps,
890
+ quality: this.deps.quality,
891
+ viewport: this.activeViewport
892
+ })
893
+ );
894
+ }
895
+ if (this.lastTabsPayload) {
896
+ sendControlMessage(
897
+ ws,
898
+ buildTabsMessage({
899
+ sessionId: this.deps.sessionId,
900
+ tabs: this.lastTabsPayload.tabs,
901
+ activeTabIndex: this.lastTabsPayload.activeTabIndex
902
+ })
903
+ );
904
+ }
905
+ if (this.lastFrameBuffer) {
906
+ const queued = this.enqueueFrameForClient(ws, this.lastFrameBuffer);
907
+ if (!queued) {
908
+ this.removeClient(ws);
909
+ return;
910
+ }
911
+ }
912
+ ws.on("close", () => {
913
+ this.removeClient(ws);
914
+ });
915
+ ws.on("error", () => {
916
+ this.removeClient(ws);
917
+ });
918
+ ws.on("message", (raw, isBinary) => {
919
+ if (isBinary) {
920
+ return;
921
+ }
922
+ const message = parseViewClientMessage(readTextFrame(raw));
923
+ if (message?.type !== "stream-config") {
924
+ return;
925
+ }
926
+ const nextSize = {
927
+ width: message.renderWidth,
928
+ height: message.renderHeight
929
+ };
930
+ const clientState = this.clientStateBySocket.get(ws);
931
+ if (!clientState) {
932
+ return;
933
+ }
934
+ const priorSize = clientState.requestedRenderSize;
935
+ if (priorSize?.width === nextSize.width && priorSize?.height === nextSize.height) {
936
+ return;
937
+ }
938
+ clientState.requestedRenderSize = nextSize;
939
+ this.maybeRebindForStreamConfigChange();
940
+ });
941
+ void this.ensureStarted();
942
+ }
943
+ maybeRebindForStreamConfigChange() {
944
+ if (!this.activePage || !this.started || this.stopped) {
945
+ return;
946
+ }
947
+ const nextSizeKey = this.getRequestedScreencastSizeKey();
948
+ if (nextSizeKey === this.activeScreencastSizeKey) {
949
+ return;
950
+ }
951
+ void this.queueBindToPage(this.activePage, { force: true }).catch(() => void 0);
952
+ }
953
+ removeClient(ws) {
954
+ this.clients.delete(ws);
955
+ const clientState = this.clientStateBySocket.get(ws);
956
+ if (clientState?.pendingFlushTimer) {
957
+ clearTimeout(clientState.pendingFlushTimer);
958
+ }
959
+ this.clientStateBySocket.delete(ws);
960
+ if (this.clients.size === 0) {
961
+ void this.stop();
962
+ return;
963
+ }
964
+ this.maybeRebindForStreamConfigChange();
965
+ }
966
+ async ensureStarted() {
967
+ if (this.stopped || this.started) {
968
+ return;
969
+ }
970
+ if (this.starting) {
971
+ return this.starting;
972
+ }
973
+ this.starting = this.start().then(() => {
974
+ if (!this.stopped) {
975
+ this.started = true;
976
+ }
977
+ }).finally(() => {
978
+ this.starting = null;
979
+ });
980
+ try {
981
+ await this.starting;
982
+ } catch {
983
+ this.broadcastControl(
984
+ buildErrorMessage({
985
+ sessionId: this.deps.sessionId,
986
+ error: "Failed to start live browser stream."
987
+ })
988
+ );
989
+ this.closeAllClients(1011, "View stream failed");
990
+ await this.stop();
991
+ }
992
+ }
993
+ async start() {
994
+ const session = await this.connectSession();
995
+ this.broadcastControl(
996
+ buildStatusMessage({
997
+ sessionId: this.deps.sessionId,
998
+ status: "starting"
999
+ })
1000
+ );
1001
+ this.browser = session.browser;
1002
+ this.browserDisconnectedHandler = () => {
1003
+ if (this.stopped) {
1004
+ return;
1005
+ }
1006
+ this.browserDisconnectedHandler = null;
1007
+ this.broadcastControl(
1008
+ buildErrorMessage({
1009
+ sessionId: this.deps.sessionId,
1010
+ error: "Live browser stream disconnected."
1011
+ })
1012
+ );
1013
+ this.closeAllClients(1011, "View stream failed");
1014
+ void this.stop();
1015
+ };
1016
+ this.browser.once("disconnected", this.browserDisconnectedHandler);
1017
+ this.context = session.context;
1018
+ this.activePage = session.page;
1019
+ this.activeViewport = await readViewportForPage(session.page);
1020
+ if (this.stopped) {
1021
+ return;
1022
+ }
1023
+ if (this.activeViewport) {
1024
+ this.broadcastControl(
1025
+ buildHelloMessage({
1026
+ sessionId: this.deps.sessionId,
1027
+ fps: this.deps.maxFps,
1028
+ quality: this.deps.quality,
1029
+ viewport: this.activeViewport
1030
+ })
1031
+ );
1032
+ }
1033
+ this.tracker = new TabStateTracker({
1034
+ browserContext: session.context,
1035
+ sessionId: this.deps.sessionId,
1036
+ pollMs: TAB_STATE_POLL_MS,
1037
+ runtimeState: this.deps.runtimeState,
1038
+ onActivePageChanged: (page) => {
1039
+ this.activePage = page;
1040
+ void this.queueBindToPage(page).catch(() => void 0);
1041
+ },
1042
+ onTabsChanged: ({ tabs, activeTabIndex }) => {
1043
+ this.lastTabsPayload = { tabs, activeTabIndex };
1044
+ this.broadcastControl(
1045
+ buildTabsMessage({
1046
+ sessionId: this.deps.sessionId,
1047
+ tabs,
1048
+ activeTabIndex
1049
+ })
1050
+ );
1051
+ }
1052
+ });
1053
+ this.tracker.start();
1054
+ await this.queueBindToPage(session.page);
1055
+ if (this.stopped) {
1056
+ return;
1057
+ }
1058
+ this.broadcastControl(
1059
+ buildStatusMessage({
1060
+ sessionId: this.deps.sessionId,
1061
+ status: "live"
1062
+ })
1063
+ );
1064
+ }
1065
+ queueBindToPage(page, options = {}) {
1066
+ this.rebinding = this.rebinding.catch(() => void 0).then(() => this.bindToPage(page, options));
1067
+ return this.rebinding;
1068
+ }
1069
+ async bindToPage(page, options = {}) {
1070
+ if (this.stopped) {
1071
+ return;
1072
+ }
1073
+ const requestedSizeKey = this.getRequestedScreencastSizeKey();
1074
+ if (!options.force && this.activePage === page && this.cdpSession && this.activeScreencastSizeKey === requestedSizeKey) {
1075
+ return;
1076
+ }
1077
+ await this.stopScreencast();
1078
+ if (this.stopped) {
1079
+ return;
1080
+ }
1081
+ const context = this.context;
1082
+ if (!context) {
1083
+ throw new Error("Browser context is unavailable.");
1084
+ }
1085
+ const requestedSize = this.getRequestedScreencastSize();
1086
+ this.activePage = page;
1087
+ this.activeScreencastSizeKey = requestedSizeKey;
1088
+ this.activeViewport = await readViewportForPage(page);
1089
+ if (this.activeViewport) {
1090
+ this.broadcastControl(
1091
+ buildHelloMessage({
1092
+ sessionId: this.deps.sessionId,
1093
+ fps: this.deps.maxFps,
1094
+ quality: this.deps.quality,
1095
+ viewport: this.activeViewport
1096
+ })
1097
+ );
1098
+ }
1099
+ const cdpSession = await context.newCDPSession(page);
1100
+ if (this.stopped) {
1101
+ await cdpSession.detach().catch(() => void 0);
1102
+ return;
1103
+ }
1104
+ this.cdpSession = cdpSession;
1105
+ const onFrame = (event) => {
1106
+ void this.handleScreencastFrame(event);
1107
+ };
1108
+ this.screencastHandler = onFrame;
1109
+ cdpSession.on("Page.screencastFrame", onFrame);
1110
+ await cdpSession.send("Page.enable");
1111
+ await cdpSession.send("Page.startScreencast", {
1112
+ format: "jpeg",
1113
+ quality: this.deps.quality,
1114
+ everyNthFrame: 1,
1115
+ ...requestedSize ? {
1116
+ maxWidth: requestedSize.width,
1117
+ maxHeight: requestedSize.height
1118
+ } : {}
1119
+ });
1120
+ this.bindPageLifecycleFrameRefresh(page, cdpSession);
1121
+ void this.seedInitialFrame(cdpSession).catch(() => void 0);
1122
+ }
1123
+ async connectSession() {
1124
+ const resolved = await resolveLocalViewSession(this.deps.sessionId);
1125
+ if (!resolved) {
1126
+ throw new Error(`Local view session ${this.deps.sessionId} is unavailable.`);
1127
+ }
1128
+ const browser = await connectPlaywrightChromiumBrowser({
1129
+ url: resolved.browserWebSocketUrl
1130
+ });
1131
+ try {
1132
+ const context = browser.contexts()[0];
1133
+ if (!context) {
1134
+ throw new Error("Connected browser did not expose a Chromium browser context.");
1135
+ }
1136
+ const page = context.pages()[0] ?? await context.newPage();
1137
+ return {
1138
+ browser,
1139
+ context,
1140
+ page
1141
+ };
1142
+ } catch (error) {
1143
+ await disconnectPlaywrightChromiumBrowser(browser).catch(() => void 0);
1144
+ throw error;
1145
+ }
1146
+ }
1147
+ async handleScreencastFrame(event) {
1148
+ const cdpSession = this.cdpSession;
1149
+ if (!cdpSession || this.stopped) {
1150
+ return;
1151
+ }
1152
+ const frameBuffer = Buffer.from(event.data, "base64");
1153
+ this.lastFrameBuffer = frameBuffer;
1154
+ const now = Date.now();
1155
+ const delayMs = Math.max(0, this.frameIntervalMs - (now - this.lastFrameSentAt));
1156
+ if (delayMs === 0) {
1157
+ this.flushScreencastFrame({
1158
+ cdpSession,
1159
+ sessionId: event.sessionId,
1160
+ frameBuffer
1161
+ });
1162
+ return;
1163
+ }
1164
+ if (this.pendingFrameAckTimer !== null) {
1165
+ return;
1166
+ }
1167
+ this.pendingFrameAckTimer = setTimeout(() => {
1168
+ this.pendingFrameAckTimer = null;
1169
+ if (this.stopped || this.cdpSession !== cdpSession) {
1170
+ return;
1171
+ }
1172
+ this.flushScreencastFrame({
1173
+ cdpSession,
1174
+ sessionId: event.sessionId,
1175
+ frameBuffer
1176
+ });
1177
+ }, delayMs);
1178
+ }
1179
+ flushScreencastFrame(args) {
1180
+ this.lastFrameSentAt = Date.now();
1181
+ this.broadcastFrame(args.frameBuffer);
1182
+ void args.cdpSession.send("Page.screencastFrameAck", { sessionId: args.sessionId }).catch(() => void 0);
1183
+ }
1184
+ broadcastFrame(frameBuffer) {
1185
+ for (const client of this.clients) {
1186
+ if (!this.enqueueFrameForClient(client, frameBuffer)) {
1187
+ this.removeClient(client);
1188
+ }
1189
+ }
1190
+ if (this.clients.size === 0) {
1191
+ void this.stop();
1192
+ }
1193
+ }
1194
+ enqueueFrameForClient(client, frameBuffer) {
1195
+ if (client.readyState !== WebSocket2__default.OPEN) {
1196
+ return false;
1197
+ }
1198
+ const clientState = this.clientStateBySocket.get(client);
1199
+ if (!clientState) {
1200
+ return false;
1201
+ }
1202
+ clientState.pendingFrameBuffer = frameBuffer;
1203
+ this.flushQueuedFrameToClient(client);
1204
+ return true;
1205
+ }
1206
+ flushQueuedFrameToClient(client) {
1207
+ if (client.readyState !== WebSocket2__default.OPEN) {
1208
+ this.removeClient(client);
1209
+ return;
1210
+ }
1211
+ const clientState = this.clientStateBySocket.get(client);
1212
+ if (!clientState || clientState.frameSendInFlight || !clientState.pendingFrameBuffer) {
1213
+ return;
1214
+ }
1215
+ if (clientState.pendingFlushTimer) {
1216
+ clearTimeout(clientState.pendingFlushTimer);
1217
+ clientState.pendingFlushTimer = null;
1218
+ }
1219
+ if (client.bufferedAmount > this.deps.maxClientBufferBytes) {
1220
+ clientState.pendingFlushTimer = setTimeout(() => {
1221
+ clientState.pendingFlushTimer = null;
1222
+ this.flushQueuedFrameToClient(client);
1223
+ }, CLIENT_FRAME_FLUSH_RETRY_MS);
1224
+ return;
1225
+ }
1226
+ const frameBuffer = clientState.pendingFrameBuffer;
1227
+ clientState.pendingFrameBuffer = null;
1228
+ clientState.frameSendInFlight = true;
1229
+ try {
1230
+ client.send(frameBuffer, { binary: true }, (error) => {
1231
+ const latestClientState = this.clientStateBySocket.get(client);
1232
+ if (latestClientState) {
1233
+ latestClientState.frameSendInFlight = false;
1234
+ }
1235
+ if (error) {
1236
+ this.removeClient(client);
1237
+ return;
1238
+ }
1239
+ this.flushQueuedFrameToClient(client);
1240
+ });
1241
+ } catch {
1242
+ clientState.frameSendInFlight = false;
1243
+ this.removeClient(client);
1244
+ }
1245
+ }
1246
+ broadcastControl(message) {
1247
+ for (const client of this.clients) {
1248
+ sendControlMessage(client, message);
1249
+ }
1250
+ }
1251
+ closeAllClients(code, reason) {
1252
+ for (const client of this.clients) {
1253
+ try {
1254
+ client.close(code, reason);
1255
+ } catch {
1256
+ }
1257
+ }
1258
+ this.clients.clear();
1259
+ for (const clientState of this.clientStateBySocket.values()) {
1260
+ if (clientState.pendingFlushTimer) {
1261
+ clearTimeout(clientState.pendingFlushTimer);
1262
+ }
1263
+ }
1264
+ this.clientStateBySocket.clear();
1265
+ }
1266
+ async stop() {
1267
+ if (this.stopped) {
1268
+ return;
1269
+ }
1270
+ this.stopped = true;
1271
+ this.started = false;
1272
+ if (this.tracker) {
1273
+ this.tracker.stop();
1274
+ this.tracker = null;
1275
+ }
1276
+ await this.rebinding.catch(() => void 0);
1277
+ await this.stopScreencast();
1278
+ const browser = this.browser;
1279
+ const browserDisconnectedHandler = this.browserDisconnectedHandler;
1280
+ this.browser = null;
1281
+ this.browserDisconnectedHandler = null;
1282
+ this.context = null;
1283
+ this.activePage = null;
1284
+ if (browser) {
1285
+ if (browserDisconnectedHandler) {
1286
+ browser.off("disconnected", browserDisconnectedHandler);
1287
+ }
1288
+ await disconnectPlaywrightChromiumBrowser(browser).catch(() => void 0);
1289
+ }
1290
+ this.deps.onDrained();
1291
+ }
1292
+ async stopScreencast() {
1293
+ const cdpSession = this.cdpSession;
1294
+ const handler = this.screencastHandler;
1295
+ const pageLifecycleCleanup = this.pageLifecycleCleanup;
1296
+ this.cdpSession = null;
1297
+ this.screencastHandler = null;
1298
+ this.pageLifecycleCleanup = null;
1299
+ this.activeScreencastSizeKey = null;
1300
+ if (this.pendingFrameAckTimer !== null) {
1301
+ clearTimeout(this.pendingFrameAckTimer);
1302
+ this.pendingFrameAckTimer = null;
1303
+ }
1304
+ pageLifecycleCleanup?.();
1305
+ if (!cdpSession) {
1306
+ return;
1307
+ }
1308
+ if (handler) {
1309
+ cdpSession.off("Page.screencastFrame", handler);
1310
+ }
1311
+ await cdpSession.send("Page.stopScreencast").catch(() => void 0);
1312
+ await cdpSession.detach().catch(() => void 0);
1313
+ }
1314
+ bindPageLifecycleFrameRefresh(page, cdpSession) {
1315
+ this.pageLifecycleCleanup?.();
1316
+ const refresh = () => {
1317
+ void this.refreshPageFrame(page, cdpSession).catch(() => void 0);
1318
+ };
1319
+ page.on("domcontentloaded", refresh);
1320
+ page.on("load", refresh);
1321
+ page.on("framenavigated", refresh);
1322
+ this.pageLifecycleCleanup = () => {
1323
+ page.off("domcontentloaded", refresh);
1324
+ page.off("load", refresh);
1325
+ page.off("framenavigated", refresh);
1326
+ };
1327
+ }
1328
+ async refreshPageFrame(page, cdpSession) {
1329
+ if (this.stopped || this.cdpSession !== cdpSession || this.activePage !== page) {
1330
+ return;
1331
+ }
1332
+ const viewport = await readViewportForPage(page);
1333
+ if (viewport && this.cdpSession === cdpSession && this.activePage === page) {
1334
+ this.activeViewport = viewport;
1335
+ this.broadcastControl(
1336
+ buildHelloMessage({
1337
+ sessionId: this.deps.sessionId,
1338
+ fps: this.deps.maxFps,
1339
+ quality: this.deps.quality,
1340
+ viewport
1341
+ })
1342
+ );
1343
+ }
1344
+ if (this.stopped || this.cdpSession !== cdpSession || this.activePage !== page) {
1345
+ return;
1346
+ }
1347
+ await this.seedInitialFrame(cdpSession);
1348
+ }
1349
+ async seedInitialFrame(cdpSession) {
1350
+ let lastError = null;
1351
+ for (let attempt = 1; attempt <= INITIAL_FRAME_CAPTURE_ATTEMPTS; attempt += 1) {
1352
+ if (this.stopped || this.cdpSession !== cdpSession) {
1353
+ return;
1354
+ }
1355
+ try {
1356
+ const screenshotData = await this.captureCurrentFrame(cdpSession);
1357
+ if (this.stopped || this.cdpSession !== cdpSession) {
1358
+ return;
1359
+ }
1360
+ const frameBuffer = Buffer.from(screenshotData, "base64");
1361
+ this.lastFrameBuffer = frameBuffer;
1362
+ this.lastFrameSentAt = Date.now();
1363
+ this.broadcastFrame(frameBuffer);
1364
+ return;
1365
+ } catch (error) {
1366
+ lastError = error;
1367
+ }
1368
+ if (attempt < INITIAL_FRAME_CAPTURE_ATTEMPTS) {
1369
+ await new Promise((resolve) => setTimeout(resolve, INITIAL_FRAME_CAPTURE_RETRY_DELAY_MS));
1370
+ }
1371
+ }
1372
+ if (lastError instanceof Error) {
1373
+ throw lastError;
1374
+ }
1375
+ throw new Error("Failed to capture initial stream screenshot.");
1376
+ }
1377
+ async captureCurrentFrame(cdpSession) {
1378
+ const primaryParams = {
1379
+ format: "jpeg",
1380
+ quality: this.deps.quality,
1381
+ optimizeForSpeed: true
1382
+ };
1383
+ try {
1384
+ const result = await cdpSession.send("Page.captureScreenshot", primaryParams);
1385
+ if (result && typeof result.data === "string" && result.data.length > 0) {
1386
+ return result.data;
1387
+ }
1388
+ } catch {
1389
+ }
1390
+ const fallbackResult = await cdpSession.send("Page.captureScreenshot", {
1391
+ format: "jpeg",
1392
+ quality: this.deps.quality
1393
+ });
1394
+ if (!fallbackResult || typeof fallbackResult.data !== "string" || fallbackResult.data.length === 0) {
1395
+ throw new Error("Failed to capture initial stream screenshot.");
1396
+ }
1397
+ return fallbackResult.data;
1398
+ }
1399
+ getRequestedScreencastSize() {
1400
+ if (this.clients.size === 0 || !this.activeViewport) {
1401
+ return null;
1402
+ }
1403
+ const requestedSizes = [];
1404
+ for (const client of this.clients) {
1405
+ const requestedSize = this.clientStateBySocket.get(client)?.requestedRenderSize ?? null;
1406
+ if (!requestedSize) {
1407
+ return null;
1408
+ }
1409
+ requestedSizes.push(requestedSize);
1410
+ }
1411
+ return selectScreencastSize({
1412
+ viewport: this.activeViewport,
1413
+ requestedSizes
1414
+ });
1415
+ }
1416
+ getRequestedScreencastSizeKey() {
1417
+ const size = this.getRequestedScreencastSize();
1418
+ return size ? `${size.width}x${size.height}` : null;
1419
+ }
1420
+ };
1421
+ async function readViewportForPage(page) {
1422
+ const cdp = await page.context().newCDPSession(page);
1423
+ try {
1424
+ const result = await cdp.send("Page.getLayoutMetrics");
1425
+ const candidates = [
1426
+ result?.cssVisualViewport,
1427
+ result?.cssLayoutViewport,
1428
+ result?.visualViewport,
1429
+ result?.layoutViewport
1430
+ ];
1431
+ for (const candidate of candidates) {
1432
+ const width = normalizeViewportDimension(candidate?.clientWidth);
1433
+ const height = normalizeViewportDimension(candidate?.clientHeight);
1434
+ if (width !== null && height !== null) {
1435
+ return { width, height };
1436
+ }
1437
+ }
1438
+ return null;
1439
+ } catch {
1440
+ const viewportSize = page.viewportSize();
1441
+ if (!viewportSize) {
1442
+ return null;
1443
+ }
1444
+ const width = normalizeViewportDimension(viewportSize.width);
1445
+ const height = normalizeViewportDimension(viewportSize.height);
1446
+ return width !== null && height !== null ? { width, height } : null;
1447
+ } finally {
1448
+ await cdp.detach().catch(() => void 0);
1449
+ }
1450
+ }
1451
+ function normalizeViewportDimension(value) {
1452
+ if (typeof value !== "number" || !Number.isFinite(value)) {
1453
+ return null;
1454
+ }
1455
+ const normalized = Math.floor(value);
1456
+ if (normalized < 100) {
1457
+ return null;
1458
+ }
1459
+ return Math.min(8192, normalized);
1460
+ }
1461
+ function readTextFrame(raw) {
1462
+ if (typeof raw === "string") {
1463
+ return raw;
1464
+ }
1465
+ if (raw instanceof ArrayBuffer) {
1466
+ return Buffer.from(raw).toString("utf8");
1467
+ }
1468
+ if (Array.isArray(raw)) {
1469
+ return Buffer.concat(raw).toString("utf8");
1470
+ }
1471
+ return raw.toString("utf8");
1472
+ }
1473
+ async function connectPlaywrightChromiumBrowser(input) {
1474
+ const { connectPlaywrightChromiumBrowser: connect } = await import('@opensteer/engine-playwright');
1475
+ return connect(input);
1476
+ }
1477
+ async function disconnectPlaywrightChromiumBrowser(browser) {
1478
+ const { disconnectPlaywrightChromiumBrowser: disconnect } = await import('@opensteer/engine-playwright');
1479
+ await disconnect(browser);
1480
+ }
1481
+
1482
+ // src/local-view/server.ts
1483
+ var DEFAULT_MAX_FPS = 12;
1484
+ var DEFAULT_QUALITY = 75;
1485
+ var DEFAULT_MAX_CLIENT_BUFFER_BYTES = 512 * 1024;
1486
+ var LOCAL_VIEW_ACCESS_EXPIRES_AT = Number.MAX_SAFE_INTEGER;
1487
+ async function startLocalViewServer(input = {}) {
1488
+ const token = input.token ?? randomBytes(24).toString("hex");
1489
+ const runtimeState = new LocalViewRuntimeState();
1490
+ const viewStreamHub = new LocalViewStreamHub({
1491
+ runtimeState,
1492
+ maxFps: DEFAULT_MAX_FPS,
1493
+ quality: DEFAULT_QUALITY,
1494
+ maxClientBufferBytes: DEFAULT_MAX_CLIENT_BUFFER_BYTES
1495
+ });
1496
+ const cdpProxy = new LocalViewCdpProxy({
1497
+ runtimeState
1498
+ });
1499
+ const httpServer = createServer((request, response) => {
1500
+ void handleHttpRequest({ request, response, token, shutdown: closeServer }).catch(() => {
1501
+ if (!response.headersSent && !response.writableEnded) {
1502
+ writeJson(response, 500, { error: "Internal server error." });
1503
+ return;
1504
+ }
1505
+ response.destroy();
1506
+ });
1507
+ });
1508
+ const viewWss = new LocalViewWebSocketServer({ noServer: true });
1509
+ viewWss.on("connection", (ws, request) => {
1510
+ const url2 = new URL(request.url ?? "/", "http://localhost");
1511
+ const parts = url2.pathname.split("/").filter(Boolean);
1512
+ const sessionId = parts[2];
1513
+ if (!sessionId) {
1514
+ ws.close(1008, "Session id is required.");
1515
+ return;
1516
+ }
1517
+ viewStreamHub.attachClient(sessionId, ws);
1518
+ });
1519
+ httpServer.on("upgrade", (request, socket, head) => {
1520
+ const url2 = new URL(request.url ?? "/", "http://localhost");
1521
+ const tokenParam = url2.searchParams.get("token");
1522
+ if (tokenParam !== token || !isAllowedOrigin(request.headers.origin)) {
1523
+ socket.destroy();
1524
+ return;
1525
+ }
1526
+ const parts = url2.pathname.split("/").filter(Boolean);
1527
+ if (parts[0] !== "ws" || parts.length !== 3) {
1528
+ socket.destroy();
1529
+ return;
1530
+ }
1531
+ if (parts[1] === "view") {
1532
+ viewWss.handleUpgrade(request, socket, head, (ws) => {
1533
+ viewWss.emit("connection", ws, request);
1534
+ });
1535
+ return;
1536
+ }
1537
+ if (parts[1] === "cdp") {
1538
+ cdpProxy.handleUpgrade(request, socket, head);
1539
+ return;
1540
+ }
1541
+ socket.destroy();
1542
+ });
1543
+ let closePromise;
1544
+ async function closeServer() {
1545
+ closePromise ??= (async () => {
1546
+ viewWss.clients.forEach((client) => {
1547
+ try {
1548
+ client.close();
1549
+ } catch {
1550
+ }
1551
+ });
1552
+ viewWss.close();
1553
+ cdpProxy.close();
1554
+ httpServer.close();
1555
+ await once(httpServer, "close");
1556
+ await clearLocalViewServiceState({ pid: process.pid, token });
1557
+ await input.onClosed?.();
1558
+ })();
1559
+ await closePromise;
1560
+ }
1561
+ httpServer.listen(input.port ?? 0, "127.0.0.1");
1562
+ await once(httpServer, "listening");
1563
+ const address = httpServer.address();
1564
+ if (!address || typeof address === "string") {
1565
+ throw new Error("Failed to resolve the local view server address.");
1566
+ }
1567
+ const url = `http://127.0.0.1:${String(address.port)}`;
1568
+ await writeLocalViewServiceState({
1569
+ layout: OPENSTEER_LOCAL_VIEW_SERVICE_LAYOUT,
1570
+ version: OPENSTEER_LOCAL_VIEW_SERVICE_VERSION,
1571
+ pid: process.pid,
1572
+ processStartedAtMs: CURRENT_PROCESS_OWNER.processStartedAtMs,
1573
+ startedAt: Date.now(),
1574
+ port: address.port,
1575
+ token,
1576
+ url
1577
+ });
1578
+ return {
1579
+ url,
1580
+ token,
1581
+ close: closeServer
1582
+ };
1583
+ }
1584
+ async function handleHttpRequest(args) {
1585
+ const url = new URL(args.request.url ?? "/", "http://localhost");
1586
+ if (url.pathname === "/api/health") {
1587
+ if (!isAuthorizedApiRequest(args.request, args.token)) {
1588
+ writeJson(args.response, 401, { error: "Unauthorized." });
1589
+ return;
1590
+ }
1591
+ writeJson(args.response, 200, { ok: true });
1592
+ return;
1593
+ }
1594
+ if (url.pathname === "/api/sessions") {
1595
+ if (!isAuthorizedApiRequest(args.request, args.token)) {
1596
+ writeJson(args.response, 401, { error: "Unauthorized." });
1597
+ return;
1598
+ }
1599
+ const sessions = await listResolvedLocalViewSessions();
1600
+ const payload = { sessions };
1601
+ writeJson(args.response, 200, payload);
1602
+ return;
1603
+ }
1604
+ if (url.pathname === "/api/service/stop") {
1605
+ if (!isAuthorizedApiRequest(args.request, args.token)) {
1606
+ writeJson(args.response, 401, { error: "Unauthorized." });
1607
+ return;
1608
+ }
1609
+ if (args.request.method !== "POST") {
1610
+ writeJson(args.response, 405, { error: "Method not allowed." });
1611
+ return;
1612
+ }
1613
+ args.response.once("finish", () => {
1614
+ void args.shutdown();
1615
+ });
1616
+ writeJson(args.response, 200, { stopped: true });
1617
+ return;
1618
+ }
1619
+ const accessMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/access$/u);
1620
+ if (accessMatch) {
1621
+ if (!isAuthorizedApiRequest(args.request, args.token)) {
1622
+ writeJson(args.response, 401, { error: "Unauthorized." });
1623
+ return;
1624
+ }
1625
+ const sessionId = decodeURIComponent(accessMatch[1]);
1626
+ if (!await resolveLocalViewSession(sessionId)) {
1627
+ writeJson(args.response, 404, { error: "Session not found." });
1628
+ return;
1629
+ }
1630
+ const payload = {
1631
+ sessionId,
1632
+ expiresAt: LOCAL_VIEW_ACCESS_EXPIRES_AT,
1633
+ grants: {
1634
+ view: {
1635
+ kind: "view",
1636
+ transport: "ws",
1637
+ url: `${resolveWsBaseUrl(args.request)}/ws/view/${encodeURIComponent(sessionId)}`,
1638
+ token: args.token,
1639
+ expiresAt: LOCAL_VIEW_ACCESS_EXPIRES_AT
1640
+ },
1641
+ cdp: {
1642
+ kind: "cdp",
1643
+ transport: "ws",
1644
+ url: `${resolveWsBaseUrl(args.request)}/ws/cdp/${encodeURIComponent(sessionId)}`,
1645
+ token: args.token,
1646
+ expiresAt: LOCAL_VIEW_ACCESS_EXPIRES_AT
1647
+ }
1648
+ }
1649
+ };
1650
+ writeJson(args.response, 200, payload);
1651
+ return;
1652
+ }
1653
+ const closeMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/close$/u);
1654
+ if (closeMatch) {
1655
+ if (!isAuthorizedApiRequest(args.request, args.token)) {
1656
+ writeJson(args.response, 401, { error: "Unauthorized." });
1657
+ return;
1658
+ }
1659
+ if (args.request.method !== "POST") {
1660
+ writeJson(args.response, 405, { error: "Method not allowed." });
1661
+ return;
1662
+ }
1663
+ const sessionId = decodeURIComponent(closeMatch[1]);
1664
+ const { closeLocalViewSessionBrowser, LocalViewSessionCloseError } = await import('./session-control-VGBFOH3Y.js');
1665
+ try {
1666
+ await closeLocalViewSessionBrowser(sessionId);
1667
+ } catch (error) {
1668
+ if (error instanceof LocalViewSessionCloseError) {
1669
+ writeJson(args.response, error.statusCode, { error: error.message });
1670
+ return;
1671
+ }
1672
+ throw error;
1673
+ }
1674
+ const payload = {
1675
+ sessionId,
1676
+ closed: true
1677
+ };
1678
+ writeJson(args.response, 200, payload);
1679
+ return;
1680
+ }
1681
+ if (url.pathname === "/favicon.ico") {
1682
+ args.response.statusCode = 204;
1683
+ args.response.end();
1684
+ return;
1685
+ }
1686
+ if (url.pathname === "/" || url.pathname.startsWith("/assets/") || url.pathname.startsWith("/images/")) {
1687
+ await serveStaticAsset(args.response, url.pathname, args.token);
1688
+ return;
1689
+ }
1690
+ args.response.statusCode = 404;
1691
+ args.response.end("not found");
1692
+ }
1693
+ async function serveStaticAsset(response, pathname, token) {
1694
+ const publicDir = resolveLocalViewPublicDir();
1695
+ const relativePath = pathname === "/" ? "index.html" : pathname.slice(1);
1696
+ const assetPath = path2.resolve(publicDir, relativePath);
1697
+ const relativeAssetPath = path2.relative(publicDir, assetPath);
1698
+ if (relativeAssetPath.startsWith("..") || path2.isAbsolute(relativeAssetPath) || !existsSync(assetPath)) {
1699
+ response.statusCode = 404;
1700
+ response.end("not found");
1701
+ return;
1702
+ }
1703
+ if (relativePath === "index.html") {
1704
+ const html = await readFile(assetPath, "utf8");
1705
+ response.setHeader("content-type", "text/html; charset=utf-8");
1706
+ response.setHeader("cache-control", "no-store");
1707
+ response.end(
1708
+ html.replace(
1709
+ "__OPENSTEER_LOCAL_BOOTSTRAP_JSON__",
1710
+ JSON.stringify({
1711
+ apiBasePath: "/api",
1712
+ token
1713
+ })
1714
+ )
1715
+ );
1716
+ return;
1717
+ }
1718
+ response.setHeader("content-type", guessContentType(assetPath));
1719
+ response.setHeader("cache-control", "no-store");
1720
+ response.end(await readFile(assetPath));
1721
+ }
1722
+ function resolveLocalViewPublicDir() {
1723
+ const moduleDir = path2.dirname(fileURLToPath(import.meta.url));
1724
+ const candidates = [
1725
+ path2.resolve(moduleDir, "local-view", "public"),
1726
+ path2.resolve(moduleDir, "public"),
1727
+ path2.resolve(moduleDir, "..", "local-view", "public")
1728
+ ];
1729
+ for (const candidate of candidates) {
1730
+ if (existsSync(candidate)) {
1731
+ return candidate;
1732
+ }
1733
+ }
1734
+ throw new Error(`Could not resolve local view public assets from ${moduleDir}.`);
1735
+ }
1736
+ function isAuthorizedApiRequest(request, token) {
1737
+ return request.headers["x-opensteer-local-token"] === token && isAllowedOrigin(request.headers.origin);
1738
+ }
1739
+ function isAllowedOrigin(origin) {
1740
+ if (origin === void 0) {
1741
+ return true;
1742
+ }
1743
+ try {
1744
+ const url = new URL(origin);
1745
+ const host = url.hostname;
1746
+ return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
1747
+ } catch {
1748
+ return false;
1749
+ }
1750
+ }
1751
+ function resolveWsBaseUrl(request) {
1752
+ const host = request.headers.host ?? "127.0.0.1";
1753
+ return `ws://${host}`;
1754
+ }
1755
+ function writeJson(response, statusCode, value) {
1756
+ response.statusCode = statusCode;
1757
+ response.setHeader("content-type", "application/json; charset=utf-8");
1758
+ response.end(JSON.stringify(value));
1759
+ }
1760
+ function guessContentType(assetPath) {
1761
+ if (assetPath.endsWith(".css")) {
1762
+ return "text/css; charset=utf-8";
1763
+ }
1764
+ if (assetPath.endsWith(".js")) {
1765
+ return "application/javascript; charset=utf-8";
1766
+ }
1767
+ if (assetPath.endsWith(".svg")) {
1768
+ return "image/svg+xml";
1769
+ }
1770
+ if (assetPath.endsWith(".json")) {
1771
+ return "application/json; charset=utf-8";
1772
+ }
1773
+ if (assetPath.endsWith(".png")) {
1774
+ return "image/png";
1775
+ }
1776
+ if (assetPath.endsWith(".ico")) {
1777
+ return "image/x-icon";
1778
+ }
1779
+ return "application/octet-stream";
1780
+ }
1781
+
1782
+ // src/local-view/serve.ts
1783
+ async function runLocalViewService() {
1784
+ const server = await startLocalViewServer({
1785
+ token: process.env.OPENSTEER_LOCAL_VIEW_BOOT_TOKEN ?? randomBytes(24).toString("hex"),
1786
+ onClosed: () => {
1787
+ process.exit(0);
1788
+ }
1789
+ });
1790
+ const handleShutdownSignal = () => {
1791
+ void server.close();
1792
+ };
1793
+ process.once("SIGINT", handleShutdownSignal);
1794
+ process.once("SIGTERM", handleShutdownSignal);
1795
+ await new Promise(() => void 0);
1796
+ }
1797
+
1798
+ export { runLocalViewService };
1799
+ //# sourceMappingURL=chunk-Z53HNZ7Z.js.map
1800
+ //# sourceMappingURL=chunk-Z53HNZ7Z.js.map