@quanta-intellect/vessel-browser 0.1.65 → 0.1.68

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
@@ -74,7 +74,7 @@ const defaults = {
74
74
  expiresAt: ""
75
75
  }
76
76
  };
77
- const SAVE_DEBOUNCE_MS$5 = 150;
77
+ const SAVE_DEBOUNCE_MS$6 = 150;
78
78
  const CHAT_PROVIDER_SECRET_FILENAME = "vessel-chat-provider-secret";
79
79
  const logger$j = createLogger("Settings");
80
80
  const SETTABLE_KEYS = new Set(Object.keys(defaults));
@@ -232,7 +232,7 @@ function saveSettings() {
232
232
  if (saveDirty) {
233
233
  void persistNow();
234
234
  }
235
- }, SAVE_DEBOUNCE_MS$5);
235
+ }, SAVE_DEBOUNCE_MS$6);
236
236
  }
237
237
  function setSetting(key, value) {
238
238
  loadSettings();
@@ -871,7 +871,7 @@ function createDebouncedJsonPersistence({
871
871
  }
872
872
  let state$4 = null;
873
873
  const listeners$2 = /* @__PURE__ */ new Set();
874
- const SAVE_DEBOUNCE_MS$4 = 250;
874
+ const SAVE_DEBOUNCE_MS$5 = 250;
875
875
  function getHighlightsPath() {
876
876
  return path.join(electron.app.getPath("userData"), "vessel-highlights.json");
877
877
  }
@@ -889,15 +889,15 @@ function load$4() {
889
889
  });
890
890
  return state$4;
891
891
  }
