opensteer 0.9.6 → 0.9.8

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 (35) hide show
  1. package/README.md +2 -2
  2. package/dist/{chunk-3I3A5OLB.js → chunk-BPGXP3RF.js} +257 -24
  3. package/dist/chunk-BPGXP3RF.js.map +1 -0
  4. package/dist/{chunk-3XBQRZZC.js → chunk-EXXRLPLI.js} +158 -46
  5. package/dist/chunk-EXXRLPLI.js.map +1 -0
  6. package/dist/{chunk-T5P2QGZ3.js → chunk-GKYBP3KD.js} +154 -13
  7. package/dist/chunk-GKYBP3KD.js.map +1 -0
  8. package/dist/{chunk-BVRIPCWA.js → chunk-LFWP5RXF.js} +500 -513
  9. package/dist/chunk-LFWP5RXF.js.map +1 -0
  10. package/dist/{chunk-L4NF74KI.js → chunk-SOJEWKSW.js} +5 -5
  11. package/dist/{chunk-L4NF74KI.js.map → chunk-SOJEWKSW.js.map} +1 -1
  12. package/dist/cli/bin.cjs +1230 -660
  13. package/dist/cli/bin.cjs.map +1 -1
  14. package/dist/cli/bin.js +166 -72
  15. package/dist/cli/bin.js.map +1 -1
  16. package/dist/index.cjs +793 -565
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +36 -51
  19. package/dist/index.d.ts +36 -51
  20. package/dist/index.js +4 -4
  21. package/dist/local-view/public/assets/app.js +10 -1
  22. package/dist/local-view/serve-entry.cjs +1022 -591
  23. package/dist/local-view/serve-entry.cjs.map +1 -1
  24. package/dist/local-view/serve-entry.js +2 -2
  25. package/dist/opensteer-XLPY343Y.js +6 -0
  26. package/dist/{opensteer-UGA6YBRN.js.map → opensteer-XLPY343Y.js.map} +1 -1
  27. package/dist/{session-control-U3L5H2ZI.js → session-control-FVKKD45R.js} +4 -4
  28. package/dist/{session-control-U3L5H2ZI.js.map → session-control-FVKKD45R.js.map} +1 -1
  29. package/package.json +5 -5
  30. package/skills/recorder/SKILL.md +2 -2
  31. package/dist/chunk-3I3A5OLB.js.map +0 -1
  32. package/dist/chunk-3XBQRZZC.js.map +0 -1
  33. package/dist/chunk-BVRIPCWA.js.map +0 -1
  34. package/dist/chunk-T5P2QGZ3.js.map +0 -1
  35. package/dist/opensteer-UGA6YBRN.js +0 -6
@@ -6,9 +6,9 @@ var crypto = require('crypto');
6
6
  var path12 = require('path');
7
7
  var url = require('url');
8
8
  var child_process = require('child_process');
9
- var util = require('util');
10
- var os = require('os');
11
9
  var fs = require('fs');
10
+ var os = require('os');
11
+ var util = require('util');
12
12
  var module$1 = require('module');
13
13
  var enginePlaywright = require('@opensteer/engine-playwright');
14
14
  var http = require('http');
@@ -203,388 +203,174 @@ function isAlreadyExistsError(error) {
203
203
  }
204
204
  async function withFilesystemLock(lockPath, task) {
205
205
  await ensureDirectory(path12__default.default.dirname(lockPath));
206
+ const ownerToken = crypto.randomUUID();
206
207
  let attempt = 0;
207
208
  while (true) {
208
209
  try {
209
210
  await promises.mkdir(lockPath);
211
+ const acquiredAt = Date.now();
212
+ await writeLockMetadata(lockPath, {
213
+ version: LOCK_METADATA_VERSION,
214
+ ownerToken,
215
+ pid: process.pid,
216
+ acquiredAt,
217
+ heartbeatAt: acquiredAt
218
+ });
210
219
  break;
211
220
  } catch (error) {
212
221
  if (!isAlreadyExistsError(error)) {
213
222
  throw error;
214
223
  }
224
+ if (await tryRecoverFilesystemLock(lockPath)) {
225
+ attempt = 0;
226
+ continue;
227
+ }
215
228
  const delayMs = LOCK_RETRY_DELAYS_MS[Math.min(attempt, LOCK_RETRY_DELAYS_MS.length - 1)];
216
229
  attempt += 1;
217
230
  await new Promise((resolve4) => setTimeout(resolve4, delayMs));
218
231
  }
219
232
  }
233
+ const heartbeatTimer = setInterval(() => {
234
+ void touchLockMetadata(lockPath, ownerToken);
235
+ }, LOCK_HEARTBEAT_INTERVAL_MS);
236
+ heartbeatTimer.unref?.();
220
237
  try {
221
238
  return await task();
222
239
  } finally {
223
- await promises.rm(lockPath, { recursive: true, force: true });
240
+ clearInterval(heartbeatTimer);
241
+ const metadata = await readLockMetadata(lockPath);
242
+ if (metadata?.ownerToken === ownerToken) {
243
+ await promises.rm(lockPath, { recursive: true, force: true });
244
+ }
224
245
  }
225
246
  }
