@quanta-intellect/vessel-browser 0.1.28 → 0.1.30

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/README.md CHANGED
@@ -29,7 +29,7 @@ https://github.com/user-attachments/assets/0a72b48a-873a-4eb0-b8f2-23e34d8472c4
29
29
 
30
30
  ## Quick Start
31
31
 
32
- Want the full agent toolkit from day one? [Start a 5-Day Free Trial of Vessel Premium](https://vesselpremium.quantaintellect.com/checkout).
32
+ Want the full agent toolkit from day one? [Start a 7-Day Free Trial of Vessel Premium — $5.99/mo](https://vesselpremium.quantaintellect.com/checkout).
33
33
 
34
34
  ### Fastest Install Today
35
35
 
@@ -162,8 +162,10 @@ The installer:
162
162
  - creates a `vessel-browser-status` helper in `~/.local/bin`
163
163
  - creates a desktop entry for Linux app launchers
164
164
  - writes `~/.config/vessel/vessel-settings.json` with MCP port `3100`
165
+ - writes `~/.config/vessel/mcp-stdio-snippet.json`
165
166
  - writes `~/.config/vessel/mcp-http-snippet.json`
166
- - prints the exact HTTP MCP snippet to paste into your harness config
167
+ - installs a `vessel-browser-mcp` helper that can run as a stdio-to-HTTP proxy (`--stdio`) or print config snippets
168
+ - prints the exact recommended stdio MCP snippet to paste into your harness config
167
169
 
168
170
  The packaged AppImage path:
169
171
 
@@ -204,7 +206,7 @@ Notes:
204
206
 
205
207
  - `npm run dev` still launches the stock Electron binary, so Linux may continue showing the default Electron gear icon in development
206
208
  - packaged builds created with `npm run dist` / `npm run dist:dir` use the Vessel app icon
207
- - the tracked smoke test runs typecheck, build, and the Electron navigation regression harness
209
+ - the tracked smoke test runs typecheck, build, the MCP stdio proxy regression check, and the Electron navigation regression harness
208
210
  - for headless CI, run the smoke test under `xvfb-run -a npm run smoke:test`
209
211
 
210
212
  ### Setting up Vessel for Hermes Agent or OpenClaw
@@ -214,7 +216,7 @@ Vessel is designed to act as the browser runtime that your external agent harnes
214
216
  1. Launch Vessel
215
217
  2. Open Settings (`Ctrl+,`) to confirm MCP status, copy the endpoint, or change the MCP port
216
218
  3. Optional: set an Obsidian vault path or session preferences
217
- 4. Start Hermes Agent or OpenClaw and configure it to connect to Vessel's MCP endpoint at `http://127.0.0.1:<mcpPort>/mcp`
219
+ 4. Start Hermes Agent or OpenClaw and point it at Vessel the easiest way is `vessel-browser-mcp --stdio` as the MCP command (auth is resolved automatically), or connect directly to `http://127.0.0.1:<mcpPort>/mcp` with the bearer token from `~/.config/vessel/mcp-auth.json`
218
220
  5. Use the Supervisor panel in Vessel's sidebar to pause the agent, change approval mode, review pending approvals, checkpoint, or restore the browser session while the harness runs
219
221
  6. Use the Bookmarks panel to organize saved pages into folders and expose those bookmarks back to the agent over MCP
220
222
 
@@ -316,7 +318,23 @@ The extraction output can distinguish:
316
318
  - active blocking overlays
317
319
  - dormant consent/modal UI present in the DOM but not active for the current session or region
318
320
 
319
- Generic HTTP MCP config:
321
+ Stdio proxy MCP config (recommended — resolves auth automatically):
322
+
323
+ ```json
324
+ {
325
+ "mcpServers": {
326
+ "vessel": {
327
+ "command": "vessel-browser-mcp",
328
+ "args": ["--stdio"]
329
+ }
330
+ }
331
+ }
332
+ ```
333
+
334
+ The stdio proxy reads the bearer token from `~/.config/vessel/mcp-auth.json` at connection time, so no manual token management is needed.
335
+ Vessel must already be running when your MCP client connects, and `~/.config/vessel/mcp-auth.json` must exist from install or first launch.
336
+
337
+ Generic HTTP MCP config (requires copying the token manually):
320
338
 
321
339
  ```json
322
340
  {
@@ -346,8 +364,9 @@ mcp_servers:
346
364
 
347
365
  ## Configuration
348
366
 
349
- The installer writes both snippets to:
367
+ The installer writes three snippets to:
350
368
 
369
+ - `~/.config/vessel/mcp-stdio-snippet.json`
351
370
  - `~/.config/vessel/mcp-http-snippet.json`
352
371
  - `~/.config/vessel/mcp-hermes-snippet.yaml`
353
372
 
@@ -360,9 +379,15 @@ vessel-browser-mcp
360
379
  Helper examples:
361
380
 
362
381
  ```bash
363
- # Generic JSON snippet with Authorization header
382
+ # Run as stdio-to-HTTP proxy (for MCP client integration)
383
+ vessel-browser-mcp --stdio
384
+
385
+ # Recommended stdio MCP snippet
364
386
  vessel-browser-mcp
365
387
 
388
+ # Generic JSON snippet with Authorization header
389
+ vessel-browser-mcp --format json
390
+
366
391
  # Hermes-ready YAML snippet with Authorization header
367
392
  vessel-browser-mcp --format hermes
368
393
 
package/out/main/index.js CHANGED
@@ -5177,9 +5177,6 @@ class AnthropicProvider {
5177
5177
  let iterationsUsed = 0;
5178
5178
  for (let i = 0; i < maxIterations; i++) {
5179
5179
  iterationsUsed = i + 1;
5180
- const msgTokenEstimate = JSON.stringify(messages).length;
5181
- const sysTokenEstimate = systemPrompt.length;
5182
- const streamStartTime = Date.now();
5183
5180
  const stream = this.client.messages.stream(
5184
5181
  {
5185
5182
  model: this.model,
@@ -5496,8 +5493,6 @@ class OpenAICompatProvider {
5496
5493
  let iterationsUsed = 0;
5497
5494
  for (let i = 0; i < maxIterations; i++) {
5498
5495
  iterationsUsed = i + 1;
5499
- const msgTokenEstimate = JSON.stringify(messages).length;
5500
- const streamStartTime = Date.now();
5501
5496
  let textAccum = "";
5502
5497
  const toolCallAccums = {};
5503
5498
  let finishReason = null;
@@ -8085,20 +8080,21 @@ function pruneToolsForContext(tools, pageType, query = "") {
8085
8080
  const ctx = pageType ?? "GENERAL";
8086
8081
  const hints = CONTEXT_HINTS[ctx] ?? {};
8087
8082
  const intents = inferIntent(query);
8088
- const scored = tools.filter((tool) => !isToolGated(tool.name)).filter((tool) => shouldIncludeTool(tool.name, ctx, intents)).map((tool) => ({
8083
+ const scored = tools.filter((tool) => shouldIncludeTool(tool.name, ctx, intents)).map((tool) => ({
8089
8084
  tool,
8090
8085
  score: scoreForContext(tool.name, ctx)
8091
8086
  }));
8092
8087
  scored.sort((a, b) => a.score - b.score);
8093
8088
  return scored.map(({ tool, score }) => {
8089
+ let description = tool.description ?? "";
8094
8090
  const hint = hints[tool.name];
8095
8091
  if (hint && score <= 20) {
8096
- return {
8097
- ...tool,
8098
- description: hint + tool.description
8099
- };
8092
+ description = hint + description;
8093
+ }
8094
+ if (isToolGated(tool.name)) {
8095
+ description = `[Premium — requires Vessel Premium] ${description}`;
8100
8096
  }
8101
- return tool;
8097
+ return description !== tool.description ? { ...tool, description } : tool;
8102
8098
  });
8103
8099
  }
8104
8100
  function trimText(value) {
@@ -8512,13 +8508,17 @@ function updateBookmark(id, updates) {
8512
8508
  emit();
8513
8509
  return { ...bookmark };
8514
8510
  }
8515
- function removeFolder(id) {
8511
+ function removeFolder(id, deleteContents = false) {
8516
8512
  load();
8517
8513
  const exists = state.folders.some((f) => f.id === id);
8518
8514
  if (!exists) return false;
8519
- state.bookmarks = state.bookmarks.map(
8520
- (b) => b.folderId === id ? { ...b, folderId: UNSORTED_ID } : b
8521
- );
8515
+ if (deleteContents) {
8516
+ state.bookmarks = state.bookmarks.filter((b) => b.folderId !== id);
8517
+ } else {
8518
+ state.bookmarks = state.bookmarks.map(
8519
+ (b) => b.folderId === id ? { ...b, folderId: UNSORTED_ID } : b
8520
+ );
8521
+ }
8522
8522
  state.folders = state.folders.filter((f) => f.id !== id);
8523
8523
  save();
8524
8524
  emit();
@@ -9204,6 +9204,18 @@ async function executePageScript(wc, script, options) {
9204
9204
  }
9205
9205
  }
9206
9206
  }
9207
+ async function waitForJsReady(wc, timeout = 8e3) {
9208
+ const start = Date.now();
9209
+ while (Date.now() - start < timeout) {
9210
+ const ready = await executePageScript(wc, "1", {
9211
+ timeoutMs: 250,
9212
+ userGesture: true,
9213
+ label: "js-ready probe"
9214
+ });
9215
+ if (ready === 1) return;
9216
+ await sleep$1(250);
9217
+ }
9218
+ }
9207
9219
  function waitForLoad$1(wc, timeout = 5e3) {
9208
9220
  return new Promise((resolve) => {
9209
9221
  let finished = false;
@@ -10492,7 +10504,89 @@ async function tryDismissConsentIframe(wc) {
10492
10504
  }
10493
10505
  return null;
10494
10506
  }
10507
+ async function tryAcceptCookiesQuickly(wc) {
10508
+ const dismissed = await executePageScript(
10509
+ wc,
10510
+ `
10511
+ (function() {
10512
+ var selectors = [
10513
+ '#onetrust-accept-btn-handler',
10514
+ '#CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll',
10515
+ '[data-cookiefirst-action="accept"]',
10516
+ '.cookie-consent-accept-all',
10517
+ '#accept-cookies',
10518
+ '.cc-accept',
10519
+ '.cc-btn.cc-allow',
10520
+ '[aria-label="Accept cookies"]',
10521
+ '[aria-label="Accept all cookies"]',
10522
+ '[data-testid="cookie-accept"]',
10523
+ '[data-testid="consent-accept"]',
10524
+ '[data-testid="accept-all"]',
10525
+ 'button[class*="consent"][class*="accept"]',
10526
+ 'button[class*="privacy"][class*="accept"]',
10527
+ '.fc-cta-consent',
10528
+ '#sp_choice_button_accept',
10529
+ '.message-component.message-button.no-children.focusable.sp_choice_type_11',
10530
+ '[class*="truste"] [class*="accept"]',
10531
+ '[id*="consent-accept"]',
10532
+ '[class*="cmp-accept"]',
10533
+ ];
10534
+ var textPatterns = [
10535
+ 'accept all',
10536
+ 'accept cookies',
10537
+ 'allow all',
10538
+ 'allow cookies',
10539
+ 'agree',
10540
+ 'got it',
10541
+ 'ok',
10542
+ 'i agree',
10543
+ 'i accept',
10544
+ 'consent',
10545
+ 'continue',
10546
+ 'accept and continue',
10547
+ 'accept & continue'
10548
+ ];
10549
+ for (var i = 0; i < selectors.length; i++) {
10550
+ var el = document.querySelector(selectors[i]);
10551
+ if (el && el instanceof HTMLElement) {
10552
+ el.click();
10553
+ return "Dismissed cookie banner via: " + selectors[i];
10554
+ }
10555
+ }
10556
+ var buttons = document.querySelectorAll('button, a[role="button"], [type="submit"]');
10557
+ for (var j = 0; j < buttons.length; j++) {
10558
+ var btn = buttons[j];
10559
+ var text = (btn.textContent || '').trim().toLowerCase();
10560
+ for (var k = 0; k < textPatterns.length; k++) {
10561
+ if (text === textPatterns[k] || text.startsWith(textPatterns[k])) {
10562
+ btn.click();
10563
+ return "Dismissed cookie banner via text match: " + text;
10564
+ }
10565
+ }
10566
+ }
10567
+ return null;
10568
+ })()
10569
+ `,
10570
+ {
10571
+ label: "accept cookies",
10572
+ timeoutMs: 1200
10573
+ }
10574
+ );
10575
+ if (dismissed) return dismissed;
10576
+ return tryDismissConsentIframe(wc);
10577
+ }
10495
10578
  async function clearOverlays(wc, strategy = "auto") {
10579
+ const quickCookieResult = await tryAcceptCookiesQuickly(wc);
10580
+ if (quickCookieResult === PAGE_SCRIPT_TIMEOUT) {
10581
+ return pageBusyError("clear_overlays");
10582
+ }
10583
+ if (quickCookieResult) {
10584
+ return [
10585
+ quickCookieResult,
10586
+ "Stopped after a lightweight consent pass to keep the page responsive. Re-run only if the banner is still blocking the page."
10587
+ ].join("\n");
10588
+ }
10589
+ await waitForJsReady(wc, 1500);
10496
10590
  const steps = [];
10497
10591
  let cleared = 0;
10498
10592
  const maxIterations = 8;
@@ -10556,6 +10650,12 @@ Submitted modal: ${submitResult}`;
10556
10650
  actionMessage = `Fallback popup handling: ${await dismissPopup$1(wc)}`;
10557
10651
  }
10558
10652
  steps.push(actionMessage);
10653
+ if (overlay.kind === "cookie_consent") {
10654
+ steps.push(
10655
+ "Stopped after a lightweight consent pass to keep the page responsive. Re-run only if the banner is still blocking the page."
10656
+ );
10657
+ return steps.join("\n");
10658
+ }
10559
10659
  await sleep$1(250);
10560
10660
  const after = await extractContent(wc);
10561
10661
  const afterState = describeOverlayState(after);
@@ -12030,7 +12130,7 @@ async function executeAction(name, args, ctx) {
12030
12130
  }
12031
12131
  case "click": {
12032
12132
  if (!wc) return "Error: No active tab";
12033
- const selector = await resolveSelector$1(wc, args.index, args.selector);
12133
+ const selector = typeof args.selector === "string" && args.selector.trim() ? await resolveSelector$1(wc, void 0, args.selector) : typeof args.index === "number" ? `__vessel_idx:${args.index}` : await resolveSelector$1(wc, args.index, args.selector);
12034
12134
  if (!selector) return "Error: No element index or selector provided";
12035
12135
  return clickResolvedSelector$1(wc, selector);
12036
12136
  }
@@ -12599,10 +12699,18 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
12599
12699
  (el) => (el.text || "").toLowerCase() === "next" || el.text === "›" || el.text === "»"
12600
12700
  );
12601
12701
  const hasOverlays = page.overlays.some((o) => o.blocksInteraction);
12702
+ const hasCookieConsent = page.overlays.some(
12703
+ (overlay) => overlay.blocksInteraction && overlay.kind === "cookie_consent"
12704
+ );
12602
12705
  if (hasOverlays) {
12603
12706
  suggestions.push("BLOCKING OVERLAY detected — dismiss it first:");
12604
- suggestions.push(" → clear_overlays for stacked modals");
12605
- suggestions.push(" → or dismiss_popup for a single popup");
12707
+ if (hasCookieConsent) {
12708
+ suggestions.push(" → accept_cookies for consent banners");
12709
+ suggestions.push(" → clear_overlays only if consent handling does not unblock the page");
12710
+ } else {
12711
+ suggestions.push(" → clear_overlays for stacked modals");
12712
+ suggestions.push(" → or dismiss_popup for a single popup");
12713
+ }
12606
12714
  suggestions.push("");
12607
12715
  }
12608
12716
  if (hasPasswordField) {
@@ -12807,64 +12915,11 @@ ${steps.join("\n")}`;
12807
12915
  }
12808
12916
  case "accept_cookies": {
12809
12917
  if (!wc) return "Error: No active tab";
12810
- const dismissed = await executePageScript(
12811
- wc,
12812
- `
12813
- (function() {
12814
- // Common cookie consent selectors — OneTrust, CookieBot, GDPR banners
12815
- var selectors = [
12816
- '#onetrust-accept-btn-handler',
12817
- '#CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll',
12818
- '[data-cookiefirst-action="accept"]',
12819
- '.cookie-consent-accept-all',
12820
- '#accept-cookies',
12821
- '.cc-accept',
12822
- '.cc-btn.cc-allow',
12823
- '[aria-label="Accept cookies"]',
12824
- '[aria-label="Accept all cookies"]',
12825
- '[data-testid="cookie-accept"]',
12826
- // CNN / WarnerMedia / common consent SDKs
12827
- '[data-testid="consent-accept"]',
12828
- '[data-testid="accept-all"]',
12829
- 'button[class*="consent"][class*="accept"]',
12830
- 'button[class*="privacy"][class*="accept"]',
12831
- '.fc-cta-consent',
12832
- '#sp_choice_button_accept',
12833
- '.message-component.message-button.no-children.focusable.sp_choice_type_11',
12834
- '[class*="truste"] [class*="accept"]',
12835
- '[id*="consent-accept"]',
12836
- '[class*="cmp-accept"]',
12837
- ];
12838
- // Also try text-matching on buttons
12839
- var textPatterns = ['accept all', 'accept cookies', 'allow all', 'allow cookies', 'agree', 'got it', 'ok', 'i agree', 'i accept', 'consent', 'continue', 'accept and continue', 'accept & continue'];
12840
- for (var i = 0; i < selectors.length; i++) {
12841
- var el = document.querySelector(selectors[i]);
12842
- if (el && el instanceof HTMLElement) { el.click(); return "Dismissed cookie banner via: " + selectors[i]; }
12843
- }
12844
- var buttons = document.querySelectorAll('button, a[role="button"], [type="submit"]');
12845
- for (var j = 0; j < buttons.length; j++) {
12846
- var btn = buttons[j];
12847
- var text = (btn.textContent || '').trim().toLowerCase();
12848
- for (var k = 0; k < textPatterns.length; k++) {
12849
- if (text === textPatterns[k] || text.startsWith(textPatterns[k])) {
12850
- btn.click();
12851
- return "Dismissed cookie banner via text match: " + text;
12852
- }
12853
- }
12854
- }
12855
- return null;
12856
- })()
12857
- `,
12858
- {
12859
- label: "accept cookies"
12860
- }
12861
- );
12918
+ const dismissed = await tryAcceptCookiesQuickly(wc);
12862
12919
  if (dismissed === PAGE_SCRIPT_TIMEOUT) {
12863
12920
  return pageBusyError("accept_cookies");
12864
12921
  }
12865
12922
  if (dismissed) return dismissed;
12866
- const iframeResult = await tryDismissConsentIframe(wc);
12867
- if (iframeResult) return iframeResult;
12868
12923
  return "No cookie consent banner detected. Try dismiss_popup for other overlays.";
12869
12924
  }
12870
12925
  case "extract_table": {
@@ -15906,7 +15961,7 @@ ${buildScopedContext(pageContent, mode)}`;
15906
15961
  { index, selector },
15907
15962
  async () => {
15908
15963
  const wc = tab.view.webContents;
15909
- const resolvedSelector = await resolveSelector(wc, index, selector);
15964
+ const resolvedSelector = typeof selector === "string" && selector.trim() ? await resolveSelector(wc, void 0, selector) : typeof index === "number" ? `__vessel_idx:${index}` : await resolveSelector(wc, index, selector);
15910
15965
  if (!resolvedSelector) {
15911
15966
  return "Error: No index or selector provided";
15912
15967
  }
@@ -17216,22 +17271,29 @@ ${JSON.stringify(otherHighlights, null, 2)}`
17216
17271
  "folder_remove",
17217
17272
  {
17218
17273
  title: "Remove Bookmark Folder",
17219
- description: "Remove a folder. Bookmarks in it are moved to Unsorted.",
17274
+ description: "Remove a folder. By default bookmarks in it are moved to Unsorted. Set delete_contents to true to delete them with the folder.",
17220
17275
  inputSchema: {
17221
- folder_id: zod.z.string().describe("ID of the folder to remove")
17276
+ folder_id: zod.z.string().describe("ID of the folder to remove"),
17277
+ delete_contents: zod.z.boolean().optional().default(false).describe(
17278
+ "If true, delete all bookmarks in the folder. If false (default), move them to Unsorted."
17279
+ )
17222
17280
  }
17223
17281
  },
17224
- async ({ folder_id }) => {
17282
+ async ({ folder_id, delete_contents }) => {
17225
17283
  return withAction(
17226
17284
  runtime2,
17227
17285
  tabManager,
17228
17286
  "remove_bookmark_folder",
17229
- { folder_id },
17287
+ { folder_id, delete_contents },
17230
17288
  async () => {
17231
- const removed = removeFolder(folder_id);
17232
- return removed ? composeFolderAwareResponse(
17233
- `Removed folder ${folder_id}. Bookmarks moved to Unsorted.`
17234
- ) : `Folder ${folder_id} not found`;
17289
+ const removed = removeFolder(
17290
+ folder_id,
17291
+ delete_contents
17292
+ );
17293
+ if (!removed) return `Folder ${folder_id} not found`;
17294
+ return composeFolderAwareResponse(
17295
+ delete_contents ? `Removed folder ${folder_id} and deleted its bookmarks.` : `Removed folder ${folder_id}. Bookmarks moved to Unsorted.`
17296
+ );
17235
17297
  }
17236
17298
  );
17237
17299
  }
@@ -19072,7 +19134,53 @@ function assertNumber(value, name) {
19072
19134
  const VALID_APPROVAL_MODES = /* @__PURE__ */ new Set(["auto", "confirm-dangerous", "manual"]);
19073
19135
  function registerIpcHandlers(windowState, runtime2) {
19074
19136
  const { tabManager, chromeView, sidebarView, devtoolsPanelView, mainWindow } = windowState;
19137
+ let sidebarResizeRecoveryTimer = null;
19138
+ let sidebarResizeActive = false;
19139
+ let runtimeUpdateTimer = null;
19140
+ let pendingRuntimeState = null;
19075
19141
  const premiumApiOrigin = process.env.VESSEL_PREMIUM_API ? new URL(process.env.VESSEL_PREMIUM_API).origin : "https://vesselpremium.quantaintellect.com";
19142
+ const clearSidebarResizeRecoveryTimer = () => {
19143
+ if (sidebarResizeRecoveryTimer) {
19144
+ clearTimeout(sidebarResizeRecoveryTimer);
19145
+ sidebarResizeRecoveryTimer = null;
19146
+ }
19147
+ };
19148
+ const restoreSidebarLayoutAfterResize = () => {
19149
+ clearSidebarResizeRecoveryTimer();
19150
+ if (!sidebarResizeActive) return;
19151
+ sidebarResizeActive = false;
19152
+ layoutViews(windowState);
19153
+ };
19154
+ const scheduleSidebarResizeRecovery = () => {
19155
+ clearSidebarResizeRecoveryTimer();
19156
+ sidebarResizeRecoveryTimer = setTimeout(() => {
19157
+ restoreSidebarLayoutAfterResize();
19158
+ }, 1200);
19159
+ };
19160
+ const flushRuntimeUpdate = () => {
19161
+ runtimeUpdateTimer = null;
19162
+ if (!pendingRuntimeState) return;
19163
+ if (!chromeView.webContents.isDestroyed()) {
19164
+ chromeView.webContents.send(
19165
+ Channels.AGENT_RUNTIME_UPDATE,
19166
+ pendingRuntimeState
19167
+ );
19168
+ }
19169
+ if (!sidebarView.webContents.isDestroyed()) {
19170
+ sidebarView.webContents.send(
19171
+ Channels.AGENT_RUNTIME_UPDATE,
19172
+ pendingRuntimeState
19173
+ );
19174
+ }
19175
+ pendingRuntimeState = null;
19176
+ };
19177
+ const scheduleRuntimeUpdate = (state2) => {
19178
+ pendingRuntimeState = state2;
19179
+ if (runtimeUpdateTimer) return;
19180
+ runtimeUpdateTimer = setTimeout(() => {
19181
+ flushRuntimeUpdate();
19182
+ }, 32);
19183
+ };
19076
19184
  const sendToRendererViews = (channel, ...args) => {
19077
19185
  chromeView.webContents.send(channel, ...args);
19078
19186
  sidebarView.webContents.send(channel, ...args);
@@ -19159,7 +19267,7 @@ function registerIpcHandlers(windowState, runtime2) {
19159
19267
  sendToRendererViews(Channels.HIGHLIGHT_COUNT_UPDATE, count);
19160
19268
  };
19161
19269
  runtime2.setUpdateListener((state2) => {
19162
- sendToRendererViews(Channels.AGENT_RUNTIME_UPDATE, state2);
19270
+ scheduleRuntimeUpdate(state2);
19163
19271
  });
19164
19272
  onRuntimeHealthChange((health) => {
19165
19273
  sendToRendererViews(Channels.SETTINGS_HEALTH_UPDATE, health);
@@ -19287,17 +19395,23 @@ function registerIpcHandlers(windowState, runtime2) {
19287
19395
  };
19288
19396
  });
19289
19397
  electron.ipcMain.handle(Channels.SIDEBAR_RESIZE_START, () => {
19398
+ sidebarResizeActive = true;
19399
+ clearSidebarResizeRecoveryTimer();
19290
19400
  const [width, height] = windowState.mainWindow.getContentSize();
19291
19401
  windowState.sidebarView.setBounds({ x: 0, y: 0, width, height });
19402
+ scheduleSidebarResizeRecovery();
19292
19403
  });
19293
19404
  electron.ipcMain.handle(Channels.SIDEBAR_RESIZE, (_, width) => {
19294
19405
  assertNumber(width, "width");
19295
19406
  const clamped = Math.max(240, Math.min(800, Math.round(width)));
19296
19407
  windowState.uiState.sidebarWidth = clamped;
19297
19408
  resizeSidebarViews(windowState);
19409
+ scheduleSidebarResizeRecovery();
19298
19410
  return clamped;
19299
19411
  });
19300
19412
  electron.ipcMain.handle(Channels.SIDEBAR_RESIZE_COMMIT, () => {
19413
+ sidebarResizeActive = false;
19414
+ clearSidebarResizeRecoveryTimer();
19301
19415
  setSetting("sidebarWidth", windowState.uiState.sidebarWidth);
19302
19416
  layoutViews(windowState);
19303
19417
  });
@@ -19389,9 +19503,9 @@ function registerIpcHandlers(windowState, runtime2) {
19389
19503
  trackBookmarkAction("remove");
19390
19504
  return removeBookmark(id);
19391
19505
  });
19392
- electron.ipcMain.handle(Channels.FOLDER_REMOVE, (_, id) => {
19506
+ electron.ipcMain.handle(Channels.FOLDER_REMOVE, (_, id, deleteContents) => {
19393
19507
  trackBookmarkAction("folder_remove");
19394
- return removeFolder(id);
19508
+ return removeFolder(id, deleteContents ?? false);
19395
19509
  });
19396
19510
  electron.ipcMain.handle(
19397
19511
  Channels.FOLDER_RENAME,
@@ -3195,6 +3195,25 @@ function interactByIndex(index, action, value) {
3195
3195
  if (action === "click") {
3196
3196
  el.focus();
3197
3197
  el.click();
3198
+ if (el instanceof HTMLInputElement) {
3199
+ if (el.type === "checkbox") {
3200
+ const label = getInputLabel(el) || el.getAttribute("aria-label") || el.name || "checkbox";
3201
+ return `${el.checked ? "Checked" : "Unchecked"}: ${label}`;
3202
+ }
3203
+ if (el.type === "radio") {
3204
+ const label = getTrimmedText(el.value) || getInputLabel(el) || el.getAttribute("aria-label") || el.name || "radio";
3205
+ return `${el.checked ? "Selected" : "Clicked"}: ${label}`;
3206
+ }
3207
+ }
3208
+ const role = el.getAttribute("role");
3209
+ if (role === "checkbox" || role === "radio") {
3210
+ const label = getTrimmedText(el.getAttribute("aria-label")) || getTrimmedText(el.textContent) || el.tagName.toLowerCase();
3211
+ const ariaChecked = el.getAttribute("aria-checked");
3212
+ if (role === "checkbox") {
3213
+ return `${ariaChecked === "true" ? "Checked" : "Unchecked"}: ${label}`;
3214
+ }
3215
+ return `${ariaChecked === "true" ? "Selected" : "Clicked"}: ${label}`;
3216
+ }
3198
3217
  return "Clicked: " + (el.getAttribute("aria-label") || el.textContent?.trim().slice(0, 60) || el.tagName.toLowerCase());
3199
3218
  }
3200
3219
  if (action === "focus") {
@@ -238,7 +238,7 @@ const api = {
238
238
  removeBookmark: (id) => electron.ipcRenderer.invoke(Channels.BOOKMARK_REMOVE, id),
239
239
  createFolder: (name) => electron.ipcRenderer.invoke(Channels.FOLDER_CREATE, name),
240
240
  createFolderWithSummary: (name, summary) => electron.ipcRenderer.invoke(Channels.FOLDER_CREATE, name, summary),
241
- removeFolder: (id) => electron.ipcRenderer.invoke(Channels.FOLDER_REMOVE, id),
241
+ removeFolder: (id, deleteContents) => electron.ipcRenderer.invoke(Channels.FOLDER_REMOVE, id, deleteContents),
242
242
  renameFolder: (id, newName, summary) => electron.ipcRenderer.invoke(Channels.FOLDER_RENAME, id, newName, summary),
243
243
  onAddContextToChat: (cb) => {
244
244
  const handler = (_, bookmarkId) => cb(bookmarkId);