@quanta-intellect/vessel-browser 0.1.145 → 0.1.147

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/out/main/index.js CHANGED
@@ -175,12 +175,32 @@ function sanitizeSidebarDetachedBounds(value) {
175
175
  height: Math.max(DETACHED_SIDEBAR_MIN_HEIGHT, Math.round(height))
176
176
  };
177
177
  }
178
+ const DETACHED_DEVTOOLS_DEFAULT_WIDTH = 920;
179
+ const DETACHED_DEVTOOLS_DEFAULT_HEIGHT = 560;
180
+ const DETACHED_DEVTOOLS_MIN_WIDTH = 520;
181
+ const DETACHED_DEVTOOLS_MIN_HEIGHT = 320;
182
+ function sanitizeDevToolsDetachedBounds(value) {
183
+ if (!value || typeof value !== "object") return null;
184
+ const bounds = value;
185
+ const width = Number(bounds.width);
186
+ const height = Number(bounds.height);
187
+ if (!Number.isFinite(width) || !Number.isFinite(height)) return null;
188
+ const x = Number(bounds.x);
189
+ const y = Number(bounds.y);
190
+ return {
191
+ ...Number.isFinite(x) ? { x: Math.round(x) } : {},
192
+ ...Number.isFinite(y) ? { y: Math.round(y) } : {},
193
+ width: Math.max(DETACHED_DEVTOOLS_MIN_WIDTH, Math.round(width)),
194
+ height: Math.max(DETACHED_DEVTOOLS_MIN_HEIGHT, Math.round(height))
195
+ };
196
+ }
178
197
  const defaults = {
179
198
  defaultUrl: "https://start.duckduckgo.com",
180
199
  theme: "dark",
181
200
  sidebarPanelMode: "docked",
182
201
  sidebarWidth: 400,
183
202
  sidebarDetachedBounds: null,
203
+ devtoolsPanelDetachedBounds: null,
184
204
  mcpPort: 3100,
185
205
  autoRestoreSession: true,
186
206
  clearBookmarksOnLaunch: false,
@@ -222,6 +242,15 @@ const SettingsValueSchemas = {
222
242
  height: zod.z.number().int().min(DETACHED_SIDEBAR_MIN_HEIGHT)
223
243
  })
224
244
  ]),
245
+ devtoolsPanelDetachedBounds: zod.z.union([
246
+ zod.z.null(),
247
+ zod.z.object({
248
+ x: zod.z.number().optional(),
249
+ y: zod.z.number().optional(),
250
+ width: zod.z.number().int().min(DETACHED_DEVTOOLS_MIN_WIDTH),
251
+ height: zod.z.number().int().min(DETACHED_DEVTOOLS_MIN_HEIGHT)
252
+ })
253
+ ]),
225
254
  mcpPort: zod.z.number().int().min(1).max(65535),
226
255
  autoRestoreSession: zod.z.boolean(),
227
256
  clearBookmarksOnLaunch: zod.z.boolean(),
@@ -477,6 +506,9 @@ function loadSettings() {
477
506
  sidebarDetachedBounds: sanitizeSidebarDetachedBounds(
478
507
  parsed.sidebarDetachedBounds
479
508
  ),
509
+ devtoolsPanelDetachedBounds: sanitizeDevToolsDetachedBounds(
510
+ parsed.devtoolsPanelDetachedBounds
511
+ ),
480
512
  sourceDoNotAllowList: sanitizeStringList(
481
513
  parsed.sourceDoNotAllowList ?? defaults.sourceDoNotAllowList
482
514
  ),
@@ -530,6 +562,8 @@ function setSetting(key2, value) {
530
562
  settings.sidebarPanelMode = sanitizeSidebarPanelMode(value);
531
563
  } else if (key2 === "sidebarDetachedBounds") {
532
564
  settings.sidebarDetachedBounds = sanitizeSidebarDetachedBounds(value);
565
+ } else if (key2 === "devtoolsPanelDetachedBounds") {
566
+ settings.devtoolsPanelDetachedBounds = sanitizeDevToolsDetachedBounds(value);
533
567
  } else if (key2 === "sourceDoNotAllowList") {
534
568
  settings.sourceDoNotAllowList = sanitizeStringList(value);
535
569
  } else if (key2 === "chatProvider") {
@@ -682,15 +716,6 @@ function loadTrustedAppURL(wc, url) {
682
716
  }
683
717
  return wc.loadURL(parsed.toString());
684
718
  }
685
- const urlSafety = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
686
- __proto__: null,
687
- assertPermittedNavigationURL,
688
- assertSafeURL,
689
- isSafeNavigationURL,
690
- loadInternalDataURL,
691
- loadPermittedNavigationURL,
692
- loadTrustedAppURL
693
- }, Symbol.toStringTag, { value: "Module" }));
694
719
  const MAX_CUSTOM_HISTORY = 50;
695
720
  const READER_MODE_DATA_URL_PREFIX = "data:text/html;charset=utf-8,";
696
721
  const logger$B = createLogger("Tab");
