diff --git a/CHANGELOG.md b/CHANGELOG.md index 17a2e7b20..153364346 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,11 @@ * **browser session model** — replace the browser-facing `--workspace` model with explicit `--session ` on `opencli browser *`. Browser commands now require a session name, `browser bind`/`unbind` use `--session`, and bind no longer accepts `--domain`, `--path-prefix`, or `--allow-navigate-bound`. Browser primitives keep their session tab by design; the browser namespace no longer exposes `--keep-tab`. * **adapter site sessions** — replace adapter metadata `browserSession: { reuse: 'site' }` with `siteSession: 'persistent'`, and replace the user override `--reuse ` / `OPENCLI_BROWSER_REUSE` with `--site-session `. Persistent site sessions keep a stable site tab open without idle expiry. +* **doctor** — remove `--no-live` and `--sessions` flags from `opencli doctor`. Doctor always runs the live browser connectivity probe (that's its core job); session enumeration was never part of health diagnosis. The underlying `'sessions'` daemon protocol action and the `BrowserSessionInfo` public type are removed as dead code. ### Internal +* **extension 1.0.12** — drop `handleSessions` action handler (no remaining consumers after doctor cleanup). * **extension 1.0.11** — switch Browser Bridge lease routing from user-facing workspaces to explicit browser sessions. ## [1.7.16](https://github.com/jackwener/opencli/compare/v1.7.15...v1.7.16) (2026-05-11) diff --git a/extension/dist/background.js b/extension/dist/background.js index c3cd0a4ac..15293f523 100644 --- a/extension/dist/background.js +++ b/extension/dist/background.js @@ -810,7 +810,8 @@ function getSessionFromKey(key) { function getIdleTimeout(key) { const session = automationSessions.get(key); if (session?.kind === "bound") return IDLE_TIMEOUT_NONE; - if (getSurfaceFromKey(key) === "adapter" && (session?.lifecycle === "persistent" || sessionLifecycleOverrides.get(key) === "persistent")) return IDLE_TIMEOUT_NONE; + const adapterPersistent = getSurfaceFromKey(key) === "adapter" && (session?.lifecycle === "persistent" || sessionLifecycleOverrides.get(key) === "persistent"); + if (adapterPersistent) return IDLE_TIMEOUT_NONE; const override = sessionTimeoutOverrides.get(key); if (override !== void 0) return override; return getSurfaceFromKey(key) === "browser" ? IDLE_TIMEOUT_INTERACTIVE : IDLE_TIMEOUT_DEFAULT; @@ -1292,8 +1293,6 @@ async function handleCommand(cmd) { return await handleCloseWindow(cmd, leaseKey); case "cdp": return await handleCdp(cmd, leaseKey); - case "sessions": - return await handleSessions(cmd); case "set-file-input": return await handleSetFileInput(cmd, leaseKey); case "insert-text": @@ -1971,24 +1970,6 @@ async function reconcileTargetLeaseRegistry() { } await persistRuntimeState(); } -async function handleSessions(cmd) { - const now = Date.now(); - const data = await Promise.all([...automationSessions.entries()].map(async ([leaseKey, session]) => ({ - session: session.session, - windowId: session.windowId, - owned: session.owned, - kind: session.kind, - surface: session.surface, - preferredTabId: session.preferredTabId, - contextId: session.contextId, - ownership: session.ownership, - lifecycle: session.lifecycle, - windowRole: session.windowRole, - tabCount: session.preferredTabId !== null ? await chrome.tabs.get(session.preferredTabId).then((tab) => isDebuggableUrl(tab.url) ? 1 : 0).catch(() => 0) : (await chrome.tabs.query({ windowId: session.windowId })).filter((tab) => isDebuggableUrl(tab.url)).length, - idleMsRemaining: session.idleDeadlineAt <= 0 ? null : Math.max(0, session.idleDeadlineAt - now) - }))); - return { id: cmd.id, ok: true, data }; -} async function handleBind(cmd, leaseKey) { const existing = automationSessions.get(leaseKey); if (existing?.owned) { diff --git a/extension/manifest.json b/extension/manifest.json index d130c9171..2136e5559 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "OpenCLI", - "version": "1.0.11", + "version": "1.0.12", "description": "Browser automation bridge for the OpenCLI CLI tool. Executes commands in Chrome tab leases via a local daemon.", "permissions": [ "debugger", diff --git a/extension/package.json b/extension/package.json index ebd8a6a02..da65b4166 100644 --- a/extension/package.json +++ b/extension/package.json @@ -1,6 +1,6 @@ { "name": "opencli-extension", - "version": "1.0.11", + "version": "1.0.12", "private": true, "opencli": { "compatRange": ">=1.7.0" diff --git a/extension/src/background.test.ts b/extension/src/background.test.ts index 81181d4b0..9a542e19c 100644 --- a/extension/src/background.test.ts +++ b/extension/src/background.test.ts @@ -568,22 +568,6 @@ describe('background tab isolation', () => { expect(mod.__test__.isTargetUrl('https://example.com/app/', 'https://example.com/app')).toBe(false); }); - it('reports sessions per session', async () => { - const { chrome } = createChromeMock(); - vi.stubGlobal('chrome', chrome); - - const mod = await import('./background'); - mod.__test__.setAutomationWindowId(adapterKey('twitter'), 1); - mod.__test__.setAutomationWindowId(adapterKey('zhihu'), 2); - - const result = await mod.__test__.handleSessions({ id: '3', action: 'sessions' }); - expect(result.ok).toBe(true); - expect(result.data).toEqual(expect.arrayContaining([ - expect.objectContaining({ session: 'twitter', surface: 'adapter', windowId: 1 }), - expect.objectContaining({ session: 'zhihu', surface: 'adapter', windowId: 2 }), - ])); - }); - it('returns the persisted profile contextId from popup status', async () => { const { chrome } = createChromeMock(); await chrome.storage.local.set({ opencli_context_id_v1: 'abc123xy' }); @@ -1292,11 +1276,12 @@ describe('background tab isolation', () => { // Default for browser:* is 10 min expect(mod.__test__.getIdleTimeout(browserKey('default'))).toBe(600_000); - // Send a command with custom idleTimeout (in seconds) + // Send a benign command with custom idleTimeout (in seconds) await mod.__test__.handleCommand({ id: 'custom-1', - action: 'sessions', + action: 'cookies', session: browserKey('default'), + domain: 'example.com', idleTimeout: 120, }); diff --git a/extension/src/background.ts b/extension/src/background.ts index 1fbd87771..abdf0ed45 100644 --- a/extension/src/background.ts +++ b/extension/src/background.ts @@ -867,8 +867,6 @@ async function handleCommand(cmd: Command): Promise { return await handleCloseWindow(cmd, leaseKey); case 'cdp': return await handleCdp(cmd, leaseKey); - case 'sessions': - return await handleSessions(cmd); case 'set-file-input': return await handleSetFileInput(cmd, leaseKey); case 'insert-text': @@ -1667,27 +1665,6 @@ async function reconcileTargetLeaseRegistry(): Promise { await persistRuntimeState(); } -async function handleSessions(cmd: Command): Promise { - const now = Date.now(); - const data = await Promise.all([...automationSessions.entries()].map(async ([leaseKey, session]) => ({ - session: session.session, - windowId: session.windowId, - owned: session.owned, - kind: session.kind, - surface: session.surface, - preferredTabId: session.preferredTabId, - contextId: session.contextId, - ownership: session.ownership, - lifecycle: session.lifecycle, - windowRole: session.windowRole, - tabCount: session.preferredTabId !== null - ? (await chrome.tabs.get(session.preferredTabId).then((tab) => isDebuggableUrl(tab.url) ? 1 : 0).catch(() => 0)) - : (await chrome.tabs.query({ windowId: session.windowId })).filter((tab) => isDebuggableUrl(tab.url)).length, - idleMsRemaining: session.idleDeadlineAt <= 0 ? null : Math.max(0, session.idleDeadlineAt - now), - }))); - return { id: cmd.id, ok: true, data }; -} - async function handleBind(cmd: Command, leaseKey: string): Promise { const existing = automationSessions.get(leaseKey); if (existing?.owned) { @@ -1734,7 +1711,6 @@ export const __test__ = { handleNavigate, isTargetUrl, handleTabs, - handleSessions, handleBind, resolveTabId, resetWindowIdleTimer, diff --git a/skills/opencli-usage/SKILL.md b/skills/opencli-usage/SKILL.md index 579e4fb73..54f75957d 100644 --- a/skills/opencli-usage/SKILL.md +++ b/skills/opencli-usage/SKILL.md @@ -28,7 +28,7 @@ cd OpenCLI && npm install npx tsx src/main.ts # same surface, no global install ``` -`opencli doctor` prints a structured `DoctorReport` — daemon status, extension connection, version checks. Scope is narrow: it diagnoses the **browser bridge** (daemon + extension + Chrome wiring). `PUBLIC` / `LOCAL` adapters, `opencli list`, `validate`, `verify`, plugin commands, and external-CLI passthrough don't need it to be green — only `COOKIE` / `INTERCEPT` / `UI` adapters and the `opencli browser *` subcommands do. Flags: `--no-live` (skip live browser test), `--sessions` (list active automation sessions), `-v` (verbose). +`opencli doctor` prints a structured `DoctorReport` — daemon status, extension connection, version checks, and a live browser connectivity probe. Scope is narrow: it diagnoses the **browser bridge** (daemon + extension + Chrome wiring). `PUBLIC` / `LOCAL` adapters, `opencli list`, `validate`, `verify`, plugin commands, and external-CLI passthrough don't need it to be green — only `COOKIE` / `INTERCEPT` / `UI` adapters and the `opencli browser *` subcommands do. Flag: `-v` (verbose). ## Prerequisites by command type diff --git a/src/browser/daemon-client.ts b/src/browser/daemon-client.ts index 2e270dea9..f2a2c0672 100644 --- a/src/browser/daemon-client.ts +++ b/src/browser/daemon-client.ts @@ -5,7 +5,6 @@ */ import { DEFAULT_DAEMON_PORT } from '../constants.js'; -import type { BrowserSessionInfo } from '../types.js'; import { sleep } from '../utils.js'; import { classifyBrowserError } from './errors.js'; import { resolveProfileContextId } from './profile.js'; @@ -22,7 +21,7 @@ function generateId(): string { export interface DaemonCommand { id: string; - action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'sessions' | 'set-file-input' | 'insert-text' | 'bind' | 'network-capture-start' | 'network-capture-read' | 'wait-download' | 'cdp' | 'frames'; + action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'set-file-input' | 'insert-text' | 'bind' | 'network-capture-start' | 'network-capture-read' | 'wait-download' | 'cdp' | 'frames'; /** Target page identity (targetId). Cross-layer contract with the extension. */ page?: string; code?: string; @@ -248,11 +247,6 @@ export async function sendCommandFull( return { data: result.data, page: result.page }; } -export async function listSessions(opts?: { contextId?: string }): Promise { - const result = await sendCommand('sessions', { ...(opts?.contextId && { contextId: opts.contextId }) }); - return Array.isArray(result) ? result : []; -} - export async function bindTab(session: string, opts: { contextId?: string } = {}): Promise { return sendCommand('bind', { session, surface: 'browser', ...opts }); } diff --git a/src/cli.ts b/src/cli.ts index 57a9302b6..7cc73cb2e 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2755,13 +2755,11 @@ cli({ program .command('doctor') .description('Diagnose opencli browser bridge connectivity') - .option('--no-live', 'Skip live browser connectivity test') - .option('--sessions', 'Show active automation sessions', false) .option('-v, --verbose', 'Debug output') .action(async (opts) => { applyVerbose(opts); const { runBrowserDoctor, renderBrowserDoctorReport } = await import('./doctor.js'); - const report = await runBrowserDoctor({ live: opts.live, sessions: opts.sessions, cliVersion: PKG_VERSION }); + const report = await runBrowserDoctor({ cliVersion: PKG_VERSION }); console.log(renderBrowserDoctorReport(report)); }); diff --git a/src/doctor.test.ts b/src/doctor.test.ts index 187a3fedd..88002ea3e 100644 --- a/src/doctor.test.ts +++ b/src/doctor.test.ts @@ -1,8 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -const { mockGetDaemonHealth, mockListSessions, mockConnect, mockClose, mockFindShadowedUserAdapters } = vi.hoisted(() => ({ +const { mockGetDaemonHealth, mockConnect, mockClose, mockFindShadowedUserAdapters } = vi.hoisted(() => ({ mockGetDaemonHealth: vi.fn(), - mockListSessions: vi.fn(), mockConnect: vi.fn(), mockClose: vi.fn(), mockFindShadowedUserAdapters: vi.fn(), @@ -10,7 +9,6 @@ const { mockGetDaemonHealth, mockListSessions, mockConnect, mockClose, mockFindS vi.mock('./browser/daemon-client.js', () => ({ getDaemonHealth: mockGetDaemonHealth, - listSessions: mockListSessions, })); vi.mock('./browser/index.js', () => ({ @@ -36,6 +34,12 @@ describe('doctor report rendering', () => { beforeEach(() => { vi.clearAllMocks(); mockFindShadowedUserAdapters.mockReturnValue([]); + // Doctor always runs live connectivity. Tests that want connect to fail override. + mockConnect.mockResolvedValue({ + evaluate: vi.fn().mockResolvedValue(2), + closeWindow: vi.fn().mockResolvedValue(undefined), + }); + mockClose.mockResolvedValue(undefined); }); it('renders OK-style report when daemon and extension connected', () => { @@ -117,38 +121,7 @@ describe('doctor report rendering', () => { expect(text).toContain('[OK] Connectivity: connected in 1.2s'); }); - it('renders connectivity SKIP when not tested', () => { - const text = strip(renderBrowserDoctorReport({ - daemonRunning: true, - extensionConnected: true, - issues: [], - })); - - expect(text).toContain('[SKIP] Connectivity: skipped (--no-live)'); - }); - - it('renders sessions with tab leases and no idle timer', () => { - const text = strip(renderBrowserDoctorReport({ - daemonRunning: true, - extensionConnected: true, - issues: [], - sessions: [ - { - session: 'default', - windowId: 2, - preferredTabId: 42, - ownership: 'borrowed', - windowRole: 'borrowed-user', - tabCount: 1, - idleMsRemaining: null, - }, - ], - })); - - expect(text).toContain('default → tab 42, mode=borrowed, window=borrowed-user, tabs=1, idle=none'); - }); - - it('renders connected profiles and groups sessions by profile', () => { + it('renders connected profiles when multiple are present', () => { const text = strip(renderBrowserDoctorReport({ daemonRunning: true, extensionConnected: false, @@ -157,36 +130,11 @@ describe('doctor report rendering', () => { { contextId: 'personal', extensionConnected: true, extensionVersion: '1.2.3', pending: 0 }, ], issues: [], - sessions: [ - { - contextId: 'work', - session: 'default', - windowId: 2, - preferredTabId: 42, - ownership: 'borrowed', - windowRole: 'borrowed-user', - tabCount: 1, - idleMsRemaining: null, - }, - { - contextId: 'personal', - session: 'site:foo', - windowId: 1, - preferredTabId: 10, - ownership: 'owned', - windowRole: 'automation', - tabCount: 1, - idleMsRemaining: 1000, - }, - ], })); expect(text).toContain('Profiles:'); expect(text).toContain('work: connected v1.2.3'); - expect(text).toContain('[profile: work]'); - expect(text).toContain('[profile: personal]'); - expect(text).toContain('default → tab 42'); - expect(text).toContain('site:foo → tab 10'); + expect(text).toContain('personal: connected v1.2.3'); }); it('renders unstable extension state when live connectivity and status disagree', () => { @@ -215,29 +163,24 @@ describe('doctor report rendering', () => { expect(text).toContain('Daemon connectivity is unstable.'); }); - it('reports daemon not running when no-live and auto-start fails', async () => { - mockGetDaemonHealth.mockResolvedValueOnce({ state: 'stopped', status: null }); + it('reports daemon not running when connectivity fails and daemon stays stopped', async () => { mockConnect.mockRejectedValueOnce(new Error('Could not start daemon')); mockGetDaemonHealth.mockResolvedValueOnce({ state: 'stopped', status: null }); - const report = await runBrowserDoctor({ live: false }); + const report = await runBrowserDoctor(); expect(report.daemonRunning).toBe(false); expect(report.extensionConnected).toBe(false); - expect(mockGetDaemonHealth).toHaveBeenCalledTimes(2); + expect(report.connectivity?.ok).toBe(false); expect(report.issues).toEqual(expect.arrayContaining([ expect.stringContaining('Daemon is not running'), ])); }); it('reports flapping when live check succeeds but final status shows extension disconnected', async () => { - mockConnect.mockResolvedValueOnce({ - evaluate: vi.fn().mockResolvedValue(2), - }); - mockClose.mockResolvedValueOnce(undefined); mockGetDaemonHealth.mockResolvedValueOnce({ state: 'no-extension', status: { extensionConnected: false } }); - const report = await runBrowserDoctor({ live: true }); + const report = await runBrowserDoctor(); expect(report.daemonRunning).toBe(true); expect(report.extensionConnected).toBe(false); @@ -248,13 +191,9 @@ describe('doctor report rendering', () => { }); it('reports daemon flapping when live check succeeds but daemon disappears afterward', async () => { - mockConnect.mockResolvedValueOnce({ - evaluate: vi.fn().mockResolvedValue(2), - }); - mockClose.mockResolvedValueOnce(undefined); mockGetDaemonHealth.mockResolvedValueOnce({ state: 'stopped', status: null }); - const report = await runBrowserDoctor({ live: true }); + const report = await runBrowserDoctor(); expect(report.daemonRunning).toBe(false); expect(report.daemonFlaky).toBe(true); @@ -274,26 +213,14 @@ describe('doctor report rendering', () => { closeWindow, }; }); - mockClose.mockResolvedValueOnce(undefined); mockGetDaemonHealth.mockResolvedValueOnce({ state: 'ready', status: { extensionConnected: true } }); - await runBrowserDoctor({ live: true }); + await runBrowserDoctor(); expect(timeoutSeen).toBe(8); expect(closeWindow).toHaveBeenCalledTimes(1); }); - it('skips auto-start in no-live mode when daemon is already running', async () => { - mockGetDaemonHealth.mockResolvedValueOnce({ state: 'no-extension', status: { extensionConnected: false } }); - mockGetDaemonHealth.mockResolvedValueOnce({ state: 'no-extension', status: { extensionConnected: false } }); - - const report = await runBrowserDoctor({ live: false }); - - expect(mockConnect).not.toHaveBeenCalled(); - expect(report.daemonRunning).toBe(true); - expect(report.extensionConnected).toBe(false); - }); - it('reports an issue when the extension is connected but does not report a version', async () => { const status = { state: 'ready' as const, @@ -302,11 +229,9 @@ describe('doctor report rendering', () => { extensionVersion: undefined, }, }; - mockGetDaemonHealth - .mockResolvedValueOnce(status) - .mockResolvedValueOnce(status); + mockGetDaemonHealth.mockResolvedValue(status); - const report = await runBrowserDoctor({ live: false }); + const report = await runBrowserDoctor(); expect(report.issues).toEqual(expect.arrayContaining([ expect.stringContaining('did not report a version'), @@ -322,11 +247,9 @@ describe('doctor report rendering', () => { extensionVersion: '1.0.3', }, }; - mockGetDaemonHealth - .mockResolvedValueOnce(status) - .mockResolvedValueOnce(status); + mockGetDaemonHealth.mockResolvedValue(status); - const report = await runBrowserDoctor({ live: false, cliVersion: '1.7.9' }); + const report = await runBrowserDoctor({ cliVersion: '1.7.9' }); expect(report.daemonStale).toBe(true); expect(report.issues).toEqual(expect.arrayContaining([ @@ -343,9 +266,7 @@ describe('doctor report rendering', () => { extensionVersion: '1.0.3', }, }; - mockGetDaemonHealth - .mockResolvedValueOnce(status) - .mockResolvedValueOnce(status); + mockGetDaemonHealth.mockResolvedValue(status); mockFindShadowedUserAdapters.mockReturnValueOnce([ { name: 'instagram/saved', @@ -354,7 +275,7 @@ describe('doctor report rendering', () => { }, ]); - const report = await runBrowserDoctor({ live: false, cliVersion: '1.7.9' }); + const report = await runBrowserDoctor({ cliVersion: '1.7.9' }); expect(report.adapterShadows).toHaveLength(1); expect(report.issues).toEqual(expect.arrayContaining([ @@ -374,11 +295,12 @@ describe('doctor report rendering', () => { ], }, }; - mockGetDaemonHealth - .mockResolvedValueOnce(status) - .mockResolvedValueOnce(status); + mockGetDaemonHealth.mockResolvedValue(status); + // Real connectivity would fail in profile-required state; force it here so + // the test exercises the profile-required issue path, not the flaky path. + mockConnect.mockRejectedValueOnce(new Error('profile required')); - const report = await runBrowserDoctor({ live: false }); + const report = await runBrowserDoctor(); expect(report.profiles).toHaveLength(2); expect(report.issues).toEqual(expect.arrayContaining([ diff --git a/src/doctor.ts b/src/doctor.ts index 7e3a0bc65..f1cc9b0e6 100644 --- a/src/doctor.ts +++ b/src/doctor.ts @@ -7,11 +7,10 @@ import { styleText } from 'node:util'; import { DEFAULT_DAEMON_PORT } from './constants.js'; import { BrowserBridge } from './browser/index.js'; -import { getDaemonHealth, listSessions } from './browser/daemon-client.js'; +import { getDaemonHealth } from './browser/daemon-client.js'; import { getErrorMessage } from './errors.js'; import { getRuntimeLabel } from './runtime-detect.js'; import { getCachedLatestExtensionVersion } from './update-check.js'; -import type { BrowserSessionInfo } from './types.js'; import type { BrowserProfileStatus } from './browser/daemon-client.js'; import { aliasForContextId, loadProfileConfig } from './browser/profile.js'; import { formatDaemonVersion, isDaemonStale, staleDaemonIssue } from './browser/daemon-version.js'; @@ -49,8 +48,6 @@ function satisfiesRange(version: string, range: string): boolean { export type DoctorOptions = { yes?: boolean; - live?: boolean; - sessions?: boolean; cliVersion?: string; }; @@ -72,7 +69,6 @@ export type DoctorReport = { extensionVersion?: string; latestExtensionVersion?: string; connectivity?: ConnectivityResult; - sessions?: BrowserSessionInfo[]; profiles?: BrowserProfileStatus[]; adapterShadows?: AdapterShadow[]; issues: string[]; @@ -100,45 +96,18 @@ export async function checkConnectivity(opts?: { timeout?: number }): Promise { - // Live connectivity check doubles as auto-start (bridge.connect spawns daemon). - let connectivity: ConnectivityResult | undefined; - if (opts.live) { - connectivity = await checkConnectivity(); - } else { - // No live probe — daemon may have idle-exited. Do a minimal auto-start - // so we don't misreport a lazy-lifecycle stop as a real failure. - const initialHealth = await getDaemonHealth(); - if (initialHealth.state === 'stopped') { - try { - const bridge = new BrowserBridge(); - await bridge.connect({ timeout: 5 }); - await bridge.close(); - } catch { - // Auto-start failed; we'll report it below. - } - } - } + // Live connectivity check is the core of doctor — it doubles as auto-start + // (bridge.connect spawns daemon) and validates end-to-end browser bridge health. + const connectivity = await checkConnectivity(); - // Single status read *after* all side-effects (live check / auto-start) settle. + // Single status read *after* connectivity side-effects settle. const health = await getDaemonHealth(); const daemonRunning = health.state !== 'stopped'; const extensionConnected = health.state === 'ready'; - const daemonFlaky = !!(connectivity?.ok && !daemonRunning); - const extensionFlaky = !!(connectivity?.ok && daemonRunning && !extensionConnected); + const daemonFlaky = connectivity.ok && !daemonRunning; + const extensionFlaky = connectivity.ok && daemonRunning && !extensionConnected; const daemonStale = isDaemonStale(health.status, opts.cliVersion); const profiles = health.status?.profiles; - let sessions: BrowserSessionInfo[] | undefined; - if (opts.sessions) { - if (profiles && profiles.length > 0) { - const grouped = await Promise.all(profiles.map(async (profile) => { - const rows = await listSessions({ contextId: profile.contextId }).catch(() => [] as BrowserSessionInfo[]); - return rows.map((row) => ({ ...row, contextId: row.contextId ?? profile.contextId })); - })); - sessions = grouped.flat(); - } else if (health.state === 'ready') { - sessions = await listSessions(); - } - } const extensionVersion = health.status?.extensionVersion; const adapterShadows = findShadowedUserAdapters(); @@ -188,7 +157,7 @@ export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise(); - for (const session of report.sessions) { - const contextId = typeof session.contextId === 'string' && session.contextId ? session.contextId : 'default'; - const rows = byContext.get(contextId) ?? []; - rows.push(session); - byContext.set(contextId, rows); - } - for (const [contextId, rows] of byContext) { - if (byContext.size > 1) lines.push(styleText('dim', ` [profile: ${contextId}]`)); - for (const session of rows) { - const idle = session.idleMsRemaining == null - ? 'none' - : `${Math.ceil(session.idleMsRemaining / 1000)}s`; - const target = session.preferredTabId != null - ? `tab ${session.preferredTabId}` - : `window ${session.windowId ?? 'unknown'}`; - const mode = session.ownership ?? (session.owned === false ? 'borrowed' : 'owned'); - const windowRole = session.windowRole ? `, window=${session.windowRole}` : ''; - lines.push(styleText('dim', ` • ${session.session ?? 'default'} → ${target}, mode=${mode}${windowRole}, tabs=${session.tabCount ?? 0}, idle=${idle}`)); - } - } - } } if (report.issues.length) { diff --git a/src/types.ts b/src/types.ts index fb56af545..7245debdd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -67,23 +67,6 @@ export interface FetchJsonOptions { timeoutMs?: number; } -export interface BrowserSessionInfo { - session?: string; - connected?: boolean; - windowId?: number; - preferredTabId?: number | null; - owned?: boolean; - kind?: 'owned' | 'bound'; - surface?: 'browser' | 'adapter'; - ownership?: 'owned' | 'borrowed'; - lifecycle?: 'ephemeral' | 'persistent' | 'pinned'; - windowRole?: 'interactive' | 'automation' | 'borrowed-user'; - contextId?: string; - tabCount?: number; - idleMsRemaining?: number | null; - [key: string]: unknown; -} - export interface IPage { goto(url: string, options?: { waitUntil?: 'load' | 'none'; settleMs?: number }): Promise; evaluate(js: string): Promise;