@quanta-intellect/vessel-browser 0.1.67 → 0.1.69

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
@@ -65,6 +65,7 @@ const defaults = {
65
65
  domainPolicy: { allowedDomains: [], blockedDomains: [] },
66
66
  downloadPath: "",
67
67
  telemetryEnabled: true,
68
+ defaultSearchEngine: "duckduckgo",
68
69
  premium: {
69
70
  status: "free",
70
71
  customerId: "",
@@ -74,7 +75,7 @@ const defaults = {
74
75
  expiresAt: ""
75
76
  }
76
77
  };
77
- const SAVE_DEBOUNCE_MS$5 = 150;
78
+ const SAVE_DEBOUNCE_MS$6 = 150;
78
79
  const CHAT_PROVIDER_SECRET_FILENAME = "vessel-chat-provider-secret";
79
80
  const logger$j = createLogger("Settings");
80
81
  const SETTABLE_KEYS = new Set(Object.keys(defaults));
@@ -232,7 +233,7 @@ function saveSettings() {
232
233
  if (saveDirty) {
233
234
  void persistNow();
234
235
  }
235
- }, SAVE_DEBOUNCE_MS$5);
236
+ }, SAVE_DEBOUNCE_MS$6);
236
237
  }
237
238
  function setSetting(key, value) {
238
239
  loadSettings();
@@ -871,7 +872,7 @@ function createDebouncedJsonPersistence({
871
872
  }
872
873
  let state$4 = null;
873
874
  const listeners$2 = /* @__PURE__ */ new Set();
874
- const SAVE_DEBOUNCE_MS$4 = 250;
875
+ const SAVE_DEBOUNCE_MS$5 = 250;
875
876
  function getHighlightsPath() {
876
877
  return path.join(electron.app.getPath("userData"), "vessel-highlights.json");
877
878
  }
@@ -889,15 +890,15 @@ function load$4() {
889
890
  });
890
891
  return state$4;
891
892
  }