226
- var LOCK_RETRY_DELAYS_MS;
227
- var init_filesystem = __esm({
228
- "../runtime-core/src/internal/filesystem.ts"() {
229
- init_json();
230
- LOCK_RETRY_DELAYS_MS = [1, 2, 5, 10, 20, 50];
231
- }
232
- });
233
-
234
- // src/internal/filesystem.ts
235
- var init_filesystem2 = __esm({
236
- "src/internal/filesystem.ts"() {
237
- init_filesystem();
247
+ async function tryRecoverFilesystemLock(lockPath) {
248
+ if (!await shouldRecoverFilesystemLock(lockPath)) {
249
+ return false;
238
250
  }
239
- });
240
- function resolveLiveSessionRecordPath(rootPath, provider) {
241
- return path12__default.default.join(rootPath, "live", provider === "local" ? "local.json" : "cloud.json");
251
+ await promises.rm(lockPath, { recursive: true, force: true });
252
+ return true;
242
253
  }
243
- async function readPersistedSessionRecord(rootPath, provider) {
244
- const sessionPath = resolveLiveSessionRecordPath(rootPath, provider);
245
- if (!await pathExists(sessionPath)) {
246
- return void 0;
254
+ async function shouldRecoverFilesystemLock(lockPath) {
255
+ const metadata = await readLockMetadata(lockPath);
256
+ if (metadata !== void 0) {
257
+ if (isProcessRunning(metadata.pid)) {
258
+ return false;
259
+ }
260
+ return Date.now() - metadata.heartbeatAt >= LOCK_ORPHAN_GRACE_MS;
247
261
  }
248
- const parsed = await readJsonFile(sessionPath);
249
- if (isPersistedLocalBrowserSessionRecord(parsed)) {
250
- return parsed;
262
+ const lockStat = await promises.stat(lockPath).catch(() => void 0);
263
+ if (lockStat === void 0) {
264
+ return false;
251
265
  }
252
- return void 0;
253
- }
254
- async function readPersistedLocalBrowserSessionRecord(rootPath) {
255
- const record = await readPersistedSessionRecord(rootPath, "local");
256
- return record?.provider === "local" ? record : void 0;
257
- }
258
- async function writePersistedSessionRecord(rootPath, record) {
259
- await writeJsonFileAtomic(resolveLiveSessionRecordPath(rootPath, record.provider), record);
260
- }
261
- async function clearPersistedSessionRecord(rootPath, provider) {
262
- await promises.rm(resolveLiveSessionRecordPath(rootPath, provider), { force: true });
266
+ return Date.now() - lockStat.mtimeMs >= LOCK_METADATALESS_STALE_MS;
263
267
  }
264
- function isPersistedLocalBrowserSessionRecord(value) {
265
- return value.layout === OPENSTEER_LIVE_SESSION_LAYOUT && value.version === OPENSTEER_LIVE_SESSION_VERSION && value.provider === "local" && (value.engine === "playwright" || value.engine === "abp") && typeof value.pid === "number" && Number.isFinite(value.pid) && typeof value.startedAt === "number" && Number.isFinite(value.startedAt) && typeof value.updatedAt === "number" && Number.isFinite(value.updatedAt) && typeof value.userDataDir === "string" && value.userDataDir.length > 0;
266
- }
267
- var OPENSTEER_LIVE_SESSION_LAYOUT, OPENSTEER_LIVE_SESSION_VERSION;
268
- var init_live_session = __esm({
269
- "src/live-session.ts"() {
270
- init_filesystem2();
271
- OPENSTEER_LIVE_SESSION_LAYOUT = "opensteer-session";
272
- OPENSTEER_LIVE_SESSION_VERSION = 1;
273
- }
274
- });
275
- function parseProcessOwner(value) {
276
- if (!value || typeof value !== "object") {
277
- return null;
278
- }
279
- const parsed = value;
280
- const pid = Number(parsed.pid);
281
- const processStartedAtMs = Number(parsed.processStartedAtMs);
282
- if (!Number.isInteger(pid) || pid <= 0) {
283
- return null;
268
+ async function readLockMetadata(lockPath) {
269
+ const metadataPath = path12__default.default.join(lockPath, LOCK_METADATA_FILENAME);
270
+ if (!await pathExists(metadataPath)) {
271
+ return void 0;
284
272
  }
285
- if (!Number.isInteger(processStartedAtMs) || processStartedAtMs <= 0) {
286
- return null;
273
+ try {
274
+ const parsed = await readJsonFile(metadataPath);
275
+ const pid = parsed.pid;
276
+ const acquiredAt = parsed.acquiredAt;
277
+ const heartbeatAt = parsed.heartbeatAt;
278
+ if (parsed.version !== LOCK_METADATA_VERSION || typeof parsed.ownerToken !== "string" || parsed.ownerToken.length === 0 || typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || typeof acquiredAt !== "number" || !Number.isFinite(acquiredAt) || typeof heartbeatAt !== "number" || !Number.isFinite(heartbeatAt)) {
279
+ return void 0;
280
+ }
281
+ return {
282
+ version: LOCK_METADATA_VERSION,
283
+ ownerToken: parsed.ownerToken,
284
+ pid,
285
+ acquiredAt,
286
+ heartbeatAt
287
+ };
288
+ } catch {
289
+ return void 0;
287
290
  }
288
- return {
289
- pid,
290
- processStartedAtMs
291
- };
292
291
  }
293
- function processOwnersEqual(left, right) {
294
- if (!left || !right) {
295
- return left === right;
292
+ async function writeLockMetadata(lockPath, metadata) {
293
+ try {
294
+ await writeJsonFileAtomic(path12__default.default.join(lockPath, LOCK_METADATA_FILENAME), metadata);
295
+ } catch (error) {
296
+ await promises.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
297
+ throw error;
296
298
  }
297
- return left.pid === right.pid && left.processStartedAtMs === right.processStartedAtMs;
298
299
  }
299
- async function getProcessLiveness(owner) {
300
- if (owner.pid === process.pid && hasMatchingProcessStartTime(owner.processStartedAtMs, PROCESS_STARTED_AT_MS)) {
301
- return "live";
302
- }
303
- const startedAtMs = await readProcessStartedAtMs(owner.pid);
304
- if (typeof startedAtMs === "number") {
305
- return hasMatchingProcessStartTime(owner.processStartedAtMs, startedAtMs) ? "live" : "dead";
300
+ async function touchLockMetadata(lockPath, ownerToken) {
301
+ const metadata = await readLockMetadata(lockPath);
302
+ if (metadata === void 0 || metadata.ownerToken !== ownerToken) {
303
+ return;
306
304
  }
307
- return isProcessRunning(owner.pid) ? "unknown" : "dead";
305
+ await writeJsonFileAtomic(path12__default.default.join(lockPath, LOCK_METADATA_FILENAME), {
306
+ ...metadata,
307
+ heartbeatAt: Date.now()
308
+ }).catch(() => void 0);
308
309
  }
309
310
  function isProcessRunning(pid) {
311
+ if (!Number.isInteger(pid) || pid <= 0) {
312
+ return false;
313
+ }
310
314
  try {
311
315
  process.kill(pid, 0);
312
316
  return true;
313
317
  } catch (error) {
314
- const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
315
- return code !== "ESRCH";
318
+ return error?.code === "EPERM";
316
319
  }
317
320
  }
318
- function hasMatchingProcessStartTime(expectedStartedAtMs, actualStartedAtMs) {
319
- return Math.abs(expectedStartedAtMs - actualStartedAtMs) <= PROCESS_START_TIME_TOLERANCE_MS;
320
- }
321
- async function readProcessStartedAtMs(pid) {
322
- if (pid <= 0) {
323
- return null;
324
- }
325
- if (process.platform === "linux") {
326
- return readLinuxProcessStartedAtMs(pid);
327
- }
328
- if (process.platform === "win32") {
329
- return readWindowsProcessStartedAtMs(pid);
321
+ var LOCK_RETRY_DELAYS_MS, LOCK_METADATA_FILENAME, LOCK_METADATA_VERSION, LOCK_HEARTBEAT_INTERVAL_MS, LOCK_ORPHAN_GRACE_MS, LOCK_METADATALESS_STALE_MS;
322
+ var init_filesystem = __esm({
323
+ "../runtime-core/src/internal/filesystem.ts"() {
324
+ init_json();
325
+ LOCK_RETRY_DELAYS_MS = [1, 2, 5, 10, 20, 50];
326
+ LOCK_METADATA_FILENAME = "owner.json";
327
+ LOCK_METADATA_VERSION = 1;
328
+ LOCK_HEARTBEAT_INTERVAL_MS = 1e3;
329
+ LOCK_ORPHAN_GRACE_MS = 2e3;
330
+ LOCK_METADATALESS_STALE_MS = 3e4;
330
331
  }
331
- return readPsProcessStartedAtMs(pid);
332
- }
333
- async function readLinuxProcessStartedAtMs(pid) {
334
- let statRaw;
335
- try {
336
- statRaw = await promises.readFile(`/proc/${String(pid)}/stat`, "utf8");
337
- } catch {
338
- return null;
332
+ });
333
+
334
+ // src/internal/filesystem.ts
335
+ var init_filesystem2 = __esm({
336
+ "src/internal/filesystem.ts"() {
337
+ init_filesystem();
339
338
  }
340
- const startTicks = parseLinuxProcessStartTicks(statRaw);
341
- if (startTicks === null) {
342
- return null;
339
+ });
340
+ function resolveBrandPlatformConfig(brand, platform = process.platform) {
341
+ if (platform === "darwin") {
342
+ return brand.darwin;
343
343
  }
344
- const [bootTimeMs, clockTicksPerSecond] = await Promise.all([
345
- readLinuxBootTimeMs(),
346
- readLinuxClockTicksPerSecond()
347
- ]);
348
- if (bootTimeMs === null || clockTicksPerSecond === null) {
349
- return null;
344
+ if (platform === "win32") {
345
+ return brand.win32;
350
346
  }
351
- return Math.floor(bootTimeMs + startTicks * 1e3 / clockTicksPerSecond);
352
- }
353
- function parseLinuxProcessStartTicks(statRaw) {
354
- const closingParenIndex = statRaw.lastIndexOf(")");
355
- if (closingParenIndex === -1) {
356
- return null;
347
+ if (platform === "linux") {
348
+ return brand.linux;
357
349
  }
358
- const fields = statRaw.slice(closingParenIndex + 2).trim().split(/\s+/);
359
- const startTicks = Number(fields[LINUX_STAT_START_TIME_FIELD_INDEX]);
360
- return Number.isFinite(startTicks) && startTicks >= 0 ? startTicks : null;
350
+ return void 0;
361
351
  }
362
- async function readLinuxBootTimeMs() {
363
- try {
364
- const statRaw = await promises.readFile("/proc/stat", "utf8");
365
- const bootTimeLine = statRaw.split("\n").find((line) => line.startsWith("btime "));
366
- if (!bootTimeLine) {
367
- return null;
352
+ function detectInstalledBrowserBrands() {
353
+ const installations = [];
354
+ for (const brand of BROWSER_BRANDS) {
355
+ const platformConfig = resolveBrandPlatformConfig(brand);
356
+ if (!platformConfig) {
357
+ continue;
368
358
  }
369
- const bootTimeSeconds = Number.parseInt(bootTimeLine.slice("btime ".length), 10);
370
- return Number.isFinite(bootTimeSeconds) ? bootTimeSeconds * 1e3 : null;
371
- } catch {
372
- return null;
373
- }
374
- }
375
- async function readLinuxClockTicksPerSecond() {
376
- if (!linuxClockTicksPerSecondPromise) {
377
- linuxClockTicksPerSecondPromise = execFileAsync("getconf", ["CLK_TCK"], {
378
- encoding: "utf8",
379
- maxBuffer: PROCESS_LIST_MAX_BUFFER_BYTES
380
- }).then(({ stdout }) => {
381
- const value = Number.parseInt(stdout.trim(), 10);
382
- return Number.isFinite(value) && value > 0 ? value : null;
383
- }).catch(() => null);
384
- }
385
- return linuxClockTicksPerSecondPromise;
386
- }
387
- async function readWindowsProcessStartedAtMs(pid) {
388
- try {
389
- const { stdout } = await execFileAsync(
390
- "powershell.exe",
391
- [
392
- "-NoProfile",
393
- "-Command",
394
- `(Get-Process -Id ${String(pid)}).StartTime.ToUniversalTime().ToString("o")`
395
- ],
396
- {
397
- encoding: "utf8",
398
- maxBuffer: PROCESS_LIST_MAX_BUFFER_BYTES
399
- }
359
+ const executablePath = firstExistingPath(
360
+ resolveExecutableCandidates(platformConfig.executableCandidates)
400
361
  );
401
- const isoTimestamp = stdout.trim();
402
- if (!isoTimestamp) {
403
- return null;
362
+ if (!executablePath) {
363
+ continue;
404
364
  }
405
- const startedAtMs = Date.parse(isoTimestamp);
406
- return Number.isFinite(startedAtMs) ? startedAtMs : null;
407
- } catch {
408
- return null;
409
- }
410
- }
411
- async function readPsProcessStartedAtMs(pid) {
412
- try {
413
- const { stdout } = await execFileAsync("ps", ["-o", "lstart=", "-p", String(pid)], {
414
- encoding: "utf8",
415
- env: PS_COMMAND_ENV,
416
- maxBuffer: PROCESS_LIST_MAX_BUFFER_BYTES
365
+ installations.push({
366
+ brand,
367
+ brandId: brand.id,
368
+ displayName: brand.displayName,
369
+ executablePath,
370
+ userDataDir: path12.resolve(expandHome(platformConfig.userDataDir))
417
371
  });
418
- const startedAt = stdout.trim();
419
- if (!startedAt) {
420
- return null;
421
- }
422
- const startedAtMs = Date.parse(startedAt.replace(/\s+/g, " "));
423
- return Number.isFinite(startedAtMs) ? startedAtMs : null;
424
- } catch {
425
- return null;
426
372
  }
427
- }
428
- var execFileAsync, PROCESS_STARTED_AT_MS, PROCESS_START_TIME_TOLERANCE_MS, PROCESS_LIST_MAX_BUFFER_BYTES, PS_COMMAND_ENV, LINUX_STAT_START_TIME_FIELD_INDEX, CURRENT_PROCESS_OWNER, linuxClockTicksPerSecondPromise;
429
- var init_process_owner = __esm({
430
- "src/local-browser/process-owner.ts"() {
431
- execFileAsync = util.promisify(child_process.execFile);
432
- PROCESS_STARTED_AT_MS = Math.floor(Date.now() - process.uptime() * 1e3);
433
- PROCESS_START_TIME_TOLERANCE_MS = 1e3;
434
- PROCESS_LIST_MAX_BUFFER_BYTES = 16 * 1024 * 1024;
435
- PS_COMMAND_ENV = { ...process.env, LC_ALL: "C" };
436
- LINUX_STAT_START_TIME_FIELD_INDEX = 19;
437
- CURRENT_PROCESS_OWNER = {
438
- pid: process.pid,
439
- processStartedAtMs: PROCESS_STARTED_AT_MS
440
- };
441
- linuxClockTicksPerSecondPromise = null;
442
- }
443
- });
444
- function resolveOpensteerStateDir() {
445
- const explicit = process.env.OPENSTEER_HOME?.trim();
446
- if (explicit) {
447
- return path12__default.default.resolve(explicit);
448
- }
449
- if (process.platform === "win32") {
450
- return path12__default.default.join(
451
- process.env.LOCALAPPDATA ?? path12__default.default.join(os.homedir(), "AppData", "Local"),
452
- "Opensteer"
453
- );
454
- }
455
- if (process.platform === "darwin") {
456
- return path12__default.default.join(os.homedir(), "Library", "Application Support", "Opensteer");
457
- }
458
- return path12__default.default.join(
459
- process.env.XDG_STATE_HOME ?? path12__default.default.join(os.homedir(), ".local", "state"),
460
- "opensteer"
461
- );
462
- }
463
- function resolveLocalViewRootDir() {
464
- return path12__default.default.join(resolveOpensteerStateDir(), "local-view");
465
- }
466
- function resolveLocalViewPreferencesPath() {
467
- return path12__default.default.join(resolveLocalViewRootDir(), "preferences.json");
468
- }
469
- function resolveLocalViewServiceDir() {
470
- return path12__default.default.join(resolveLocalViewRootDir(), "service");
471
- }
472
- function resolveLocalViewSessionsDir() {
473
- return path12__default.default.join(resolveLocalViewRootDir(), "sessions");
474
- }
475
- function resolveLocalViewServiceLockDir() {
476
- return path12__default.default.join(resolveLocalViewServiceDir(), "startup.lock");
477
- }
478
- function resolveLocalViewServiceStatePath() {
479
- return path12__default.default.join(resolveLocalViewServiceDir(), "state.json");
480
- }
481
- var init_runtime_dir = __esm({
482
- "src/local-view/runtime-dir.ts"() {
483
- }
484
- });
485
- function buildLocalViewSessionId(input) {
486
- const hash = crypto.createHash("sha256").update(`${input.rootPath}
487
- ${String(input.pid)}
488
- ${String(input.startedAt)}`).digest("hex");
489
- return `local_${hash.slice(0, 24)}`;
490
- }
491
- function createLocalViewSessionManifest(input) {
492
- return {
493
- layout: OPENSTEER_LOCAL_VIEW_SESSION_LAYOUT,
494
- version: OPENSTEER_LOCAL_VIEW_SESSION_VERSION,
495
- sessionId: buildLocalViewSessionId({
496
- rootPath: input.rootPath,
497
- pid: input.live.pid,
498
- startedAt: input.live.startedAt
499
- }),
500
- rootPath: input.rootPath,
501
- ...input.workspace === void 0 ? {} : { workspace: input.workspace },
502
- engine: input.live.engine,
503
- ownership: input.ownership,
504
- pid: input.live.pid,
505
- startedAt: input.live.startedAt,
506
- updatedAt: Date.now()
507
- };
508
- }
509
- async function writeLocalViewSessionManifest(manifest) {
510
- await ensureDirectory(resolveLocalViewSessionsDir());
511
- await writeJsonFileAtomic(resolveLocalViewSessionManifestPath(manifest.sessionId), manifest);
512
- }
513
- async function deleteLocalViewSessionManifest(sessionId) {
514
- await promises.rm(resolveLocalViewSessionManifestPath(sessionId), { force: true }).catch(() => void 0);
515
- }
516
- async function readLocalViewSessionManifest(sessionId) {
517
- const manifestPath = resolveLocalViewSessionManifestPath(sessionId);
518
- if (!await pathExists(manifestPath)) {
519
- return void 0;
520
- }
521
- const parsed = await readJsonFile(manifestPath);
522
- return isPersistedLocalViewSessionManifest(parsed) ? parsed : void 0;
523
- }
524
- async function listLocalViewSessionManifests() {
525
- const directoryPath = resolveLocalViewSessionsDir();
526
- const fileNames = await listJsonFiles(directoryPath);
527
- const manifests = await Promise.all(
528
- fileNames.map(async (fileName) => {
529
- const parsed = await readJsonFile(
530
- path12__default.default.join(directoryPath, fileName)
531
- ).catch(() => void 0);
532
- return isPersistedLocalViewSessionManifest(parsed) ? parsed : void 0;
533
- })
534
- );
535
- return manifests.filter((manifest) => manifest !== void 0).sort(
536
- (left, right) => left.startedAt - right.startedAt || left.sessionId.localeCompare(right.sessionId)
537
- );
538
- }
539
- function resolveLocalViewSessionManifestPath(sessionId) {
540
- return path12__default.default.join(resolveLocalViewSessionsDir(), `${sessionId}.json`);
541
- }
542
- function isPersistedLocalViewSessionManifest(value) {
543
- return value?.layout === OPENSTEER_LOCAL_VIEW_SESSION_LAYOUT && value.version === OPENSTEER_LOCAL_VIEW_SESSION_VERSION && typeof value.sessionId === "string" && value.sessionId.length > 0 && typeof value.rootPath === "string" && value.rootPath.length > 0 && (value.engine === "playwright" || value.engine === "abp") && (value.ownership === "owned" || value.ownership === "attached" || value.ownership === "managed") && typeof value.pid === "number" && Number.isFinite(value.pid) && typeof value.startedAt === "number" && Number.isFinite(value.startedAt) && typeof value.updatedAt === "number" && Number.isFinite(value.updatedAt);
544
- }
545
- var OPENSTEER_LOCAL_VIEW_SESSION_LAYOUT, OPENSTEER_LOCAL_VIEW_SESSION_VERSION;
546
- var init_session_manifest = __esm({
547
- "src/local-view/session-manifest.ts"() {
548
- init_filesystem2();
549
- init_runtime_dir();
550
- OPENSTEER_LOCAL_VIEW_SESSION_LAYOUT = "opensteer-local-view-session";
551
- OPENSTEER_LOCAL_VIEW_SESSION_VERSION = 1;
552
- }
553
- });
554
- function resolveBrandPlatformConfig(brand, platform = process.platform) {
555
- if (platform === "darwin") {
556
- return brand.darwin;
557
- }
558
- if (platform === "win32") {
559
- return brand.win32;
560
- }
561
- if (platform === "linux") {
562
- return brand.linux;
563
- }
564
- return void 0;
565
- }
566
- function detectInstalledBrowserBrands() {
567
- const installations = [];
568
- for (const brand of BROWSER_BRANDS) {
569
- const platformConfig = resolveBrandPlatformConfig(brand);
570
- if (!platformConfig) {
571
- continue;
572
- }
573
- const executablePath = firstExistingPath(
574
- resolveExecutableCandidates(platformConfig.executableCandidates)
575
- );
576
- if (!executablePath) {
577
- continue;
578
- }
579
- installations.push({
580
- brand,
581
- brandId: brand.id,
582
- displayName: brand.displayName,
583
- executablePath,
584
- userDataDir: path12.resolve(expandHome(platformConfig.userDataDir))
585
- });
586
- }
587
- return installations;
373
+ return installations;
588
374
  }
589
375
  function resolveExecutableCandidates(candidates) {
590
376
  return candidates.map((candidate) => candidate ? path12.resolve(expandHome(candidate)) : null);
@@ -1033,116 +819,473 @@ async function probeCdpEndpoint(input) {
1033
819
  httpUrl: target.httpUrl
1034
820
  });
1035
821
  }
1036
- if (target.fallbackBrowserWebSocketUrl !== void 0 && await isHttpEndpointReachable(target.httpUrl, timeoutMs)) {
1037
- return createInspectedCdpEndpoint({
1038
- browserWebSocketUrl: target.fallbackBrowserWebSocketUrl,
1039
- httpUrl: target.httpUrl
822
+ if (target.fallbackBrowserWebSocketUrl !== void 0 && await isHttpEndpointReachable(target.httpUrl, timeoutMs)) {
823
+ return createInspectedCdpEndpoint({
824
+ browserWebSocketUrl: target.fallbackBrowserWebSocketUrl,
825
+ httpUrl: target.httpUrl
826
+ });
827
+ }
828
+ return null;
829
+ }
830
+ function dedupeAndSortCandidates(candidates) {
831
+ const deduped = /* @__PURE__ */ new Map();
832
+ for (const candidate of [...candidates].sort(compareCandidates)) {
833
+ const existing = deduped.get(candidate.endpoint);
834
+ if (!existing || compareCandidates(candidate, existing) < 0) {
835
+ deduped.set(candidate.endpoint, candidate);
836
+ }
837
+ }
838
+ return [...deduped.values()].sort(compareCandidates);
839
+ }
840
+ function compareCandidates(left, right) {
841
+ return getAttachCandidatePriority(right) - getAttachCandidatePriority(left) || left.endpoint.localeCompare(right.endpoint) || (left.userDataDir ?? "").localeCompare(right.userDataDir ?? "");
842
+ }
843
+ function getAttachCandidatePriority(candidate) {
844
+ return candidate.source === "devtools-active-port" ? 2 : 1;
845
+ }
846
+ function createInspectedCdpEndpoint(input) {
847
+ const port = readPort(input.httpUrl);
848
+ return {
849
+ endpoint: input.browserWebSocketUrl,
850
+ ...input.browser === void 0 ? {} : { browser: input.browser },
851
+ ...input.protocolVersion === void 0 ? {} : { protocolVersion: input.protocolVersion },
852
+ httpUrl: input.httpUrl.toString(),
853
+ ...port === void 0 ? {} : { port }
854
+ };
855
+ }
856
+ function normalizeProbeTarget(endpoint) {
857
+ if (/^\d+$/.test(endpoint)) {
858
+ return {
859
+ httpUrl: new URL(`http://127.0.0.1:${endpoint}`)
860
+ };
861
+ }
862
+ if (endpoint.startsWith("ws://") || endpoint.startsWith("wss://")) {
863
+ const url = new URL(endpoint);
864
+ return {
865
+ fallbackBrowserWebSocketUrl: url.toString(),
866
+ httpUrl: new URL(`${url.protocol === "wss:" ? "https:" : "http:"}//${url.host}`)
867
+ };
868
+ }
869
+ try {
870
+ const url = endpoint.startsWith("http://") || endpoint.startsWith("https://") ? new URL(endpoint) : new URL(`http://${endpoint}`);
871
+ return {
872
+ httpUrl: new URL(`${url.protocol}//${url.host}`)
873
+ };
874
+ } catch {
875
+ throw new Error(`Invalid CDP endpoint "${endpoint}".`);
876
+ }
877
+ }
878
+ async function fetchJson(url, headers, timeoutMs) {
879
+ const response = await fetch(url, {
880
+ ...headers === void 0 ? {} : { headers },
881
+ signal: AbortSignal.timeout(timeoutMs)
882
+ }).catch(() => null);
883
+ if (!response?.ok) {
884
+ return null;
885
+ }
886
+ return await response.json();
887
+ }
888
+ async function isHttpEndpointReachable(url, timeoutMs) {
889
+ const response = await fetch(url, {
890
+ signal: AbortSignal.timeout(timeoutMs)
891
+ }).catch(() => null);
892
+ return response !== null;
893
+ }
894
+ function buildBrowserWebSocketUrl(httpUrl, webSocketPath) {
895
+ const protocol = httpUrl.protocol === "https:" ? "wss:" : "ws:";
896
+ return `${protocol}//${httpUrl.host}${normalizeWebSocketPath(webSocketPath)}`;
897
+ }
898
+ function normalizeWebSocketPath(path15) {
899
+ return path15.startsWith("/") ? path15 : `/${path15}`;
900
+ }
901
+ function rewriteBrowserWebSocketHost(browserWsUrl, requestedUrl) {
902
+ try {
903
+ const parsed = new URL(browserWsUrl);
904
+ parsed.protocol = requestedUrl.protocol === "https:" ? "wss:" : "ws:";
905
+ parsed.hostname = requestedUrl.hostname;
906
+ parsed.port = requestedUrl.port;
907
+ return parsed.toString();
908
+ } catch {
909
+ return browserWsUrl;
910
+ }
911
+ }
912
+ function readPort(url) {
913
+ const port = Number.parseInt(url.port, 10);
914
+ return Number.isInteger(port) && port > 0 ? port : void 0;
915
+ }
916
+ var DEFAULT_DISCOVERY_TIMEOUT_MS, DISCOVERY_FALLBACK_PORT, OpensteerAttachAmbiguousError;
917
+ var init_cdp_discovery = __esm({
918
+ "src/local-browser/cdp-discovery.ts"() {
919
+ init_chrome_discovery();
920
+ DEFAULT_DISCOVERY_TIMEOUT_MS = 2e3;
921
+ DISCOVERY_FALLBACK_PORT = 9222;
922
+ OpensteerAttachAmbiguousError = class extends Error {
923
+ constructor(candidates) {
924
+ super(
925
+ "Multiple running Chromium DevTools endpoints were discovered. Specify the desired endpoint explicitly."
926
+ );
927
+ this.candidates = candidates;
928
+ this.name = "OpensteerAttachAmbiguousError";
929
+ }
930
+ code = "attach-ambiguous";
931
+ };
932
+ }
933
+ });
934
+ function resolveLiveSessionRecordPath(rootPath, provider) {
935
+ return path12__default.default.join(rootPath, "live", provider === "local" ? "local.json" : "cloud.json");
936
+ }
937
+ async function readPersistedSessionRecord(rootPath, provider) {
938
+ const sessionPath = resolveLiveSessionRecordPath(rootPath, provider);
939
+ if (!await pathExists(sessionPath)) {
940
+ return void 0;
941
+ }
942
+ const parsed = await readJsonFile(sessionPath);
943
+ if (isPersistedLocalBrowserSessionRecord(parsed)) {
944
+ return parsed;
945
+ }
946
+ return void 0;
947
+ }
948
+ async function readPersistedLocalBrowserSessionRecord(rootPath) {
949
+ const record = await readPersistedSessionRecord(rootPath, "local");
950
+ return record?.provider === "local" ? record : void 0;
951
+ }
952
+ async function writePersistedSessionRecord(rootPath, record) {
953
+ await writeJsonFileAtomic(resolveLiveSessionRecordPath(rootPath, record.provider), record);
954
+ }
955
+ async function clearPersistedSessionRecord(rootPath, provider) {
956
+ await promises.rm(resolveLiveSessionRecordPath(rootPath, provider), { force: true });
957
+ }
958
+ function getPersistedLocalBrowserSessionOwnership(record) {
959
+ return record.ownership === "attached" ? "attached" : "owned";
960
+ }
961
+ async function isAttachedLocalBrowserSessionReachable(record) {
962
+ if (getPersistedLocalBrowserSessionOwnership(record) !== "attached") {
963
+ return false;
964
+ }
965
+ if (record.engine !== "playwright" || record.endpoint === void 0) {
966
+ return false;
967
+ }
968
+ try {
969
+ await inspectCdpEndpoint({
970
+ endpoint: record.endpoint,
971
+ timeoutMs: 1500
972
+ });
973
+ return true;
974
+ } catch {
975
+ return false;
976
+ }
977
+ }
978
+ function isPersistedLocalBrowserSessionRecord(value) {
979
+ return value.layout === OPENSTEER_LIVE_SESSION_LAYOUT && value.version === OPENSTEER_LIVE_SESSION_VERSION && value.provider === "local" && (value.engine === "playwright" || value.engine === "abp") && (value.ownership === void 0 || value.ownership === "owned" || value.ownership === "attached") && (value.activePageUrl === void 0 || typeof value.activePageUrl === "string") && (value.activePageTitle === void 0 || typeof value.activePageTitle === "string") && typeof value.pid === "number" && Number.isFinite(value.pid) && typeof value.startedAt === "number" && Number.isFinite(value.startedAt) && typeof value.updatedAt === "number" && Number.isFinite(value.updatedAt) && typeof value.userDataDir === "string" && value.userDataDir.length > 0;
980
+ }
981
+ var OPENSTEER_LIVE_SESSION_LAYOUT, OPENSTEER_LIVE_SESSION_VERSION;
982
+ var init_live_session = __esm({
983
+ "src/live-session.ts"() {
984
+ init_filesystem2();
985
+ init_cdp_discovery();
986
+ OPENSTEER_LIVE_SESSION_LAYOUT = "opensteer-session";
987
+ OPENSTEER_LIVE_SESSION_VERSION = 1;
988
+ }
989
+ });
990
+ function parseProcessOwner(value) {
991
+ if (!value || typeof value !== "object") {
992
+ return null;
993
+ }
994
+ const parsed = value;
995
+ const pid = Number(parsed.pid);
996
+ const processStartedAtMs = Number(parsed.processStartedAtMs);
997
+ if (!Number.isInteger(pid) || pid <= 0) {
998
+ return null;
999
+ }
1000
+ if (!Number.isInteger(processStartedAtMs) || processStartedAtMs <= 0) {
1001
+ return null;
1002
+ }
1003
+ return {
1004
+ pid,
1005
+ processStartedAtMs
1006
+ };
1007
+ }
1008
+ function processOwnersEqual(left, right) {
1009
+ if (!left || !right) {
1010
+ return left === right;
1011
+ }
1012
+ return left.pid === right.pid && left.processStartedAtMs === right.processStartedAtMs;
1013
+ }
1014
+ async function getProcessLiveness(owner) {
1015
+ if (owner.pid === process.pid && hasMatchingProcessStartTime(owner.processStartedAtMs, PROCESS_STARTED_AT_MS)) {
1016
+ return "live";
1017
+ }
1018
+ const startedAtMs = await readProcessStartedAtMs(owner.pid);
1019
+ if (typeof startedAtMs === "number") {
1020
+ return hasMatchingProcessStartTime(owner.processStartedAtMs, startedAtMs) ? "live" : "dead";
1021
+ }
1022
+ return isProcessRunning2(owner.pid) ? "unknown" : "dead";
1023
+ }
1024
+ function isProcessRunning2(pid) {
1025
+ try {
1026
+ process.kill(pid, 0);
1027
+ return true;
1028
+ } catch (error) {
1029
+ const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
1030
+ return code !== "ESRCH";
1031
+ }
1032
+ }
1033
+ function hasMatchingProcessStartTime(expectedStartedAtMs, actualStartedAtMs) {
1034
+ return Math.abs(expectedStartedAtMs - actualStartedAtMs) <= PROCESS_START_TIME_TOLERANCE_MS;
1035
+ }
1036
+ async function readProcessStartedAtMs(pid) {
1037
+ if (pid <= 0) {
1038
+ return null;
1039
+ }
1040
+ if (process.platform === "linux") {
1041
+ return readLinuxProcessStartedAtMs(pid);
1042
+ }
1043
+ if (process.platform === "win32") {
1044
+ return readWindowsProcessStartedAtMs(pid);
1045
+ }
1046
+ return readPsProcessStartedAtMs(pid);
1047
+ }
1048
+ async function readLinuxProcessStartedAtMs(pid) {
1049
+ let statRaw;
1050
+ try {
1051
+ statRaw = await promises.readFile(`/proc/${String(pid)}/stat`, "utf8");
1052
+ } catch {
1053
+ return null;
1054
+ }
1055
+ const startTicks = parseLinuxProcessStartTicks(statRaw);
1056
+ if (startTicks === null) {
1057
+ return null;
1058
+ }
1059
+ const [bootTimeMs, clockTicksPerSecond] = await Promise.all([
1060
+ readLinuxBootTimeMs(),
1061
+ readLinuxClockTicksPerSecond()
1062
+ ]);
1063
+ if (bootTimeMs === null || clockTicksPerSecond === null) {
1064
+ return null;
1065
+ }
1066
+ return Math.floor(bootTimeMs + startTicks * 1e3 / clockTicksPerSecond);
1067
+ }
1068
+ function parseLinuxProcessStartTicks(statRaw) {
1069
+ const closingParenIndex = statRaw.lastIndexOf(")");
1070
+ if (closingParenIndex === -1) {
1071
+ return null;
1072
+ }
1073
+ const fields = statRaw.slice(closingParenIndex + 2).trim().split(/\s+/);
1074
+ const startTicks = Number(fields[LINUX_STAT_START_TIME_FIELD_INDEX]);
1075
+ return Number.isFinite(startTicks) && startTicks >= 0 ? startTicks : null;
1076
+ }
1077
+ async function readLinuxBootTimeMs() {
1078
+ try {
1079
+ const statRaw = await promises.readFile("/proc/stat", "utf8");
1080
+ const bootTimeLine = statRaw.split("\n").find((line) => line.startsWith("btime "));
1081
+ if (!bootTimeLine) {
1082
+ return null;
1083
+ }
1084
+ const bootTimeSeconds = Number.parseInt(bootTimeLine.slice("btime ".length), 10);
1085
+ return Number.isFinite(bootTimeSeconds) ? bootTimeSeconds * 1e3 : null;
1086
+ } catch {
1087
+ return null;
1088
+ }
1089
+ }
1090
+ async function readLinuxClockTicksPerSecond() {
1091
+ if (!linuxClockTicksPerSecondPromise) {
1092
+ linuxClockTicksPerSecondPromise = execFileAsync("getconf", ["CLK_TCK"], {
1093
+ encoding: "utf8",
1094
+ maxBuffer: PROCESS_LIST_MAX_BUFFER_BYTES2
1095
+ }).then(({ stdout }) => {
1096
+ const value = Number.parseInt(stdout.trim(), 10);
1097
+ return Number.isFinite(value) && value > 0 ? value : null;
1098
+ }).catch(() => null);
1099
+ }
1100
+ return linuxClockTicksPerSecondPromise;
1101
+ }
1102
+ async function readWindowsProcessStartedAtMs(pid) {
1103
+ try {
1104
+ const { stdout } = await execFileAsync(
1105
+ "powershell.exe",
1106
+ [
1107
+ "-NoProfile",
1108
+ "-Command",
1109
+ `(Get-Process -Id ${String(pid)}).StartTime.ToUniversalTime().ToString("o")`
1110
+ ],
1111
+ {
1112
+ encoding: "utf8",
1113
+ maxBuffer: PROCESS_LIST_MAX_BUFFER_BYTES2
1114
+ }
1115
+ );
1116
+ const isoTimestamp = stdout.trim();
1117
+ if (!isoTimestamp) {
1118
+ return null;
1119
+ }
1120
+ const startedAtMs = Date.parse(isoTimestamp);
1121
+ return Number.isFinite(startedAtMs) ? startedAtMs : null;
1122
+ } catch {
1123
+ return null;
1124
+ }
1125
+ }
1126
+ async function readPsProcessStartedAtMs(pid) {
1127
+ try {
1128
+ const { stdout } = await execFileAsync("ps", ["-o", "lstart=", "-p", String(pid)], {
1129
+ encoding: "utf8",
1130
+ env: PS_COMMAND_ENV2,
1131
+ maxBuffer: PROCESS_LIST_MAX_BUFFER_BYTES2
1040
1132
  });
1133
+ const startedAt = stdout.trim();
1134
+ if (!startedAt) {
1135
+ return null;
1136
+ }
1137
+ const startedAtMs = Date.parse(startedAt.replace(/\s+/g, " "));
1138
+ return Number.isFinite(startedAtMs) ? startedAtMs : null;
1139
+ } catch {
1140
+ return null;
1041
1141
  }
1042
- return null;
1043
1142
  }
1044
- function dedupeAndSortCandidates(candidates) {
1045
- const deduped = /* @__PURE__ */ new Map();
1046
- for (const candidate of [...candidates].sort(compareCandidates)) {
1047
- const existing = deduped.get(candidate.endpoint);
1048
- if (!existing || compareCandidates(candidate, existing) < 0) {
1049
- deduped.set(candidate.endpoint, candidate);
1050
- }
1143
+ var execFileAsync, PROCESS_STARTED_AT_MS, PROCESS_START_TIME_TOLERANCE_MS, PROCESS_LIST_MAX_BUFFER_BYTES2, PS_COMMAND_ENV2, LINUX_STAT_START_TIME_FIELD_INDEX, CURRENT_PROCESS_OWNER, linuxClockTicksPerSecondPromise;
1144
+ var init_process_owner = __esm({
1145
+ "src/local-browser/process-owner.ts"() {
1146
+ execFileAsync = util.promisify(child_process.execFile);
1147
+ PROCESS_STARTED_AT_MS = Math.floor(Date.now() - process.uptime() * 1e3);
1148
+ PROCESS_START_TIME_TOLERANCE_MS = 1e3;
1149
+ PROCESS_LIST_MAX_BUFFER_BYTES2 = 16 * 1024 * 1024;
1150
+ PS_COMMAND_ENV2 = { ...process.env, LC_ALL: "C" };
1151
+ LINUX_STAT_START_TIME_FIELD_INDEX = 19;
1152
+ CURRENT_PROCESS_OWNER = {
1153
+ pid: process.pid,
1154
+ processStartedAtMs: PROCESS_STARTED_AT_MS
1155
+ };
1156
+ linuxClockTicksPerSecondPromise = null;
1051
1157
  }
1052
- return [...deduped.values()].sort(compareCandidates);
1158
+ });
1159
+ function resolveOpensteerStateDir() {
1160
+ const explicit = process.env.OPENSTEER_HOME?.trim();
1161
+ if (explicit) {
1162
+ return path12__default.default.resolve(explicit);
1163
+ }
1164
+ if (process.platform === "win32") {
1165
+ return path12__default.default.join(
1166
+ process.env.LOCALAPPDATA ?? path12__default.default.join(os.homedir(), "AppData", "Local"),
1167
+ "Opensteer"
1168
+ );
1169
+ }
1170
+ if (process.platform === "darwin") {
1171
+ return path12__default.default.join(os.homedir(), "Library", "Application Support", "Opensteer");
1172
+ }
1173
+ return path12__default.default.join(
1174
+ process.env.XDG_STATE_HOME ?? path12__default.default.join(os.homedir(), ".local", "state"),
1175
+ "opensteer"
1176
+ );
1053
1177
  }
1054
- function compareCandidates(left, right) {
1055
- return getAttachCandidatePriority(right) - getAttachCandidatePriority(left) || left.endpoint.localeCompare(right.endpoint) || (left.userDataDir ?? "").localeCompare(right.userDataDir ?? "");
1178
+ function resolveLocalViewRootDir() {
1179
+ return path12__default.default.join(resolveOpensteerStateDir(), "local-view");
1056
1180
  }
1057
- function getAttachCandidatePriority(candidate) {
1058
- return candidate.source === "devtools-active-port" ? 2 : 1;
1181
+ function resolveLocalViewPreferencesPath() {
1182
+ return path12__default.default.join(resolveLocalViewRootDir(), "preferences.json");
1059
1183
  }
1060
- function createInspectedCdpEndpoint(input) {
1061
- const port = readPort(input.httpUrl);
1062
- return {
1063
- endpoint: input.browserWebSocketUrl,
1064
- ...input.browser === void 0 ? {} : { browser: input.browser },
1065
- ...input.protocolVersion === void 0 ? {} : { protocolVersion: input.protocolVersion },
1066
- httpUrl: input.httpUrl.toString(),
1067
- ...port === void 0 ? {} : { port }
1068
- };
1184
+ function resolveLocalViewServiceDir() {
1185
+ return path12__default.default.join(resolveLocalViewRootDir(), "service");
1069
1186
  }
1070
- function normalizeProbeTarget(endpoint) {
1071
- if (/^\d+$/.test(endpoint)) {
1072
- return {
1073
- httpUrl: new URL(`http://127.0.0.1:${endpoint}`)
1074
- };
1075
- }
1076
- if (endpoint.startsWith("ws://") || endpoint.startsWith("wss://")) {
1077
- const url = new URL(endpoint);
1078
- return {
1079
- fallbackBrowserWebSocketUrl: url.toString(),
1080
- httpUrl: new URL(`${url.protocol === "wss:" ? "https:" : "http:"}//${url.host}`)
1081
- };
1082
- }
1083
- try {
1084
- const url = endpoint.startsWith("http://") || endpoint.startsWith("https://") ? new URL(endpoint) : new URL(`http://${endpoint}`);
1085
- return {
1086
- httpUrl: new URL(`${url.protocol}//${url.host}`)
1087
- };
1088
- } catch {
1089
- throw new Error(`Invalid CDP endpoint "${endpoint}".`);
1187
+ function resolveLocalViewSessionsDir() {
1188
+ return path12__default.default.join(resolveLocalViewRootDir(), "sessions");
1189
+ }
1190
+ function resolveLocalViewServiceLockDir() {
1191
+ return path12__default.default.join(resolveLocalViewServiceDir(), "startup.lock");
1192
+ }
1193
+ function resolveLocalViewServiceStatePath() {
1194
+ return path12__default.default.join(resolveLocalViewServiceDir(), "state.json");
1195
+ }
1196
+ var init_runtime_dir = __esm({
1197
+ "src/local-view/runtime-dir.ts"() {
1090
1198
  }
1199
+ });
1200
+ function buildLocalViewSessionId(input) {
1201
+ const ownership = input.ownership ?? "owned";
1202
+ const identity = ownership === "attached" ? input.endpoint ?? input.remoteDebuggingUrl ?? input.baseUrl ?? "attached" : `pid:${String(input.pid ?? 0)}`;
1203
+ const hash = crypto.createHash("sha256").update(`${input.rootPath}
1204
+ ${ownership}
1205
+ ${identity}
1206
+ ${String(input.startedAt)}`).digest("hex");
1207
+ return `local_${hash.slice(0, 24)}`;
1091
1208
  }
1092
- async function fetchJson(url, headers, timeoutMs) {
1093
- const response = await fetch(url, {
1094
- ...headers === void 0 ? {} : { headers },
1095
- signal: AbortSignal.timeout(timeoutMs)
1096
- }).catch(() => null);
1097
- if (!response?.ok) {
1098
- return null;
1209
+ function buildLocalViewSessionIdForRecord(input) {
1210
+ const ownership = getPersistedLocalBrowserSessionOwnership(input.live);
1211
+ if (ownership === "attached") {
1212
+ return buildLocalViewSessionId({
1213
+ rootPath: input.rootPath,
1214
+ ownership,
1215
+ startedAt: input.live.startedAt,
1216
+ ...input.live.endpoint === void 0 ? {} : { endpoint: input.live.endpoint },
1217
+ ...input.live.baseUrl === void 0 ? {} : { baseUrl: input.live.baseUrl },
1218
+ ...input.live.remoteDebuggingUrl === void 0 ? {} : { remoteDebuggingUrl: input.live.remoteDebuggingUrl }
1219
+ });
1099
1220
  }
1100
- return await response.json();
1221
+ return buildLocalViewSessionId({
1222
+ rootPath: input.rootPath,
1223
+ ownership,
1224
+ startedAt: input.live.startedAt,
1225
+ pid: input.live.pid
1226
+ });
1101
1227
  }
1102
- async function isHttpEndpointReachable(url, timeoutMs) {
1103
- const response = await fetch(url, {
1104
- signal: AbortSignal.timeout(timeoutMs)
1105
- }).catch(() => null);
1106
- return response !== null;
1228
+ function createLocalViewSessionManifest(input) {
1229
+ return {
1230
+ layout: OPENSTEER_LOCAL_VIEW_SESSION_LAYOUT,
1231
+ version: OPENSTEER_LOCAL_VIEW_SESSION_VERSION,
1232
+ sessionId: buildLocalViewSessionIdForRecord({
1233
+ rootPath: input.rootPath,
1234
+ live: input.live
1235
+ }),
1236
+ rootPath: input.rootPath,
1237
+ ...input.workspace === void 0 ? {} : { workspace: input.workspace },
1238
+ engine: input.live.engine,
1239
+ ownership: input.ownership,
1240
+ pid: input.live.pid,
1241
+ startedAt: input.live.startedAt,
1242
+ updatedAt: Date.now()
1243
+ };
1107
1244
  }
1108
- function buildBrowserWebSocketUrl(httpUrl, webSocketPath) {
1109
- const protocol = httpUrl.protocol === "https:" ? "wss:" : "ws:";
1110
- return `${protocol}//${httpUrl.host}${normalizeWebSocketPath(webSocketPath)}`;
1245
+ async function writeLocalViewSessionManifest(manifest) {
1246
+ await ensureDirectory(resolveLocalViewSessionsDir());
1247
+ await writeJsonFileAtomic(resolveLocalViewSessionManifestPath(manifest.sessionId), manifest);
1111
1248
  }
1112
- function normalizeWebSocketPath(path15) {
1113
- return path15.startsWith("/") ? path15 : `/${path15}`;
1249
+ async function deleteLocalViewSessionManifest(sessionId) {
1250
+ await promises.rm(resolveLocalViewSessionManifestPath(sessionId), { force: true }).catch(() => void 0);
1114
1251
  }
1115
- function rewriteBrowserWebSocketHost(browserWsUrl, requestedUrl) {
1116
- try {
1117
- const parsed = new URL(browserWsUrl);
1118
- parsed.protocol = requestedUrl.protocol === "https:" ? "wss:" : "ws:";
1119
- parsed.hostname = requestedUrl.hostname;
1120
- parsed.port = requestedUrl.port;
1121
- return parsed.toString();
1122
- } catch {
1123
- return browserWsUrl;
1252
+ async function readLocalViewSessionManifest(sessionId) {
1253
+ const manifestPath = resolveLocalViewSessionManifestPath(sessionId);
1254
+ if (!await pathExists(manifestPath)) {
1255
+ return void 0;
1124
1256
  }
1257
+ const parsed = await readJsonFile(manifestPath);
1258
+ return isPersistedLocalViewSessionManifest(parsed) ? parsed : void 0;
1125
1259
  }
1126
- function readPort(url) {
1127
- const port = Number.parseInt(url.port, 10);
1128
- return Number.isInteger(port) && port > 0 ? port : void 0;
1260
+ async function listLocalViewSessionManifests() {
1261
+ const directoryPath = resolveLocalViewSessionsDir();
1262
+ const fileNames = await listJsonFiles(directoryPath);
1263
+ const manifests = await Promise.all(
1264
+ fileNames.map(async (fileName) => {
1265
+ const parsed = await readJsonFile(
1266
+ path12__default.default.join(directoryPath, fileName)
1267
+ ).catch(() => void 0);
1268
+ return isPersistedLocalViewSessionManifest(parsed) ? parsed : void 0;
1269
+ })
1270
+ );
1271
+ return manifests.filter((manifest) => manifest !== void 0).sort(
1272
+ (left, right) => left.startedAt - right.startedAt || left.sessionId.localeCompare(right.sessionId)
1273
+ );
1129
1274
  }
1130
- var DEFAULT_DISCOVERY_TIMEOUT_MS, DISCOVERY_FALLBACK_PORT, OpensteerAttachAmbiguousError;
1131
- var init_cdp_discovery = __esm({
1132
- "src/local-browser/cdp-discovery.ts"() {
1133
- init_chrome_discovery();
1134
- DEFAULT_DISCOVERY_TIMEOUT_MS = 2e3;
1135
- DISCOVERY_FALLBACK_PORT = 9222;
1136
- OpensteerAttachAmbiguousError = class extends Error {
1137
- constructor(candidates) {
1138
- super(
1139
- "Multiple running Chromium DevTools endpoints were discovered. Specify the desired endpoint explicitly."
1140
- );
1141
- this.candidates = candidates;
1142
- this.name = "OpensteerAttachAmbiguousError";
1143
- }
1144
- code = "attach-ambiguous";
1145
- };
1275
+ function resolveLocalViewSessionManifestPath(sessionId) {
1276
+ return path12__default.default.join(resolveLocalViewSessionsDir(), `${sessionId}.json`);
1277
+ }
1278
+ function isPersistedLocalViewSessionManifest(value) {
1279
+ return value?.layout === OPENSTEER_LOCAL_VIEW_SESSION_LAYOUT && value.version === OPENSTEER_LOCAL_VIEW_SESSION_VERSION && typeof value.sessionId === "string" && value.sessionId.length > 0 && typeof value.rootPath === "string" && value.rootPath.length > 0 && (value.engine === "playwright" || value.engine === "abp") && (value.ownership === "owned" || value.ownership === "attached" || value.ownership === "managed") && typeof value.pid === "number" && Number.isFinite(value.pid) && typeof value.startedAt === "number" && Number.isFinite(value.startedAt) && typeof value.updatedAt === "number" && Number.isFinite(value.updatedAt);
1280
+ }
1281
+ var OPENSTEER_LOCAL_VIEW_SESSION_LAYOUT, OPENSTEER_LOCAL_VIEW_SESSION_VERSION;
1282
+ var init_session_manifest = __esm({
1283
+ "src/local-view/session-manifest.ts"() {
1284
+ init_filesystem2();
1285
+ init_live_session();
1286
+ init_runtime_dir();
1287
+ OPENSTEER_LOCAL_VIEW_SESSION_LAYOUT = "opensteer-local-view-session";
1288
+ OPENSTEER_LOCAL_VIEW_SESSION_VERSION = 1;
1146
1289
  }
1147
1290
  });
1148
1291
  async function readLocalViewServiceState() {
@@ -2228,7 +2371,7 @@ var init_version = __esm({
2228
2371
  "../protocol/src/version.ts"() {
2229
2372
  init_json2();
2230
2373
  OPENSTEER_PROTOCOL_NAME = "opensteer";
2231
- OPENSTEER_PROTOCOL_COMPATIBILITY_REVISION = 2;
2374
+ OPENSTEER_PROTOCOL_COMPATIBILITY_REVISION = 3;
2232
2375
  OPENSTEER_PROTOCOL_VERSION = `0.${OPENSTEER_PROTOCOL_COMPATIBILITY_REVISION}.0`;
2233
2376
  OPENSTEER_PROTOCOL_MEDIA_TYPE = `application/vnd.${OPENSTEER_PROTOCOL_NAME}+json;version=${OPENSTEER_PROTOCOL_VERSION}`;
2234
2377
  OPENSTEER_PROTOCOL_REST_BASE_PATH = `/api/v${OPENSTEER_PROTOCOL_COMPATIBILITY_REVISION}`;
@@ -2478,6 +2621,9 @@ var init_metadata = __esm({
2478
2621
  {
2479
2622
  pageRef: pageRefSchema,
2480
2623
  sessionRef: sessionRefSchema,
2624
+ targetId: stringSchema({
2625
+ description: "Underlying browser target identifier when available."
2626
+ }),
2481
2627
  openerPageRef: pageRefSchema,
2482
2628
  url: stringSchema({
2483
2629
  description: "Current main-frame URL."
@@ -6252,14 +6398,14 @@ var init_interaction = __esm({
6252
6398
  function defineSemanticOperationSpec(spec) {
6253
6399
  return spec;
6254
6400
  }
6255
- var opensteerComputerAnnotationNames, opensteerExposedSemanticOperationNames, opensteerPackageRunnableSemanticOperationNames, snapshotModeSchema, viewportSchema, opensteerBrowserLaunchOptionsSchema, attachBrowserOptionsSchema, opensteerBrowserOptionsSchema, opensteerBrowserContextOptionsSchema, targetByElementSchema2, targetByPersistSchema2, targetBySelectorSchema2, opensteerTargetInputSchema, opensteerResolvedTargetSchema, opensteerActionResultSchema, opensteerSnapshotCounterSchema, opensteerSessionStateSchema, opensteerOpenInputSchema, opensteerPageListInputSchema, opensteerPageListOutputSchema, opensteerPageNewInputSchema, opensteerPageActivateInputSchema, opensteerPageCloseInputSchema, opensteerPageCloseOutputSchema, opensteerPageGotoInputSchema, opensteerPageEvaluateInputSchema, opensteerPageEvaluateOutputSchema, opensteerAddInitScriptInputSchema, opensteerAddInitScriptOutputSchema, opensteerCapturedScriptSchema, opensteerCaptureScriptsInputSchema, opensteerCaptureScriptsOutputSchema, opensteerPageSnapshotInputSchema, opensteerPageSnapshotOutputSchema, opensteerComputerMouseButtonSchema, opensteerComputerKeyModifierSchema, opensteerDomClickInputSchema, opensteerDomHoverInputSchema, opensteerDomInputInputSchema, opensteerDomScrollInputSchema, opensteerExtractTemplateSchema, opensteerDomExtractInputSchema, jsonValueSchema2, opensteerDomExtractOutputSchema, opensteerSessionCloseInputSchema, opensteerSessionCloseOutputSchema, opensteerComputerAnnotationSchema, opensteerComputerClickActionSchema, opensteerComputerMoveActionSchema, opensteerComputerScrollActionSchema, opensteerComputerTypeActionSchema, opensteerComputerKeyActionSchema, opensteerComputerDragActionSchema, opensteerComputerScreenshotActionSchema, opensteerComputerWaitActionSchema, opensteerComputerActionSchema, opensteerComputerScreenshotOptionsSchema, opensteerComputerExecuteInputSchema, opensteerComputerTracePointSchema, opensteerComputerTraceEnrichmentSchema, opensteerComputerExecuteTimingSchema, opensteerComputerDisplayScaleSchema, opensteerComputerExecuteOutputSchema, opensteerSemanticOperationSpecificationsBase, exposedSemanticOperationNameSet, opensteerSemanticOperationSpecificationsInternal, opensteerSemanticOperationSpecifications, semanticRestBasePath;
6401
+ var opensteerComputerAnnotationNames, opensteerExposedSemanticOperationNames, opensteerPackageRunnableSemanticOperationNames, snapshotModeSchema, viewportSchema, opensteerBrowserLaunchOptionsSchema, attachBrowserOptionsSchema, opensteerBrowserOptionsSchema, opensteerBrowserContextOptionsSchema, targetByElementSchema2, targetByPersistSchema2, targetBySelectorSchema2, opensteerTargetInputSchema, opensteerActionResultSchema, opensteerSnapshotCounterSchema, opensteerNavigationSummarySchema, opensteerOpenInputSchema, opensteerPageListInputSchema, opensteerPageListOutputSchema, opensteerPageNewInputSchema, opensteerPageActivateInputSchema, opensteerPageCloseInputSchema, opensteerPageCloseOutputSchema, opensteerPageNewOutputSchema, opensteerPageGotoInputSchema, opensteerPageEvaluateInputSchema, opensteerPageEvaluateOutputSchema, opensteerAddInitScriptInputSchema, opensteerAddInitScriptOutputSchema, opensteerCapturedScriptSchema, opensteerCaptureScriptsInputSchema, opensteerCaptureScriptsOutputSchema, opensteerPageSnapshotInputSchema, opensteerPageSnapshotOutputSchema, opensteerComputerMouseButtonSchema, opensteerComputerKeyModifierSchema, opensteerDomClickInputSchema, opensteerDomHoverInputSchema, opensteerDomInputInputSchema, opensteerDomScrollInputSchema, opensteerExtractTemplateSchema, opensteerDomExtractInputSchema, jsonValueSchema2, opensteerDomExtractOutputSchema, opensteerSessionCloseInputSchema, opensteerSessionCloseOutputSchema, opensteerComputerAnnotationSchema, opensteerComputerClickActionSchema, opensteerComputerMoveActionSchema, opensteerComputerScrollActionSchema, opensteerComputerTypeActionSchema, opensteerComputerKeyActionSchema, opensteerComputerDragActionSchema, opensteerComputerScreenshotActionSchema, opensteerComputerWaitActionSchema, opensteerComputerActionSchema, opensteerComputerScreenshotOptionsSchema, opensteerComputerExecuteInputSchema, opensteerScreenshotSummarySchema, opensteerComputerExecuteOutputSchema, opensteerSemanticOperationSpecificationsBase, exposedSemanticOperationNameSet, opensteerSemanticOperationSpecificationsInternal, opensteerSemanticOperationSpecifications, semanticRestBasePath;
6256
6402
  var init_semantic = __esm({
6257
6403
  "../protocol/src/semantic.ts"() {
6258
6404
  init_json2();
6405
+ init_binary_location();
6259
6406
  init_identity();
6260
6407
  init_geometry();
6261
6408
  init_metadata();
6262
- init_events();
6263
6409
  init_envelopes();
6264
6410
  init_snapshots();
6265
6411
  init_artifacts2();
@@ -6482,39 +6628,14 @@ var init_semantic = __esm({
6482
6628
  title: "OpensteerTargetInput"
6483
6629
  }
6484
6630
  );
6485
- opensteerResolvedTargetSchema = objectSchema(
6486
- {
6487
- pageRef: pageRefSchema,
6488
- frameRef: frameRefSchema,
6489
- documentRef: documentRefSchema,
6490
- documentEpoch: documentEpochSchema,
6491
- nodeRef: nodeRefSchema,
6492
- tagName: stringSchema(),
6493
- pathHint: stringSchema(),
6494
- persist: stringSchema(),
6495
- selectorUsed: stringSchema()
6496
- },
6497
- {
6498
- title: "OpensteerResolvedTarget",
6499
- required: [
6500
- "pageRef",
6501
- "frameRef",
6502
- "documentRef",
6503
- "documentEpoch",
6504
- "nodeRef",
6505
- "tagName",
6506
- "pathHint"
6507
- ]
6508
- }
6509
- );
6510
6631
  opensteerActionResultSchema = objectSchema(
6511
6632
  {
6512
- target: opensteerResolvedTargetSchema,
6513
- point: pointSchema
6633
+ tagName: stringSchema({ minLength: 1 }),
6634
+ persist: stringSchema({ minLength: 1 })
6514
6635
  },
6515
6636
  {
6516
6637
  title: "OpensteerActionResult",
6517
- required: ["target"]
6638
+ required: ["tagName"]
6518
6639
  }
6519
6640
  );
6520
6641
  opensteerSnapshotCounterSchema = objectSchema(
@@ -6560,16 +6681,14 @@ var init_semantic = __esm({
6560
6681
  ]
6561
6682
  }
6562
6683
  );
6563
- opensteerSessionStateSchema = objectSchema(
6684
+ opensteerNavigationSummarySchema = objectSchema(
6564
6685
  {
6565
- sessionRef: sessionRefSchema,
6566
- pageRef: pageRefSchema,
6567
6686
  url: stringSchema(),
6568
6687
  title: stringSchema()
6569
6688
  },
6570
6689
  {
6571
- title: "OpensteerSessionState",
6572
- required: ["sessionRef", "pageRef", "url", "title"]
6690
+ title: "OpensteerNavigationSummary",
6691
+ required: ["url", "title"]
6573
6692
  }
6574
6693
  );
6575
6694
  opensteerOpenInputSchema = objectSchema(
@@ -6633,8 +6752,19 @@ var init_semantic = __esm({
6633
6752
  pages: arraySchema(pageInfoSchema)
6634
6753
  },
6635
6754
  {
6636
- title: "OpensteerPageCloseOutput",
6637
- required: ["closedPageRef", "pages"]
6755
+ title: "OpensteerPageCloseOutput",
6756
+ required: ["closedPageRef", "pages"]
6757
+ }
6758
+ );
6759
+ opensteerPageNewOutputSchema = objectSchema(
6760
+ {
6761
+ pageRef: pageRefSchema,
6762
+ url: stringSchema(),
6763
+ title: stringSchema()
6764
+ },
6765
+ {
6766
+ title: "OpensteerPageNewOutput",
6767
+ required: ["pageRef", "url", "title"]
6638
6768
  }
6639
6769
  );
6640
6770
  opensteerPageGotoInputSchema = objectSchema(
@@ -7023,72 +7153,28 @@ var init_semantic = __esm({
7023
7153
  required: ["action"]
7024
7154
  }
7025
7155
  );
7026
- opensteerComputerTracePointSchema = objectSchema(
7027
- {
7028
- role: enumSchema(["point", "start", "end"]),
7029
- point: pointSchema,
7030
- hitTest: hitTestResultSchema,
7031
- target: opensteerResolvedTargetSchema
7032
- },
7033
- {
7034
- title: "OpensteerComputerTracePoint",
7035
- required: ["role", "point"]
7036
- }
7037
- );
7038
- opensteerComputerTraceEnrichmentSchema = objectSchema(
7039
- {
7040
- points: arraySchema(opensteerComputerTracePointSchema)
7041
- },
7042
- {
7043
- title: "OpensteerComputerTraceEnrichment",
7044
- required: ["points"]
7045
- }
7046
- );
7047
- opensteerComputerExecuteTimingSchema = objectSchema(
7156
+ opensteerScreenshotSummarySchema = objectSchema(
7048
7157
  {
7049
- actionMs: integerSchema({ minimum: 0 }),
7050
- waitMs: integerSchema({ minimum: 0 }),
7051
- totalMs: integerSchema({ minimum: 0 })
7052
- },
7053
- {
7054
- title: "OpensteerComputerExecuteTiming",
7055
- required: ["actionMs", "waitMs", "totalMs"]
7056
- }
7057
- );
7058
- opensteerComputerDisplayScaleSchema = objectSchema(
7059
- {
7060
- x: numberSchema({ exclusiveMinimum: 0 }),
7061
- y: numberSchema({ exclusiveMinimum: 0 })
7158
+ payload: externalBinaryLocationSchema,
7159
+ format: screenshotFormatSchema,
7160
+ size: sizeSchema,
7161
+ coordinateSpace: coordinateSpaceSchema,
7162
+ clip: rectSchema
7062
7163
  },
7063
7164
  {
7064
- title: "OpensteerComputerDisplayScale",
7065
- required: ["x", "y"]
7165
+ title: "OpensteerScreenshotSummary",
7166
+ required: ["payload", "format", "size", "coordinateSpace"]
7066
7167
  }
7067
7168
  );
7068
7169
  opensteerComputerExecuteOutputSchema = objectSchema(
7069
7170
  {
7070
- action: opensteerComputerActionSchema,
7071
- pageRef: pageRefSchema,
7072
- screenshot: screenshotArtifactSchema,
7073
- displayViewport: viewportMetricsSchema,
7074
- nativeViewport: viewportMetricsSchema,
7075
- displayScale: opensteerComputerDisplayScaleSchema,
7076
- events: arraySchema(opensteerEventSchema),
7077
- timing: opensteerComputerExecuteTimingSchema,
7078
- trace: opensteerComputerTraceEnrichmentSchema
7171
+ url: stringSchema(),
7172
+ title: stringSchema(),
7173
+ screenshot: opensteerScreenshotSummarySchema
7079
7174
  },
7080
7175
  {
7081
7176
  title: "OpensteerComputerExecuteOutput",
7082
- required: [
7083
- "action",
7084
- "pageRef",
7085
- "screenshot",
7086
- "displayViewport",
7087
- "nativeViewport",
7088
- "displayScale",
7089
- "events",
7090
- "timing"
7091
- ]
7177
+ required: ["url", "title", "screenshot"]
7092
7178
  }
7093
7179
  );
7094
7180
  opensteerSemanticOperationSpecificationsBase = [
@@ -7096,7 +7182,7 @@ var init_semantic = __esm({
7096
7182
  name: "session.open",
7097
7183
  description: "Open or resume the current Opensteer session and primary page.",
7098
7184
  inputSchema: opensteerOpenInputSchema,
7099
- outputSchema: opensteerSessionStateSchema,
7185
+ outputSchema: opensteerNavigationSummarySchema,
7100
7186
  requiredCapabilities: ["sessions.manage", "pages.manage"],
7101
7187
  resolveRequiredCapabilities: (input) => input.url === void 0 ? ["sessions.manage", "pages.manage"] : ["sessions.manage", "pages.manage", "pages.navigate"]
7102
7188
  }),
@@ -7111,7 +7197,7 @@ var init_semantic = __esm({
7111
7197
  name: "page.new",
7112
7198
  description: "Create and optionally navigate a new top-level page in the current session.",
7113
7199
  inputSchema: opensteerPageNewInputSchema,
7114
- outputSchema: opensteerSessionStateSchema,
7200
+ outputSchema: opensteerPageNewOutputSchema,
7115
7201
  requiredCapabilities: ["pages.manage"],
7116
7202
  resolveRequiredCapabilities: (input) => input.url === void 0 ? ["pages.manage"] : ["pages.manage", "pages.navigate"]
7117
7203
  }),
@@ -7119,7 +7205,7 @@ var init_semantic = __esm({
7119
7205
  name: "page.activate",
7120
7206
  description: "Activate an existing top-level page in the current session.",
7121
7207
  inputSchema: opensteerPageActivateInputSchema,
7122
- outputSchema: opensteerSessionStateSchema,
7208
+ outputSchema: opensteerNavigationSummarySchema,
7123
7209
  requiredCapabilities: ["pages.manage", "inspect.pages"]
7124
7210
  }),
7125
7211
  defineSemanticOperationSpec({
@@ -7133,7 +7219,7 @@ var init_semantic = __esm({
7133
7219
  name: "page.goto",
7134
7220
  description: "Navigate the current Opensteer page to a new URL.",
7135
7221
  inputSchema: opensteerPageGotoInputSchema,
7136
- outputSchema: opensteerSessionStateSchema,
7222
+ outputSchema: opensteerNavigationSummarySchema,
7137
7223
  requiredCapabilities: ["pages.navigate"]
7138
7224
  }),
7139
7225
  defineSemanticOperationSpec({
@@ -8658,6 +8744,9 @@ var init_observations = __esm({
8658
8744
  this.store = store;
8659
8745
  this.sessionId = sessionId;
8660
8746
  }
8747
+ configure(input) {
8748
+ return this.store.configureSession(this.sessionId, input);
8749
+ }
8661
8750
  append(input) {
8662
8751
  return this.store.appendEvent(this.sessionId, input);
8663
8752
  }
@@ -8688,40 +8777,14 @@ var init_observations = __esm({
8688
8777
  const sessionId = normalizeNonEmptyString("sessionId", input.sessionId);
8689
8778
  const openedAt = normalizeTimestamp("openedAt", input.openedAt ?? Date.now());
8690
8779
  const config = normalizeObservabilityConfig(input.config);
8691
- const redactor = createObservationRedactor(config);
8692
- this.redactors.set(sessionId, redactor);
8693
- const redactedLabels = redactor.redactLabels(config.labels);
8694
- const redactedTraceContext = redactor.redactTraceContext(config.traceContext);
8695
- await withFilesystemLock(this.sessionLockPath(sessionId), async () => {
8696
- const existing = await this.reconcileSessionManifest(sessionId);
8697
- if (existing === void 0) {
8698
- await ensureDirectory(this.sessionEventsDirectory(sessionId));
8699
- await ensureDirectory(this.sessionArtifactsDirectory(sessionId));
8700
- const session = {
8701
- sessionId,
8702
- profile: config.profile,
8703
- ...redactedLabels === void 0 ? {} : { labels: redactedLabels },
8704
- ...redactedTraceContext === void 0 ? {} : { traceContext: redactedTraceContext },
8705
- openedAt,
8706
- updatedAt: openedAt,
8707
- currentSequence: 0,
8708
- eventCount: 0,
8709
- artifactCount: 0
8710
- };
8711
- await writeJsonFileExclusive(this.sessionManifestPath(sessionId), session);
8712
- return;
8713
- }
8714
- const patched = {
8715
- ...existing,
8716
- profile: config.profile,
8717
- ...redactedLabels === void 0 ? {} : { labels: redactedLabels },
8718
- ...redactedTraceContext === void 0 ? {} : { traceContext: redactedTraceContext },
8719
- updatedAt: Math.max(existing.updatedAt, openedAt)
8720
- };
8721
- await writeJsonFileAtomic(this.sessionManifestPath(sessionId), patched);
8722
- });
8780
+ await this.applySessionConfiguration(sessionId, config, openedAt);
8723
8781
  return new FilesystemSessionSink(this, sessionId);
8724
8782
  }
8783
+ async configureSession(sessionId, input) {
8784
+ const updatedAt = normalizeTimestamp("updatedAt", input.updatedAt ?? Date.now());
8785
+ const config = normalizeObservabilityConfig(input.config);
8786
+ await this.applySessionConfiguration(sessionId, config, updatedAt);
8787
+ }
8725
8788
  async getSession(sessionId) {
8726
8789
  const manifestPath = this.sessionManifestPath(sessionId);
8727
8790
  if (!await pathExists(manifestPath)) {
@@ -8942,6 +9005,40 @@ var init_observations = __esm({
8942
9005
  sessionLockPath(sessionId) {
8943
9006
  return path12__default.default.join(this.sessionDirectory(sessionId), ".lock");
8944
9007
  }
9008
+ async applySessionConfiguration(sessionId, config, timestamp) {
9009
+ const redactor = createObservationRedactor(config);
9010
+ this.redactors.set(sessionId, redactor);
9011
+ const redactedLabels = redactor.redactLabels(config.labels);
9012
+ const redactedTraceContext = redactor.redactTraceContext(config.traceContext);
9013
+ await withFilesystemLock(this.sessionLockPath(sessionId), async () => {
9014
+ const existing = await this.reconcileSessionManifest(sessionId);
9015
+ if (existing === void 0) {
9016
+ await ensureDirectory(this.sessionEventsDirectory(sessionId));
9017
+ await ensureDirectory(this.sessionArtifactsDirectory(sessionId));
9018
+ const session = {
9019
+ sessionId,
9020
+ profile: config.profile,
9021
+ ...redactedLabels === void 0 ? {} : { labels: redactedLabels },
9022
+ ...redactedTraceContext === void 0 ? {} : { traceContext: redactedTraceContext },
9023
+ openedAt: timestamp,
9024
+ updatedAt: timestamp,
9025
+ currentSequence: 0,
9026
+ eventCount: 0,
9027
+ artifactCount: 0
9028
+ };
9029
+ await writeJsonFileExclusive(this.sessionManifestPath(sessionId), session);
9030
+ return;
9031
+ }
9032
+ const patched = {
9033
+ ...existing,
9034
+ profile: config.profile,
9035
+ ...redactedLabels === void 0 ? {} : { labels: redactedLabels },
9036
+ ...redactedTraceContext === void 0 ? {} : { traceContext: redactedTraceContext },
9037
+ updatedAt: Math.max(existing.updatedAt, timestamp)
9038
+ };
9039
+ await writeJsonFileAtomic(this.sessionManifestPath(sessionId), patched);
9040
+ });
9041
+ }
8945
9042
  async reconcileSessionManifest(sessionId) {
8946
9043
  const session = await this.getSession(sessionId);
8947
9044
  if (session === void 0) {
@@ -9800,6 +9897,7 @@ function toPersistedLocalBrowserSessionRecord(workspace, live) {
9800
9897
  version: 1,
9801
9898
  provider: "local",
9802
9899
  ...workspace === void 0 ? {} : { workspace },
9900
+ ownership: live.ownership,
9803
9901
  engine: live.engine,
9804
9902
  ...live.endpoint === void 0 ? {} : { endpoint: live.endpoint },
9805
9903
  ...live.baseUrl === void 0 ? {} : { baseUrl: live.baseUrl },
@@ -9815,6 +9913,7 @@ function toPersistedLocalBrowserSessionRecord(workspace, live) {
9815
9913
  function toWorkspaceLiveBrowserRecord(record) {
9816
9914
  return {
9817
9915
  mode: "persistent",
9916
+ ownership: getPersistedLocalBrowserSessionOwnership(record),
9818
9917
  engine: record.engine,
9819
9918
  ...record.endpoint === void 0 ? {} : { endpoint: record.endpoint },
9820
9919
  ...record.baseUrl === void 0 ? {} : { baseUrl: record.baseUrl },
@@ -9841,7 +9940,12 @@ function isAttachBrowserOptions(browser) {
9841
9940
  async function resolveAttachEndpoint(browser) {
9842
9941
  const endpoint = browser?.endpoint?.trim();
9843
9942
  if (endpoint && endpoint.length > 0) {
9844
- return endpoint;
9943
+ const inspected = await inspectCdpEndpoint({
9944
+ endpoint,
9945
+ ...browser?.headers === void 0 ? {} : { headers: browser.headers },
9946
+ timeoutMs: DEFAULT_TIMEOUT_MS
9947
+ });
9948
+ return inspected.endpoint;
9845
9949
  }
9846
9950
  const selection = await selectAttachBrowserCandidate({
9847
9951
  timeoutMs: DEFAULT_TIMEOUT_MS
@@ -10105,12 +10209,12 @@ async function waitForProcessExit(pid, timeoutMs) {
10105
10209
  }
10106
10210
  const deadline = Date.now() + timeoutMs;
10107
10211
  while (Date.now() < deadline) {
10108
- if (!isProcessRunning(pid)) {
10212
+ if (!isProcessRunning2(pid)) {
10109
10213
  return true;
10110
10214
  }
10111
10215
  await sleep2(50);
10112
10216
  }
10113
- return !isProcessRunning(pid);
10217
+ return !isProcessRunning2(pid);
10114
10218
  }
10115
10219
  function resolveAbpSessionDir(workspace) {
10116
10220
  return path12__default.default.join(workspace.livePath, "abp-session");
@@ -10267,7 +10371,7 @@ var init_browser_manager = __esm({
10267
10371
  }
10268
10372
  const liveRecord = await this.readLivePersistentBrowser(await this.ensureWorkspaceStore());
10269
10373
  return {
10270
- mode: this.mode,
10374
+ mode: liveRecord?.ownership === "attached" ? "attach" : this.mode,
10271
10375
  engine: liveRecord?.engine ?? this.engineName,
10272
10376
  ...this.workspace === void 0 ? {} : { workspace: this.workspace },
10273
10377
  live: liveRecord !== void 0
@@ -10395,6 +10499,7 @@ var init_browser_manager = __esm({
10395
10499
  });
10396
10500
  const liveRecord = {
10397
10501
  mode: "persistent",
10502
+ ownership: "owned",
10398
10503
  engine: "abp",
10399
10504
  baseUrl: launched.baseUrl,
10400
10505
  remoteDebuggingUrl: launched.remoteDebuggingUrl,
@@ -10481,11 +10586,78 @@ var init_browser_manager = __esm({
10481
10586
  }
10482
10587
  async createAttachEngine() {
10483
10588
  const endpoint = await resolveAttachEndpoint(this.browserOptions);
10484
- return this.createAttachedEngine({
10485
- endpoint,
10486
- ...this.browserOptions?.headers === void 0 ? {} : { headers: this.browserOptions.headers },
10487
- freshTab: this.browserOptions?.freshTab ?? true,
10488
- onDispose: async () => void 0
10589
+ if (this.workspace === void 0) {
10590
+ return this.createAttachedEngine({
10591
+ endpoint,
10592
+ ...this.browserOptions?.headers === void 0 ? {} : { headers: this.browserOptions.headers },
10593
+ freshTab: this.browserOptions?.freshTab ?? true,
10594
+ onDispose: async () => void 0
10595
+ });
10596
+ }
10597
+ const workspace = await this.ensureWorkspaceStore();
10598
+ return workspace.lock(async () => {
10599
+ const live = await this.readLivePersistentBrowser(workspace);
10600
+ if (live) {
10601
+ if (live.engine !== "playwright") {
10602
+ throw new Error(
10603
+ `workspace "${this.workspace}" already has a live ${live.engine} browser. Close it before attaching a Playwright browser.`
10604
+ );
10605
+ }
10606
+ if (live.ownership !== "attached") {
10607
+ throw new Error(
10608
+ `workspace "${this.workspace}" already has a live Opensteer-owned browser. Close it before attaching another browser.`
10609
+ );
10610
+ }
10611
+ if (live.endpoint === void 0) {
10612
+ throw new Error("workspace live browser record is missing a DevTools endpoint.");
10613
+ }
10614
+ if (live.endpoint !== endpoint) {
10615
+ throw new Error(
10616
+ `workspace "${this.workspace}" is already attached to a different browser endpoint. Close it before reattaching.`
10617
+ );
10618
+ }
10619
+ await bestEffortRegisterLocalViewSession({
10620
+ rootPath: workspace.rootPath,
10621
+ ...this.workspace === void 0 ? {} : { workspace: this.workspace },
10622
+ live: toPersistedLocalBrowserSessionRecord(this.workspace, live),
10623
+ ownership: "attached"
10624
+ });
10625
+ return this.createAttachedEngine({
10626
+ endpoint: live.endpoint,
10627
+ ...this.browserOptions?.headers === void 0 ? {} : { headers: this.browserOptions.headers },
10628
+ freshTab: this.browserOptions?.freshTab ?? true,
10629
+ onDispose: async () => void 0
10630
+ });
10631
+ }
10632
+ const liveRecord = {
10633
+ mode: "persistent",
10634
+ ownership: "attached",
10635
+ engine: "playwright",
10636
+ endpoint,
10637
+ pid: 0,
10638
+ startedAt: Date.now(),
10639
+ userDataDir: workspace.browserUserDataDir
10640
+ };
10641
+ await this.writeLivePersistentBrowser(workspace, liveRecord);
10642
+ const persistedLiveRecord = toPersistedLocalBrowserSessionRecord(this.workspace, liveRecord);
10643
+ await bestEffortRegisterLocalViewSession({
10644
+ rootPath: workspace.rootPath,
10645
+ ...this.workspace === void 0 ? {} : { workspace: this.workspace },
10646
+ live: persistedLiveRecord,
10647
+ ownership: "attached"
10648
+ });
10649
+ try {
10650
+ return await this.createAttachedEngine({
10651
+ endpoint,
10652
+ ...this.browserOptions?.headers === void 0 ? {} : { headers: this.browserOptions.headers },
10653
+ freshTab: this.browserOptions?.freshTab ?? true,
10654
+ onDispose: async () => void 0
10655
+ });
10656
+ } catch (error) {
10657
+ await this.unregisterLocalViewSessionForRecord(workspace.rootPath, persistedLiveRecord);
10658
+ await clearPersistedSessionRecord(workspace.rootPath, "local").catch(() => void 0);
10659
+ throw error;
10660
+ }
10489
10661
  });
10490
10662
  }
10491
10663
  async createPersistentEngine() {
@@ -10505,7 +10677,7 @@ var init_browser_manager = __esm({
10505
10677
  rootPath: workspace.rootPath,
10506
10678
  ...this.workspace === void 0 ? {} : { workspace: this.workspace },
10507
10679
  live: toPersistedLocalBrowserSessionRecord(this.workspace, live),
10508
- ownership: "owned"
10680
+ ownership: live.ownership
10509
10681
  });
10510
10682
  return this.createAttachedEngine({
10511
10683
  endpoint: live.endpoint,
@@ -10521,6 +10693,7 @@ var init_browser_manager = __esm({
10521
10693
  });
10522
10694
  const liveRecord = {
10523
10695
  mode: "persistent",
10696
+ ownership: "owned",
10524
10697
  engine: "playwright",
10525
10698
  endpoint: launched.endpoint,
10526
10699
  pid: launched.pid,
@@ -10650,7 +10823,20 @@ var init_browser_manager = __esm({
10650
10823
  if (live === void 0) {
10651
10824
  return void 0;
10652
10825
  }
10653
- if (!isProcessRunning(live.pid)) {
10826
+ if (live.ownership === "attached") {
10827
+ const attachedRecord = toPersistedLocalBrowserSessionRecord(this.workspace, live);
10828
+ if (!await isAttachedLocalBrowserSessionReachable(attachedRecord)) {
10829
+ await this.unregisterLocalViewSessionForRecord(workspace.rootPath, attachedRecord);
10830
+ await clearPersistedSessionRecord(workspace.rootPath, "local").catch(() => void 0);
10831
+ return void 0;
10832
+ }
10833
+ return live;
10834
+ }
10835
+ if (!isProcessRunning2(live.pid)) {
10836
+ await this.unregisterLocalViewSessionForRecord(
10837
+ workspace.rootPath,
10838
+ toPersistedLocalBrowserSessionRecord(this.workspace, live)
10839
+ );
10654
10840
  await clearPersistedSessionRecord(workspace.rootPath, "local").catch(() => void 0);
10655
10841
  return void 0;
10656
10842
  }
@@ -10698,6 +10884,10 @@ var init_browser_manager = __esm({
10698
10884
  workspace.rootPath,
10699
10885
  toPersistedLocalBrowserSessionRecord(this.workspace, live)
10700
10886
  );
10887
+ if (live.ownership === "attached") {
10888
+ await clearPersistedSessionRecord(workspace.rootPath, "local").catch(() => void 0);
10889
+ return;
10890
+ }
10701
10891
  if (live.engine === "playwright") {
10702
10892
  if (live.endpoint !== void 0) {
10703
10893
  await requestBrowserClose(live.endpoint).catch(() => void 0);
@@ -10726,10 +10916,18 @@ var init_browser_manager = __esm({
10726
10916
  }
10727
10917
  async unregisterLocalViewSessionForRecord(rootPath, record) {
10728
10918
  await bestEffortUnregisterLocalViewSession(
10729
- buildLocalViewSessionId({
10919
+ getPersistedLocalBrowserSessionOwnership(record) === "attached" ? buildLocalViewSessionId({
10730
10920
  rootPath,
10731
- pid: record.pid,
10732
- startedAt: record.startedAt
10921
+ startedAt: record.startedAt,
10922
+ ownership: "attached",
10923
+ ...record.endpoint === void 0 ? {} : { endpoint: record.endpoint },
10924
+ ...record.baseUrl === void 0 ? {} : { baseUrl: record.baseUrl },
10925
+ ...record.remoteDebuggingUrl === void 0 ? {} : { remoteDebuggingUrl: record.remoteDebuggingUrl }
10926
+ }) : buildLocalViewSessionId({
10927
+ rootPath,
10928
+ startedAt: record.startedAt,
10929
+ ownership: "owned",
10930
+ pid: record.pid
10733
10931
  })
10734
10932
  );
10735
10933
  }
@@ -10833,13 +11031,13 @@ async function resolveSessionSummary(manifest) {
10833
11031
  return {
10834
11032
  sessionId: manifest.sessionId,
10835
11033
  label: manifest.workspace ?? (path12__default.default.basename(manifest.rootPath) || manifest.sessionId),
10836
- status: isProcessRunning(record.pid) ? "live" : "stale",
11034
+ status: getPersistedLocalBrowserSessionOwnership(record) === "attached" || isProcessRunning2(record.pid) ? "live" : "stale",
10837
11035
  ...manifest.workspace === void 0 ? {} : { workspace: manifest.workspace },
10838
11036
  rootPath: manifest.rootPath,
10839
11037
  engine: record.engine,
10840
11038
  ownership: manifest.ownership,
10841
- pid: record.pid,
10842
11039
  startedAt: record.startedAt,
11040
+ ...record.pid > 0 ? { pid: record.pid } : {},
10843
11041
  ...browserName === void 0 ? {} : { browserName }
10844
11042
  };
10845
11043
  }
@@ -10867,7 +11065,16 @@ async function readLiveRecord(manifest) {
10867
11065
  if (!record) {
10868
11066
  return void 0;
10869
11067
  }
10870
- if (record.pid !== manifest.pid || record.startedAt !== manifest.startedAt || !isProcessRunning(record.pid)) {
11068
+ if (buildLocalViewSessionIdForRecord({
11069
+ rootPath: manifest.rootPath,
11070
+ live: record
11071
+ }) !== manifest.sessionId || record.startedAt !== manifest.startedAt || record.engine !== manifest.engine || getPersistedLocalBrowserSessionOwnership(record) !== manifest.ownership) {
11072
+ return void 0;
11073
+ }
11074
+ if (getPersistedLocalBrowserSessionOwnership(record) === "attached") {
11075
+ return await isAttachedLocalBrowserSessionReachable(record) ? record : void 0;
11076
+ }
11077
+ if (record.pid !== manifest.pid || !isProcessRunning2(record.pid)) {
10871
11078
  return void 0;
10872
11079
  }
10873
11080
  return record;
@@ -11161,6 +11368,172 @@ init_process_owner();
11161
11368
  init_service_state();
11162
11369
  init_service_state();
11163
11370
 
11371
+ // src/local-view/browser-target-order.ts
11372
+ async function readPageTargetId(page) {
11373
+ const cdp = await page.context().newCDPSession(page);
11374
+ try {
11375
+ const result = await cdp.send("Target.getTargetInfo");
11376
+ const targetId = result?.targetInfo?.targetId;
11377
+ return typeof targetId === "string" && targetId.length > 0 ? targetId : null;
11378
+ } finally {
11379
+ await cdp.detach().catch(() => void 0);
11380
+ }
11381
+ }
11382
+ async function readBrowserPageTargetOrder(browserContext) {
11383
+ const browser = browserContext.browser();
11384
+ if (!browser || !hasBrowserCdpSession(browser)) {
11385
+ return [];
11386
+ }
11387
+ const cdp = await browser.newBrowserCDPSession();
11388
+ try {
11389
+ const result = await cdp.send("Target.getTargets");
11390
+ return normalizeBrowserPageTargetOrder(result?.targetInfos ?? []);
11391
+ } catch {
11392
+ return [];
11393
+ } finally {
11394
+ await cdp.detach().catch(() => void 0);
11395
+ }
11396
+ }
11397
+ async function orderPagesByBrowserTargetOrder(browserContext, pages) {
11398
+ if (pages.length < 2) {
11399
+ return pages;
11400
+ }
11401
+ const orderedTargetIds = await readBrowserPageTargetOrder(browserContext);
11402
+ if (orderedTargetIds.length === 0) {
11403
+ return pages;
11404
+ }
11405
+ const rankByTargetId = new Map(orderedTargetIds.map((targetId, index) => [targetId, index]));
11406
+ const targetIds = await Promise.all(
11407
+ pages.map((page) => readPageTargetId(page).catch(() => null))
11408
+ );
11409
+ return pages.map((page, index) => {
11410
+ const targetId = targetIds[index] ?? void 0;
11411
+ return {
11412
+ page,
11413
+ index,
11414
+ rank: targetId === void 0 ? void 0 : rankByTargetId.get(targetId)
11415
+ };
11416
+ }).sort((left, right) => {
11417
+ if (left.rank !== void 0 && right.rank !== void 0) {
11418
+ return left.rank - right.rank;
11419
+ }
11420
+ if (left.rank !== void 0) {
11421
+ return -1;
11422
+ }
11423
+ if (right.rank !== void 0) {
11424
+ return 1;
11425
+ }
11426
+ return left.index - right.index;
11427
+ }).map((entry) => entry.page);
11428
+ }
11429
+ function hasBrowserCdpSession(browser) {
11430
+ return typeof browser.newBrowserCDPSession === "function";
11431
+ }
11432
+ function normalizeBrowserPageTargetOrder(targetInfos) {
11433
+ const reversedPageInfos = [];
11434
+ for (const targetInfo of targetInfos) {
11435
+ if (targetInfo.type !== "page") {
11436
+ continue;
11437
+ }
11438
+ const targetId = typeof targetInfo.targetId === "string" && targetInfo.targetId.length > 0 ? targetInfo.targetId : void 0;
11439
+ if (targetId === void 0) {
11440
+ continue;
11441
+ }
11442
+ reversedPageInfos.push({
11443
+ targetId,
11444
+ openerId: typeof targetInfo.openerId === "string" && targetInfo.openerId.length > 0 ? targetInfo.openerId : void 0
11445
+ });
11446
+ }
11447
+ reversedPageInfos.reverse();
11448
+ const rawTargetInfoById = new Map(
11449
+ reversedPageInfos.map((targetInfo) => [targetInfo.targetId, targetInfo])
11450
+ );
11451
+ const targetInfoById = new Map(
11452
+ reversedPageInfos.map(
11453
+ (targetInfo) => [
11454
+ targetInfo.targetId,
11455
+ {
11456
+ ...targetInfo,
11457
+ openerId: resolveAcyclicOpenerId(targetInfo.targetId, rawTargetInfoById)
11458
+ }
11459
+ ]
11460
+ )
11461
+ );
11462
+ const orderedTargetIds = [];
11463
+ const placed = /* @__PURE__ */ new Set();
11464
+ const placeTarget = (targetId) => {
11465
+ if (placed.has(targetId)) {
11466
+ return;
11467
+ }
11468
+ const targetInfo = targetInfoById.get(targetId);
11469
+ if (!targetInfo) {
11470
+ return;
11471
+ }
11472
+ const openerId = targetInfo.openerId;
11473
+ if (openerId === void 0) {
11474
+ orderedTargetIds.push(targetId);
11475
+ placed.add(targetId);
11476
+ return;
11477
+ }
11478
+ placeTarget(openerId);
11479
+ const openerIndex = orderedTargetIds.indexOf(openerId);
11480
+ const insertionIndex = openerIndex === -1 ? orderedTargetIds.length : findPopupInsertionIndex(orderedTargetIds, openerIndex, openerId, targetInfoById);
11481
+ orderedTargetIds.splice(insertionIndex, 0, targetId);
11482
+ placed.add(targetId);
11483
+ };
11484
+ for (const targetInfo of reversedPageInfos) {
11485
+ placeTarget(targetInfo.targetId);
11486
+ }
11487
+ return orderedTargetIds;
11488
+ }
11489
+ function resolveAcyclicOpenerId(targetId, targetInfoById) {
11490
+ const openerId = targetInfoById.get(targetId)?.openerId;
11491
+ if (openerId === void 0 || !targetInfoById.has(openerId)) {
11492
+ return void 0;
11493
+ }
11494
+ const visitedTargetIds = /* @__PURE__ */ new Set([targetId]);
11495
+ let currentTargetId = openerId;
11496
+ while (currentTargetId !== void 0) {
11497
+ if (visitedTargetIds.has(currentTargetId)) {
11498
+ return void 0;
11499
+ }
11500
+ visitedTargetIds.add(currentTargetId);
11501
+ currentTargetId = targetInfoById.get(currentTargetId)?.openerId;
11502
+ }
11503
+ return openerId;
11504
+ }
11505
+ function findPopupInsertionIndex(orderedTargetIds, openerIndex, openerTargetId, targetInfoById) {
11506
+ let index = openerIndex + 1;
11507
+ while (index < orderedTargetIds.length) {
11508
+ const candidateTargetId = orderedTargetIds[index];
11509
+ if (!candidateTargetId || !isDescendantTarget(candidateTargetId, openerTargetId, targetInfoById)) {
11510
+ break;
11511
+ }
11512
+ index += 1;
11513
+ }
11514
+ return index;
11515
+ }
11516
+ function isDescendantTarget(targetId, ancestorTargetId, targetInfoById) {
11517
+ const visitedTargetIds = /* @__PURE__ */ new Set();
11518
+ let currentTargetId = targetId;
11519
+ while (currentTargetId !== void 0) {
11520
+ if (visitedTargetIds.has(currentTargetId)) {
11521
+ return false;
11522
+ }
11523
+ visitedTargetIds.add(currentTargetId);
11524
+ const currentTargetInfo = targetInfoById.get(currentTargetId);
11525
+ const openerId = currentTargetInfo?.openerId;
11526
+ if (openerId === void 0) {
11527
+ return false;
11528
+ }
11529
+ if (openerId === ancestorTargetId) {
11530
+ return true;
11531
+ }
11532
+ currentTargetId = openerId;
11533
+ }
11534
+ return false;
11535
+ }
11536
+
11164
11537
  // src/local-view/tab-state-tracker.ts
11165
11538
  var ACTIVATION_INTENT_DISCOVERY_GRACE_MS = 2e3;
11166
11539
  var TabStateTracker = class {
@@ -11176,6 +11549,7 @@ var TabStateTracker = class {
11176
11549
  boundContextCleanup = null;
11177
11550
  constructor(deps) {
11178
11551
  this.deps = deps;
11552
+ this.lastActivePage = deps.initialActivePage ?? null;
11179
11553
  }
11180
11554
  start() {
11181
11555
  if (this.running) {
@@ -11300,7 +11674,7 @@ var TabStateTracker = class {
11300
11674
  this.updatePolling(pages.length);
11301
11675
  const preferredActivePage = this.lastActivePage ?? pages[0] ?? null;
11302
11676
  const pageStates = await Promise.all(
11303
- pages.map(async (page, index) => {
11677
+ pages.map(async (page, originalIndex) => {
11304
11678
  const metadata = await this.readPageMetadata(page, {
11305
11679
  refresh: args.refreshMetadata
11306
11680
  });
@@ -11310,7 +11684,7 @@ var TabStateTracker = class {
11310
11684
  };
11311
11685
  return {
11312
11686
  page,
11313
- index,
11687
+ originalIndex,
11314
11688
  targetId: metadata.targetId,
11315
11689
  url: page.url(),
11316
11690
  title: metadata.title,
@@ -11319,17 +11693,18 @@ var TabStateTracker = class {
11319
11693
  };
11320
11694
  })
11321
11695
  );
11696
+ const orderedPageStates = await this.orderPageStates(pageStates);
11322
11697
  const activePage = this.pickActivePage(
11323
- pageStates,
11698
+ orderedPageStates,
11324
11699
  this.lastActivePage,
11325
11700
  preferredActivePage,
11326
- this.resolveIntentPage(pageStates)
11701
+ this.resolveIntentPage(orderedPageStates)
11327
11702
  );
11328
11703
  if (activePage && activePage !== this.lastActivePage) {
11329
11704
  this.lastActivePage = activePage;
11330
11705
  this.deps.onActivePageChanged(activePage);
11331
11706
  }
11332
- const tabs = pageStates.map((state) => ({
11707
+ const tabs = orderedPageStates.map((state) => ({
11333
11708
  index: state.index,
11334
11709
  ...state.targetId === void 0 ? {} : { targetId: state.targetId },
11335
11710
  url: state.url,
@@ -11379,18 +11754,11 @@ var TabStateTracker = class {
11379
11754
  if (cached) {
11380
11755
  return cached;
11381
11756
  }
11382
- const cdp = await page.context().newCDPSession(page);
11383
- try {
11384
- const result = await cdp.send("Target.getTargetInfo");
11385
- const targetId = result?.targetInfo?.targetId;
11386
- if (typeof targetId === "string" && targetId.length > 0) {
11387
- this.targetIdByPage.set(page, targetId);
11388
- return targetId;
11389
- }
11390
- return null;
11391
- } finally {
11392
- await cdp.detach().catch(() => void 0);
11757
+ const targetId = await readPageTargetId(page);
11758
+ if (targetId) {
11759
+ this.targetIdByPage.set(page, targetId);
11393
11760
  }
11761
+ return targetId;
11394
11762
  }
11395
11763
  async readFocusState(page) {
11396
11764
  try {
@@ -11458,6 +11826,33 @@ var TabStateTracker = class {
11458
11826
  this.deps.runtimeState.clearPageActivationIntent(this.deps.sessionId, intent.targetId);
11459
11827
  return { page: matched.page };
11460
11828
  }
11829
+ async orderPageStates(pageStates) {
11830
+ if (pageStates.length < 2) {
11831
+ return pageStates.map(({ originalIndex: _originalIndex, ...state }, index) => ({
11832
+ ...state,
11833
+ index
11834
+ }));
11835
+ }
11836
+ const orderedTargetIds = await readBrowserPageTargetOrder(this.deps.browserContext);
11837
+ const rankByTargetId = new Map(orderedTargetIds.map((targetId, index) => [targetId, index]));
11838
+ return [...pageStates].sort((left, right) => {
11839
+ const leftRank = left.targetId === void 0 ? void 0 : rankByTargetId.get(left.targetId);
11840
+ const rightRank = right.targetId === void 0 ? void 0 : rankByTargetId.get(right.targetId);
11841
+ if (leftRank !== void 0 && rightRank !== void 0) {
11842
+ return leftRank - rightRank;
11843
+ }
11844
+ if (leftRank !== void 0) {
11845
+ return -1;
11846
+ }
11847
+ if (rightRank !== void 0) {
11848
+ return 1;
11849
+ }
11850
+ return left.originalIndex - right.originalIndex;
11851
+ }).map(({ originalIndex: _originalIndex, ...state }, index) => ({
11852
+ ...state,
11853
+ index
11854
+ }));
11855
+ }
11461
11856
  };
11462
11857
 
11463
11858
  // src/local-view/view-stream-capture-policy.ts
@@ -11821,6 +12216,7 @@ var SessionViewStreamProducer = class {
11821
12216
  sessionId: this.deps.sessionId,
11822
12217
  pollMs: TAB_STATE_POLL_MS,
11823
12218
  runtimeState: this.deps.runtimeState,
12219
+ initialActivePage: session.page,
11824
12220
  onActivePageChanged: (page) => {
11825
12221
  this.activePage = page;
11826
12222
  void this.queueBindToPage(page).catch(() => void 0);
@@ -11919,7 +12315,20 @@ var SessionViewStreamProducer = class {
11919
12315
  if (!context) {
11920
12316
  throw new Error("Connected browser did not expose a Chromium browser context.");
11921
12317
  }
11922
- const page = context.pages()[0] ?? await context.newPage();
12318
+ const existingPages = context.pages();
12319
+ if (existingPages.length === 0) {
12320
+ const page2 = await context.newPage();
12321
+ return {
12322
+ browser,
12323
+ context,
12324
+ page: page2
12325
+ };
12326
+ }
12327
+ const orderedPages = await orderPagesByBrowserTargetOrder(context, existingPages);
12328
+ const page = await resolvePersistedActivePage(orderedPages, {
12329
+ ...resolved.record.activePageUrl === void 0 ? {} : { activePageUrl: resolved.record.activePageUrl },
12330
+ ...resolved.record.activePageTitle === void 0 ? {} : { activePageTitle: resolved.record.activePageTitle }
12331
+ }) ?? orderedPages[0];
11923
12332
  return {
11924
12333
  browser,
11925
12334
  context,
@@ -12264,6 +12673,28 @@ async function disconnectPlaywrightChromiumBrowser(browser) {
12264
12673
  const { disconnectPlaywrightChromiumBrowser: disconnect } = await import('@opensteer/engine-playwright');
12265
12674
  await disconnect(browser);
12266
12675
  }
12676
+ async function resolvePersistedActivePage(pages, input) {
12677
+ if (pages.length === 0) {
12678
+ return null;
12679
+ }
12680
+ if (input.activePageUrl === void 0 && input.activePageTitle === void 0) {
12681
+ return null;
12682
+ }
12683
+ const matchesByUrl = input.activePageUrl === void 0 ? pages : pages.filter((page) => page.url() === input.activePageUrl);
12684
+ if (matchesByUrl.length === 0) {
12685
+ return null;
12686
+ }
12687
+ if (input.activePageTitle === void 0) {
12688
+ return matchesByUrl[0] ?? null;
12689
+ }
12690
+ for (const page of matchesByUrl) {
12691
+ const title = await page.title().catch(() => "");
12692
+ if (title === input.activePageTitle) {
12693
+ return page;
12694
+ }
12695
+ }
12696
+ return matchesByUrl[0] ?? null;
12697
+ }
12267
12698
 
12268
12699
  // src/local-view/server.ts
12269
12700
  var DEFAULT_MAX_FPS = 12;