892
- const persistence$4 = createDebouncedJsonPersistence({
893
- debounceMs: SAVE_DEBOUNCE_MS$4,
892
+ const persistence$5 = createDebouncedJsonPersistence({
893
+ debounceMs: SAVE_DEBOUNCE_MS$5,
894
894
  filePath: getHighlightsPath(),
895
895
  getValue: () => state$4,
896
896
  logLabel: "highlights",
897
897
  resetOnSchedule: true
898
898
  });
899
899
  function save$2() {
900
- persistence$4.schedule();
900
+ persistence$5.schedule();
901
901
  }
902
902
  function emit$2() {
903
903
  if (!state$4) return;
@@ -979,7 +979,7 @@ function clearHighlightsForUrl(url) {
979
979
  return removed;
980
980
  }
981
981
  function flushPersist$4() {
982
- return persistence$4.flush();
982
+ return persistence$5.flush();
983
983
  }
984
984
  const SKIP_TAGS_JS = "var SKIP_TAGS = {SCRIPT:1,STYLE:1,NOSCRIPT:1,TEMPLATE:1,IFRAME:1,SVG:1};";
985
985
  const CONTENT_ROOTS_JS = `
@@ -1551,7 +1551,7 @@ function persistHighlight(url, text) {
1551
1551
  return { success: true, text: capped, id: highlight.id };
1552
1552
  }
1553
1553
  const MAX_HISTORY_ENTRIES = 5e3;
1554
- const SAVE_DEBOUNCE_MS$3 = 250;
1554
+ const SAVE_DEBOUNCE_MS$4 = 250;
1555
1555
  let state$3 = null;
1556
1556
  const listeners$1 = /* @__PURE__ */ new Set();
1557
1557
  function getHistoryPath() {
@@ -1571,14 +1571,14 @@ function load$3() {
1571
1571
  });
1572
1572
  return state$3;
1573
1573
  }
1574
- const persistence$3 = createDebouncedJsonPersistence({
1575
- debounceMs: SAVE_DEBOUNCE_MS$3,
1574
+ const persistence$4 = createDebouncedJsonPersistence({
1575
+ debounceMs: SAVE_DEBOUNCE_MS$4,
1576
1576
  filePath: getHistoryPath(),
1577
1577
  getValue: () => state$3,
1578
1578
  logLabel: "history"
1579
1579
  });
1580
1580
  function save$1() {
1581
- persistence$3.schedule();
1581
+ persistence$4.schedule();
1582
1582
  }
1583
1583
  function emit$1() {
1584
1584
  if (!state$3) return;
@@ -1635,7 +1635,7 @@ function clearAll$1() {
1635
1635
  emit$1();
1636
1636
  }
1637
1637
  function flushPersist$3() {
1638
- return persistence$3.flush();
1638
+ return persistence$4.flush();
1639
1639
  }
1640
1640
  const MAX_CONSOLE_ENTRIES = 500;
1641
1641
  const MAX_NETWORK_ENTRIES = 200;
@@ -2684,6 +2684,8 @@ const Channels = {
2684
2684
  AGENT_APPROVAL_RESOLVE: "agent:approval-resolve",
2685
2685
  AGENT_CHECKPOINT_CREATE: "agent:checkpoint-create",
2686
2686
  AGENT_CHECKPOINT_RESTORE: "agent:checkpoint-restore",
2687
+ AGENT_CHECKPOINT_UPDATE_NOTE: "agent:checkpoint-update-note",
2688
+ AGENT_UNDO_LAST_ACTION: "agent:undo-last-action",
2687
2689
  AGENT_SESSION_CAPTURE: "agent:session-capture",
2688
2690
  AGENT_SESSION_RESTORE: "agent:session-restore",
2689
2691
  // Content
@@ -2691,6 +2693,7 @@ const Channels = {
2691
2693
  READER_MODE_TOGGLE: "reader:toggle",
2692
2694
  // UI state
2693
2695
  SIDEBAR_TOGGLE: "ui:sidebar-toggle",
2696
+ SIDEBAR_NAVIGATE: "ui:sidebar-navigate",
2694
2697
  SIDEBAR_RESIZE: "ui:sidebar-resize",
2695
2698
  SIDEBAR_RESIZE_START: "ui:sidebar-resize-start",
2696
2699
  SIDEBAR_RESIZE_COMMIT: "ui:sidebar-resize-commit",
@@ -2707,6 +2710,7 @@ const Channels = {
2707
2710
  BOOKMARKS_GET: "bookmarks:get",
2708
2711
  BOOKMARKS_UPDATE: "bookmarks:update",
2709
2712
  BOOKMARK_SAVE: "bookmarks:save",
2713
+ BOOKMARK_UPDATE: "bookmarks:update-item",
2710
2714
  BOOKMARK_REMOVE: "bookmarks:remove",
2711
2715
  BOOKMARK_ADD_CONTEXT_TO_CHAT: "bookmarks:add-context-to-chat",
2712
2716
  FOLDER_CREATE: "bookmarks:folder-create",
@@ -2786,6 +2790,7 @@ const Channels = {
2786
2790
  PAGE_DIFF_ACTIVITY: "page:diff-activity",
2787
2791
  PAGE_CHANGED: "page:changed",
2788
2792
  PAGE_DIFF_GET: "page:diff-get",
2793
+ PAGE_DIFF_HISTORY: "page:diff-history",
2789
2794
  PAGE_DIFF_DIRTY: "page:diff-dirty"
2790
2795
  };
2791
2796
  const MAX_DETAIL_ITEMS = 3;
@@ -3101,7 +3106,7 @@ function isTrackablePageUrl(rawUrl) {
3101
3106
  return false;
3102
3107
  }
3103
3108
  }
3104
- const SAVE_DEBOUNCE_MS$2 = 500;
3109
+ const SAVE_DEBOUNCE_MS$3 = 500;
3105
3110
  const MAX_TEXT_LENGTH = 8e3;
3106
3111
  let snapshots = null;
3107
3112
  function getFilePath$1() {
@@ -3139,8 +3144,8 @@ function load$2() {
3139
3144
  });
3140
3145
  return snapshots;
3141
3146
  }
3142
- const persistence$2 = createDebouncedJsonPersistence({
3143
- debounceMs: SAVE_DEBOUNCE_MS$2,
3147
+ const persistence$3 = createDebouncedJsonPersistence({
3148
+ debounceMs: SAVE_DEBOUNCE_MS$3,
3144
3149
  filePath: getFilePath$1(),
3145
3150
  getValue: () => snapshots,
3146
3151
  logLabel: "page snapshots",
@@ -3168,11 +3173,11 @@ function saveSnapshot(rawUrl, title, textContent, headings) {
3168
3173
  };
3169
3174
  s.delete(key);
3170
3175
  s.set(key, snapshot);
3171
- persistence$2.schedule();
3176
+ persistence$3.schedule();
3172
3177
  return snapshot;
3173
3178
  }
3174
3179
  function flushPersist$2() {
3175
- return persistence$2.flush();
3180
+ return persistence$3.flush();
3176
3181
  }
3177
3182
  const SEARCH_ENGINE_HOSTS = [
3178
3183
  "google.",
@@ -4419,6 +4424,13 @@ function inferPageSchema(page) {
4419
4424
  confidence
4420
4425
  };
4421
4426
  }
4427
+ const DEFAULT_PAGE_SCRIPT_TIMEOUT_MS$1 = 1500;
4428
+ const EXTRACT_SCRIPT_TIMEOUT_MS = 3e3;
4429
+ const EXTRACT_TIMEOUT_BASE_MS = 12e3;
4430
+ const EXTRACT_TIMEOUT_MAX_MS = 2e4;
4431
+ const MUTATION_CAPTURE_INTERVAL_MS = 5e3;
4432
+ const MUTATION_SETTLE_AFTER_MS = 1500;
4433
+ const AGENT_STREAM_IDLE_TIMEOUT_MS = 3e4;
4422
4434
  const logger$e = createLogger("Extractor");
4423
4435
  const EMPTY_PAGE_CONTENT = {
4424
4436
  title: "",
@@ -5136,8 +5148,7 @@ const DIRECT_EXTRACTION_SCRIPT = String.raw`
5136
5148
  function delay(ms) {
5137
5149
  return new Promise((resolve) => setTimeout(resolve, ms));
5138
5150
  }
5139
- const EXECUTE_SCRIPT_TIMEOUT_MS = 3e3;
5140
- async function waitForDomReady(webContents, timeoutMs = 1500) {
5151
+ async function waitForDomReady(webContents, timeoutMs = DEFAULT_PAGE_SCRIPT_TIMEOUT_MS$1) {
5141
5152
  const deadline = Date.now() + timeoutMs;
5142
5153
  while (Date.now() < deadline) {
5143
5154
  const readyState = await executeScript(
@@ -5163,7 +5174,10 @@ async function executeScript(webContents, script) {
5163
5174
  return await Promise.race([
5164
5175
  webContents.executeJavaScript(script),
5165
5176
  new Promise((resolve) => {
5166
- timer = setTimeout(() => resolve(null), EXECUTE_SCRIPT_TIMEOUT_MS);
5177
+ timer = setTimeout(
5178
+ () => resolve(null),
5179
+ EXTRACT_SCRIPT_TIMEOUT_MS
5180
+ );
5167
5181
  })
5168
5182
  ]);
5169
5183
  } catch (err) {
@@ -5260,8 +5274,6 @@ function mergePageContent(candidates, webContents) {
5260
5274
  url: mergedBase.url || webContents.getURL() || ""
5261
5275
  };
5262
5276
  }
5263
- const EXTRACT_TIMEOUT_BASE_MS = 12e3;
5264
- const EXTRACT_TIMEOUT_MAX_MS = 2e4;
5265
5277
  async function estimateExtractionTimeout(webContents) {
5266
5278
  try {
5267
5279
  const elementCount = await executeScript(
@@ -5348,8 +5360,32 @@ function normalizePageContent(value) {
5348
5360
  pageSchema: page.pageSchema
5349
5361
  };
5350
5362
  }
5363
+ function normalizePageDiffHistoryItem(value) {
5364
+ if (!value || typeof value !== "object") return null;
5365
+ const raw = value;
5366
+ if (typeof raw.detectedAt !== "string" || typeof raw.summary !== "string") {
5367
+ return null;
5368
+ }
5369
+ return {
5370
+ detectedAt: raw.detectedAt,
5371
+ summary: raw.summary
5372
+ };
5373
+ }
5374
+ function prunePageDiffHistory(items, options) {
5375
+ const cutoff = (options.now ?? Date.now()) - options.maxAgeDays * 24 * 60 * 60 * 1e3;
5376
+ return items.filter((item) => {
5377
+ const detectedAt = Date.parse(item.detectedAt);
5378
+ return Number.isFinite(detectedAt) && detectedAt >= cutoff;
5379
+ }).sort(
5380
+ (left, right) => Date.parse(left.detectedAt) - Date.parse(right.detectedAt)
5381
+ ).slice(-options.maxItems);
5382
+ }
5383
+ function appendPageDiffHistoryItem(items, next, options) {
5384
+ return prunePageDiffHistory([...items, next], options);
5385
+ }
5351
5386
  const latestPageDiffs = /* @__PURE__ */ new Map();
5352
5387
  const recentPageDiffBursts = /* @__PURE__ */ new Map();
5388
+ let historyLoaded = false;
5353
5389
  const pendingPageSnapshotTimers = /* @__PURE__ */ new Map();
5354
5390
  const pendingPageSnapshotDueAt = /* @__PURE__ */ new Map();
5355
5391
  const lastMutationSnapshotAt = /* @__PURE__ */ new Map();
@@ -5370,12 +5406,81 @@ function attachDestroyCleanup(wc) {
5370
5406
  cleanupTimersForWcId(wc.id);
5371
5407
  });
5372
5408
  }
5373
- const MIN_MUTATION_CAPTURE_INTERVAL_MS = 5e3;
5374
- const SETTLE_AFTER_ACTIVITY_MS = 1500;
5409
+ const MAX_PERSISTED_DIFF_BURSTS = 50;
5410
+ const MAX_HISTORY_DAYS = 30;
5411
+ const SAVE_DEBOUNCE_MS$2 = 500;
5412
+ function getHistoryFilePath() {
5413
+ return path.join(electron.app.getPath("userData"), "vessel-page-diff-history.json");
5414
+ }
5415
+ function loadHistory() {
5416
+ if (historyLoaded) return recentPageDiffBursts;
5417
+ historyLoaded = true;
5418
+ const loaded = loadJsonFile({
5419
+ filePath: getHistoryFilePath(),
5420
+ fallback: /* @__PURE__ */ new Map(),
5421
+ secure: true,
5422
+ parse: (raw) => {
5423
+ const next = /* @__PURE__ */ new Map();
5424
+ if (!Array.isArray(raw)) return next;
5425
+ for (const entry of raw) {
5426
+ if (!entry || typeof entry !== "object") continue;
5427
+ const record = entry;
5428
+ if (typeof record.url !== "string" || !Array.isArray(record.bursts)) {
5429
+ continue;
5430
+ }
5431
+ next.set(
5432
+ record.url,
5433
+ prunePageDiffHistory(
5434
+ record.bursts.map((item) => normalizePageDiffHistoryItem(item)).filter((item) => item !== null),
5435
+ {
5436
+ maxAgeDays: MAX_HISTORY_DAYS,
5437
+ maxItems: MAX_PERSISTED_DIFF_BURSTS
5438
+ }
5439
+ )
5440
+ );
5441
+ }
5442
+ return next;
5443
+ }
5444
+ });
5445
+ for (const [key, bursts] of loaded.entries()) {
5446
+ recentPageDiffBursts.set(key, bursts);
5447
+ }
5448
+ return recentPageDiffBursts;
5449
+ }
5450
+ const persistence$2 = createDebouncedJsonPersistence({
5451
+ debounceMs: SAVE_DEBOUNCE_MS$2,
5452
+ filePath: getHistoryFilePath(),
5453
+ getValue: () => recentPageDiffBursts,
5454
+ logLabel: "page diff history",
5455
+ secure: true,
5456
+ serialize: (value) => Array.from(value.entries()).map(([url, bursts]) => ({
5457
+ url,
5458
+ bursts
5459
+ }))
5460
+ });
5375
5461
  function getLatestPageDiff(rawUrl) {
5376
5462
  if (!shouldTrackSnapshotUrl(rawUrl)) return null;
5377
5463
  return latestPageDiffs.get(normalizeUrl(rawUrl)) ?? null;
5378
5464
  }
5465
+ function getPageDiffBursts(rawUrl) {
5466
+ if (!shouldTrackSnapshotUrl(rawUrl)) return [];
5467
+ const key = normalizeUrl(rawUrl);
5468
+ const history = loadHistory();
5469
+ const bursts = prunePageDiffHistory(history.get(key) ?? [], {
5470
+ maxAgeDays: MAX_HISTORY_DAYS,
5471
+ maxItems: MAX_PERSISTED_DIFF_BURSTS
5472
+ });
5473
+ const current = history.get(key) ?? [];
5474
+ if (current.length !== bursts.length) {
5475
+ if (bursts.length > 0) {
5476
+ history.set(key, bursts);
5477
+ } else {
5478
+ history.delete(key);
5479
+ }
5480
+ persistence$2.schedule();
5481
+ }
5482
+ return bursts.slice().reverse();
5483
+ }
5379
5484
  function summarizeDiffBurst(diff) {
5380
5485
  const items = diff.changes.slice(0, 2).map((change) => `${change.section}: ${change.summary}`);
5381
5486
  return items.join(" | ");
@@ -5386,16 +5491,21 @@ function enrichWithBurstHistory(key, diff) {
5386
5491
  detectedAt,
5387
5492
  summary: summarizeDiffBurst(diff)
5388
5493
  };
5389
- const bursts = [...recentPageDiffBursts.get(key) || [], nextBurst].slice(
5390
- -5
5391
- );
5392
- recentPageDiffBursts.set(key, bursts);
5494
+ const history = loadHistory();
5495
+ const bursts = appendPageDiffHistoryItem(history.get(key) ?? [], nextBurst, {
5496
+ maxAgeDays: MAX_HISTORY_DAYS,
5497
+ maxItems: MAX_PERSISTED_DIFF_BURSTS,
5498
+ now: Date.parse(detectedAt)
5499
+ });
5500
+ history.set(key, bursts);
5501
+ persistence$2.schedule();
5502
+ const recentBursts = bursts.slice(-5);
5393
5503
  return {
5394
5504
  ...diff,
5395
5505
  burstCount: bursts.length,
5396
5506
  firstDetectedAt: bursts[0]?.detectedAt,
5397
5507
  lastDetectedAt: bursts[bursts.length - 1]?.detectedAt,
5398
- recentBursts: bursts
5508
+ recentBursts: recentBursts.slice().reverse()
5399
5509
  };
5400
5510
  }
5401
5511
  async function capturePageSnapshot(url, wc, sendToRendererViews) {
@@ -5416,11 +5526,9 @@ async function capturePageSnapshot(url, wc, sendToRendererViews) {
5416
5526
  sendToRendererViews(Channels.PAGE_CHANGED, enrichedDiff);
5417
5527
  } else {
5418
5528
  latestPageDiffs.delete(key);
5419
- recentPageDiffBursts.delete(key);
5420
5529
  }
5421
5530
  } else {
5422
5531
  latestPageDiffs.delete(key);
5423
- recentPageDiffBursts.delete(key);
5424
5532
  }
5425
5533
  saveSnapshot(url, title, textContent, headings);
5426
5534
  } catch {
@@ -5429,8 +5537,8 @@ async function capturePageSnapshot(url, wc, sendToRendererViews) {
5429
5537
  function computeNextSnapshotDueAt(wcId, now, delayMs) {
5430
5538
  const lastCaptureAt = lastMutationSnapshotAt.get(wcId) || 0;
5431
5539
  const lastActivityAt = lastMutationActivityAt.get(wcId) || 0;
5432
- const earliestAllowedAt = lastCaptureAt + MIN_MUTATION_CAPTURE_INTERVAL_MS;
5433
- const stableAfterActivityAt = lastActivityAt ? lastActivityAt + SETTLE_AFTER_ACTIVITY_MS : 0;
5540
+ const earliestAllowedAt = lastCaptureAt + MUTATION_CAPTURE_INTERVAL_MS;
5541
+ const stableAfterActivityAt = lastActivityAt ? lastActivityAt + MUTATION_SETTLE_AFTER_MS : 0;
5434
5542
  return Math.max(now + delayMs, earliestAllowedAt, stableAfterActivityAt);
5435
5543
  }
5436
5544
  function scheduleTimerAt(wc, sendToRendererViews, dueAt) {
@@ -6135,7 +6243,7 @@ class AnthropicProvider {
6135
6243
  let textContent = "";
6136
6244
  const toolUseBlocks = [];
6137
6245
  let currentToolUse = null;
6138
- const STREAM_IDLE_TIMEOUT_MS = 3e4;
6246
+ const STREAM_IDLE_TIMEOUT_MS = AGENT_STREAM_IDLE_TIMEOUT_MS;
6139
6247
  let idleTimer = null;
6140
6248
  const resetIdleTimer = () => {
6141
6249
  if (idleTimer) clearTimeout(idleTimer);
@@ -6991,9 +7099,16 @@ class OpenAICompatProvider {
6991
7099
  constructor(config) {
6992
7100
  const meta = PROVIDERS[config.id];
6993
7101
  const baseURL = config.baseUrl || meta?.defaultBaseUrl || "https://api.openai.com/v1";
7102
+ const isOpenRouter = baseURL.includes("openrouter.ai");
6994
7103
  this.client = new OpenAI({
6995
7104
  apiKey: config.apiKey || "ollama",
6996
- baseURL
7105
+ baseURL,
7106
+ ...isOpenRouter && {
7107
+ defaultHeaders: {
7108
+ "HTTP-Referer": "https://github.com/unmodeled/vessel-browser",
7109
+ "X-Title": "Vessel"
7110
+ }
7111
+ }
6997
7112
  });
6998
7113
  this.providerId = config.id;
6999
7114
  this.model = config.model || meta?.defaultModel || "gpt-4o";
@@ -10068,6 +10183,13 @@ const TOOL_DEFINITIONS = [
10068
10183
  tier: 2,
10069
10184
  hiddenByDefault: true
10070
10185
  },
10186
+ // --- Undo ---
10187
+ {
10188
+ name: "undo_last_action",
10189
+ title: "Undo Last Action",
10190
+ 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.",
10191
+ tier: 1
10192
+ },
10071
10193
  // --- Speedee System: Suggestion Engine ---
10072
10194
  {
10073
10195
  name: "suggest",
@@ -10592,6 +10714,60 @@ function getBookmarkSearchMatch(args) {
10592
10714
  }
10593
10715
  return { matchedFields, score };
10594
10716
  }
10717
+ function normalizeOptionalString(value) {
10718
+ if (typeof value !== "string") return void 0;
10719
+ const trimmed = value.trim();
10720
+ return trimmed || void 0;
10721
+ }
10722
+ function normalizeKeyFields(value) {
10723
+ if (!Array.isArray(value)) return void 0;
10724
+ const normalized = value.filter((field) => typeof field === "string").map((field) => field.trim()).filter(Boolean);
10725
+ return normalized.length > 0 ? normalized : void 0;
10726
+ }
10727
+ function normalizeAgentHints(value) {
10728
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
10729
+ return void 0;
10730
+ }
10731
+ const normalized = Object.fromEntries(
10732
+ Object.entries(value).map(([key, hint]) => [key.trim(), normalizeOptionalString(hint)]).filter((entry) => Boolean(entry[0] && entry[1]))
10733
+ );
10734
+ return Object.keys(normalized).length > 0 ? normalized : void 0;
10735
+ }
10736
+ function hasOwn(value, key) {
10737
+ return Object.prototype.hasOwnProperty.call(value, key);
10738
+ }
10739
+ function normalizeBookmarkMetadata(input) {
10740
+ const normalized = {};
10741
+ const intent = normalizeOptionalString(input.intent);
10742
+ const expectedContent = normalizeOptionalString(input.expectedContent);
10743
+ const keyFields = normalizeKeyFields(input.keyFields);
10744
+ const agentHints = normalizeAgentHints(input.agentHints);
10745
+ if (intent !== void 0) normalized.intent = intent;
10746
+ if (expectedContent !== void 0) {
10747
+ normalized.expectedContent = expectedContent;
10748
+ }
10749
+ if (keyFields !== void 0) normalized.keyFields = keyFields;
10750
+ if (agentHints !== void 0) normalized.agentHints = agentHints;
10751
+ return normalized;
10752
+ }
10753
+ function normalizeBookmarkMetadataUpdate(input) {
10754
+ const normalized = {};
10755
+ if (hasOwn(input, "intent")) {
10756
+ normalized.intent = normalizeOptionalString(input.intent);
10757
+ }
10758
+ if (hasOwn(input, "expectedContent")) {
10759
+ normalized.expectedContent = normalizeOptionalString(
10760
+ input.expectedContent
10761
+ );
10762
+ }
10763
+ if (hasOwn(input, "keyFields")) {
10764
+ normalized.keyFields = normalizeKeyFields(input.keyFields);
10765
+ }
10766
+ if (hasOwn(input, "agentHints")) {
10767
+ normalized.agentHints = normalizeAgentHints(input.agentHints);
10768
+ }
10769
+ return normalized;
10770
+ }
10595
10771
  const UNSORTED_ID = "unsorted";
10596
10772
  const ARCHIVE_FOLDER_NAME = "Archive";
10597
10773
  const SAVE_DEBOUNCE_MS$1 = 250;
@@ -10630,6 +10806,13 @@ const persistence$1 = createDebouncedJsonPersistence({
10630
10806
  function save() {
10631
10807
  persistence$1.schedule();
10632
10808
  }
10809
+ function assignDefinedBookmarkFields(bookmark, fields) {
10810
+ if (!fields) return;
10811
+ for (const [key, value] of Object.entries(fields)) {
10812
+ if (value === void 0) continue;
10813
+ Object.assign(bookmark, { [key]: value });
10814
+ }
10815
+ }
10633
10816
  function emit() {
10634
10817
  if (!state$1) return;
10635
10818
  const snapshot = cloneState(state$1);
@@ -10803,9 +10986,7 @@ function saveBookmarkWithPolicy(url, title, folderId, note, options) {
10803
10986
  if (note !== void 0) {
10804
10987
  bookmark2.note = note.trim() || void 0;
10805
10988
  }
10806
- if (options?.extra) {
10807
- Object.assign(bookmark2, options.extra);
10808
- }
10989
+ assignDefinedBookmarkFields(bookmark2, options?.extra);
10809
10990
  bookmark2.savedAt = (/* @__PURE__ */ new Date()).toISOString();
10810
10991
  save();
10811
10992
  emit();
@@ -10847,6 +11028,12 @@ function updateBookmark(id, updates) {
10847
11028
  load$1();
10848
11029
  const bookmark = state$1.bookmarks.find((item) => item.id === id);
10849
11030
  if (!bookmark) return null;
11031
+ const metadataUpdates = normalizeBookmarkMetadataUpdate({
11032
+ intent: updates.intent,
11033
+ expectedContent: updates.expectedContent,
11034
+ keyFields: updates.keyFields,
11035
+ agentHints: updates.agentHints
11036
+ });
10850
11037
  if (typeof updates.title === "string") {
10851
11038
  const trimmed = updates.title.trim();
10852
11039
  bookmark.title = trimmed || bookmark.url;
@@ -10858,20 +11045,20 @@ function updateBookmark(id, updates) {
10858
11045
  if (typeof updates.folderId === "string") {
10859
11046
  bookmark.folderId = updates.folderId && updates.folderId !== UNSORTED_ID ? state$1.folders.find((item) => item.id === updates.folderId)?.id ?? UNSORTED_ID : UNSORTED_ID;
10860
11047
  }
10861
- if (typeof updates.intent === "string") {
10862
- bookmark.intent = updates.intent.trim() || void 0;
11048
+ if ("intent" in metadataUpdates) {
11049
+ bookmark.intent = metadataUpdates.intent;
10863
11050
  }
10864
- if (typeof updates.expectedContent === "string") {
10865
- bookmark.expectedContent = updates.expectedContent.trim() || void 0;
11051
+ if ("expectedContent" in metadataUpdates) {
11052
+ bookmark.expectedContent = metadataUpdates.expectedContent;
10866
11053
  }
10867
- if (updates.keyFields !== void 0) {
10868
- bookmark.keyFields = updates.keyFields;
11054
+ if ("keyFields" in metadataUpdates) {
11055
+ bookmark.keyFields = metadataUpdates.keyFields;
10869
11056
  }
10870
11057
  if (updates.pageSchema !== void 0) {
10871
11058
  bookmark.pageSchema = updates.pageSchema;
10872
11059
  }
10873
- if (updates.agentHints !== void 0) {
10874
- bookmark.agentHints = updates.agentHints;
11060
+ if ("agentHints" in metadataUpdates) {
11061
+ bookmark.agentHints = metadataUpdates.agentHints;
10875
11062
  }
10876
11063
  save();
10877
11064
  emit();
@@ -11231,9 +11418,13 @@ function formatDeadLinkMessage(label, result) {
11231
11418
  return `Skipped stale link "${label}" because ${destination} returned ${status}. Try a different link or URL instead.`;
11232
11419
  }
11233
11420
  const logger$b = createLogger("Screenshot");
11421
+ const SCREENSHOT_RETRY_COUNT = 3;
11422
+ const SCREENSHOT_RETRY_BASE_DELAY_MS = 120;
11234
11423
  async function captureScreenshot(wc) {
11235
- for (let attempt = 0; attempt < 3; attempt += 1) {
11236
- await new Promise((resolve) => setTimeout(resolve, 120 * (attempt + 1)));
11424
+ for (let attempt = 0; attempt < SCREENSHOT_RETRY_COUNT; attempt += 1) {
11425
+ await new Promise(
11426
+ (resolve) => setTimeout(resolve, SCREENSHOT_RETRY_BASE_DELAY_MS * (attempt + 1))
11427
+ );
11237
11428
  try {
11238
11429
  const image = await wc.capturePage();
11239
11430
  if (!image.isEmpty()) {
@@ -11862,33 +12053,6 @@ function formatCompactToolResult(name, result) {
11862
12053
  return limitText(result, 18, 1400);
11863
12054
  }
11864
12055
  }
11865
- function normalizeOptionalString(value) {
11866
- if (typeof value !== "string") return void 0;
11867
- const trimmed = value.trim();
11868
- return trimmed || void 0;
11869
- }
11870
- function normalizeKeyFields(value) {
11871
- if (!Array.isArray(value)) return void 0;
11872
- const normalized = value.filter((field) => typeof field === "string").map((field) => field.trim()).filter(Boolean);
11873
- return normalized.length > 0 ? normalized : void 0;
11874
- }
11875
- function normalizeAgentHints(value) {
11876
- if (!value || typeof value !== "object" || Array.isArray(value)) {
11877
- return void 0;
11878
- }
11879
- const normalized = Object.fromEntries(
11880
- Object.entries(value).map(([key, hint]) => [key.trim(), normalizeOptionalString(hint)]).filter((entry) => Boolean(entry[0] && entry[1]))
11881
- );
11882
- return Object.keys(normalized).length > 0 ? normalized : void 0;
11883
- }
11884
- function normalizeBookmarkMetadata(input) {
11885
- return {
11886
- intent: normalizeOptionalString(input.intent),
11887
- expectedContent: normalizeOptionalString(input.expectedContent),
11888
- keyFields: normalizeKeyFields(input.keyFields),
11889
- agentHints: normalizeAgentHints(input.agentHints)
11890
- };
11891
- }
11892
12056
  const HUGGING_FACE_HUB_HOSTS = /* @__PURE__ */ new Set(["huggingface.co", "www.huggingface.co"]);
11893
12057
  const HUGGING_FACE_MODEL_TASKS = [
11894
12058
  {
@@ -16040,6 +16204,11 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
16040
16204
  ctx.runtime.clearFlow();
16041
16205
  return "Workflow ended.";
16042
16206
  }
16207
+ case "undo_last_action": {
16208
+ const undone = ctx.runtime.undoLastAction();
16209
+ if (!undone) return "Nothing to undo. No undo snapshots available.";
16210
+ return `Undid action: ${undone}. Browser restored to state before that action.`;
16211
+ }
16043
16212
  case "suggest": {
16044
16213
  if (!wc) return "No active tab. Use navigate to open a page.";
16045
16214
  let page;
@@ -20050,6 +20219,23 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
20050
20219
  return asTextResponse("Workflow ended.");
20051
20220
  }
20052
20221
  );
20222
+ server.registerTool(
20223
+ "undo_last_action",
20224
+ {
20225
+ title: "Undo Last Action",
20226
+ 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."
20227
+ },
20228
+ async () => {
20229
+ const undone = runtime2.undoLastAction();
20230
+ if (!undone)
20231
+ return asTextResponse(
20232
+ "Nothing to undo. No undo snapshots available."
20233
+ );
20234
+ return asTextResponse(
20235
+ `Undid action: ${undone}. Browser restored to state before that action.`
20236
+ );
20237
+ }
20238
+ );
20053
20239
  server.registerTool(
20054
20240
  "suggest",
20055
20241
  {
@@ -21923,6 +22109,19 @@ function registerPageDiffHandlers(windowState, sendToRendererViews) {
21923
22109
  if (!wc) return null;
21924
22110
  return getLatestPageDiff(wc.getURL());
21925
22111
  });
22112
+ electron.ipcMain.handle(Channels.PAGE_DIFF_HISTORY, () => {
22113
+ try {
22114
+ if (!isPremiumActiveState(getPremiumState())) {
22115
+ return { error: "Premium required" };
22116
+ }
22117
+ const activeTab = windowState.tabManager.getActiveTab();
22118
+ const wc = activeTab?.view.webContents;
22119
+ if (!wc) return [];
22120
+ return getPageDiffBursts(wc.getURL());
22121
+ } catch {
22122
+ return [];
22123
+ }
22124
+ });
21926
22125
  electron.ipcMain.on(Channels.PAGE_DIFF_ACTIVITY, (event) => {
21927
22126
  const wc = event.sender;
21928
22127
  if (!wc || wc.isDestroyed()) return;
@@ -22283,6 +22482,20 @@ function registerIpcHandlers(windowState, runtime2) {
22283
22482
  width: windowState.uiState.sidebarWidth
22284
22483
  };
22285
22484
  });
22485
+ electron.ipcMain.handle(Channels.SIDEBAR_NAVIGATE, (_, tab) => {
22486
+ assertString(tab, "tab");
22487
+ if (!windowState.uiState.sidebarOpen) {
22488
+ windowState.uiState.sidebarOpen = true;
22489
+ layoutViews(windowState);
22490
+ }
22491
+ if (!sidebarView.webContents.isDestroyed()) {
22492
+ sidebarView.webContents.send(Channels.SIDEBAR_NAVIGATE, tab);
22493
+ }
22494
+ return {
22495
+ open: windowState.uiState.sidebarOpen,
22496
+ width: windowState.uiState.sidebarWidth
22497
+ };
22498
+ });
22286
22499
  electron.ipcMain.handle(Channels.SIDEBAR_RESIZE_START, () => {
22287
22500
  sidebarResizeActive = true;
22288
22501
  clearSidebarResizeRecoveryTimer();
@@ -22381,6 +22594,14 @@ function registerIpcHandlers(windowState, runtime2) {
22381
22594
  Channels.AGENT_CHECKPOINT_RESTORE,
22382
22595
  (_, checkpointId) => runtime2.restoreCheckpoint(checkpointId)
22383
22596
  );
22597
+ electron.ipcMain.handle(
22598
+ Channels.AGENT_CHECKPOINT_UPDATE_NOTE,
22599
+ (_, checkpointId, note) => runtime2.updateCheckpointNote(checkpointId, note || "")
22600
+ );
22601
+ electron.ipcMain.handle(
22602
+ Channels.AGENT_UNDO_LAST_ACTION,
22603
+ () => runtime2.undoLastAction()
22604
+ );
22384
22605
  electron.ipcMain.handle(
22385
22606
  Channels.AGENT_SESSION_CAPTURE,
22386
22607
  (_, note) => runtime2.captureSession(note)
@@ -22420,6 +22641,13 @@ function registerIpcHandlers(windowState, runtime2) {
22420
22641
  return result.bookmark;
22421
22642
  }
22422
22643
  );
22644
+ electron.ipcMain.handle(
22645
+ Channels.BOOKMARK_UPDATE,
22646
+ (_, id, updates) => {
22647
+ trackBookmarkAction("save");
22648
+ return updateBookmark(id, updates);
22649
+ }
22650
+ );
22423
22651
  electron.ipcMain.handle(Channels.BOOKMARK_REMOVE, (_, id) => {
22424
22652
  trackBookmarkAction("remove");
22425
22653
  return removeBookmark(id);
@@ -22709,6 +22937,41 @@ function registerIpcHandlers(windowState, runtime2) {
22709
22937
  registerAutofillHandlers(windowState);
22710
22938
  registerPageDiffHandlers(windowState, sendToRendererViews);
22711
22939
  }
22940
+ const UNDOABLE_ACTIONS = /* @__PURE__ */ new Set([
22941
+ "accept_cookies",
22942
+ "clear_overlays",
22943
+ "click",
22944
+ "close_tab",
22945
+ "create_tab",
22946
+ "dismiss_popup",
22947
+ "fill_form",
22948
+ "focus",
22949
+ "go_back",
22950
+ "go_forward",
22951
+ "load_session",
22952
+ "login",
22953
+ "navigate",
22954
+ "open_bookmark",
22955
+ "paginate",
22956
+ "press_key",
22957
+ "reload",
22958
+ "restore_checkpoint",
22959
+ "scroll",
22960
+ "scroll_to_element",
22961
+ "search",
22962
+ "select_option",
22963
+ "set_ad_blocking",
22964
+ "submit_form",
22965
+ "switch_tab",
22966
+ "type_text"
22967
+ ]);
22968
+ function isUndoableAction(name) {
22969
+ return UNDOABLE_ACTIONS.has(name);
22970
+ }
22971
+ function isUndoableResult(result) {
22972
+ const normalized = result.trim().toLowerCase();
22973
+ return normalized.length > 0 && !normalized.startsWith("error:") && !normalized.startsWith("nothing to ") && !normalized.startsWith("no active ") && !normalized.startsWith("action rejected:");
22974
+ }
22712
22975
  function makeStep(label, status = "pending") {
22713
22976
  return { label, status };
22714
22977
  }
@@ -23105,6 +23368,7 @@ class AgentRuntime {
23105
23368
  state;
23106
23369
  updateListener = null;
23107
23370
  pendingResolvers = /* @__PURE__ */ new Map();
23371
+ undoSnapshots = [];
23108
23372
  setUpdateListener(listener) {
23109
23373
  this.updateListener = listener;
23110
23374
  if (listener) {
@@ -23114,6 +23378,8 @@ class AgentRuntime {
23114
23378
  getState() {
23115
23379
  const snapshot = clone(this.state);
23116
23380
  snapshot.mcpStatus = getMcpStatus();
23381
+ snapshot.canUndo = this.canUndo();
23382
+ snapshot.undoInfo = this.getUndoInfo();
23117
23383
  return snapshot;
23118
23384
  }
23119
23385
  onTabStateChanged() {
@@ -23156,6 +23422,37 @@ class AgentRuntime {
23156
23422
  this.captureSession(`Restored ${checkpoint.name}`);
23157
23423
  return clone(checkpoint);
23158
23424
  }
23425
+ updateCheckpointNote(checkpointId, note) {
23426
+ const index = this.state.checkpoints.findIndex((item) => item.id === checkpointId);
23427
+ if (index === -1) return null;
23428
+ this.state.checkpoints[index] = {
23429
+ ...this.state.checkpoints[index],
23430
+ note: note.trim() || void 0
23431
+ };
23432
+ this.emit();
23433
+ return clone(this.state.checkpoints[index]);
23434
+ }
23435
+ canUndo() {
23436
+ return this.undoSnapshots.length > 0;
23437
+ }
23438
+ getUndoInfo() {
23439
+ const latest = this.undoSnapshots[this.undoSnapshots.length - 1];
23440
+ if (!latest) return null;
23441
+ return { actionName: latest.actionName, capturedAt: latest.capturedAt };
23442
+ }
23443
+ undoLastAction() {
23444
+ const snapshot = this.undoSnapshots.at(-1);
23445
+ if (!snapshot) return null;
23446
+ try {
23447
+ this.tabManager.restoreSession(snapshot.snapshot);
23448
+ this.undoSnapshots.pop();
23449
+ } catch (error) {
23450
+ logger$3.error("Failed to restore undo snapshot", error);
23451
+ return null;
23452
+ }
23453
+ this.captureSession(`Undid ${snapshot.actionName}`);
23454
+ return snapshot.actionName;
23455
+ }
23159
23456
  captureSession(note) {
23160
23457
  const snapshot = this.tabManager.snapshotSession(note);
23161
23458
  this.state.session = snapshot;
@@ -23305,6 +23602,7 @@ ${progress}
23305
23602
  args = {},
23306
23603
  tabId = null,
23307
23604
  dangerous = false,
23605
+ undoable,
23308
23606
  executor
23309
23607
  }) {
23310
23608
  const action = this.startAction({
@@ -23358,8 +23656,13 @@ ${progress}
23358
23656
  streamId: transcriptStreamId,
23359
23657
  mode: "replace"
23360
23658
  });
23659
+ const shouldCaptureUndo = undoable ?? isUndoableAction(name);
23660
+ const undoSnapshot = shouldCaptureUndo ? this.createUndoSnapshot(name) : null;
23361
23661
  try {
23362
23662
  const result = await executor();
23663
+ if (undoSnapshot && isUndoableResult(result)) {
23664
+ this.pushUndoSnapshot(undoSnapshot);
23665
+ }
23363
23666
  this.finishAction(action.id, "completed", summarizeText(result));
23364
23667
  this.publishTranscript({
23365
23668
  source,
@@ -23386,6 +23689,21 @@ ${progress}
23386
23689
  throw error;
23387
23690
  }
23388
23691
  }
23692
+ createUndoSnapshot(name) {
23693
+ return {
23694
+ id: crypto$2.randomUUID(),
23695
+ actionName: name,
23696
+ snapshot: this.tabManager.snapshotSession(
23697
+ `Auto-checkpoint before ${name}`
23698
+ ),
23699
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString()
23700
+ };
23701
+ }
23702
+ pushUndoSnapshot(snapshot) {
23703
+ this.undoSnapshots = [...this.undoSnapshots, snapshot].slice(
23704
+ -10
23705
+ );
23706
+ }
23389
23707
  resolveApproval(approvalId, approved) {
23390
23708
  const approval = this.state.supervisor.pendingApprovals.find(
23391
23709
  (item) => item.id === approvalId