@@ -2598,6 +2623,9 @@ class DevToolsSession {
2598
2623
  entryCounter = 0;
2599
2624
  // Track in-flight network requests for matching response data
2600
2625
  pendingRequests = /* @__PURE__ */ new Map();
2626
+ // Notified whenever a captured buffer mutates so the DevTools panel can
2627
+ // refresh independent of agent actions (e.g. while a user browses).
2628
+ onCaptureChange = null;
2601
2629
  // Named handlers so we can remove them on detach/destroy (fixes listener leak)
2602
2630
  onDetach = () => {
2603
2631
  this.attached = false;
@@ -2613,6 +2641,27 @@ class DevToolsSession {
2613
2641
  get isAttached() {
2614
2642
  return this.attached;
2615
2643
  }
2644
+ /**
2645
+ * Register a callback invoked whenever a captured console/network/error
2646
+ * entry is added or updated. Used to push live updates to the DevTools
2647
+ * panel while a user browses (not just when an agent acts).
2648
+ */
2649
+ setOnCaptureChange(cb) {
2650
+ this.onCaptureChange = cb;
2651
+ }
2652
+ /**
2653
+ * Enable all capture domains (console, network, errors) in one call.
2654
+ * Idempotent per-domain. Used by the panel-open path so manual browsing is
2655
+ * captured without requiring an agent tool invocation.
2656
+ */
2657
+ async enableCapture() {
2658
+ await this.ensureConsoleDomain();
2659
+ await this.ensureNetworkDomain();
2660
+ await this.ensureErrorCapture();
2661
+ }
2662
+ notifyCaptureChange() {
2663
+ this.onCaptureChange?.();
2664
+ }
2616
2665
  async ensureAttached() {
2617
2666
  if (this.attached) return;
2618
2667
  if (this.attachingPromise) return this.attachingPromise;
@@ -2663,6 +2712,7 @@ class DevToolsSession {
2663
2712
  }
2664
2713
  destroy() {
2665
2714
  this.detach();
2715
+ this.onCaptureChange = null;
2666
2716
  this.consoleBuffer = [];
2667
2717
  this.networkBuffer = [];
2668
2718
  this.errorBuffer = [];
@@ -3120,6 +3170,7 @@ class DevToolsSession {
3120
3170
  if (this.consoleBuffer.length > MAX_CONSOLE_ENTRIES) {
3121
3171
  this.consoleBuffer = this.consoleBuffer.slice(-MAX_CONSOLE_ENTRIES);
3122
3172
  }
3173
+ this.notifyCaptureChange();
3123
3174
  }
3124
3175
  // --- Network events ---
3125
3176
  onNetworkRequest(params) {
@@ -3148,6 +3199,7 @@ class DevToolsSession {
3148
3199
  if (oldest !== void 0) this.pendingRequests.delete(oldest);
3149
3200
  }
3150
3201
  this.pendingRequests.set(requestId, { entry });
3202
+ this.notifyCaptureChange();
3151
3203
  }
3152
3204
  onNetworkResponse(params) {
3153
3205
  const requestId = params.requestId;
@@ -3163,6 +3215,7 @@ class DevToolsSession {
3163
3215
  if (contentLength) {
3164
3216
  pending.entry.contentLength = parseInt(contentLength, 10) || void 0;
3165
3217
  }
3218
+ this.notifyCaptureChange();
3166
3219
  }
3167
3220
  onNetworkFinished(params) {
3168
3221
  const requestId = params.requestId;
@@ -3177,6 +3230,7 @@ class DevToolsSession {
3177
3230
  }
3178
3231
  pending.entry.contentLength = pending.entry.contentLength ?? (params.encodedDataLength || void 0);
3179
3232
  this.pendingRequests.delete(requestId);
3233
+ this.notifyCaptureChange();
3180
3234
  }
3181
3235
  onNetworkFailed(params) {
3182
3236
  const requestId = params.requestId;
@@ -3191,6 +3245,7 @@ class DevToolsSession {
3191
3245
  );
3192
3246
  }
3193
3247
  this.pendingRequests.delete(requestId);
3248
+ this.notifyCaptureChange();
3194
3249
  }
3195
3250
  // --- Error events ---
3196
3251
  onExceptionThrown(params) {
@@ -3217,6 +3272,7 @@ class DevToolsSession {
3217
3272
  if (this.errorBuffer.length > MAX_ERROR_ENTRIES) {
3218
3273
  this.errorBuffer = this.errorBuffer.slice(-MAX_ERROR_ENTRIES);
3219
3274
  }
3275
+ this.notifyCaptureChange();
3220
3276
  }
3221
3277
  }
3222
3278
  function mapConsoleLevel(level) {
@@ -3940,8 +3996,19 @@ const ContentChannels = {
3940
3996
  };
3941
3997
  const DevToolsChannels = {
3942
3998
  DEVTOOLS_PANEL_TOGGLE: "devtools-panel:toggle",
3999
+ DEVTOOLS_PANEL_CLOSE: "devtools-panel:close",
4000
+ DEVTOOLS_PANEL_OPEN_TAB: "devtools-panel:open-tab",
4001
+ DEVTOOLS_PANEL_SELECT_TAB: "devtools-panel:select-tab",
4002
+ DEVTOOLS_PANEL_STATE_GET: "devtools-panel:state-get",
3943
4003
  DEVTOOLS_PANEL_STATE: "devtools-panel:state",
3944
- DEVTOOLS_PANEL_RESIZE: "devtools-panel:resize"
4004
+ DEVTOOLS_PANEL_RESIZE_START: "devtools-panel:resize-start",
4005
+ DEVTOOLS_PANEL_RESIZE: "devtools-panel:resize",
4006
+ DEVTOOLS_PANEL_RESIZE_COMMIT: "devtools-panel:resize-commit",
4007
+ DEVTOOLS_PANEL_POPOUT: "devtools-panel:popout",
4008
+ DEVTOOLS_PANEL_DOCK: "devtools-panel:dock",
4009
+ DEVTOOLS_PANEL_HOST_STATE_GET: "devtools-panel:host-state-get",
4010
+ DEVTOOLS_PANEL_HOST_STATE: "devtools-panel:host-state",
4011
+ DEVTOOLS_PAGE_MAP_REVEAL: "devtools-panel:page-map:reveal"
3945
4012
  };
3946
4013
  const DownloadChannels = {
3947
4014
  DOWNLOAD_STARTED: "download:started",
@@ -7215,6 +7282,236 @@ function sendSafe(wc, channel, ...args) {
7215
7282
  }
7216
7283
  }
7217
7284
  }
7285
+ function closeDetachedViewWindow(state2, host) {
7286
+ const detachedWindow = host.getWindow(state2);
7287
+ if (!detachedWindow) return false;
7288
+ host.setWindow(state2, null);
7289
+ host.setClosing(state2, true);
7290
+ detachedWindow.once("closed", () => {
7291
+ host.setClosing(state2, false);
7292
+ });
7293
+ detachedWindow.close();
7294
+ return true;
7295
+ }
7296
+ function moveDetachedViewToMainWindow(state2, host) {
7297
+ const view = host.getView(state2);
7298
+ host.getWindow(state2)?.contentView.removeChildView(view);
7299
+ state2.mainWindow.contentView.addChildView(view);
7300
+ }
7301
+ function createDetachedViewWindow(state2, host) {
7302
+ const detachedWindow = new electron.BaseWindow(host.createWindowOptions(state2));
7303
+ const view = host.getView(state2);
7304
+ state2.mainWindow.contentView.removeChildView(view);
7305
+ detachedWindow.contentView.addChildView(view);
7306
+ host.setWindow(state2, detachedWindow);
7307
+ detachedWindow.on("resize", () => {
7308
+ host.layoutView(state2);
7309
+ host.persistBounds(state2);
7310
+ });
7311
+ detachedWindow.on("move", () => host.persistBounds(state2));
7312
+ detachedWindow.on("close", (event) => {
7313
+ if (host.isClosing(state2)) return;
7314
+ event.preventDefault();
7315
+ host.onNativeClose(state2);
7316
+ });
7317
+ detachedWindow.on("closed", () => {
7318
+ if (host.getWindow(state2) !== detachedWindow) return;
7319
+ host.onUnexpectedClosed(state2, detachedWindow);
7320
+ });
7321
+ return detachedWindow;
7322
+ }
7323
+ const devToolsDetachedHost = {
7324
+ getWindow: (state2) => state2.devtoolsPanelWindow,
7325
+ setWindow: (state2, window2) => {
7326
+ state2.devtoolsPanelWindow = window2;
7327
+ },
7328
+ isClosing: (state2) => state2.devtoolsPanelWindowClosing,
7329
+ setClosing: (state2, closing) => {
7330
+ state2.devtoolsPanelWindowClosing = closing;
7331
+ },
7332
+ getView: (state2) => state2.devtoolsPanelView
7333
+ };
7334
+ function setDevToolsPanelMode(state2, mode) {
7335
+ state2.uiState.devtoolsPanelMode = mode;
7336
+ }
7337
+ function persistDetachedBounds$1(state2) {
7338
+ const devtoolsWindow = state2.devtoolsPanelWindow;
7339
+ if (!devtoolsWindow || devtoolsWindow.isDestroyed()) return;
7340
+ const bounds = devtoolsWindow.getBounds();
7341
+ state2.uiState.devtoolsPanelDetachedBounds = {
7342
+ x: bounds.x,
7343
+ y: bounds.y,
7344
+ width: bounds.width,
7345
+ height: bounds.height
7346
+ };
7347
+ setSetting(
7348
+ "devtoolsPanelDetachedBounds",
7349
+ state2.uiState.devtoolsPanelDetachedBounds
7350
+ );
7351
+ }
7352
+ function moveDevToolsToMainWindow(state2) {
7353
+ moveDetachedViewToMainWindow(state2, devToolsDetachedHost);
7354
+ }
7355
+ function isDevToolsPanelDocked(state2) {
7356
+ return state2.uiState.devtoolsPanelMode === "docked";
7357
+ }
7358
+ function isDevToolsPanelDetached(state2) {
7359
+ return state2.uiState.devtoolsPanelMode === "detached";
7360
+ }
7361
+ function getDevToolsPanelHostState(state2) {
7362
+ return {
7363
+ open: state2.uiState.devtoolsPanelMode !== "closed",
7364
+ detached: isDevToolsPanelDetached(state2),
7365
+ height: state2.uiState.devtoolsPanelHeight
7366
+ };
7367
+ }
7368
+ function emitDevToolsPanelHostState(state2) {
7369
+ const panelState = getDevToolsPanelHostState(state2);
7370
+ sendSafe(
7371
+ state2.chromeView.webContents,
7372
+ Channels.DEVTOOLS_PANEL_HOST_STATE,
7373
+ panelState
7374
+ );
7375
+ sendSafe(
7376
+ state2.devtoolsPanelView.webContents,
7377
+ Channels.DEVTOOLS_PANEL_HOST_STATE,
7378
+ panelState
7379
+ );
7380
+ return panelState;
7381
+ }
7382
+ function closeDetachedDevToolsPanelWindow(state2) {
7383
+ return closeDetachedViewWindow(state2, devToolsDetachedHost);
7384
+ }
7385
+ function layoutDetachedDevToolsPanel(state2) {
7386
+ if (!state2.devtoolsPanelWindow) return;
7387
+ const [width, height] = state2.devtoolsPanelWindow.getContentSize();
7388
+ state2.devtoolsPanelView.setBounds({ x: 0, y: 0, width, height });
7389
+ }
7390
+ function toggleDockedDevToolsPanel(state2, hooks) {
7391
+ if (isDevToolsPanelDetached(state2)) {
7392
+ state2.devtoolsPanelWindow?.focus();
7393
+ return getDevToolsPanelHostState(state2);
7394
+ }
7395
+ setDevToolsPanelMode(
7396
+ state2,
7397
+ isDevToolsPanelDocked(state2) ? "closed" : "docked"
7398
+ );
7399
+ hooks.relayout();
7400
+ return emitDevToolsPanelHostState(state2);
7401
+ }
7402
+ function resizeDockedDevToolsPanel(state2, height, relayout) {
7403
+ state2.uiState.devtoolsPanelHeight = Math.round(height);
7404
+ if (isDevToolsPanelDocked(state2)) {
7405
+ relayout();
7406
+ }
7407
+ return emitDevToolsPanelHostState(state2);
7408
+ }
7409
+ function detachDevToolsPanel(state2, hooks) {
7410
+ if (state2.devtoolsPanelWindow) {
7411
+ state2.devtoolsPanelWindow.focus();
7412
+ return getDevToolsPanelHostState(state2);
7413
+ }
7414
+ const detachedBounds = state2.uiState.devtoolsPanelDetachedBounds;
7415
+ const devtoolsWindow = createDetachedViewWindow(state2, {
7416
+ ...devToolsDetachedHost,
7417
+ createWindowOptions: () => ({
7418
+ ...typeof detachedBounds?.x === "number" ? { x: detachedBounds.x } : {},
7419
+ ...typeof detachedBounds?.y === "number" ? { y: detachedBounds.y } : {},
7420
+ width: Math.max(
7421
+ DETACHED_DEVTOOLS_MIN_WIDTH,
7422
+ Math.round(detachedBounds?.width ?? DETACHED_DEVTOOLS_DEFAULT_WIDTH)
7423
+ ),
7424
+ height: Math.max(
7425
+ DETACHED_DEVTOOLS_MIN_HEIGHT,
7426
+ Math.round(detachedBounds?.height ?? DETACHED_DEVTOOLS_DEFAULT_HEIGHT)
7427
+ ),
7428
+ minWidth: DETACHED_DEVTOOLS_MIN_WIDTH,
7429
+ minHeight: DETACHED_DEVTOOLS_MIN_HEIGHT,
7430
+ frame: true,
7431
+ show: false,
7432
+ backgroundColor: "#1a1a1e",
7433
+ title: "Vessel DevTools",
7434
+ icon: hooks.getWindowIconPath()
7435
+ }),
7436
+ layoutView: layoutDetachedDevToolsPanel,
7437
+ persistBounds: persistDetachedBounds$1,
7438
+ onNativeClose: () => dockDevToolsPanel(state2, hooks),
7439
+ onUnexpectedClosed: () => {
7440
+ state2.devtoolsPanelWindow = null;
7441
+ setDevToolsPanelMode(state2, "docked");
7442
+ state2.mainWindow.contentView.addChildView(state2.devtoolsPanelView);
7443
+ hooks.relayout();
7444
+ emitDevToolsPanelHostState(state2);
7445
+ }
7446
+ });
7447
+ setDevToolsPanelMode(state2, "detached");
7448
+ hooks.relayout();
7449
+ layoutDetachedDevToolsPanel(state2);
7450
+ devtoolsWindow.show();
7451
+ devtoolsWindow.focus();
7452
+ return emitDevToolsPanelHostState(state2);
7453
+ }
7454
+ function dockDevToolsPanel(state2, hooks) {
7455
+ const devtoolsWindow = state2.devtoolsPanelWindow;
7456
+ setDevToolsPanelMode(state2, "docked");
7457
+ if (devtoolsWindow) {
7458
+ moveDevToolsToMainWindow(state2);
7459
+ hooks.relayout();
7460
+ closeDetachedDevToolsPanelWindow(state2);
7461
+ state2.mainWindow.focus();
7462
+ } else {
7463
+ hooks.relayout();
7464
+ }
7465
+ return emitDevToolsPanelHostState(state2);
7466
+ }
7467
+ function closeDevToolsPanel(state2, hooks) {
7468
+ if (state2.devtoolsPanelWindow) {
7469
+ moveDevToolsToMainWindow(state2);
7470
+ closeDetachedDevToolsPanelWindow(state2);
7471
+ }
7472
+ setDevToolsPanelMode(state2, "closed");
7473
+ hooks.relayout();
7474
+ return emitDevToolsPanelHostState(state2);
7475
+ }
7476
+ function registerDisabledDevToolsPanelHandlers(ipc) {
7477
+ const disabledDevToolsState = {
7478
+ open: false,
7479
+ detached: false,
7480
+ height: 0
7481
+ };
7482
+ const disabledPanelState = {
7483
+ console: [],
7484
+ network: [],
7485
+ errors: [],
7486
+ activity: [],
7487
+ agentTrace: [],
7488
+ pageMap: null
7489
+ };
7490
+ ipc.handle(Channels.DEVTOOLS_PANEL_TOGGLE, () => disabledDevToolsState);
7491
+ ipc.handle(Channels.DEVTOOLS_PANEL_CLOSE, () => disabledDevToolsState);
7492
+ ipc.handle(Channels.DEVTOOLS_PANEL_OPEN_TAB, () => disabledDevToolsState);
7493
+ ipc.handle(Channels.DEVTOOLS_PANEL_STATE_GET, () => disabledPanelState);
7494
+ ipc.handle(Channels.DEVTOOLS_PANEL_RESIZE_START, () => void 0);
7495
+ ipc.handle(Channels.DEVTOOLS_PANEL_RESIZE, () => 0);
7496
+ ipc.handle(Channels.DEVTOOLS_PANEL_RESIZE_COMMIT, () => void 0);
7497
+ ipc.handle(Channels.DEVTOOLS_PANEL_POPOUT, () => disabledDevToolsState);
7498
+ ipc.handle(Channels.DEVTOOLS_PANEL_DOCK, () => disabledDevToolsState);
7499
+ ipc.handle(
7500
+ Channels.DEVTOOLS_PANEL_HOST_STATE_GET,
7501
+ () => disabledDevToolsState
7502
+ );
7503
+ }
7504
+ const sidebarDetachedHost = {
7505
+ getWindow: (state2) => state2.sidebarWindow,
7506
+ setWindow: (state2, window2) => {
7507
+ state2.sidebarWindow = window2;
7508
+ },
7509
+ isClosing: (state2) => state2.sidebarWindowClosing,
7510
+ setClosing: (state2, closing) => {
7511
+ state2.sidebarWindowClosing = closing;
7512
+ },
7513
+ getView: (state2) => state2.sidebarView
7514
+ };
7218
7515
  function setSidebarPanelMode(state2, mode, reason = "user") {
7219
7516
  state2.uiState.sidebarPanelMode = mode;
7220
7517
  if (reason === "user") {
@@ -7234,19 +7531,10 @@ function persistDetachedBounds(state2) {
7234
7531
  setSetting("sidebarDetachedBounds", state2.uiState.sidebarDetachedBounds);
7235
7532
  }
7236
7533
  function closeDetachedSidebarWindow(state2) {
7237
- const sidebarWindow = state2.sidebarWindow;
7238
- if (!sidebarWindow) return false;
7239
- state2.sidebarWindow = null;
7240
- state2.sidebarWindowClosing = true;
7241
- sidebarWindow.once("closed", () => {
7242
- state2.sidebarWindowClosing = false;
7243
- });
7244
- sidebarWindow.close();
7245
- return true;
7534
+ return closeDetachedViewWindow(state2, sidebarDetachedHost);
7246
7535
  }
7247
7536
  function moveSidebarToMainWindow(state2) {
7248
- state2.sidebarWindow?.contentView.removeChildView(state2.sidebarView);
7249
- state2.mainWindow.contentView.addChildView(state2.sidebarView);
7537
+ moveDetachedViewToMainWindow(state2, sidebarDetachedHost);
7250
7538
  }
7251
7539
  function getSidebarPanelState(state2) {
7252
7540
  return {
@@ -7306,41 +7594,33 @@ function detachSidebar(state2, hooks) {
7306
7594
  const detachedBounds = state2.uiState.sidebarDetachedBounds;
7307
7595
  const detachedWidth = detachedBounds?.width ?? Math.max(DETACHED_SIDEBAR_DEFAULT_WIDTH, state2.uiState.sidebarWidth);
7308
7596
  const detachedHeight = detachedBounds?.height ?? DETACHED_SIDEBAR_DEFAULT_HEIGHT;
7309
- const sidebarWindow = new electron.BaseWindow({
7310
- ...typeof detachedBounds?.x === "number" ? { x: detachedBounds.x } : {},
7311
- ...typeof detachedBounds?.y === "number" ? { y: detachedBounds.y } : {},
7312
- width: Math.max(DETACHED_SIDEBAR_MIN_WIDTH, Math.round(detachedWidth)),
7313
- height: Math.max(DETACHED_SIDEBAR_MIN_HEIGHT, Math.round(detachedHeight)),
7314
- minWidth: DETACHED_SIDEBAR_MIN_WIDTH,
7315
- minHeight: DETACHED_SIDEBAR_MIN_HEIGHT,
7316
- frame: true,
7317
- show: false,
7318
- backgroundColor: "#1a1a1e",
7319
- title: "Vessel Agent",
7320
- icon: hooks.getWindowIconPath()
7597
+ const sidebarWindow = createDetachedViewWindow(state2, {
7598
+ ...sidebarDetachedHost,
7599
+ createWindowOptions: () => ({
7600
+ ...typeof detachedBounds?.x === "number" ? { x: detachedBounds.x } : {},
7601
+ ...typeof detachedBounds?.y === "number" ? { y: detachedBounds.y } : {},
7602
+ width: Math.max(DETACHED_SIDEBAR_MIN_WIDTH, Math.round(detachedWidth)),
7603
+ height: Math.max(DETACHED_SIDEBAR_MIN_HEIGHT, Math.round(detachedHeight)),
7604
+ minWidth: DETACHED_SIDEBAR_MIN_WIDTH,
7605
+ minHeight: DETACHED_SIDEBAR_MIN_HEIGHT,
7606
+ frame: true,
7607
+ show: false,
7608
+ backgroundColor: "#1a1a1e",
7609
+ title: "Vessel Agent",
7610
+ icon: hooks.getWindowIconPath()
7611
+ }),
7612
+ layoutView: layoutDetachedSidebar,
7613
+ persistBounds: persistDetachedBounds,
7614
+ onNativeClose: () => dockSidebar(state2, hooks),
7615
+ onUnexpectedClosed: () => {
7616
+ state2.sidebarWindow = null;
7617
+ setSidebarPanelMode(state2, "docked");
7618
+ state2.mainWindow.contentView.addChildView(state2.sidebarView);
7619
+ hooks.relayout();
7620
+ emitSidebarPanelState(state2);
7621
+ }
7321
7622
  });
7322
- state2.mainWindow.contentView.removeChildView(state2.sidebarView);
7323
- sidebarWindow.contentView.addChildView(state2.sidebarView);
7324
- state2.sidebarWindow = sidebarWindow;
7325
7623
  setSidebarPanelMode(state2, "detached");
7326
- sidebarWindow.on("resize", () => {
7327
- layoutDetachedSidebar(state2);
7328
- persistDetachedBounds(state2);
7329
- });
7330
- sidebarWindow.on("move", () => persistDetachedBounds(state2));
7331
- sidebarWindow.on("close", (event) => {
7332
- if (state2.sidebarWindowClosing) return;
7333
- event.preventDefault();
7334
- dockSidebar(state2, hooks);
7335
- });
7336
- sidebarWindow.on("closed", () => {
7337
- if (state2.sidebarWindow !== sidebarWindow) return;
7338
- state2.sidebarWindow = null;
7339
- setSidebarPanelMode(state2, "docked");
7340
- state2.mainWindow.contentView.addChildView(state2.sidebarView);
7341
- hooks.relayout();
7342
- emitSidebarPanelState(state2);
7343
- });
7344
7624
  hooks.relayout();
7345
7625
  layoutDetachedSidebar(state2);
7346
7626
  sidebarWindow.show();
@@ -7563,8 +7843,9 @@ function createMainWindow(onTabStateChange) {
7563
7843
  sidebarDetachedBounds: settings2.sidebarDetachedBounds,
7564
7844
  focusMode: false,
7565
7845
  settingsOpen: false,
7566
- devtoolsPanelOpen: false,
7567
- devtoolsPanelHeight: DEFAULT_DEVTOOLS_PANEL_HEIGHT
7846
+ devtoolsPanelMode: "closed",
7847
+ devtoolsPanelHeight: DEFAULT_DEVTOOLS_PANEL_HEIGHT,
7848
+ devtoolsPanelDetachedBounds: settings2.devtoolsPanelDetachedBounds
7568
7849
  };
7569
7850
  const tabManager = new TabManager(mainWindow, onTabStateChange);
7570
7851
  const sendToRendererViews = (channel, ...args) => {
@@ -7580,6 +7861,8 @@ function createMainWindow(onTabStateChange) {
7580
7861
  mainWindow,
7581
7862
  sidebarWindow: null,
7582
7863
  sidebarWindowClosing: false,
7864
+ devtoolsPanelWindow: null,
7865
+ devtoolsPanelWindowClosing: false,
7583
7866
  chromeView,
7584
7867
  sidebarView,
7585
7868
  devtoolsPanelView,
@@ -7591,6 +7874,7 @@ function createMainWindow(onTabStateChange) {
7591
7874
  mainWindow.on("focus", () => layoutViews(state2));
7592
7875
  mainWindow.on("closed", () => {
7593
7876
  closeDetachedSidebarWindow(state2);
7877
+ closeDetachedDevToolsPanelWindow(state2);
7594
7878
  });
7595
7879
  sidebarView.webContents.on("context-menu", (event, params) => {
7596
7880
  event.preventDefault();
@@ -7619,7 +7903,9 @@ function layoutViews(state2) {
7619
7903
  const chromeHeight = uiState.focusMode ? 0 : CHROME_HEIGHT;
7620
7904
  const sidebarAttached = isSidebarAttached(state2);
7621
7905
  const sidebarWidth = sidebarAttached ? uiState.sidebarWidth : 0;
7622
- const devtoolsHeight = uiState.devtoolsPanelOpen ? uiState.devtoolsPanelHeight : 0;
7906
+ const devtoolsDocked = isDevToolsPanelDocked(state2);
7907
+ const devtoolsDetached = isDevToolsPanelDetached(state2);
7908
+ const devtoolsHeight = devtoolsDocked ? uiState.devtoolsPanelHeight : 0;
7623
7909
  const chromeNeedsFullHeight = uiState.settingsOpen;
7624
7910
  if (chromeNeedsFullHeight) {
7625
7911
  chromeView.setBounds({ x: 0, y: 0, width, height });
@@ -7637,14 +7923,14 @@ function layoutViews(state2) {
7637
7923
  sidebarView.setBounds({ x: width, y: 0, width: 0, height: 0 });
7638
7924
  }
7639
7925
  const contentWidth = width - sidebarWidth;
7640
- if (uiState.devtoolsPanelOpen) {
7926
+ if (devtoolsDocked) {
7641
7927
  devtoolsPanelView.setBounds({
7642
7928
  x: 0,
7643
7929
  y: height - devtoolsHeight,
7644
7930
  width: contentWidth,
7645
7931
  height: devtoolsHeight
7646
7932
  });
7647
- } else {
7933
+ } else if (!devtoolsDetached) {
7648
7934
  devtoolsPanelView.setBounds({ x: 0, y: height, width: 0, height: 0 });
7649
7935
  }
7650
7936
  mainWindow.contentView.removeChildView(chromeView);
@@ -7653,8 +7939,10 @@ function layoutViews(state2) {
7653
7939
  mainWindow.contentView.removeChildView(sidebarView);
7654
7940
  mainWindow.contentView.addChildView(sidebarView);
7655
7941
  }
7656
- mainWindow.contentView.removeChildView(devtoolsPanelView);
7657
- mainWindow.contentView.addChildView(devtoolsPanelView);
7942
+ if (!devtoolsDetached) {
7943
+ mainWindow.contentView.removeChildView(devtoolsPanelView);
7944
+ mainWindow.contentView.addChildView(devtoolsPanelView);
7945
+ }
7658
7946
  const activeTab = tabManager.getActiveTab();
7659
7947
  if (activeTab) {
7660
7948
  activeTab.view.setBounds({
@@ -7670,7 +7958,8 @@ function resizeSidebarViews(state2) {
7670
7958
  const [width, height] = mainWindow.getContentSize();
7671
7959
  const chromeHeight = uiState.focusMode ? 0 : CHROME_HEIGHT;
7672
7960
  const sidebarWidth = isSidebarAttached(state2) ? uiState.sidebarWidth : 0;
7673
- const devtoolsHeight = uiState.devtoolsPanelOpen ? uiState.devtoolsPanelHeight : 0;
7961
+ const devtoolsDocked = isDevToolsPanelDocked(state2);
7962
+ const devtoolsHeight = devtoolsDocked ? uiState.devtoolsPanelHeight : 0;
7674
7963
  const contentWidth = width - sidebarWidth;
7675
7964
  if (uiState.sidebarPanelMode !== "detached") {
7676
7965
  sidebarView.setBounds({
@@ -7680,7 +7969,7 @@ function resizeSidebarViews(state2) {
7680
7969
  height: height - chromeHeight
7681
7970
  });
7682
7971
  }
7683
- if (uiState.devtoolsPanelOpen) {
7972
+ if (devtoolsDocked) {
7684
7973
  devtoolsPanelView.setBounds({
7685
7974
  x: 0,
7686
7975
  y: height - devtoolsHeight,
@@ -7806,6 +8095,96 @@ function isClickReadLoop(names) {
7806
8095
  }
7807
8096
  return clickReadPairs >= 2;
7808
8097
  }
8098
+ const CLICK_READ_LOOP_SUPPRESS_THRESHOLD = 3;
8099
+ function classifyClickFailure(output) {
8100
+ if (/Error\[hidden\]/i.test(output)) return "hidden";
8101
+ if (/Error\[stale-index\]/i.test(output)) return "stale";
8102
+ if (/^\s*Error:/i.test(output)) return "other";
8103
+ return null;
8104
+ }
8105
+ function buildClickReadLoopIntervention(strikes, lastClickFailureKind) {
8106
+ if (strikes <= 0) return null;
8107
+ if (strikes >= CLICK_READ_LOOP_SUPPRESS_THRESHOLD) {
8108
+ const lines = [
8109
+ `Error: Suppressed repeated click — you have alternated click and read_page ${strikes} times without making progress and the clicks are not landing.`,
8110
+ `Stop calling click. Instead do one of: scroll (scroll or scroll_to_element) to load more of the page then read_page to refresh, inspect_element on a specific indexed result, or answer from the results already visible in the conversation.`
8111
+ ];
8112
+ if (lastClickFailureKind === "hidden") {
8113
+ lines.push(
8114
+ `The last click target was hidden / not laid out — scrolling toward it first usually reveals it.`
8115
+ );
8116
+ } else if (lastClickFailureKind === "stale") {
8117
+ lines.push(
8118
+ `The last click target was stale — refresh page state with read_page and choose a currently listed target before clicking again.`
8119
+ );
8120
+ }
8121
+ return { kind: "suppress", message: lines.join("\n") };
8122
+ }
8123
+ if (strikes >= 2) {
8124
+ const lines = [
8125
+ `[System] You are alternating between click and read_page without advancing the task, and the last click did not complete.`,
8126
+ `The click result already includes a page snapshot, so do not read_page after every click.`
8127
+ ];
8128
+ if (lastClickFailureKind === "hidden") {
8129
+ lines.push(
8130
+ `The click failed on a hidden element — call scroll (scroll or scroll_to_element) to reveal it, then read_page to refresh visible elements, before clicking again.`
8131
+ );
8132
+ } else if (lastClickFailureKind === "stale") {
8133
+ lines.push(
8134
+ `The click failed on a stale element index — call read_page to refresh current indexes before clicking again.`
8135
+ );
8136
+ } else {
8137
+ lines.push(
8138
+ `If you need detail on a specific element, use inspect_element. Otherwise continue the original task directly.`
8139
+ );
8140
+ }
8141
+ return { kind: "nudge", message: lines.join("\n") };
8142
+ }
8143
+ return {
8144
+ kind: "nudge",
8145
+ message: `[System] You are alternating between click and read_page without advancing the task. The click result already includes a page snapshot when it navigates, so do not read_page after every click. If you need detail on a specific element, use inspect_element. Otherwise continue the original task directly.`
8146
+ };
8147
+ }
8148
+ class ClickReadLoopGuard {
8149
+ recentToolNames = [];
8150
+ strikes = 0;
8151
+ lastClickFailureKind = null;
8152
+ beforeTool(toolName) {
8153
+ if (toolName === "click" && this.strikes >= CLICK_READ_LOOP_SUPPRESS_THRESHOLD && isClickReadLoop(this.recentToolNames)) {
8154
+ return buildClickReadLoopIntervention(
8155
+ this.strikes,
8156
+ this.lastClickFailureKind
8157
+ );
8158
+ }
8159
+ return null;
8160
+ }
8161
+ afterToolResult(toolName, output, succeeded) {
8162
+ if (toolName === "click") {
8163
+ this.lastClickFailureKind = succeeded ? null : classifyClickFailure(output);
8164
+ }
8165
+ this.recentToolNames.push(toolName);
8166
+ if (this.recentToolNames.length > 8) this.recentToolNames.shift();
8167
+ if (toolName === "click" && succeeded) {
8168
+ this.strikes = 0;
8169
+ return null;
8170
+ }
8171
+ if (toolName !== "click" && toolName !== "read_page") {
8172
+ this.strikes = 0;
8173
+ return null;
8174
+ }
8175
+ if (isClickReadLoop(this.recentToolNames) && this.lastClickFailureKind) {
8176
+ this.strikes += 1;
8177
+ if (this.strikes >= CLICK_READ_LOOP_SUPPRESS_THRESHOLD) {
8178
+ return null;
8179
+ }
8180
+ return buildClickReadLoopIntervention(
8181
+ this.strikes,
8182
+ this.lastClickFailureKind
8183
+ );
8184
+ }
8185
+ return null;
8186
+ }
8187
+ }
7809
8188
  const TERMINAL_TOOL_RESULT = "__VESSEL_TERMINAL_TOOL_RESULT__";
7810
8189
  const logger$w = createLogger("PromptCache");
7811
8190
  function shortHash(value) {
@@ -7966,8 +8345,7 @@ class AnthropicProvider {
7966
8345
  try {
7967
8346
  const maxIterations = getEffectiveMaxIterations();
7968
8347
  let iterationsUsed = 0;
7969
- const recentToolNames = [];
7970
- let clickReadLoopNudged = false;
8348
+ const clickReadLoopGuard = new ClickReadLoopGuard();
7971
8349
  for (let i = 0; i < maxIterations; i++) {
7972
8350
  iterationsUsed = i + 1;
7973
8351
  const stream = this.client.messages.stream(
@@ -8074,6 +8452,7 @@ class AnthropicProvider {
8074
8452
  break;
8075
8453
  }
8076
8454
  const toolResults = [];
8455
+ const loopNudges = [];
8077
8456
  for (const tb of toolUseBlocks) {
8078
8457
  if (tb._malformedArgs !== void 0) {
8079
8458
  onChunk(`
@@ -8087,6 +8466,19 @@ class AnthropicProvider {
8087
8466
  });
8088
8467
  continue;
8089
8468
  }
8469
+ const clickLoopPreflight = clickReadLoopGuard.beforeTool(tb.name);
8470
+ if (clickLoopPreflight?.kind === "suppress") {
8471
+ onChunk(`
8472
+ <<tool:click:↻ loop suppressed>>
8473
+ `);
8474
+ toolResults.push({
8475
+ type: "tool_result",
8476
+ tool_use_id: tb.id,
8477
+ content: clickLoopPreflight.message,
8478
+ is_error: true
8479
+ });
8480
+ continue;
8481
+ }
8090
8482
  const argSummary = [tb.input.url, tb.input.query, tb.input.text, tb.input.direction].map((v) => typeof v === "string" ? v : "").find((v) => v.length > 0) ?? "";
8091
8483
  onChunk(`
8092
8484
  <<tool:${tb.name}${argSummary ? ":" + argSummary : ""}>>
@@ -8101,6 +8493,7 @@ class AnthropicProvider {
8101
8493
  if (result === TERMINAL_TOOL_RESULT) {
8102
8494
  return;
8103
8495
  }
8496
+ const toolSucceeded = !/^Error:/i.test(result.trim());
8104
8497
  let parsedRich = null;
8105
8498
  try {
8106
8499
  const parsed = JSON.parse(result);
@@ -8132,16 +8525,18 @@ class AnthropicProvider {
8132
8525
  content: result
8133
8526
  });
8134
8527
  }
8135
- recentToolNames.push(tb.name);
8136
- if (recentToolNames.length > 8) recentToolNames.shift();
8528
+ const clickLoopIntervention = clickReadLoopGuard.afterToolResult(
8529
+ tb.name,
8530
+ result,
8531
+ toolSucceeded
8532
+ );
8533
+ if (clickLoopIntervention?.kind === "nudge") {
8534
+ loopNudges.push(clickLoopIntervention.message);
8535
+ }
8137
8536
  }
8138
8537
  messages.push({ role: "user", content: toolResults });
8139
- if (!clickReadLoopNudged && recentToolNames.length >= 6 && isClickReadLoop(recentToolNames)) {
8140
- clickReadLoopNudged = true;
8141
- messages.push({
8142
- role: "user",
8143
- content: `You are alternating between click and read_page without advancing the task. The click result already includes a page snapshot when it navigates — you do not need read_page after every click. If you need detail on a specific element, use inspect_element instead. If you have enough context, proceed with the next action directly.`
8144
- });
8538
+ for (const nudge of loopNudges) {
8539
+ messages.push({ role: "user", content: nudge });
8145
8540
  }
8146
8541
  }
8147
8542
  if (iterationsUsed >= maxIterations) {
@@ -9432,10 +9827,9 @@ class OpenAICompatProvider {
9432
9827
  let highlightCompletionRecoveryCount = 0;
9433
9828
  let compactCorrectionCount = 0;
9434
9829
  const recentCompactToolSignatures = [];
9435
- const recentToolNames = [];
9436
9830
  const successfulToolNames = [];
9437
9831
  const searchLoopGuard = new SearchLoopGuard(isSearchContextResettingTool);
9438
- let clickReadLoopNudged = false;
9832
+ const clickReadLoopGuard = new ClickReadLoopGuard();
9439
9833
  for (let i = 0; i < maxIterations; i++) {
9440
9834
  iterationsUsed = i + 1;
9441
9835
  let textAccum = "";
@@ -9725,6 +10119,19 @@ class OpenAICompatProvider {
9725
10119
  }
9726
10120
  continue;
9727
10121
  }
10122
+ const clickLoopPreflight = clickReadLoopGuard.beforeTool(tc.name);
10123
+ if (clickLoopPreflight?.kind === "suppress") {
10124
+ onChunk(`
10125
+ <<tool:click:↻ loop suppressed>>
10126
+ `);
10127
+ messages.push({
10128
+ role: "tool",
10129
+ tool_call_id: tc.id,
10130
+ content: clickLoopPreflight.message
10131
+ });
10132
+ compactCorrectionCount += 1;
10133
+ continue;
10134
+ }
9728
10135
  const argSummary = [args.url, args.query, args.text, args.direction].map((v) => typeof v === "string" ? v : "").find((v) => v.length > 0) ?? "";
9729
10136
  onChunk(`
9730
10137
  <<tool:${tc.name}${argSummary ? ":" + argSummary : ""}>>
@@ -9762,15 +10169,11 @@ class OpenAICompatProvider {
9762
10169
  searchToolQuery,
9763
10170
  toolSucceeded
9764
10171
  );
9765
- recentToolNames.push(tc.name);
9766
- if (recentToolNames.length > 8) recentToolNames.shift();
9767
- if (!clickReadLoopNudged && recentToolNames.length >= 6 && isClickReadLoop(recentToolNames)) {
9768
- clickReadLoopNudged = true;
9769
- messages.push({
9770
- role: "user",
9771
- content: `[System] You are alternating between click and read_page without advancing the task. The click result already includes a page snapshot when it navigates — you do not need read_page after every click. If you need detail on a specific element, use inspect_element instead. If you have enough context, proceed with the next action directly.`
9772
- });
9773
- }
10172
+ const clickLoopIntervention = clickReadLoopGuard.afterToolResult(
10173
+ tc.name,
10174
+ toolContent,
10175
+ toolSucceeded
10176
+ );
9774
10177
  compactCorrectionCount = 0;
9775
10178
  iterationToolResultPreviews.push(toolContent);
9776
10179
  messages.push({
@@ -9778,6 +10181,9 @@ class OpenAICompatProvider {
9778
10181
  tool_call_id: tc.id,
9779
10182
  content: toolContent
9780
10183
  });
10184
+ if (clickLoopIntervention?.kind === "nudge") {
10185
+ messages.push({ role: "user", content: clickLoopIntervention.message });
10186
+ }
9781
10187
  }
9782
10188
  const followUpReminder = followUpReminderForProfile(
9783
10189
  this.agentToolProfile,
@@ -10187,9 +10593,18 @@ function summarizeToolArg(args) {
10187
10593
  function normalizeCodexText(text) {
10188
10594
  return text.trim().toLowerCase().replace(/[‘’]/g, "'").replace(/[“”]/g, '"');
10189
10595
  }
10190
- function looksLikeFailedToolOutput(output) {
10596
+ function looksLikeFailedToolOutput(output, toolName) {
10191
10597
  const normalized = normalizeCodexText(output);
10192
- return normalized.startsWith("error") || normalized.startsWith("warning") || normalized.startsWith("target") || normalized.startsWith("no active tab") || normalized.includes("same page — results may have loaded dynamically") || normalized.includes("could not ") || normalized.includes("did not ");
10598
+ const firstLine = normalized.split(/\n+/).map((line) => line.trim()).find(Boolean) ?? "";
10599
+ if (/^(?:error\b|error\[|blocked\b|warning\b|target\b|no active tab\b|cannot\b|can't\b)/.test(
10600
+ firstLine
10601
+ )) {
10602
+ return true;
10603
+ }
10604
+ if (toolName === "click" && (normalized.includes("page did not change after click") || normalized.includes("element may need a different interaction method"))) {
10605
+ return true;
10606
+ }
10607
+ return toolName === "type_text" && /^(?:could not|did not|no focused|no visible)/.test(firstLine);
10193
10608
  }
10194
10609
  function looksLikeTravelFareContext(text) {
10195
10610
  const normalized = normalizeCodexText(text);
@@ -10197,7 +10612,7 @@ function looksLikeTravelFareContext(text) {
10197
10612
  }
10198
10613
  function emitCodexToolChunk(onChunk, name, args, output) {
10199
10614
  const summary = summarizeToolArg(args);
10200
- const argSummary = looksLikeFailedToolOutput(output) ? ["⚠ failed", summary].filter(Boolean).join(" ") : summary;
10615
+ const argSummary = looksLikeFailedToolOutput(output, name) ? ["⚠ failed", summary].filter(Boolean).join(" ") : summary;
10201
10616
  onChunk(`
10202
10617
  <<tool:${name}${argSummary ? ":" + argSummary : ""}>>
10203
10618
  `);
@@ -10449,12 +10864,25 @@ function buildCodexFlightPriceEvidenceRecoveryInput(userMessage, assistantText,
10449
10864
  ]
10450
10865
  };
10451
10866
  }
10452
- function buildCodexFailedClickRecoveryInput(attemptedTarget, latestToolResultPreview, failedClickCount = 1) {
10867
+ function buildCodexFailedClickRecoveryInput(attemptedTarget, latestToolResultPreview, failedClickCount = 1, errorOutput = "") {
10453
10868
  const stateReminder = buildLatestStateReminder(latestToolResultPreview);
10869
+ const clickFailureKind = classifyClickFailure(errorOutput);
10454
10870
  const lines = [
10455
- `[System] The previous click did not complete${attemptedTarget ? ` for ${attemptedTarget}` : ""}.`,
10456
- `Take the next step yourself: try a different target, refresh the page state with read_page, call inspect_element on the intended element, or answer from the results already visible in the conversation. Do not ask the user to inspect or click the result for you.`
10871
+ `[System] The previous click did not complete${attemptedTarget ? ` for ${attemptedTarget}` : ""}.`
10457
10872
  ];
10873
+ if (clickFailureKind === "hidden") {
10874
+ lines.push(
10875
+ `The click target was hidden / not laid out (collapsed, lazy-loaded, or virtual-scroll). Call scroll or scroll_to_element toward it to reveal it, then read_page to refresh visible elements, before clicking again — or inspect_element on the intended index, or answer from the results already visible. Do not ask the user to inspect or click the result for you.`
10876
+ );
10877
+ } else if (clickFailureKind === "stale") {
10878
+ lines.push(
10879
+ `The click target was stale — the page changed since the last snapshot. Call read_page to refresh current indexes, then choose a currently listed target, inspect_element on the intended element, or answer from the results already visible. Do not ask the user to inspect or click the result for you.`
10880
+ );
10881
+ } else {
10882
+ lines.push(
10883
+ `Take the next step yourself: try a different target, refresh the page state with read_page, call inspect_element on the intended element, or answer from the results already visible in the conversation. Do not ask the user to inspect or click the result for you.`
10884
+ );
10885
+ }
10458
10886
  if (failedClickCount >= 2) {
10459
10887
  lines.push(
10460
10888
  `You have already had multiple failed clicks without making page progress. Do not keep clicking similar search result titles. Use the latest read_page/search result text to answer, or inspect a specific indexed result/control only if essential.`
@@ -10665,8 +11093,7 @@ class CodexProvider {
10665
11093
  let flightPriceEvidenceRecoveryCount = 0;
10666
11094
  let correctionCount = 0;
10667
11095
  const recentToolSignatures = [];
10668
- const recentToolNames = [];
10669
- let clickReadLoopNudged = false;
11096
+ const clickReadLoopGuard = new ClickReadLoopGuard();
10670
11097
  let latestToolResultPreview = null;
10671
11098
  let failedClickCountSinceProgress = 0;
10672
11099
  const searchLoopGuard = new SearchLoopGuard(isRealProgressTool);
@@ -10822,6 +11249,22 @@ ${latestToolResultPreview || ""}`
10822
11249
  correctionCount += 1;
10823
11250
  continue;
10824
11251
  }
11252
+ const clickLoopPreflight = clickReadLoopGuard.beforeTool(
11253
+ prepared.prepared.name
11254
+ );
11255
+ if (clickLoopPreflight?.kind === "suppress") {
11256
+ onChunk(`
11257
+ <<tool:click:↻ loop suppressed>>
11258
+ `);
11259
+ const suppressed = createCodexToolOutput(
11260
+ prepared.prepared.callId,
11261
+ clickLoopPreflight.message
11262
+ );
11263
+ currentInput.push(suppressed);
11264
+ latestToolResultPreview = previewToolResult(suppressed.output);
11265
+ correctionCount += 1;
11266
+ continue;
11267
+ }
10825
11268
  const output = await executePreparedCodexFunctionCall(
10826
11269
  prepared.prepared,
10827
11270
  onChunk,
@@ -10834,7 +11277,10 @@ ${latestToolResultPreview || ""}`
10834
11277
  toolHistoryCount += 1;
10835
11278
  latestToolResultPreview = previewToolResult(output.output);
10836
11279
  const outputText = toolResultTextContent(output.output);
10837
- const toolSucceeded = !looksLikeFailedToolOutput(outputText);
11280
+ const toolSucceeded = !looksLikeFailedToolOutput(
11281
+ outputText,
11282
+ prepared.prepared.name
11283
+ );
10838
11284
  if (toolSucceeded && isRealProgressTool(prepared.prepared.name)) {
10839
11285
  failedClickCountSinceProgress = 0;
10840
11286
  }
@@ -10843,13 +11289,14 @@ ${latestToolResultPreview || ""}`
10843
11289
  searchToolQuery,
10844
11290
  toolSucceeded
10845
11291
  );
10846
- if (prepared.prepared.name === "click" && looksLikeFailedToolOutput(outputText)) {
11292
+ if (prepared.prepared.name === "click" && looksLikeFailedToolOutput(outputText, prepared.prepared.name)) {
10847
11293
  failedClickCountSinceProgress += 1;
10848
11294
  currentInput.push(
10849
11295
  buildCodexFailedClickRecoveryInput(
10850
11296
  summarizeToolArg(prepared.prepared.args),
10851
11297
  latestToolResultPreview,
10852
- failedClickCountSinceProgress
11298
+ failedClickCountSinceProgress,
11299
+ outputText
10853
11300
  )
10854
11301
  );
10855
11302
  }
@@ -10857,17 +11304,16 @@ ${latestToolResultPreview || ""}`
10857
11304
  if (recentToolSignatures.length > 4) {
10858
11305
  recentToolSignatures.shift();
10859
11306
  }
10860
- recentToolNames.push(prepared.prepared.name);
10861
- if (recentToolNames.length > 8) recentToolNames.shift();
10862
- if (!clickReadLoopNudged && recentToolNames.length >= 6 && isClickReadLoop(recentToolNames)) {
10863
- clickReadLoopNudged = true;
11307
+ const clickLoopIntervention = clickReadLoopGuard.afterToolResult(
11308
+ prepared.prepared.name,
11309
+ outputText,
11310
+ toolSucceeded
11311
+ );
11312
+ if (clickLoopIntervention?.kind === "nudge") {
10864
11313
  currentInput.push({
10865
11314
  type: "message",
10866
11315
  role: "user",
10867
- content: [{
10868
- type: "input_text",
10869
- text: `[System] You are alternating between click and read_page without advancing the task. The click result already includes a page snapshot when it navigates, so do not read_page after every click. If you need detail on a specific element, use inspect_element. Otherwise continue the original task directly.`
10870
- }]
11316
+ content: [{ type: "input_text", text: clickLoopIntervention.message }]
10871
11317
  });
10872
11318
  }
10873
11319
  correctionCount = 0;
@@ -12091,8 +12537,7 @@ function pageBusyError(action) {
12091
12537
  return `Error: Page is still busy; ${action} timed out waiting for page scripts. Retry in a moment.`;
12092
12538
  }
12093
12539
  async function loadPermittedUrl(wc, url) {
12094
- const { assertPermittedNavigationURL: assertPermittedNavigationURL2 } = await Promise.resolve().then(() => urlSafety);
12095
- assertPermittedNavigationURL2(url);
12540
+ assertPermittedNavigationURL(url);
12096
12541
  await wc.loadURL(url);
12097
12542
  }
12098
12543
  async function executePageScript(wc, script, options) {
@@ -16432,6 +16877,60 @@ async function clickElement(wc, selector) {
16432
16877
  }));
16433
16878
  }
16434
16879
 
16880
+ // Sum offsetTop up the offsetParent chain until reaching "container",
16881
+ // giving the element's vertical position within that scroll container.
16882
+ function offsetTopWithin(el, container) {
16883
+ let top = 0;
16884
+ let node = el;
16885
+ while (node && node !== container) {
16886
+ top += node.offsetTop || 0;
16887
+ node = node.offsetParent;
16888
+ }
16889
+ return top;
16890
+ }
16891
+
16892
+ function nearestScrollableAncestor(el) {
16893
+ let node = el.parentElement;
16894
+ while (node) {
16895
+ if (node instanceof HTMLElement) {
16896
+ const style = window.getComputedStyle(node);
16897
+ if (
16898
+ (style.overflowY === "auto" || style.overflowY === "scroll") &&
16899
+ node.scrollHeight > node.clientHeight
16900
+ ) {
16901
+ return node;
16902
+ }
16903
+ }
16904
+ node = node.parentElement;
16905
+ }
16906
+ return null;
16907
+ }
16908
+
16909
+ // Wait for the element to gain a non-zero layout box, polling for up to
16910
+ // maxFrames animation frames. Lazy / virtual-scroll renderers
16911
+ // (content-visibility, intersection-triggered list items) often lay out
16912
+ // a frame or two after the scroller moves. Falls back to setTimeout when
16913
+ // the window is hidden (requestAnimationFrame does not fire then).
16914
+ function waitForBox(el, maxFrames) {
16915
+ return new Promise((resolve) => {
16916
+ let frames = 0;
16917
+ const rafAvailable =
16918
+ typeof requestAnimationFrame === "function" &&
16919
+ document.visibilityState === "visible";
16920
+ const schedule = rafAvailable
16921
+ ? (cb) => requestAnimationFrame(cb)
16922
+ : (cb) => setTimeout(cb, 16);
16923
+ const check = () => {
16924
+ const r = el.getBoundingClientRect();
16925
+ if (r.width > 0 && r.height > 0) return resolve(true);
16926
+ if (frames >= maxFrames) return resolve(false);
16927
+ frames += 1;
16928
+ schedule(check);
16929
+ };
16930
+ check();
16931
+ });
16932
+ }
16933
+
16435
16934
  const el = document.querySelector(${JSON.stringify(selector)});
16436
16935
  if (!el) return { error: "Error[stale-index]: Element not found — the page may have changed. Call read_page to refresh." };
16437
16936
 
@@ -16439,21 +16938,26 @@ async function clickElement(wc, selector) {
16439
16938
  el.scrollIntoView({ behavior: "instant", block: "center", inline: "center" });
16440
16939
  }
16441
16940
 
16442
- await new Promise((resolve) => {
16443
- let settled = false;
16444
- const finish = () => {
16445
- if (settled) return;
16446
- settled = true;
16447
- resolve(undefined);
16448
- };
16449
- if (
16450
- typeof requestAnimationFrame === "function" &&
16451
- document.visibilityState === "visible"
16452
- ) {
16453
- requestAnimationFrame(() => finish());
16941
+ // Give the renderer a brief grace to lay the element out after the
16942
+ // initial scroll. Already-visible elements resolve on the first check.
16943
+ let revealed = await waitForBox(el, 4);
16944
+
16945
+ // scrollIntoView is a no-op on zero-rect elements (collapsed, lazy, or
16946
+ // virtual-scroll content). Force the nearest scrollable ancestor to bring
16947
+ // the element's offset position into view, then wait longer for the
16948
+ // renderer to produce a layout box. This recovers many hidden targets
16949
+ // without the model having to scroll manually.
16950
+ if (!revealed) {
16951
+ const scroller = nearestScrollableAncestor(el);
16952
+ if (scroller) {
16953
+ const targetTop = offsetTopWithin(el, scroller) - scroller.clientHeight / 2;
16954
+ scroller.scrollTop = Math.max(0, targetTop);
16454
16955
  }
16455
- setTimeout(finish, 32);
16456
- });
16956
+ if (el instanceof HTMLElement) {
16957
+ el.scrollIntoView({ behavior: "instant", block: "center", inline: "center" });
16958
+ }
16959
+ revealed = await waitForBox(el, 24);
16960
+ }
16457
16961
 
16458
16962
  const rect = el.getBoundingClientRect();
16459
16963
  if (rect.width <= 0 || rect.height <= 0) {
@@ -18016,9 +18520,13 @@ Go back and select a different product.`;
18016
18520
  const clickText = `Clicked: ${elInfo.text}${tagLabel}`;
18017
18521
  const clickResult = await clickElement(wc, selector);
18018
18522
  if (clickResult.startsWith("Error:")) return clickResult;
18019
- await waitForPotentialNavigation$1(wc, beforeUrl);
18523
+ const initialNavigationWaitMs = /DOM activation/i.test(clickResult) && !elInfo.href ? 800 : void 0;
18524
+ await waitForPotentialNavigation$1(wc, beforeUrl, initialNavigationWaitMs);
18020
18525
  const afterUrl = wc.getURL();
18021
18526
  if (afterUrl !== beforeUrl) {
18527
+ if (/DOM activation/i.test(clickResult)) {
18528
+ return `${clickText} -> ${afterUrl} (recovered via DOM activation)`;
18529
+ }
18022
18530
  return `${clickText} -> ${afterUrl}`;
18023
18531
  }
18024
18532
  const overlayHint = await detectPostClickOverlay(wc);
@@ -18045,6 +18553,9 @@ ${overlayHint}`;
18045
18553
  }
18046
18554
  return `${clickText} (${clickResult})${await buildCartSuccessSuffix(wc, beforeUrl)}`;
18047
18555
  }
18556
+ if (/DOM activation/i.test(clickResult) && (!elInfo.href || elInfo.target === "_blank")) {
18557
+ return `${clickText} (${clickResult})`;
18558
+ }
18048
18559
  const activationResult = await activateElement(wc, selector);
18049
18560
  if (!activationResult.startsWith("Error:")) {
18050
18561
  await waitForPotentialNavigation$1(wc, beforeUrl);
@@ -22777,7 +23288,7 @@ function registerPrivateIpcHandlers(state2) {
22777
23288
  });
22778
23289
  ipc.handle(Channels.FOCUS_MODE_TOGGLE, () => false);
22779
23290
  ipc.handle(Channels.SIDEBAR_TOGGLE, () => ({ open: false, width: 0 }));
22780
- ipc.handle(Channels.DEVTOOLS_PANEL_TOGGLE, () => ({ open: false }));
23291
+ registerDisabledDevToolsPanelHandlers(ipc);
22781
23292
  ipc.handle(
22782
23293
  Channels.FIND_IN_PAGE_START,
22783
23294
  (_e, text, options) => {
@@ -23041,7 +23552,7 @@ function registerSecondaryIpcHandlers(state2) {
23041
23552
  ipc.handle(Channels.SETTINGS_VISIBILITY, () => false);
23042
23553
  ipc.handle(Channels.FOCUS_MODE_TOGGLE, () => false);
23043
23554
  ipc.handle(Channels.SIDEBAR_TOGGLE, () => ({ open: false, width: 0 }));
23044
- ipc.handle(Channels.DEVTOOLS_PANEL_TOGGLE, () => ({ open: false }));
23555
+ registerDisabledDevToolsPanelHandlers(ipc);
23045
23556
  ipc.handle(
23046
23557
  Channels.FIND_IN_PAGE_START,
23047
23558
  (_e, text, options) => {
@@ -23847,6 +24358,9 @@ All open tabs: ${allTabs.map((t) => `${t.id === activeTabId ? "→ " : ""}${t.ti
23847
24358
  let isError = false;
23848
24359
  try {
23849
24360
  output = await executeAction(name, args, actionCtx);
24361
+ if (/^\s*Error:/i.test(output)) {
24362
+ isError = true;
24363
+ }
23850
24364
  if (provider.agentToolProfile === "compact") {
23851
24365
  runtime2.updateTaskTracker(name, output);
23852
24366
  const trackerCtx = runtime2.getTaskTrackerContext();
@@ -24571,8 +25085,14 @@ const DANGEROUS_DEVTOOLS_ACTIONS = /* @__PURE__ */ new Set([
24571
25085
  ]);
24572
25086
  let stateListener = null;
24573
25087
  const activityLog = [];
25088
+ const agentTraceLog = [];
24574
25089
  const MAX_ACTIVITY_ENTRIES = 100;
25090
+ const MAX_TRACE_ENTRIES = 200;
24575
25091
  let activityCounter = 0;
25092
+ let traceCounter = 0;
25093
+ let latestPageMap = null;
25094
+ let pageMapRefreshInFlight = false;
25095
+ const traceEntriesByActionId = /* @__PURE__ */ new Map();
24576
25096
  function setDevToolsPanelListener(listener) {
24577
25097
  stateListener = listener;
24578
25098
  }
@@ -24582,14 +25102,325 @@ function getDevToolsPanelState(tabId) {
24582
25102
  console: session?.getConsoleLogs() ?? [],
24583
25103
  network: session?.getNetworkLog() ?? [],
24584
25104
  errors: session?.getErrors() ?? [],
24585
- activity: activityLog
25105
+ activity: activityLog,
25106
+ agentTrace: agentTraceLog,
25107
+ pageMap: latestPageMap
24586
25108
  };
24587
25109
  }
24588
- function broadcastState(tabManager) {
25110
+ function broadcastDevToolsPanelState(tabManager) {
24589
25111
  if (!stateListener) return;
24590
25112
  const tabId = tabManager.getActiveTabId();
24591
25113
  stateListener(getDevToolsPanelState(tabId));
24592
25114
  }
25115
+ let panelBroadcastScheduled = false;
25116
+ let panelBroadcastTabManager = null;
25117
+ let panelCapturedTabId = null;
25118
+ const panelOwnedCaptureTabs = /* @__PURE__ */ new Set();
25119
+ function schedulePanelBroadcast(tabManager) {
25120
+ panelBroadcastTabManager = tabManager;
25121
+ if (panelBroadcastScheduled) return;
25122
+ panelBroadcastScheduled = true;
25123
+ queueMicrotask(() => {
25124
+ panelBroadcastScheduled = false;
25125
+ if (panelBroadcastTabManager) {
25126
+ broadcastDevToolsPanelState(panelBroadcastTabManager);
25127
+ }
25128
+ });
25129
+ }
25130
+ function releasePanelCaptureForTab(tabId) {
25131
+ const session = getSession(tabId);
25132
+ session?.setOnCaptureChange(null);
25133
+ if (panelCapturedTabId === tabId) {
25134
+ panelCapturedTabId = null;
25135
+ }
25136
+ if (panelOwnedCaptureTabs.delete(tabId)) {
25137
+ destroySession(tabId);
25138
+ }
25139
+ }
25140
+ function markActiveSessionToolOwned(tabManager) {
25141
+ const tabId = tabManager.getActiveTabId();
25142
+ if (tabId) {
25143
+ panelOwnedCaptureTabs.delete(tabId);
25144
+ }
25145
+ }
25146
+ async function enableCaptureForTab(tabManager) {
25147
+ const tabId = tabManager.getActiveTabId();
25148
+ if (!tabId) return;
25149
+ if (panelCapturedTabId && panelCapturedTabId !== tabId) {
25150
+ releasePanelCaptureForTab(panelCapturedTabId);
25151
+ }
25152
+ let session = getSession(tabId);
25153
+ const panelCreatedSession = !session;
25154
+ session ??= getOrCreateSession(tabManager);
25155
+ if (panelCreatedSession) {
25156
+ panelOwnedCaptureTabs.add(tabId);
25157
+ }
25158
+ panelCapturedTabId = tabId;
25159
+ session.setOnCaptureChange(() => {
25160
+ if (panelCapturedTabId === tabId) {
25161
+ schedulePanelBroadcast(tabManager);
25162
+ }
25163
+ });
25164
+ await session.enableCapture();
25165
+ if (panelCapturedTabId === tabId) {
25166
+ broadcastDevToolsPanelState(tabManager);
25167
+ }
25168
+ }
25169
+ function disableCaptureForTab(tabId) {
25170
+ const targetTabId = panelCapturedTabId;
25171
+ if (targetTabId) {
25172
+ releasePanelCaptureForTab(targetTabId);
25173
+ }
25174
+ {
25175
+ for (const ownedTabId of [...panelOwnedCaptureTabs]) {
25176
+ releasePanelCaptureForTab(ownedTabId);
25177
+ }
25178
+ panelBroadcastTabManager = null;
25179
+ }
25180
+ }
25181
+ function pushTrace(entry) {
25182
+ const traceEntry = {
25183
+ id: ++traceCounter,
25184
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
25185
+ ...entry
25186
+ };
25187
+ agentTraceLog.push(traceEntry);
25188
+ if (agentTraceLog.length > MAX_TRACE_ENTRIES) {
25189
+ const removed = agentTraceLog.splice(
25190
+ 0,
25191
+ agentTraceLog.length - MAX_TRACE_ENTRIES
25192
+ );
25193
+ for (const trace of removed) {
25194
+ if (trace.actionId) traceEntriesByActionId.delete(trace.actionId);
25195
+ }
25196
+ }
25197
+ return traceEntry;
25198
+ }
25199
+ function formatTraceActionName(name) {
25200
+ return name.replace(/^devtools_/, "").split(/[_-]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
25201
+ }
25202
+ function traceDetail(event) {
25203
+ const sourcePrefix = event.source === "ai" ? "Agent" : event.source.toUpperCase();
25204
+ return [sourcePrefix, event.detail].filter(Boolean).join(": ").slice(0, 240);
25205
+ }
25206
+ function recordDevToolsAgentAction(event, tabManager) {
25207
+ const actionName = formatTraceActionName(event.name);
25208
+ if (event.phase === "started") {
25209
+ const entry = pushTrace({
25210
+ actionId: event.actionId,
25211
+ kind: "tool-start",
25212
+ title: `Started ${actionName}`,
25213
+ detail: traceDetail(event),
25214
+ status: "running",
25215
+ tool: event.name
25216
+ });
25217
+ traceEntriesByActionId.set(event.actionId, entry);
25218
+ broadcastDevToolsPanelState(tabManager);
25219
+ return;
25220
+ }
25221
+ const startEntry = traceEntriesByActionId.get(event.actionId);
25222
+ if (startEntry) {
25223
+ startEntry.status = event.phase === "completed" ? "completed" : "failed";
25224
+ startEntry.durationMs = event.durationMs;
25225
+ if (event.phase === "waiting-approval") {
25226
+ startEntry.status = "running";
25227
+ startEntry.detail = traceDetail(event);
25228
+ }
25229
+ }
25230
+ if (event.phase === "waiting-approval") {
25231
+ broadcastDevToolsPanelState(tabManager);
25232
+ return;
25233
+ }
25234
+ pushTrace({
25235
+ actionId: event.actionId,
25236
+ kind: event.phase === "completed" ? "tool-complete" : "tool-error",
25237
+ title: event.phase === "completed" ? `Completed ${actionName}` : `${event.phase === "rejected" ? "Rejected" : "Failed"} ${actionName}`,
25238
+ detail: traceDetail(event),
25239
+ status: event.phase === "completed" ? "completed" : "failed",
25240
+ tool: event.name,
25241
+ durationMs: event.durationMs
25242
+ });
25243
+ traceEntriesByActionId.delete(event.actionId);
25244
+ broadcastDevToolsPanelState(tabManager);
25245
+ }
25246
+ const PAGE_MAP_SCRIPT = `
25247
+ (() => {
25248
+ const INTERACTIVE_SELECTOR = [
25249
+ "a[href]",
25250
+ "button",
25251
+ "input",
25252
+ "select",
25253
+ "textarea",
25254
+ "summary",
25255
+ "[role='button']",
25256
+ "[role='link']",
25257
+ "[role='checkbox']",
25258
+ "[role='radio']",
25259
+ "[role='switch']",
25260
+ "[role='tab']",
25261
+ "[role='menuitem']",
25262
+ "[contenteditable='true']",
25263
+ "[tabindex]:not([tabindex='-1'])"
25264
+ ].join(",");
25265
+
25266
+ function round(value) {
25267
+ return Math.round(value * 10) / 10;
25268
+ }
25269
+
25270
+ function textFor(el) {
25271
+ const direct =
25272
+ el.getAttribute("aria-label") ||
25273
+ el.getAttribute("title") ||
25274
+ el.getAttribute("placeholder") ||
25275
+ el.getAttribute("alt") ||
25276
+ el.value ||
25277
+ el.innerText ||
25278
+ el.textContent ||
25279
+ "";
25280
+ return String(direct).replace(/\\s+/g, " ").trim().slice(0, 140);
25281
+ }
25282
+
25283
+ function selectorFor(el) {
25284
+ if (el.id) return "#" + CSS.escape(el.id);
25285
+ const parts = [];
25286
+ let node = el;
25287
+ while (node && node.nodeType === Node.ELEMENT_NODE && parts.length < 4) {
25288
+ let part = node.localName;
25289
+ const classes = Array.from(node.classList || []).slice(0, 2);
25290
+ if (classes.length) part += "." + classes.map((c) => CSS.escape(c)).join(".");
25291
+ const parent = node.parentElement;
25292
+ if (parent) {
25293
+ const siblings = Array.from(parent.children).filter((child) => child.localName === node.localName);
25294
+ if (siblings.length > 1) part += ":nth-of-type(" + (siblings.indexOf(node) + 1) + ")";
25295
+ }
25296
+ parts.unshift(part);
25297
+ node = parent;
25298
+ }
25299
+ return parts.join(" > ");
25300
+ }
25301
+
25302
+ function isVisible(el, style, rect) {
25303
+ return rect.width > 0 &&
25304
+ rect.height > 0 &&
25305
+ style.display !== "none" &&
25306
+ style.visibility !== "hidden" &&
25307
+ Number(style.opacity || "1") > 0.01;
25308
+ }
25309
+
25310
+ const viewport = {
25311
+ width: window.innerWidth,
25312
+ height: window.innerHeight,
25313
+ scrollX: window.scrollX,
25314
+ scrollY: window.scrollY,
25315
+ };
25316
+
25317
+ const candidates = Array.from(document.querySelectorAll(INTERACTIVE_SELECTOR));
25318
+ const elements = candidates.slice(0, 120).map((el, index) => {
25319
+ const style = window.getComputedStyle(el);
25320
+ const rect = el.getBoundingClientRect();
25321
+ const visible = isVisible(el, style, rect);
25322
+ const disabled = Boolean(el.disabled) || el.getAttribute("aria-disabled") === "true";
25323
+ const inViewport = rect.bottom >= 0 && rect.right >= 0 && rect.top <= viewport.height && rect.left <= viewport.width;
25324
+ let issue = "";
25325
+ let blocked = false;
25326
+ if (!visible) issue = "hidden";
25327
+ else if (!inViewport) issue = "offscreen";
25328
+ else {
25329
+ const centerX = Math.min(Math.max(rect.left + rect.width / 2, 0), viewport.width - 1);
25330
+ const centerY = Math.min(Math.max(rect.top + rect.height / 2, 0), viewport.height - 1);
25331
+ const top = document.elementFromPoint(centerX, centerY);
25332
+ if (top && top !== el && !el.contains(top)) {
25333
+ blocked = true;
25334
+ issue = "covered by " + top.localName;
25335
+ }
25336
+ }
25337
+ return {
25338
+ id: index + 1,
25339
+ tag: el.localName,
25340
+ role: el.getAttribute("role") || undefined,
25341
+ label: textFor(el) || "(unlabeled)",
25342
+ selector: selectorFor(el),
25343
+ href: el.href || undefined,
25344
+ type: el.type || undefined,
25345
+ visible,
25346
+ interactable: visible && inViewport && !disabled && !blocked,
25347
+ disabled,
25348
+ issue: issue || undefined,
25349
+ bounds: {
25350
+ x: round(rect.x),
25351
+ y: round(rect.y),
25352
+ width: round(rect.width),
25353
+ height: round(rect.height),
25354
+ },
25355
+ };
25356
+ });
25357
+
25358
+ const counts = elements.reduce((acc, element) => {
25359
+ acc.total += 1;
25360
+ if (element.visible) acc.visible += 1;
25361
+ if (element.interactable) acc.interactable += 1;
25362
+ if (element.disabled) acc.disabled += 1;
25363
+ if (element.issue && element.issue.startsWith("covered")) acc.blocked += 1;
25364
+ return acc;
25365
+ }, { total: 0, visible: 0, interactable: 0, disabled: 0, blocked: 0 });
25366
+
25367
+ return {
25368
+ timestamp: new Date().toISOString(),
25369
+ pageUrl: location.href,
25370
+ title: document.title || "",
25371
+ viewport,
25372
+ counts,
25373
+ elements,
25374
+ accessIssues: elements.length === 0 ? ["No obvious interactive elements found."] : [],
25375
+ };
25376
+ })()
25377
+ `;
25378
+ async function capturePageMapSnapshot(tabManager) {
25379
+ const tab = tabManager.getActiveTab();
25380
+ if (!tab || tab.view.webContents.isDestroyed()) {
25381
+ return {
25382
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
25383
+ pageUrl: "",
25384
+ title: "No active tab",
25385
+ viewport: { width: 0, height: 0, scrollX: 0, scrollY: 0 },
25386
+ counts: { total: 0, visible: 0, interactable: 0, disabled: 0, blocked: 0 },
25387
+ elements: [],
25388
+ accessIssues: ["No active tab is available."]
25389
+ };
25390
+ }
25391
+ try {
25392
+ return await tab.view.webContents.executeJavaScript(PAGE_MAP_SCRIPT, true);
25393
+ } catch (error) {
25394
+ return {
25395
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
25396
+ pageUrl: tab.view.webContents.getURL(),
25397
+ title: "Page map unavailable",
25398
+ viewport: { width: 0, height: 0, scrollX: 0, scrollY: 0 },
25399
+ counts: { total: 0, visible: 0, interactable: 0, disabled: 0, blocked: 0 },
25400
+ elements: [],
25401
+ accessIssues: [
25402
+ error instanceof Error ? error.message : "Could not inspect active page."
25403
+ ]
25404
+ };
25405
+ }
25406
+ }
25407
+ async function refreshDevToolsPageMap(tabManager) {
25408
+ if (pageMapRefreshInFlight) {
25409
+ return getDevToolsPanelState(tabManager.getActiveTabId());
25410
+ }
25411
+ pageMapRefreshInFlight = true;
25412
+ try {
25413
+ latestPageMap = await capturePageMapSnapshot(tabManager);
25414
+ } finally {
25415
+ pageMapRefreshInFlight = false;
25416
+ }
25417
+ broadcastDevToolsPanelState(tabManager);
25418
+ return getDevToolsPanelState(tabManager.getActiveTabId());
25419
+ }
25420
+ function broadcastState(tabManager) {
25421
+ broadcastDevToolsPanelState(tabManager);
25422
+ void refreshDevToolsPageMap(tabManager);
25423
+ }
24593
25424
  async function withDevToolsAction(runtime2, tabManager, name, args, executor) {
24594
25425
  try {
24595
25426
  assertFeatureUnlocked("devtools", "DevTools");
@@ -24598,6 +25429,7 @@ async function withDevToolsAction(runtime2, tabManager, name, args, executor) {
24598
25429
  `Error: ${error instanceof Error ? error.message : "DevTools require Vessel Premium."}`
24599
25430
  );
24600
25431
  }
25432
+ markActiveSessionToolOwned(tabManager);
24601
25433
  const activityEntry = {
24602
25434
  id: ++activityCounter,
24603
25435
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -30216,6 +31048,76 @@ async function togglePictureInPicture(tabManager) {
30216
31048
  return false;
30217
31049
  }
30218
31050
  }
31051
+ const REVEAL_SCRIPT = `
31052
+ (function () {
31053
+ var selector = __SELECTOR_JSON__;
31054
+ try {
31055
+ var el = document.querySelector(selector);
31056
+ } catch (e) {
31057
+ return "invalid-selector";
31058
+ }
31059
+ if (!el) return "not-found";
31060
+ try {
31061
+ el.scrollIntoView({ block: "center", behavior: "smooth" });
31062
+ } catch (e) {}
31063
+ var rect = el.getBoundingClientRect();
31064
+ var overlay = document.createElement("div");
31065
+ overlay.setAttribute("data-vessel-devtools-reveal", "");
31066
+ overlay.style.cssText = [
31067
+ "position: fixed",
31068
+ "left: " + rect.left + "px",
31069
+ "top: " + rect.top + "px",
31070
+ "width: " + rect.width + "px",
31071
+ "height: " + rect.height + "px",
31072
+ "box-sizing: border-box",
31073
+ "border: 2px solid #4f8cff",
31074
+ "background: rgba(79, 140, 255, 0.18)",
31075
+ "border-radius: 4px",
31076
+ "pointer-events: none",
31077
+ "z-index: 2147483647",
31078
+ "transition: opacity 250ms ease-out",
31079
+ "opacity: 1",
31080
+ ].join("; ");
31081
+ document.documentElement.appendChild(overlay);
31082
+ var start = performance.now();
31083
+ function track(now) {
31084
+ var r = el.getBoundingClientRect();
31085
+ overlay.style.left = r.left + "px";
31086
+ overlay.style.top = r.top + "px";
31087
+ overlay.style.width = r.width + "px";
31088
+ overlay.style.height = r.height + "px";
31089
+ if (now - start < 1000) {
31090
+ requestAnimationFrame(track);
31091
+ } else {
31092
+ overlay.style.opacity = "0";
31093
+ setTimeout(function () {
31094
+ if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
31095
+ }, 300);
31096
+ }
31097
+ }
31098
+ requestAnimationFrame(track);
31099
+ return "revealed";
31100
+ })();
31101
+ `;
31102
+ async function revealPageMapElement(tabManager, selector) {
31103
+ const tab = tabManager.getActiveTab();
31104
+ if (!tab || tab.view.webContents.isDestroyed()) {
31105
+ return "no-active-tab";
31106
+ }
31107
+ try {
31108
+ const script = REVEAL_SCRIPT.replace(
31109
+ "__SELECTOR_JSON__",
31110
+ JSON.stringify(selector)
31111
+ );
31112
+ const result = await tab.view.webContents.executeJavaScript(script, true);
31113
+ if (result === "revealed" || result === "not-found" || result === "invalid-selector") {
31114
+ return result;
31115
+ }
31116
+ return "revealed";
31117
+ } catch {
31118
+ return "invalid-selector";
31119
+ }
31120
+ }
30219
31121
  const VALID_KIT_CATEGORIES = /* @__PURE__ */ new Set([
30220
31122
  "research",
30221
31123
  "shopping",
@@ -30736,21 +31638,177 @@ function stopScheduler() {
30736
31638
  const KitIdSchema = zod.z.string().min(1);
30737
31639
  const SkillSourceSchema = zod.z.string().min(1).max(1e5);
30738
31640
  const OriginSchema = zod.z.string().min(1);
31641
+ const DevToolsHeightSchema = zod.z.number().finite().min(0).max(2e3);
31642
+ const DevToolsPageMapRevealSchema = zod.z.object({
31643
+ selector: zod.z.string().min(1)
31644
+ });
31645
+ const DevToolsPanelTabSchema = zod.z.enum([
31646
+ "console",
31647
+ "network",
31648
+ "activity",
31649
+ "agentTrace",
31650
+ "pageMap"
31651
+ ]);
31652
+ const RendererViewSchema = zod.z.enum(["chrome", "sidebar", "devtools"]);
30739
31653
  function registerSystemHandlers(windowState, sendToRendererViews) {
30740
31654
  const { tabManager } = windowState;
31655
+ let devToolsResizeRecoveryTimer = null;
31656
+ let devToolsResizeActive = false;
31657
+ const relayout = () => layoutViews(windowState);
31658
+ const clearDevToolsResizeRecoveryTimer = () => {
31659
+ if (!devToolsResizeRecoveryTimer) return;
31660
+ clearTimeout(devToolsResizeRecoveryTimer);
31661
+ devToolsResizeRecoveryTimer = null;
31662
+ };
31663
+ const stopDevToolsResize = () => {
31664
+ devToolsResizeActive = false;
31665
+ clearDevToolsResizeRecoveryTimer();
31666
+ };
31667
+ const restoreDevToolsLayoutAfterResize = () => {
31668
+ clearDevToolsResizeRecoveryTimer();
31669
+ if (!devToolsResizeActive) return;
31670
+ devToolsResizeActive = false;
31671
+ relayout();
31672
+ };
31673
+ const scheduleDevToolsResizeRecovery = () => {
31674
+ clearDevToolsResizeRecoveryTimer();
31675
+ devToolsResizeRecoveryTimer = setTimeout(() => {
31676
+ restoreDevToolsLayoutAfterResize();
31677
+ }, 1200);
31678
+ };
31679
+ const maxDockedDevToolsHeight = () => {
31680
+ const [, windowHeight] = windowState.mainWindow.getContentSize();
31681
+ const chromeHeight = windowState.uiState.focusMode ? 0 : CHROME_HEIGHT;
31682
+ return Math.max(
31683
+ MIN_DEVTOOLS_PANEL,
31684
+ Math.min(MAX_DEVTOOLS_PANEL, windowHeight - chromeHeight - 80)
31685
+ );
31686
+ };
31687
+ windowState.mainWindow.once("closed", stopDevToolsResize);
30741
31688
  electron.ipcMain.handle(Channels.DEVTOOLS_PANEL_TOGGLE, (event) => {
30742
31689
  assertTrustedIpcSender(event);
30743
- windowState.uiState.devtoolsPanelOpen = !windowState.uiState.devtoolsPanelOpen;
30744
- layoutViews(windowState);
30745
- return { open: windowState.uiState.devtoolsPanelOpen };
31690
+ stopDevToolsResize();
31691
+ const hostState = toggleDockedDevToolsPanel(windowState, { relayout });
31692
+ if (hostState.open) {
31693
+ void enableCaptureForTab(tabManager);
31694
+ } else {
31695
+ disableCaptureForTab();
31696
+ }
31697
+ return hostState;
31698
+ });
31699
+ electron.ipcMain.handle(Channels.DEVTOOLS_PANEL_CLOSE, (event) => {
31700
+ assertTrustedIpcSender(event);
31701
+ stopDevToolsResize();
31702
+ const hostState = closeDevToolsPanel(windowState, { relayout });
31703
+ disableCaptureForTab();
31704
+ return hostState;
31705
+ });
31706
+ electron.ipcMain.handle(Channels.DEVTOOLS_PANEL_OPEN_TAB, (event, tab) => {
31707
+ assertTrustedIpcSender(event);
31708
+ const selectedTab = parseIpc(DevToolsPanelTabSchema, tab, "tab");
31709
+ stopDevToolsResize();
31710
+ const wasOpen = getDevToolsPanelHostState(windowState).open;
31711
+ if (!wasOpen) {
31712
+ toggleDockedDevToolsPanel(windowState, { relayout });
31713
+ void enableCaptureForTab(tabManager);
31714
+ } else if (getDevToolsPanelHostState(windowState).detached) {
31715
+ windowState.devtoolsPanelWindow?.focus();
31716
+ }
31717
+ emitDevToolsPanelHostState(windowState);
31718
+ sendSafe(
31719
+ windowState.devtoolsPanelView.webContents,
31720
+ Channels.DEVTOOLS_PANEL_SELECT_TAB,
31721
+ selectedTab
31722
+ );
31723
+ if (selectedTab === "pageMap") {
31724
+ void refreshDevToolsPageMap(tabManager);
31725
+ }
31726
+ return getDevToolsPanelHostState(windowState);
31727
+ });
31728
+ electron.ipcMain.handle(Channels.DEVTOOLS_PANEL_RESIZE_START, (event) => {
31729
+ assertTrustedIpcSender(event);
31730
+ if (!getDevToolsPanelHostState(windowState).open) return;
31731
+ if (getDevToolsPanelHostState(windowState).detached) return;
31732
+ devToolsResizeActive = true;
31733
+ clearDevToolsResizeRecoveryTimer();
31734
+ const [windowWidth, windowHeight] = windowState.mainWindow.getContentSize();
31735
+ const chromeHeight = windowState.uiState.focusMode ? 0 : CHROME_HEIGHT;
31736
+ const sidebarWidth = isSidebarAttached(windowState) ? windowState.uiState.sidebarWidth : 0;
31737
+ windowState.devtoolsPanelView.setBounds({
31738
+ x: 0,
31739
+ y: chromeHeight,
31740
+ width: windowWidth - sidebarWidth,
31741
+ height: windowHeight - chromeHeight
31742
+ });
31743
+ scheduleDevToolsResizeRecovery();
30746
31744
  });
30747
31745
  electron.ipcMain.handle(Channels.DEVTOOLS_PANEL_RESIZE, (event, height) => {
30748
31746
  assertTrustedIpcSender(event);
30749
- const clamped = Math.max(MIN_DEVTOOLS_PANEL, Math.min(MAX_DEVTOOLS_PANEL, Math.round(height)));
30750
- windowState.uiState.devtoolsPanelHeight = clamped;
30751
- layoutViews(windowState);
31747
+ const validatedHeight = parseIpc(DevToolsHeightSchema, height, "height");
31748
+ const clamped = Math.max(
31749
+ MIN_DEVTOOLS_PANEL,
31750
+ Math.min(maxDockedDevToolsHeight(), Math.round(validatedHeight))
31751
+ );
31752
+ if (devToolsResizeActive) {
31753
+ windowState.uiState.devtoolsPanelHeight = clamped;
31754
+ scheduleDevToolsResizeRecovery();
31755
+ emitDevToolsPanelHostState(windowState);
31756
+ return clamped;
31757
+ }
31758
+ resizeDockedDevToolsPanel(windowState, clamped, relayout);
30752
31759
  return clamped;
30753
31760
  });
31761
+ electron.ipcMain.handle(Channels.DEVTOOLS_PANEL_RESIZE_COMMIT, (event) => {
31762
+ assertTrustedIpcSender(event);
31763
+ stopDevToolsResize();
31764
+ relayout();
31765
+ });
31766
+ electron.ipcMain.handle(Channels.DEVTOOLS_PANEL_POPOUT, (event) => {
31767
+ assertTrustedIpcSender(event);
31768
+ stopDevToolsResize();
31769
+ return detachDevToolsPanel(windowState, {
31770
+ relayout,
31771
+ getWindowIconPath
31772
+ });
31773
+ });
31774
+ electron.ipcMain.handle(Channels.DEVTOOLS_PANEL_DOCK, (event) => {
31775
+ assertTrustedIpcSender(event);
31776
+ stopDevToolsResize();
31777
+ return dockDevToolsPanel(windowState, { relayout });
31778
+ });
31779
+ electron.ipcMain.handle(Channels.DEVTOOLS_PANEL_STATE_GET, async (event) => {
31780
+ assertTrustedIpcSender(event);
31781
+ if (getDevToolsPanelHostState(windowState).open) {
31782
+ void enableCaptureForTab(tabManager);
31783
+ }
31784
+ return await refreshDevToolsPageMap(tabManager);
31785
+ });
31786
+ electron.ipcMain.handle(
31787
+ Channels.DEVTOOLS_PAGE_MAP_REVEAL,
31788
+ async (event, payload) => {
31789
+ assertTrustedIpcSender(event);
31790
+ const { selector } = parseIpc(
31791
+ DevToolsPageMapRevealSchema,
31792
+ payload,
31793
+ "payload"
31794
+ );
31795
+ return await revealPageMapElement(tabManager, selector);
31796
+ }
31797
+ );
31798
+ electron.ipcMain.handle(Channels.DEVTOOLS_PANEL_HOST_STATE_GET, (event) => {
31799
+ assertTrustedIpcSender(event);
31800
+ return getDevToolsPanelHostState(windowState);
31801
+ });
31802
+ electron.ipcMain.on(Channels.RENDERER_VIEW_READY, (event, view) => {
31803
+ assertTrustedIpcSender(event);
31804
+ const readyView = parseIpc(RendererViewSchema, view, "view");
31805
+ if (readyView !== "devtools") return;
31806
+ emitDevToolsPanelHostState(windowState);
31807
+ void refreshDevToolsPageMap(tabManager);
31808
+ if (getDevToolsPanelHostState(windowState).open) {
31809
+ void enableCaptureForTab(tabManager);
31810
+ }
31811
+ });
30754
31812
  electron.ipcMain.handle(Channels.AUTOMATION_GET_INSTALLED, async (event) => {
30755
31813
  assertTrustedIpcSender(event);
30756
31814
  assertFeatureUnlocked("automation_kits", "Skills");
@@ -33140,6 +34198,7 @@ class AgentRuntime {
33140
34198
  tabManager;
33141
34199
  state;
33142
34200
  updateListener = null;
34201
+ actionLifecycleListener = null;
33143
34202
  pendingResolvers = /* @__PURE__ */ new Map();
33144
34203
  undoSnapshots = [];
33145
34204
  mcpUnsubscribe = null;
@@ -33149,6 +34208,9 @@ class AgentRuntime {
33149
34208
  listener(this.getState());
33150
34209
  }
33151
34210
  }
34211
+ setActionLifecycleListener(listener) {
34212
+ this.actionLifecycleListener = listener;
34213
+ }
33152
34214
  /**
33153
34215
  * Release all resources, listeners, and pending promises.
33154
34216
  * Call when the window is closing to prevent memory leaks.
@@ -33167,6 +34229,7 @@ class AgentRuntime {
33167
34229
  this.state.flowState = null;
33168
34230
  this.state.taskTracker = null;
33169
34231
  this.updateListener = null;
34232
+ this.actionLifecycleListener = null;
33170
34233
  }
33171
34234
  getState() {
33172
34235
  const snapshot2 = clone(this.state);
@@ -33471,8 +34534,18 @@ ${progress}
33471
34534
  args,
33472
34535
  tabId
33473
34536
  });
34537
+ const actionStartedAt = Date.now();
33474
34538
  const transcriptStreamId = `action:${action.id}`;
33475
34539
  const transcriptTitle = humanizeActionName(name);
34540
+ this.emitActionLifecycle({
34541
+ actionId: action.id,
34542
+ source,
34543
+ name,
34544
+ args,
34545
+ tabId,
34546
+ phase: "started",
34547
+ detail: summarizeArgs(args)
34548
+ });
33476
34549
  this.publishTranscript({
33477
34550
  source,
33478
34551
  kind: "status",
@@ -33483,6 +34556,16 @@ ${progress}
33483
34556
  });
33484
34557
  const approvalReason = this.getApprovalReason(dangerous, requiresApproval);
33485
34558
  if (approvalReason) {
34559
+ this.emitActionLifecycle({
34560
+ actionId: action.id,
34561
+ source,
34562
+ name,
34563
+ args,
34564
+ tabId,
34565
+ phase: "waiting-approval",
34566
+ detail: approvalReason,
34567
+ durationMs: Date.now() - actionStartedAt
34568
+ });
33486
34569
  this.publishTranscript({
33487
34570
  source,
33488
34571
  kind: "status",
@@ -33493,6 +34576,16 @@ ${progress}
33493
34576
  });
33494
34577
  const approved = await this.awaitApproval(action, approvalReason);
33495
34578
  if (!approved) {
34579
+ this.emitActionLifecycle({
34580
+ actionId: action.id,
34581
+ source,
34582
+ name,
34583
+ args,
34584
+ tabId,
34585
+ phase: "rejected",
34586
+ detail: approvalReason,
34587
+ durationMs: Date.now() - actionStartedAt
34588
+ });
33496
34589
  this.publishTranscript({
33497
34590
  source,
33498
34591
  kind: "status",
@@ -33524,6 +34617,16 @@ ${progress}
33524
34617
  this.pushUndoSnapshot(undoSnapshot);
33525
34618
  }
33526
34619
  this.finishAction(action.id, "completed", summarizeText(result));
34620
+ this.emitActionLifecycle({
34621
+ actionId: action.id,
34622
+ source,
34623
+ name,
34624
+ args,
34625
+ tabId,
34626
+ phase: "completed",
34627
+ detail: summarizeText(result),
34628
+ durationMs: Date.now() - actionStartedAt
34629
+ });
33527
34630
  this.publishTranscript({
33528
34631
  source,
33529
34632
  kind: "status",
@@ -33538,6 +34641,16 @@ ${progress}
33538
34641
  const message = error instanceof Error ? error.message : "Unknown action failure";
33539
34642
  this.state.supervisor.lastError = message;
33540
34643
  this.finishAction(action.id, "failed", void 0, message);
34644
+ this.emitActionLifecycle({
34645
+ actionId: action.id,
34646
+ source,
34647
+ name,
34648
+ args,
34649
+ tabId,
34650
+ phase: "failed",
34651
+ detail: summarizeText(message),
34652
+ durationMs: Date.now() - actionStartedAt
34653
+ });
33541
34654
  this.publishTranscript({
33542
34655
  source,
33543
34656
  kind: "status",
@@ -33549,6 +34662,15 @@ ${progress}
33549
34662
  throw error;
33550
34663
  }
33551
34664
  }
34665
+ emitActionLifecycle(event) {
34666
+ try {
34667
+ this.actionLifecycleListener?.(event);
34668
+ } catch (error) {
34669
+ logger$3.warn("Action lifecycle listener failed", {
34670
+ error: error instanceof Error ? error.message : String(error)
34671
+ });
34672
+ }
34673
+ }
33552
34674
  createUndoSnapshot(name) {
33553
34675
  return {
33554
34676
  id: crypto$1.randomUUID(),
@@ -34122,6 +35244,10 @@ async function bootstrap() {
34122
35244
  if (meta.persistSession) {
34123
35245
  runtime?.onTabStateChanged();
34124
35246
  }
35247
+ if (windowState.uiState.devtoolsPanelMode !== "closed") {
35248
+ void enableCaptureForTab(windowState.tabManager);
35249
+ void refreshDevToolsPageMap(windowState.tabManager);
35250
+ }
34125
35251
  });
34126
35252
  windowStateForShutdown = windowState;
34127
35253
  let didRevealMainWindow = false;
@@ -34138,6 +35264,9 @@ async function bootstrap() {
34138
35264
  }, 8e3);
34139
35265
  const { chromeView, sidebarView, devtoolsPanelView, tabManager } = windowState;
34140
35266
  runtime = new AgentRuntime(tabManager);
35267
+ runtime.setActionLifecycleListener((event) => {
35268
+ recordDevToolsAgentAction(event, tabManager);
35269
+ });
34141
35270
  installAdBlocking(tabManager);
34142
35271
  setDevToolsPanelListener((state2) => {
34143
35272
  sendSafe(devtoolsPanelView.webContents, Channels.DEVTOOLS_PANEL_STATE, state2);