892
- const persistence$4 = createDebouncedJsonPersistence({
893
- debounceMs: SAVE_DEBOUNCE_MS$4,
893
+ const persistence$5 = createDebouncedJsonPersistence({
894
+ debounceMs: SAVE_DEBOUNCE_MS$5,
894
895
  filePath: getHighlightsPath(),
895
896
  getValue: () => state$4,
896
897
  logLabel: "highlights",
897
898
  resetOnSchedule: true
898
899
  });
899
900
  function save$2() {
900
- persistence$4.schedule();
901
+ persistence$5.schedule();
901
902
  }
902
903
  function emit$2() {
903
904
  if (!state$4) return;
@@ -979,7 +980,7 @@ function clearHighlightsForUrl(url) {
979
980
  return removed;
980
981
  }
981
982
  function flushPersist$4() {
982
- return persistence$4.flush();
983
+ return persistence$5.flush();
983
984
  }
984
985
  const SKIP_TAGS_JS = "var SKIP_TAGS = {SCRIPT:1,STYLE:1,NOSCRIPT:1,TEMPLATE:1,IFRAME:1,SVG:1};";
985
986
  const CONTENT_ROOTS_JS = `
@@ -1551,7 +1552,7 @@ function persistHighlight(url, text) {
1551
1552
  return { success: true, text: capped, id: highlight.id };
1552
1553
  }
1553
1554
  const MAX_HISTORY_ENTRIES = 5e3;
1554
- const SAVE_DEBOUNCE_MS$3 = 250;
1555
+ const SAVE_DEBOUNCE_MS$4 = 250;
1555
1556
  let state$3 = null;
1556
1557
  const listeners$1 = /* @__PURE__ */ new Set();
1557
1558
  function getHistoryPath() {
@@ -1571,14 +1572,14 @@ function load$3() {
1571
1572
  });
1572
1573
  return state$3;
1573
1574
  }
1574
- const persistence$3 = createDebouncedJsonPersistence({
1575
- debounceMs: SAVE_DEBOUNCE_MS$3,
1575
+ const persistence$4 = createDebouncedJsonPersistence({
1576
+ debounceMs: SAVE_DEBOUNCE_MS$4,
1576
1577
  filePath: getHistoryPath(),
1577
1578
  getValue: () => state$3,
1578
1579
  logLabel: "history"
1579
1580
  });
1580
1581
  function save$1() {
1581
- persistence$3.schedule();
1582
+ persistence$4.schedule();
1582
1583
  }
1583
1584
  function emit$1() {
1584
1585
  if (!state$3) return;
@@ -1635,7 +1636,7 @@ function clearAll$1() {
1635
1636
  emit$1();
1636
1637
  }
1637
1638
  function flushPersist$3() {
1638
- return persistence$3.flush();
1639
+ return persistence$4.flush();
1639
1640
  }
1640
1641
  const MAX_CONSOLE_ENTRIES = 500;
1641
1642
  const MAX_NETWORK_ENTRIES = 200;
@@ -2684,6 +2685,8 @@ const Channels = {
2684
2685
  AGENT_APPROVAL_RESOLVE: "agent:approval-resolve",
2685
2686
  AGENT_CHECKPOINT_CREATE: "agent:checkpoint-create",
2686
2687
  AGENT_CHECKPOINT_RESTORE: "agent:checkpoint-restore",
2688
+ AGENT_CHECKPOINT_UPDATE_NOTE: "agent:checkpoint-update-note",
2689
+ AGENT_UNDO_LAST_ACTION: "agent:undo-last-action",
2687
2690
  AGENT_SESSION_CAPTURE: "agent:session-capture",
2688
2691
  AGENT_SESSION_RESTORE: "agent:session-restore",
2689
2692
  // Content
@@ -2691,6 +2694,7 @@ const Channels = {
2691
2694
  READER_MODE_TOGGLE: "reader:toggle",
2692
2695
  // UI state
2693
2696
  SIDEBAR_TOGGLE: "ui:sidebar-toggle",
2697
+ SIDEBAR_NAVIGATE: "ui:sidebar-navigate",
2694
2698
  SIDEBAR_RESIZE: "ui:sidebar-resize",
2695
2699
  SIDEBAR_RESIZE_START: "ui:sidebar-resize-start",
2696
2700
  SIDEBAR_RESIZE_COMMIT: "ui:sidebar-resize-commit",
@@ -2707,6 +2711,7 @@ const Channels = {
2707
2711
  BOOKMARKS_GET: "bookmarks:get",
2708
2712
  BOOKMARKS_UPDATE: "bookmarks:update",
2709
2713
  BOOKMARK_SAVE: "bookmarks:save",
2714
+ BOOKMARK_UPDATE: "bookmarks:update-item",
2710
2715
  BOOKMARK_REMOVE: "bookmarks:remove",
2711
2716
  BOOKMARK_ADD_CONTEXT_TO_CHAT: "bookmarks:add-context-to-chat",
2712
2717
  FOLDER_CREATE: "bookmarks:folder-create",
@@ -2786,6 +2791,7 @@ const Channels = {
2786
2791
  PAGE_DIFF_ACTIVITY: "page:diff-activity",
2787
2792
  PAGE_CHANGED: "page:changed",
2788
2793
  PAGE_DIFF_GET: "page:diff-get",
2794
+ PAGE_DIFF_HISTORY: "page:diff-history",
2789
2795
  PAGE_DIFF_DIRTY: "page:diff-dirty"
2790
2796
  };
2791
2797
  const MAX_DETAIL_ITEMS = 3;
@@ -3101,7 +3107,7 @@ function isTrackablePageUrl(rawUrl) {
3101
3107
  return false;
3102
3108
  }
3103
3109
  }
3104
- const SAVE_DEBOUNCE_MS$2 = 500;
3110
+ const SAVE_DEBOUNCE_MS$3 = 500;
3105
3111
  const MAX_TEXT_LENGTH = 8e3;
3106
3112
  let snapshots = null;
3107
3113
  function getFilePath$1() {
@@ -3139,8 +3145,8 @@ function load$2() {
3139
3145
  });
3140
3146
  return snapshots;
3141
3147
  }
3142
- const persistence$2 = createDebouncedJsonPersistence({
3143
- debounceMs: SAVE_DEBOUNCE_MS$2,
3148
+ const persistence$3 = createDebouncedJsonPersistence({
3149
+ debounceMs: SAVE_DEBOUNCE_MS$3,
3144
3150
  filePath: getFilePath$1(),
3145
3151
  getValue: () => snapshots,
3146
3152
  logLabel: "page snapshots",
@@ -3168,11 +3174,11 @@ function saveSnapshot(rawUrl, title, textContent, headings) {
3168
3174
  };
3169
3175
  s.delete(key);
3170
3176
  s.set(key, snapshot);
3171
- persistence$2.schedule();
3177
+ persistence$3.schedule();
3172
3178
  return snapshot;
3173
3179
  }
3174
3180
  function flushPersist$2() {
3175
- return persistence$2.flush();
3181
+ return persistence$3.flush();
3176
3182
  }
3177
3183
  const SEARCH_ENGINE_HOSTS = [
3178
3184
  "google.",
@@ -3749,6 +3755,24 @@ const VERIFICATION_API = process.env.VESSEL_PREMIUM_API || "https://vesselpremiu
3749
3755
  const FREE_TOOL_ITERATION_LIMIT = 50;
3750
3756
  const REVALIDATION_INTERVAL_MS = 24 * 60 * 60 * 1e3;
3751
3757
  const OFFLINE_GRACE_PERIOD_MS = 7 * 24 * 60 * 60 * 1e3;
3758
+ const MAX_API_ERROR_LOG_LENGTH = 300;
3759
+ async function readApiErrorDetail(res) {
3760
+ try {
3761
+ const text = (await res.text()).trim();
3762
+ if (!text) return "";
3763
+ try {
3764
+ const data = JSON.parse(text);
3765
+ const detail = data.error || data.message;
3766
+ if (typeof detail === "string" && detail.trim()) {
3767
+ return detail.trim().slice(0, MAX_API_ERROR_LOG_LENGTH);
3768
+ }
3769
+ } catch {
3770
+ }
3771
+ return text.slice(0, MAX_API_ERROR_LOG_LENGTH);
3772
+ } catch {
3773
+ return "";
3774
+ }
3775
+ }
3752
3776
  const PREMIUM_TOOLS = /* @__PURE__ */ new Set([
3753
3777
  "screenshot",
3754
3778
  "save_session",
@@ -3838,7 +3862,12 @@ async function verifySubscription(identifier) {
3838
3862
  body: JSON.stringify({ identifier: verificationIdentifier })
3839
3863
  });
3840
3864
  if (!res.ok) {
3841
- logger$f.warn("Verification API returned a non-OK status:", res.status);
3865
+ const detail = await readApiErrorDetail(res);
3866
+ logger$f.warn(
3867
+ "Verification API returned a non-OK status:",
3868
+ res.status,
3869
+ detail
3870
+ );
3842
3871
  return current;
3843
3872
  }
3844
3873
  const data = await res.json();
@@ -5355,8 +5384,32 @@ function normalizePageContent(value) {
5355
5384
  pageSchema: page.pageSchema
5356
5385
  };
5357
5386
  }
5387
+ function normalizePageDiffHistoryItem(value) {
5388
+ if (!value || typeof value !== "object") return null;
5389
+ const raw = value;
5390
+ if (typeof raw.detectedAt !== "string" || typeof raw.summary !== "string") {
5391
+ return null;
5392
+ }
5393
+ return {
5394
+ detectedAt: raw.detectedAt,
5395
+ summary: raw.summary
5396
+ };
5397
+ }
5398
+ function prunePageDiffHistory(items, options) {
5399
+ const cutoff = (options.now ?? Date.now()) - options.maxAgeDays * 24 * 60 * 60 * 1e3;
5400
+ return items.filter((item) => {
5401
+ const detectedAt = Date.parse(item.detectedAt);
5402
+ return Number.isFinite(detectedAt) && detectedAt >= cutoff;
5403
+ }).sort(
5404
+ (left, right) => Date.parse(left.detectedAt) - Date.parse(right.detectedAt)
5405
+ ).slice(-options.maxItems);
5406
+ }
5407
+ function appendPageDiffHistoryItem(items, next, options) {
5408
+ return prunePageDiffHistory([...items, next], options);
5409
+ }
5358
5410
  const latestPageDiffs = /* @__PURE__ */ new Map();
5359
5411
  const recentPageDiffBursts = /* @__PURE__ */ new Map();
5412
+ let historyLoaded = false;
5360
5413
  const pendingPageSnapshotTimers = /* @__PURE__ */ new Map();
5361
5414
  const pendingPageSnapshotDueAt = /* @__PURE__ */ new Map();
5362
5415
  const lastMutationSnapshotAt = /* @__PURE__ */ new Map();
@@ -5377,10 +5430,81 @@ function attachDestroyCleanup(wc) {
5377
5430
  cleanupTimersForWcId(wc.id);
5378
5431
  });
5379
5432
  }
5433
+ const MAX_PERSISTED_DIFF_BURSTS = 50;
5434
+ const MAX_HISTORY_DAYS = 30;
5435
+ const SAVE_DEBOUNCE_MS$2 = 500;
5436
+ function getHistoryFilePath() {
5437
+ return path.join(electron.app.getPath("userData"), "vessel-page-diff-history.json");
5438
+ }
5439
+ function loadHistory() {
5440
+ if (historyLoaded) return recentPageDiffBursts;
5441
+ historyLoaded = true;
5442
+ const loaded = loadJsonFile({
5443
+ filePath: getHistoryFilePath(),
5444
+ fallback: /* @__PURE__ */ new Map(),
5445
+ secure: true,
5446
+ parse: (raw) => {
5447
+ const next = /* @__PURE__ */ new Map();
5448
+ if (!Array.isArray(raw)) return next;
5449
+ for (const entry of raw) {
5450
+ if (!entry || typeof entry !== "object") continue;
5451
+ const record = entry;
5452
+ if (typeof record.url !== "string" || !Array.isArray(record.bursts)) {
5453
+ continue;
5454
+ }
5455
+ next.set(
5456
+ record.url,
5457
+ prunePageDiffHistory(
5458
+ record.bursts.map((item) => normalizePageDiffHistoryItem(item)).filter((item) => item !== null),
5459
+ {
5460
+ maxAgeDays: MAX_HISTORY_DAYS,
5461
+ maxItems: MAX_PERSISTED_DIFF_BURSTS
5462
+ }
5463
+ )
5464
+ );
5465
+ }
5466
+ return next;
5467
+ }
5468
+ });
5469
+ for (const [key, bursts] of loaded.entries()) {
5470
+ recentPageDiffBursts.set(key, bursts);
5471
+ }
5472
+ return recentPageDiffBursts;
5473
+ }
5474
+ const persistence$2 = createDebouncedJsonPersistence({
5475
+ debounceMs: SAVE_DEBOUNCE_MS$2,
5476
+ filePath: getHistoryFilePath(),
5477
+ getValue: () => recentPageDiffBursts,
5478
+ logLabel: "page diff history",
5479
+ secure: true,
5480
+ serialize: (value) => Array.from(value.entries()).map(([url, bursts]) => ({
5481
+ url,
5482
+ bursts
5483
+ }))
5484
+ });
5380
5485
  function getLatestPageDiff(rawUrl) {
5381
5486
  if (!shouldTrackSnapshotUrl(rawUrl)) return null;
5382
5487
  return latestPageDiffs.get(normalizeUrl(rawUrl)) ?? null;
5383
5488
  }
5489
+ function getPageDiffBursts(rawUrl) {
5490
+ if (!shouldTrackSnapshotUrl(rawUrl)) return [];
5491
+ const key = normalizeUrl(rawUrl);
5492
+ const history = loadHistory();
5493
+ const bursts = prunePageDiffHistory(history.get(key) ?? [], {
5494
+ maxAgeDays: MAX_HISTORY_DAYS,
5495
+ maxItems: MAX_PERSISTED_DIFF_BURSTS
5496
+ });
5497
+ const current = history.get(key) ?? [];
5498
+ if (current.length !== bursts.length) {
5499
+ if (bursts.length > 0) {
5500
+ history.set(key, bursts);
5501
+ } else {
5502
+ history.delete(key);
5503
+ }
5504
+ persistence$2.schedule();
5505
+ }
5506
+ return bursts.slice().reverse();
5507
+ }
5384
5508
  function summarizeDiffBurst(diff) {
5385
5509
  const items = diff.changes.slice(0, 2).map((change) => `${change.section}: ${change.summary}`);
5386
5510
  return items.join(" | ");
@@ -5391,16 +5515,21 @@ function enrichWithBurstHistory(key, diff) {
5391
5515
  detectedAt,
5392
5516
  summary: summarizeDiffBurst(diff)
5393
5517
  };
5394
- const bursts = [...recentPageDiffBursts.get(key) || [], nextBurst].slice(
5395
- -5
5396
- );
5397
- recentPageDiffBursts.set(key, bursts);
5518
+ const history = loadHistory();
5519
+ const bursts = appendPageDiffHistoryItem(history.get(key) ?? [], nextBurst, {
5520
+ maxAgeDays: MAX_HISTORY_DAYS,
5521
+ maxItems: MAX_PERSISTED_DIFF_BURSTS,
5522
+ now: Date.parse(detectedAt)
5523
+ });
5524
+ history.set(key, bursts);
5525
+ persistence$2.schedule();
5526
+ const recentBursts = bursts.slice(-5);
5398
5527
  return {
5399
5528
  ...diff,
5400
5529
  burstCount: bursts.length,
5401
5530
  firstDetectedAt: bursts[0]?.detectedAt,
5402
5531
  lastDetectedAt: bursts[bursts.length - 1]?.detectedAt,
5403
- recentBursts: bursts
5532
+ recentBursts: recentBursts.slice().reverse()
5404
5533
  };
5405
5534
  }
5406
5535
  async function capturePageSnapshot(url, wc, sendToRendererViews) {
@@ -5421,11 +5550,9 @@ async function capturePageSnapshot(url, wc, sendToRendererViews) {
5421
5550
  sendToRendererViews(Channels.PAGE_CHANGED, enrichedDiff);
5422
5551
  } else {
5423
5552
  latestPageDiffs.delete(key);
5424
- recentPageDiffBursts.delete(key);
5425
5553
  }
5426
5554
  } else {
5427
5555
  latestPageDiffs.delete(key);
5428
- recentPageDiffBursts.delete(key);
5429
5556
  }
5430
5557
  saveSnapshot(url, title, textContent, headings);
5431
5558
  } catch {
@@ -6223,7 +6350,7 @@ class AnthropicProvider {
6223
6350
  });
6224
6351
  continue;
6225
6352
  }
6226
- const argSummary = [tb.input.url, tb.input.text, tb.input.direction].map((v) => typeof v === "string" ? v : "").find((v) => v.length > 0) ?? "";
6353
+ 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) ?? "";
6227
6354
  onChunk(`
6228
6355
  <<tool:${tb.name}${argSummary ? ":" + argSummary : ""}>>
6229
6356
  `);
@@ -6414,11 +6541,52 @@ const SAFE_TOOL_ALIASES = {
6414
6541
  scroll_up: "scroll",
6415
6542
  read: "read_page",
6416
6543
  read_current_page: "read_page",
6417
- scan_page: "read_page"
6544
+ scan_page: "read_page",
6545
+ save_bookmark: "save_bookmark",
6546
+ bookmark: "save_bookmark",
6547
+ bookmark_page: "save_bookmark",
6548
+ bookmark_url: "save_bookmark",
6549
+ add_bookmark: "save_bookmark",
6550
+ create_bookmark: "save_bookmark"
6418
6551
  };
6552
+ const CANONICAL_TOOL_NAMES = /* @__PURE__ */ new Set([
6553
+ "archive_bookmark",
6554
+ "click",
6555
+ "create_bookmark_folder",
6556
+ "current_tab",
6557
+ "go_back",
6558
+ "go_forward",
6559
+ "inspect_element",
6560
+ "list_bookmarks",
6561
+ "navigate",
6562
+ "open_bookmark",
6563
+ "organize_bookmark",
6564
+ "read_page",
6565
+ "save_bookmark",
6566
+ "scroll",
6567
+ "search",
6568
+ "type_text"
6569
+ ]);
6570
+ function repeatedTokenMatch(value, token) {
6571
+ if (value === token) return true;
6572
+ if (token.length === 0 || value.length <= token.length) return false;
6573
+ if (value.length % token.length !== 0) return false;
6574
+ return token.repeat(value.length / token.length) === value;
6575
+ }
6419
6576
  function normalizeToolAlias(name) {
6420
6577
  const normalized = name.trim().toLowerCase().replace(/[.\s/-]+/g, "_");
6421
- return SAFE_TOOL_ALIASES[normalized] ?? name;
6578
+ const direct = SAFE_TOOL_ALIASES[normalized] ?? normalized;
6579
+ if (CANONICAL_TOOL_NAMES.has(direct)) return direct;
6580
+ const knownTokens = [
6581
+ ...Object.keys(SAFE_TOOL_ALIASES),
6582
+ ...CANONICAL_TOOL_NAMES
6583
+ ];
6584
+ for (const token of knownTokens) {
6585
+ if (repeatedTokenMatch(normalized, token)) {
6586
+ return SAFE_TOOL_ALIASES[token] ?? token;
6587
+ }
6588
+ }
6589
+ return name;
6422
6590
  }
6423
6591
  function parseModelSizeInBillions(model) {
6424
6592
  const match = model.toLowerCase().match(/(?:^|[:/_\-\s])(\d+(?:\.\d+)?)b(?:$|[:/_\-\s])/i);
@@ -6742,8 +6910,62 @@ function scalarArgsForTool(name, scalar) {
6742
6910
  const mode = trimmed.replace(/^["']|["']$/g, "").toLowerCase();
6743
6911
  if (mode) return { mode };
6744
6912
  }
6913
+ if (name === "save_bookmark") {
6914
+ const url = toLikelyUrl(trimmed);
6915
+ if (url) return { url };
6916
+ const lastSpace = trimmed.lastIndexOf(" ");
6917
+ if (lastSpace > 0) {
6918
+ const maybeUrl = toLikelyUrl(trimmed.slice(lastSpace + 1));
6919
+ if (maybeUrl) return { url: maybeUrl, title: trimmed.slice(0, lastSpace).replace(/^["']|["']$/g, "") };
6920
+ }
6921
+ }
6922
+ return null;
6923
+ }
6924
+ function firstStringArg(args, keys) {
6925
+ for (const key of keys) {
6926
+ const value = args[key];
6927
+ if (typeof value === "string" && value.trim()) {
6928
+ return value.trim();
6929
+ }
6930
+ }
6745
6931
  return null;
6746
6932
  }
6933
+ function normalizeElementTargetArgs(args) {
6934
+ const normalized = { ...args };
6935
+ if (typeof normalized.index === "string" && /^\d+$/.test(normalized.index.trim())) {
6936
+ normalized.index = Number(normalized.index.trim());
6937
+ }
6938
+ if (typeof normalized.selector !== "string" || !normalized.selector.trim()) {
6939
+ const selector = firstStringArg(normalized, [
6940
+ "cssSelector",
6941
+ "css_selector",
6942
+ "querySelector",
6943
+ "query_selector"
6944
+ ]);
6945
+ if (selector) normalized.selector = selector;
6946
+ }
6947
+ if (typeof normalized.text !== "string" || !normalized.text.trim()) {
6948
+ const text = firstStringArg(normalized, [
6949
+ "label",
6950
+ "title",
6951
+ "name",
6952
+ "target",
6953
+ "element",
6954
+ "linkText",
6955
+ "link_text",
6956
+ "ariaLabel",
6957
+ "aria_label"
6958
+ ]);
6959
+ if (text) normalized.text = text;
6960
+ }
6961
+ return normalized;
6962
+ }
6963
+ function hasElementTarget(args) {
6964
+ return typeof args.index === "number" || typeof args.selector === "string" && args.selector.trim().length > 0 || typeof args.text === "string" && args.text.trim().length > 0;
6965
+ }
6966
+ function isTargetlessClickArgs(args) {
6967
+ return !hasElementTarget(normalizeElementTargetArgs(args));
6968
+ }
6747
6969
  function tryParseJsonWithCommonRepairs(raw) {
6748
6970
  const trimmed = raw.trim();
6749
6971
  if (!trimmed) return {};
@@ -6797,7 +7019,10 @@ function parseToolArgsWithRepair(name, argsJson) {
6797
7019
  return scalarArgs ? { args: scalarArgs, repaired: true } : null;
6798
7020
  }
6799
7021
  function coerceToolArgsForExecution(name, args) {
6800
- const coerced = { ...args };
7022
+ let coerced = { ...args };
7023
+ if (name === "click" || name === "inspect_element" || name === "scroll_to_element") {
7024
+ coerced = normalizeElementTargetArgs(coerced);
7025
+ }
6801
7026
  if (name === "search") {
6802
7027
  if (typeof coerced.query !== "string" || !coerced.query.trim()) {
6803
7028
  if (typeof coerced.text === "string" && coerced.text.trim()) {
@@ -6818,6 +7043,25 @@ function coerceToolArgsForExecution(name, args) {
6818
7043
  }
6819
7044
  }
6820
7045
  }
7046
+ if (name === "save_bookmark") {
7047
+ if (typeof coerced.url !== "string" || !coerced.url.trim()) {
7048
+ if (typeof coerced.link === "string" && coerced.link.trim()) {
7049
+ coerced.url = coerced.link.trim();
7050
+ } else if (typeof coerced.href === "string" && coerced.href.trim()) {
7051
+ coerced.url = coerced.href.trim();
7052
+ }
7053
+ }
7054
+ if (typeof coerced.folderName !== "string" || !coerced.folderName.trim()) {
7055
+ if (typeof coerced.folder === "string" && coerced.folder.trim()) {
7056
+ coerced.folderName = coerced.folder.trim();
7057
+ } else if (typeof coerced.category === "string" && coerced.category.trim()) {
7058
+ coerced.folderName = coerced.category.trim();
7059
+ }
7060
+ }
7061
+ if (coerced.folderName && typeof coerced.createFolderIfMissing === "undefined") {
7062
+ coerced.createFolderIfMissing = true;
7063
+ }
7064
+ }
6821
7065
  return coerced;
6822
7066
  }
6823
7067
  function canonicalizeArgsForTool(name, args) {
@@ -6834,6 +7078,24 @@ function canonicalizeArgsForTool(name, args) {
6834
7078
  }
6835
7079
  return canonical;
6836
7080
  }
7081
+ function unsupportedToolHint(name) {
7082
+ const normalized = name.trim().toLowerCase().replace(/[.\s/-]+/g, "_");
7083
+ const BOOKMARK_NAMES = [
7084
+ "organize_bookmark",
7085
+ "organize_bookmarks",
7086
+ "manage_bookmark",
7087
+ "manage_bookmarks",
7088
+ "add_to_bookmarks",
7089
+ "save_to_bookmarks",
7090
+ "bookmark_link",
7091
+ "save_link",
7092
+ "store_bookmark"
7093
+ ];
7094
+ if (BOOKMARK_NAMES.includes(normalized) || /bookmark|save.*link|organize/.test(normalized)) {
7095
+ return `Error: "${name}" is not a supported tool. Use save_bookmark to save a page as a bookmark, or create_bookmark_folder to create a folder. Example: save_bookmark with {"url": "...", "title": "...", "folderName": "..."}`;
7096
+ }
7097
+ return `Error: ${name} is not a supported tool. Choose one of the available browser tools instead.`;
7098
+ }
6837
7099
  function resolveToolCallName(rawName, args, availableToolNames) {
6838
7100
  const aliased = normalizeToolAlias(rawName);
6839
7101
  if (availableToolNames.has(aliased)) return aliased;
@@ -7138,6 +7400,7 @@ class OpenAICompatProvider {
7138
7400
  );
7139
7401
  if (recoveredToolCalls.length > 0) {
7140
7402
  toolCalls = recoveredToolCalls;
7403
+ if (textAccum.trim()) onChunk("<<erase_prev>>");
7141
7404
  } else {
7142
7405
  const narratedToolCalls = recoverNarratedActionToolCalls(
7143
7406
  textAccum,
@@ -7145,6 +7408,7 @@ class OpenAICompatProvider {
7145
7408
  );
7146
7409
  if (narratedToolCalls.length > 0) {
7147
7410
  toolCalls = narratedToolCalls;
7411
+ if (textAccum.trim()) onChunk("<<erase_prev>>");
7148
7412
  }
7149
7413
  }
7150
7414
  }
@@ -7211,6 +7475,25 @@ class OpenAICompatProvider {
7211
7475
  compactRecoveryCount = 0;
7212
7476
  const iterationToolResultPreviews = [];
7213
7477
  for (const tc of toolCalls) {
7478
+ if (!availableToolNames.has(tc.name)) {
7479
+ const hint = unsupportedToolHint(tc.name);
7480
+ onChunk(`
7481
+ <<tool:${tc.name}:⚠ unsupported>>
7482
+ `);
7483
+ messages.push({
7484
+ role: "tool",
7485
+ tool_call_id: tc.id,
7486
+ content: hint
7487
+ });
7488
+ compactCorrectionCount += 1;
7489
+ if (compactCorrectionCount >= 2) {
7490
+ messages.push({
7491
+ role: "user",
7492
+ content: `[System] You are calling unsupported tools. Stop inventing tool names. Use the supported tools you were given and take the next concrete step.`
7493
+ });
7494
+ }
7495
+ continue;
7496
+ }
7214
7497
  if (malformedToolCalls.has(tc.id)) {
7215
7498
  onChunk(`
7216
7499
  <<tool:${tc.name}:⚠ invalid args>>
@@ -7237,25 +7520,23 @@ class OpenAICompatProvider {
7237
7520
  }
7238
7521
  args = repairedArgs.args;
7239
7522
  args = coerceToolArgsForExecution(tc.name, args);
7240
- if (!availableToolNames.has(tc.name)) {
7523
+ const toolSignature = stableToolSignature(tc.name, args);
7524
+ if (this.agentToolProfile === "compact" && tc.name === "click" && isTargetlessClickArgs(args)) {
7241
7525
  onChunk(`
7242
- <<tool:unsupported_tool:⚠ unsupported>>
7526
+ <<tool:${tc.name}:⚠ missing target>>
7243
7527
  `);
7244
7528
  messages.push({
7245
7529
  role: "tool",
7246
7530
  tool_call_id: tc.id,
7247
- content: `Error: ${tc.name} is not a supported tool. Choose one of the available browser tools instead.`
7531
+ content: `Error: click requires an element target. Use click with {"index": N} from the latest read_page result, or {"text": "exact visible link/button text"}. If you do not have a current result index, call read_page(mode="results_only") first and then click one listed result.`
7532
+ });
7533
+ messages.push({
7534
+ role: "user",
7535
+ content: `[System] Your last click had no target. Do not call click with empty arguments. Refresh the page state with read_page(mode="results_only") if needed, then click exactly one result by index or exact visible text.`
7248
7536
  });
7249
7537
  compactCorrectionCount += 1;
7250
- if (compactCorrectionCount >= 2) {
7251
- messages.push({
7252
- role: "user",
7253
- content: `[System] You are calling unsupported tools. Stop inventing tool names. Use the supported tools you were given and take the next concrete step.`
7254
- });
7255
- }
7256
7538
  continue;
7257
7539
  }
7258
- const toolSignature = stableToolSignature(tc.name, args);
7259
7540
  const neverSuppressDuplicate = [
7260
7541
  "read_page",
7261
7542
  "current_tab",
@@ -10080,6 +10361,13 @@ const TOOL_DEFINITIONS = [
10080
10361
  tier: 2,
10081
10362
  hiddenByDefault: true
10082
10363
  },
10364
+ // --- Undo ---
10365
+ {
10366
+ name: "undo_last_action",
10367
+ title: "Undo Last Action",
10368
+ description: "Undo the most recent agent action by restoring the browser to its state before that action ran. Works for click, type, submit, navigate, and similar mutating actions. Returns the name of the undone action, or an error if nothing can be undone.",
10369
+ tier: 1
10370
+ },
10083
10371
  // --- Speedee System: Suggestion Engine ---
10084
10372
  {
10085
10373
  name: "suggest",
@@ -10469,6 +10757,14 @@ function pruneToolsForContext(tools, pageType, query = "", options = {}) {
10469
10757
  return description !== tool.description ? { ...tool, description } : tool;
10470
10758
  });
10471
10759
  }
10760
+ const SEARCH_ENGINE_PRESETS = {
10761
+ duckduckgo: { label: "DuckDuckGo", url: "https://duckduckgo.com/?q=" },
10762
+ google: { label: "Google", url: "https://www.google.com/search?q=" },
10763
+ bing: { label: "Bing", url: "https://www.bing.com/search?q=" },
10764
+ brave: { label: "Brave Search", url: "https://search.brave.com/search?q=" },
10765
+ ecosia: { label: "Ecosia", url: "https://www.ecosia.org/search?q=" },
10766
+ kagi: { label: "Kagi", url: "https://kagi.com/search?q=" }
10767
+ };
10472
10768
  function trimText(value) {
10473
10769
  return typeof value === "string" ? value.trim() : "";
10474
10770
  }
@@ -10604,6 +10900,60 @@ function getBookmarkSearchMatch(args) {
10604
10900
  }
10605
10901
  return { matchedFields, score };
10606
10902
  }
10903
+ function normalizeOptionalString(value) {
10904
+ if (typeof value !== "string") return void 0;
10905
+ const trimmed = value.trim();
10906
+ return trimmed || void 0;
10907
+ }
10908
+ function normalizeKeyFields(value) {
10909
+ if (!Array.isArray(value)) return void 0;
10910
+ const normalized = value.filter((field) => typeof field === "string").map((field) => field.trim()).filter(Boolean);
10911
+ return normalized.length > 0 ? normalized : void 0;
10912
+ }
10913
+ function normalizeAgentHints(value) {
10914
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
10915
+ return void 0;
10916
+ }
10917
+ const normalized = Object.fromEntries(
10918
+ Object.entries(value).map(([key, hint]) => [key.trim(), normalizeOptionalString(hint)]).filter((entry) => Boolean(entry[0] && entry[1]))
10919
+ );
10920
+ return Object.keys(normalized).length > 0 ? normalized : void 0;
10921
+ }
10922
+ function hasOwn(value, key) {
10923
+ return Object.prototype.hasOwnProperty.call(value, key);
10924
+ }
10925
+ function normalizeBookmarkMetadata(input) {
10926
+ const normalized = {};
10927
+ const intent = normalizeOptionalString(input.intent);
10928
+ const expectedContent = normalizeOptionalString(input.expectedContent);
10929
+ const keyFields = normalizeKeyFields(input.keyFields);
10930
+ const agentHints = normalizeAgentHints(input.agentHints);
10931
+ if (intent !== void 0) normalized.intent = intent;
10932
+ if (expectedContent !== void 0) {
10933
+ normalized.expectedContent = expectedContent;
10934
+ }
10935
+ if (keyFields !== void 0) normalized.keyFields = keyFields;
10936
+ if (agentHints !== void 0) normalized.agentHints = agentHints;
10937
+ return normalized;
10938
+ }
10939
+ function normalizeBookmarkMetadataUpdate(input) {
10940
+ const normalized = {};
10941
+ if (hasOwn(input, "intent")) {
10942
+ normalized.intent = normalizeOptionalString(input.intent);
10943
+ }
10944
+ if (hasOwn(input, "expectedContent")) {
10945
+ normalized.expectedContent = normalizeOptionalString(
10946
+ input.expectedContent
10947
+ );
10948
+ }
10949
+ if (hasOwn(input, "keyFields")) {
10950
+ normalized.keyFields = normalizeKeyFields(input.keyFields);
10951
+ }
10952
+ if (hasOwn(input, "agentHints")) {
10953
+ normalized.agentHints = normalizeAgentHints(input.agentHints);
10954
+ }
10955
+ return normalized;
10956
+ }
10607
10957
  const UNSORTED_ID = "unsorted";
10608
10958
  const ARCHIVE_FOLDER_NAME = "Archive";
10609
10959
  const SAVE_DEBOUNCE_MS$1 = 250;
@@ -10642,6 +10992,13 @@ const persistence$1 = createDebouncedJsonPersistence({
10642
10992
  function save() {
10643
10993
  persistence$1.schedule();
10644
10994
  }
10995
+ function assignDefinedBookmarkFields(bookmark, fields) {
10996
+ if (!fields) return;
10997
+ for (const [key, value] of Object.entries(fields)) {
10998
+ if (value === void 0) continue;
10999
+ Object.assign(bookmark, { [key]: value });
11000
+ }
11001
+ }
10645
11002
  function emit() {
10646
11003
  if (!state$1) return;
10647
11004
  const snapshot = cloneState(state$1);
@@ -10815,9 +11172,7 @@ function saveBookmarkWithPolicy(url, title, folderId, note, options) {
10815
11172
  if (note !== void 0) {
10816
11173
  bookmark2.note = note.trim() || void 0;
10817
11174
  }
10818
- if (options?.extra) {
10819
- Object.assign(bookmark2, options.extra);
10820
- }
11175
+ assignDefinedBookmarkFields(bookmark2, options?.extra);
10821
11176
  bookmark2.savedAt = (/* @__PURE__ */ new Date()).toISOString();
10822
11177
  save();
10823
11178
  emit();
@@ -10859,6 +11214,12 @@ function updateBookmark(id, updates) {
10859
11214
  load$1();
10860
11215
  const bookmark = state$1.bookmarks.find((item) => item.id === id);
10861
11216
  if (!bookmark) return null;
11217
+ const metadataUpdates = normalizeBookmarkMetadataUpdate({
11218
+ intent: updates.intent,
11219
+ expectedContent: updates.expectedContent,
11220
+ keyFields: updates.keyFields,
11221
+ agentHints: updates.agentHints
11222
+ });
10862
11223
  if (typeof updates.title === "string") {
10863
11224
  const trimmed = updates.title.trim();
10864
11225
  bookmark.title = trimmed || bookmark.url;
@@ -10870,20 +11231,20 @@ function updateBookmark(id, updates) {
10870
11231
  if (typeof updates.folderId === "string") {
10871
11232
  bookmark.folderId = updates.folderId && updates.folderId !== UNSORTED_ID ? state$1.folders.find((item) => item.id === updates.folderId)?.id ?? UNSORTED_ID : UNSORTED_ID;
10872
11233
  }
10873
- if (typeof updates.intent === "string") {
10874
- bookmark.intent = updates.intent.trim() || void 0;
11234
+ if ("intent" in metadataUpdates) {
11235
+ bookmark.intent = metadataUpdates.intent;
10875
11236
  }
10876
- if (typeof updates.expectedContent === "string") {
10877
- bookmark.expectedContent = updates.expectedContent.trim() || void 0;
11237
+ if ("expectedContent" in metadataUpdates) {
11238
+ bookmark.expectedContent = metadataUpdates.expectedContent;
10878
11239
  }
10879
- if (updates.keyFields !== void 0) {
10880
- bookmark.keyFields = updates.keyFields;
11240
+ if ("keyFields" in metadataUpdates) {
11241
+ bookmark.keyFields = metadataUpdates.keyFields;
10881
11242
  }
10882
11243
  if (updates.pageSchema !== void 0) {
10883
11244
  bookmark.pageSchema = updates.pageSchema;
10884
11245
  }
10885
- if (updates.agentHints !== void 0) {
10886
- bookmark.agentHints = updates.agentHints;
11246
+ if ("agentHints" in metadataUpdates) {
11247
+ bookmark.agentHints = metadataUpdates.agentHints;
10887
11248
  }
10888
11249
  save();
10889
11250
  emit();
@@ -11878,33 +12239,6 @@ function formatCompactToolResult(name, result) {
11878
12239
  return limitText(result, 18, 1400);
11879
12240
  }
11880
12241
  }
11881
- function normalizeOptionalString(value) {
11882
- if (typeof value !== "string") return void 0;
11883
- const trimmed = value.trim();
11884
- return trimmed || void 0;
11885
- }
11886
- function normalizeKeyFields(value) {
11887
- if (!Array.isArray(value)) return void 0;
11888
- const normalized = value.filter((field) => typeof field === "string").map((field) => field.trim()).filter(Boolean);
11889
- return normalized.length > 0 ? normalized : void 0;
11890
- }
11891
- function normalizeAgentHints(value) {
11892
- if (!value || typeof value !== "object" || Array.isArray(value)) {
11893
- return void 0;
11894
- }
11895
- const normalized = Object.fromEntries(
11896
- Object.entries(value).map(([key, hint]) => [key.trim(), normalizeOptionalString(hint)]).filter((entry) => Boolean(entry[0] && entry[1]))
11897
- );
11898
- return Object.keys(normalized).length > 0 ? normalized : void 0;
11899
- }
11900
- function normalizeBookmarkMetadata(input) {
11901
- return {
11902
- intent: normalizeOptionalString(input.intent),
11903
- expectedContent: normalizeOptionalString(input.expectedContent),
11904
- keyFields: normalizeKeyFields(input.keyFields),
11905
- agentHints: normalizeAgentHints(input.agentHints)
11906
- };
11907
- }
11908
12242
  const HUGGING_FACE_HUB_HOSTS = /* @__PURE__ */ new Set(["huggingface.co", "www.huggingface.co"]);
11909
12243
  const HUGGING_FACE_MODEL_TASKS = [
11910
12244
  {
@@ -14818,8 +15152,19 @@ function buildCommonSearchUrlShortcut(currentUrl, rawQuery) {
14818
15152
  appliedFilters: existingParam ? [`updated ${existingParam} query`] : []
14819
15153
  };
14820
15154
  }
14821
- function buildSearchShortcut(currentUrl, rawQuery) {
14822
- return buildHuggingFaceSearchShortcut(currentUrl, rawQuery) ?? buildCommonSearchUrlShortcut(currentUrl, rawQuery);
15155
+ function buildDefaultEngineShortcut(rawQuery) {
15156
+ const settings2 = loadSettings();
15157
+ const engineId = settings2.defaultSearchEngine ?? "duckduckgo";
15158
+ if (engineId === "none") return null;
15159
+ const preset = SEARCH_ENGINE_PRESETS[engineId];
15160
+ if (!preset) return null;
15161
+ const query = normalizeSearchQuery(rawQuery);
15162
+ if (!query) return null;
15163
+ return {
15164
+ url: preset.url + encodeURIComponent(query),
15165
+ source: "default search engine",
15166
+ appliedFilters: []
15167
+ };
14823
15168
  }
14824
15169
  async function locateSearchTarget(wc, explicitSelector) {
14825
15170
  if (explicitSelector) {
@@ -14872,7 +15217,7 @@ async function locateSearchTarget(wc, explicitSelector) {
14872
15217
  const seen = new Set();
14873
15218
  const ordered = [];
14874
15219
  const specific = document.querySelectorAll(
14875
- 'input[type="search"], input[name="q"], input[name="query"], input[name="search"], input[role="searchbox"], input[aria-label*="search" i], input[placeholder*="search" i]'
15220
+ 'input[type="search"], input[name="q"], input[name="query"], input[name="search"], input[role="searchbox"], input[aria-label*="search" i], input[placeholder*="search" i], textarea[name="q"], textarea[name="query"], textarea[name="search"], textarea[role="searchbox"], textarea[aria-label*="search" i], textarea[placeholder*="search" i]'
14876
15221
  );
14877
15222
  specific.forEach((el) => {
14878
15223
  if (!seen.has(el)) {
@@ -14880,7 +15225,7 @@ async function locateSearchTarget(wc, explicitSelector) {
14880
15225
  ordered.push(el);
14881
15226
  }
14882
15227
  });
14883
- document.querySelectorAll('input[type="text"], input:not([type])').forEach((el) => {
15228
+ document.querySelectorAll('input[type="text"], input:not([type]), textarea').forEach((el) => {
14884
15229
  if (seen.has(el)) return;
14885
15230
  const scope = nearestSearchScope(el);
14886
15231
  if (!scope) return;
@@ -14891,9 +15236,10 @@ async function locateSearchTarget(wc, explicitSelector) {
14891
15236
  }
14892
15237
 
14893
15238
  function scoreInput(el) {
14894
- if (!(el instanceof HTMLInputElement)) return -1;
15239
+ if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) return -1;
14895
15240
  if (isDisabled(el) || !isVisible(el)) return -1;
14896
- const type = normalize(el.getAttribute("type") || el.type);
15241
+ const isTextarea = el instanceof HTMLTextAreaElement;
15242
+ const type = isTextarea ? "text" : normalize(el.getAttribute("type") || el.type);
14897
15243
  if (type && !["search", "text", ""].includes(type)) return -1;
14898
15244
 
14899
15245
  let score = 0;
@@ -15040,16 +15386,19 @@ async function searchPage(wc, args) {
15040
15386
  if (looksLikeCurrentSiteNameQuery(query, wc.getURL(), wc.getTitle() || "")) {
15041
15387
  return `Error: "${query}" looks like the current site's name, not a product query. You are already on ${wc.getURL()}. Open a section like staff picks/new releases or search for actual book titles, authors, or genres instead.`;
15042
15388
  }
15389
+ const runShortcut = async (shortcut) => {
15390
+ const beforeUrl2 = wc.getURL();
15391
+ await loadPermittedUrl(wc, shortcut.url);
15392
+ await waitForPotentialNavigation(wc, beforeUrl2, 4e3);
15393
+ const afterUrl2 = wc.getURL();
15394
+ const applied = shortcut.appliedFilters.length > 0 ? ` (${shortcut.appliedFilters.join(", ")})` : "";
15395
+ const destination = shortcut.section ? ` ${shortcut.section}` : "";
15396
+ return `Searched "${query}" via ${shortcut.source}${destination} shortcut${applied} → ${afterUrl2}${await getPostSearchSummary(wc)}`;
15397
+ };
15043
15398
  if (typeof args.selector !== "string") {
15044
- const shortcut = buildSearchShortcut(wc.getURL(), query);
15399
+ const shortcut = buildHuggingFaceSearchShortcut(wc.getURL(), query) ?? buildCommonSearchUrlShortcut(wc.getURL(), query);
15045
15400
  if (shortcut) {
15046
- const beforeUrl2 = wc.getURL();
15047
- await loadPermittedUrl(wc, shortcut.url);
15048
- await waitForPotentialNavigation(wc, beforeUrl2, 4e3);
15049
- const afterUrl2 = wc.getURL();
15050
- const applied = shortcut.appliedFilters.length > 0 ? ` (${shortcut.appliedFilters.join(", ")})` : "";
15051
- const destination = shortcut.section ? ` ${shortcut.section}` : "";
15052
- return `Searched "${query}" via ${shortcut.source}${destination} shortcut${applied} → ${afterUrl2}${await getPostSearchSummary(wc)}`;
15401
+ return runShortcut(shortcut);
15053
15402
  }
15054
15403
  }
15055
15404
  const searchInfo = await locateSearchTarget(
@@ -15060,6 +15409,12 @@ async function searchPage(wc, args) {
15060
15409
  return pageBusyError("search");
15061
15410
  }
15062
15411
  if (!searchInfo?.selector) {
15412
+ if (typeof args.selector !== "string") {
15413
+ const fallback = buildDefaultEngineShortcut(query);
15414
+ if (fallback) {
15415
+ return runShortcut(fallback);
15416
+ }
15417
+ }
15063
15418
  return 'Error: Could not find a visible search input. Try read_page(mode="visible_only") or provide a selector.';
15064
15419
  }
15065
15420
  const fillResult = await setElementValue(wc, searchInfo.selector, query);
@@ -16056,6 +16411,11 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
16056
16411
  ctx.runtime.clearFlow();
16057
16412
  return "Workflow ended.";
16058
16413
  }
16414
+ case "undo_last_action": {
16415
+ const undone = ctx.runtime.undoLastAction();
16416
+ if (!undone) return "Nothing to undo. No undo snapshots available.";
16417
+ return `Undid action: ${undone}. Browser restored to state before that action.`;
16418
+ }
16059
16419
  case "suggest": {
16060
16420
  if (!wc) return "No active tab. Use navigate to open a page.";
16061
16421
  let page;
@@ -20066,6 +20426,23 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
20066
20426
  return asTextResponse("Workflow ended.");
20067
20427
  }
20068
20428
  );
20429
+ server.registerTool(
20430
+ "undo_last_action",
20431
+ {
20432
+ title: "Undo Last Action",
20433
+ description: "Undo the most recent agent action by restoring the browser to its state before that action ran. Works for click, type, submit, navigate, and similar mutating actions."
20434
+ },
20435
+ async () => {
20436
+ const undone = runtime2.undoLastAction();
20437
+ if (!undone)
20438
+ return asTextResponse(
20439
+ "Nothing to undo. No undo snapshots available."
20440
+ );
20441
+ return asTextResponse(
20442
+ `Undid action: ${undone}. Browser restored to state before that action.`
20443
+ );
20444
+ }
20445
+ );
20069
20446
  server.registerTool(
20070
20447
  "suggest",
20071
20448
  {
@@ -21939,6 +22316,19 @@ function registerPageDiffHandlers(windowState, sendToRendererViews) {
21939
22316
  if (!wc) return null;
21940
22317
  return getLatestPageDiff(wc.getURL());
21941
22318
  });
22319
+ electron.ipcMain.handle(Channels.PAGE_DIFF_HISTORY, () => {
22320
+ try {
22321
+ if (!isPremiumActiveState(getPremiumState())) {
22322
+ return { error: "Premium required" };
22323
+ }
22324
+ const activeTab = windowState.tabManager.getActiveTab();
22325
+ const wc = activeTab?.view.webContents;
22326
+ if (!wc) return [];
22327
+ return getPageDiffBursts(wc.getURL());
22328
+ } catch {
22329
+ return [];
22330
+ }
22331
+ });
21942
22332
  electron.ipcMain.on(Channels.PAGE_DIFF_ACTIVITY, (event) => {
21943
22333
  const wc = event.sender;
21944
22334
  if (!wc || wc.isDestroyed()) return;
@@ -22299,6 +22689,20 @@ function registerIpcHandlers(windowState, runtime2) {
22299
22689
  width: windowState.uiState.sidebarWidth
22300
22690
  };
22301
22691
  });
22692
+ electron.ipcMain.handle(Channels.SIDEBAR_NAVIGATE, (_, tab) => {
22693
+ assertString(tab, "tab");
22694
+ if (!windowState.uiState.sidebarOpen) {
22695
+ windowState.uiState.sidebarOpen = true;
22696
+ layoutViews(windowState);
22697
+ }
22698
+ if (!sidebarView.webContents.isDestroyed()) {
22699
+ sidebarView.webContents.send(Channels.SIDEBAR_NAVIGATE, tab);
22700
+ }
22701
+ return {
22702
+ open: windowState.uiState.sidebarOpen,
22703
+ width: windowState.uiState.sidebarWidth
22704
+ };
22705
+ });
22302
22706
  electron.ipcMain.handle(Channels.SIDEBAR_RESIZE_START, () => {
22303
22707
  sidebarResizeActive = true;
22304
22708
  clearSidebarResizeRecoveryTimer();
@@ -22397,6 +22801,14 @@ function registerIpcHandlers(windowState, runtime2) {
22397
22801
  Channels.AGENT_CHECKPOINT_RESTORE,
22398
22802
  (_, checkpointId) => runtime2.restoreCheckpoint(checkpointId)
22399
22803
  );
22804
+ electron.ipcMain.handle(
22805
+ Channels.AGENT_CHECKPOINT_UPDATE_NOTE,
22806
+ (_, checkpointId, note) => runtime2.updateCheckpointNote(checkpointId, note || "")
22807
+ );
22808
+ electron.ipcMain.handle(
22809
+ Channels.AGENT_UNDO_LAST_ACTION,
22810
+ () => runtime2.undoLastAction()
22811
+ );
22400
22812
  electron.ipcMain.handle(
22401
22813
  Channels.AGENT_SESSION_CAPTURE,
22402
22814
  (_, note) => runtime2.captureSession(note)
@@ -22436,6 +22848,13 @@ function registerIpcHandlers(windowState, runtime2) {
22436
22848
  return result.bookmark;
22437
22849
  }
22438
22850
  );
22851
+ electron.ipcMain.handle(
22852
+ Channels.BOOKMARK_UPDATE,
22853
+ (_, id, updates) => {
22854
+ trackBookmarkAction("save");
22855
+ return updateBookmark(id, updates);
22856
+ }
22857
+ );
22439
22858
  electron.ipcMain.handle(Channels.BOOKMARK_REMOVE, (_, id) => {
22440
22859
  trackBookmarkAction("remove");
22441
22860
  return removeBookmark(id);
@@ -22725,6 +23144,41 @@ function registerIpcHandlers(windowState, runtime2) {
22725
23144
  registerAutofillHandlers(windowState);
22726
23145
  registerPageDiffHandlers(windowState, sendToRendererViews);
22727
23146
  }
23147
+ const UNDOABLE_ACTIONS = /* @__PURE__ */ new Set([
23148
+ "accept_cookies",
23149
+ "clear_overlays",
23150
+ "click",
23151
+ "close_tab",
23152
+ "create_tab",
23153
+ "dismiss_popup",
23154
+ "fill_form",
23155
+ "focus",
23156
+ "go_back",
23157
+ "go_forward",
23158
+ "load_session",
23159
+ "login",
23160
+ "navigate",
23161
+ "open_bookmark",
23162
+ "paginate",
23163
+ "press_key",
23164
+ "reload",
23165
+ "restore_checkpoint",
23166
+ "scroll",
23167
+ "scroll_to_element",
23168
+ "search",
23169
+ "select_option",
23170
+ "set_ad_blocking",
23171
+ "submit_form",
23172
+ "switch_tab",
23173
+ "type_text"
23174
+ ]);
23175
+ function isUndoableAction(name) {
23176
+ return UNDOABLE_ACTIONS.has(name);
23177
+ }
23178
+ function isUndoableResult(result) {
23179
+ const normalized = result.trim().toLowerCase();
23180
+ return normalized.length > 0 && !normalized.startsWith("error:") && !normalized.startsWith("nothing to ") && !normalized.startsWith("no active ") && !normalized.startsWith("action rejected:");
23181
+ }
22728
23182
  function makeStep(label, status = "pending") {
22729
23183
  return { label, status };
22730
23184
  }
@@ -23121,6 +23575,7 @@ class AgentRuntime {
23121
23575
  state;
23122
23576
  updateListener = null;
23123
23577
  pendingResolvers = /* @__PURE__ */ new Map();
23578
+ undoSnapshots = [];
23124
23579
  setUpdateListener(listener) {
23125
23580
  this.updateListener = listener;
23126
23581
  if (listener) {
@@ -23130,6 +23585,8 @@ class AgentRuntime {
23130
23585
  getState() {
23131
23586
  const snapshot = clone(this.state);
23132
23587
  snapshot.mcpStatus = getMcpStatus();
23588
+ snapshot.canUndo = this.canUndo();
23589
+ snapshot.undoInfo = this.getUndoInfo();
23133
23590
  return snapshot;
23134
23591
  }
23135
23592
  onTabStateChanged() {
@@ -23137,6 +23594,21 @@ class AgentRuntime {
23137
23594
  }
23138
23595
  setApprovalMode(mode) {
23139
23596
  this.state.supervisor.approvalMode = mode;
23597
+ if (mode === "auto" && !this.state.supervisor.paused) {
23598
+ const approvals = this.state.supervisor.pendingApprovals;
23599
+ if (approvals.length > 0) {
23600
+ const actionIds = new Set(approvals.map((approval) => approval.actionId));
23601
+ this.state.supervisor.pendingApprovals = [];
23602
+ this.state.actions = this.state.actions.map(
23603
+ (action) => actionIds.has(action.id) ? { ...action, status: "running", error: void 0 } : action
23604
+ );
23605
+ for (const approval of approvals) {
23606
+ const resolve = this.pendingResolvers.get(approval.id);
23607
+ this.pendingResolvers.delete(approval.id);
23608
+ resolve?.(true);
23609
+ }
23610
+ }
23611
+ }
23140
23612
  this.emit();
23141
23613
  return this.getState();
23142
23614
  }
@@ -23172,6 +23644,37 @@ class AgentRuntime {
23172
23644
  this.captureSession(`Restored ${checkpoint.name}`);
23173
23645
  return clone(checkpoint);
23174
23646
  }
23647
+ updateCheckpointNote(checkpointId, note) {
23648
+ const index = this.state.checkpoints.findIndex((item) => item.id === checkpointId);
23649
+ if (index === -1) return null;
23650
+ this.state.checkpoints[index] = {
23651
+ ...this.state.checkpoints[index],
23652
+ note: note.trim() || void 0
23653
+ };
23654
+ this.emit();
23655
+ return clone(this.state.checkpoints[index]);
23656
+ }
23657
+ canUndo() {
23658
+ return this.undoSnapshots.length > 0;
23659
+ }
23660
+ getUndoInfo() {
23661
+ const latest = this.undoSnapshots[this.undoSnapshots.length - 1];
23662
+ if (!latest) return null;
23663
+ return { actionName: latest.actionName, capturedAt: latest.capturedAt };
23664
+ }
23665
+ undoLastAction() {
23666
+ const snapshot = this.undoSnapshots.at(-1);
23667
+ if (!snapshot) return null;
23668
+ try {
23669
+ this.tabManager.restoreSession(snapshot.snapshot);
23670
+ this.undoSnapshots.pop();
23671
+ } catch (error) {
23672
+ logger$3.error("Failed to restore undo snapshot", error);
23673
+ return null;
23674
+ }
23675
+ this.captureSession(`Undid ${snapshot.actionName}`);
23676
+ return snapshot.actionName;
23677
+ }
23175
23678
  captureSession(note) {
23176
23679
  const snapshot = this.tabManager.snapshotSession(note);
23177
23680
  this.state.session = snapshot;
@@ -23321,6 +23824,7 @@ ${progress}
23321
23824
  args = {},
23322
23825
  tabId = null,
23323
23826
  dangerous = false,
23827
+ undoable,
23324
23828
  executor
23325
23829
  }) {
23326
23830
  const action = this.startAction({
@@ -23374,8 +23878,13 @@ ${progress}
23374
23878
  streamId: transcriptStreamId,
23375
23879
  mode: "replace"
23376
23880
  });
23881
+ const shouldCaptureUndo = undoable ?? isUndoableAction(name);
23882
+ const undoSnapshot = shouldCaptureUndo ? this.createUndoSnapshot(name) : null;
23377
23883
  try {
23378
23884
  const result = await executor();
23885
+ if (undoSnapshot && isUndoableResult(result)) {
23886
+ this.pushUndoSnapshot(undoSnapshot);
23887
+ }
23379
23888
  this.finishAction(action.id, "completed", summarizeText(result));
23380
23889
  this.publishTranscript({
23381
23890
  source,
@@ -23402,6 +23911,21 @@ ${progress}
23402
23911
  throw error;
23403
23912
  }
23404
23913
  }
23914
+ createUndoSnapshot(name) {
23915
+ return {
23916
+ id: crypto$2.randomUUID(),
23917
+ actionName: name,
23918
+ snapshot: this.tabManager.snapshotSession(
23919
+ `Auto-checkpoint before ${name}`
23920
+ ),
23921
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString()
23922
+ };
23923
+ }
23924
+ pushUndoSnapshot(snapshot) {
23925
+ this.undoSnapshots = [...this.undoSnapshots, snapshot].slice(
23926
+ -10
23927
+ );
23928
+ }
23405
23929
  resolveApproval(approvalId, approved) {
23406
23930
  const approval = this.state.supervisor.pendingApprovals.find(
23407
23931
  (item) => item.id === approvalId
@@ -23983,6 +24507,11 @@ function closeSplash(splash, delayMs = 0) {
23983
24507
  }
23984
24508
  const logger = createLogger("Bootstrap");
23985
24509
  let runtime = null;
24510
+ function configureUserAgent() {
24511
+ const originalUA = electron.session.defaultSession.getUserAgent();
24512
+ const maskedUA = originalUA.replace(/ Electron\/[^\s]+/, "") + " Vessel/" + electron.app.getVersion();
24513
+ electron.session.defaultSession.setUserAgent(maskedUA);
24514
+ }
23986
24515
  function checkWritableUserData(userDataPath) {
23987
24516
  const issues = [];
23988
24517
  try {
@@ -24046,6 +24575,7 @@ Action: Open Settings (Ctrl+,) to choose a different port, then save to restart
24046
24575
  });
24047
24576
  }
24048
24577
  async function bootstrap() {
24578
+ configureUserAgent();
24049
24579
  const splash = createSplashWindow();
24050
24580
  const settings2 = loadSettings();
24051
24581
  const userDataPath = electron.app.getPath("userData");