surf-cli 2.8.0 → 2.9.0

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.
Files changed (42) hide show
  1. package/README.md +98 -4
  2. package/dist/content/index.js +116 -0
  3. package/dist/content/index.js.map +1 -0
  4. package/dist/manifest.json +2 -11
  5. package/dist/options/options.js +3 -3
  6. package/dist/options/options.js.map +1 -1
  7. package/dist/service-worker/index.js +261 -61
  8. package/dist/service-worker/index.js.map +1 -1
  9. package/native/abort.cjs +65 -0
  10. package/native/ai-queue.cjs +64 -0
  11. package/native/aistudio-build.cjs +21 -13
  12. package/native/aistudio-client.cjs +40 -20
  13. package/native/browser-lock.cjs +2 -2
  14. package/native/chatgpt-client.cjs +47 -31
  15. package/native/cli.cjs +300 -204
  16. package/native/client-transport.cjs +168 -0
  17. package/native/do-executor.cjs +25 -44
  18. package/native/doctor.cjs +55 -5
  19. package/native/endpoint.cjs +174 -0
  20. package/native/file-transfer.cjs +734 -0
  21. package/native/gemini-client.cjs +156 -71
  22. package/native/grok-client.cjs +98 -89
  23. package/native/host-helpers.cjs +37 -12
  24. package/native/host-sessions.cjs +283 -0
  25. package/native/host.cjs +800 -620
  26. package/native/listener.cjs +20 -0
  27. package/native/mcp-server.cjs +60 -65
  28. package/native/network-export.cjs +113 -0
  29. package/native/perplexity-client.cjs +46 -17
  30. package/native/remote-auth.cjs +279 -0
  31. package/native/remote-transport.cjs +337 -0
  32. package/native/request-pending.cjs +148 -0
  33. package/native/socket-path.cjs +1 -1
  34. package/package.json +8 -6
  35. package/scripts/install-native-host.cjs +36 -5
  36. package/skills/README.md +11 -5
  37. package/skills/deep-x-research/SKILL.md +106 -0
  38. package/skills/surf/SKILL.md +31 -4
  39. package/dist/content/accessibility-tree.js +0 -11
  40. package/dist/content/accessibility-tree.js.map +0 -1
  41. package/dist/content/visual-indicator.js +0 -111
  42. package/dist/content/visual-indicator.js.map +0 -1
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  const { loadConfig, getConfigPath, clearCache } = require("./config.cjs");
9
+ const { abortableDelay, raceAbort, throwIfAborted } = require("./abort.cjs");
9
10
 
10
11
  const GROK_URL = "https://x.com/i/grok";
11
12
  const DEFAULT_MODEL = "fast";
@@ -38,8 +39,8 @@ const GROK_MODELS = DEFAULT_GROK_MODELS;
38
39
  // Helpers
39
40
  // ============================================================================
40
41
 
41
- function delay(ms) {
42
- return new Promise(resolve => setTimeout(resolve, ms));
42
+ function delay(ms, signal) {
43
+ return abortableDelay(ms, signal);
43
44
  }
44
45
 
45
46
  function buildClickDispatcher() {
@@ -132,8 +133,10 @@ function hasRequiredCookies(cookies) {
132
133
  return Boolean(authToken);
133
134
  }
134
135
 
135
- async function evaluate(cdp, expression) {
136
+ async function evaluate(cdp, expression, signal) {
137
+ throwIfAborted(signal);
136
138
  const result = await cdp(expression);
139
+ throwIfAborted(signal);
137
140
  if (result.exceptionDetails) {
138
141
  const desc = result.exceptionDetails.exception?.description ||
139
142
  result.exceptionDetails.text ||
@@ -450,53 +453,17 @@ async function submitPrompt(cdp, inputCdp) {
450
453
  // Response Handling
451
454
  // ============================================================================
452
455
 
453
- function looksLikeTrailingSuggestion(line) {
454
- if (!line || line.length < 4 || line.length > 90) return false;
455
- if (/[.!:;)]$/.test(line)) return false;
456
- const words = line.split(/\s+/).filter(Boolean);
457
- if (words.length < 2 || words.length > 9) return false;
458
- if (/^[-*•\d]/.test(line)) return false;
459
- if (/\b(https?:\/\/|www\.)\b/i.test(line)) return false;
460
- return true;
461
- }
462
-
463
- function trimTrailingSuggestionLines(lines) {
464
- let end = lines.length;
465
- while (end > 0 && looksLikeTrailingSuggestion(lines[end - 1])) {
466
- end--;
467
- }
468
-
469
- const trimmedCount = lines.length - end;
470
- if (trimmedCount >= 2 && end > 0) {
471
- return lines.slice(0, end);
472
- }
473
-
474
- const last = lines[lines.length - 1];
475
- if (
476
- trimmedCount === 1 &&
477
- lines.length > 1 &&
478
- /^(explain|tell|share|compare|derive|make|show|summari[sz]e|expand|rewrite)\b/i.test(last)
479
- ) {
480
- return lines.slice(0, -1);
481
- }
482
-
483
- const previous = lines[lines.length - 2];
484
- if (
485
- last &&
486
- previous &&
487
- /^[A-Z][a-z]{3,}$/.test(last) &&
488
- /(?:[.!?)]|^\d+(?:\.\d+)?$)/.test(previous)
489
- ) {
490
- return lines.slice(0, -1);
491
- }
492
-
493
- return lines;
494
- }
495
-
496
456
  // Extract Grok's response from the full page body text
497
- function extractGrokResponse(bodyText, userPrompt = '') {
457
+ function extractGrokResponse(bodyText, userPrompt = '', chipTexts = []) {
498
458
  if (!bodyText) return null;
499
459
 
460
+ // Suggestion chips are captured from the DOM as button texts; count occurrences.
461
+ const chipCounts = new Map();
462
+ for (const t of chipTexts || []) {
463
+ const key = String(t).trim();
464
+ if (key) chipCounts.set(key, (chipCounts.get(key) || 0) + 1);
465
+ }
466
+
500
467
  // Split into lines and filter out navigation/UI elements
501
468
  const lines = bodyText.split('\n').map(l => l.trim()).filter(l => l);
502
469
 
@@ -543,7 +510,15 @@ function extractGrokResponse(bodyText, userPrompt = '') {
543
510
  contentLines.push(line);
544
511
  }
545
512
 
546
- const responseLines = trimTrailingSuggestionLines(contentLines);
513
+ // Chips follow the answer; strip trailing chip lines but never the first line.
514
+ let contentEnd = contentLines.length;
515
+ while (contentEnd > 1) {
516
+ const remaining = chipCounts.get(contentLines[contentEnd - 1]) || 0;
517
+ if (remaining <= 0) break;
518
+ chipCounts.set(contentLines[contentEnd - 1], remaining - 1);
519
+ contentEnd--;
520
+ }
521
+ const responseLines = contentLines.slice(0, contentEnd);
547
522
 
548
523
  // If we found content after the question, return the response
549
524
  if (responseLines.length > 0) {
@@ -561,7 +536,8 @@ function extractGrokResponse(bodyText, userPrompt = '') {
561
536
  return null;
562
537
  }
563
538
 
564
- async function waitForResponse(cdp, timeoutMs = 300000, userPrompt = '') {
539
+ async function waitForResponse(cdp, timeoutMs = 300000, userPrompt = '', signal) {
540
+ throwIfAborted(signal);
565
541
  // Grok can take a long time:
566
542
  // - Thinking models: 40-60+ seconds to think, then streams
567
543
  // - Fast/Auto models: No thinking phase, just streams directly
@@ -569,6 +545,7 @@ async function waitForResponse(cdp, timeoutMs = 300000, userPrompt = '') {
569
545
  const deadline = Date.now() + timeoutMs;
570
546
  let previousText = '';
571
547
  let previousLength = 0;
548
+ let lastChipTexts = [];
572
549
  let lastChangeAt = Date.now();
573
550
  let thinkingTime = null;
574
551
  let thinkingComplete = false;
@@ -583,44 +560,55 @@ async function waitForResponse(cdp, timeoutMs = 300000, userPrompt = '') {
583
560
  // Check for stop/cancel button (indicates still generating)
584
561
  const hasStopBtn = !!document.querySelector('button[aria-label*="Stop"], button[aria-label*="stop"], button[aria-label*="Cancel"]');
585
562
 
586
- // Check for "Thought for Xs" which indicates thinking model completed thinking
587
- const thinkMatch = bodyText.match(/Thought for (\\d+)s/i);
588
- const thinkingDone = !!thinkMatch;
589
- const thinkingSecs = thinkMatch ? parseInt(thinkMatch[1], 10) : null;
590
-
591
- // Check if actively showing "thinking..." or similar loading state
592
- const isThinking = /\\bthinking\\.\\.\\./i.test(bodyText) ||
593
- /\\bSearching\\.\\.\\./i.test(bodyText) ||
594
- bodyText.includes('Grok is thinking') ||
595
- bodyText.includes('is thinking...');
596
-
597
563
  // Try to find the actual Grok response in the DOM
598
564
  // Look for the main content area - Grok responses appear in the conversation area
599
565
  let responseText = '';
566
+ let responseRoot = null;
567
+ let preciseRoot = false;
600
568
 
601
569
  // Strategy 1: Look for article elements or main content containers
602
570
  const articles = document.querySelectorAll('article');
603
571
  if (articles.length > 0) {
604
572
  // Get the last article which should be the response
605
- const lastArticle = articles[articles.length - 1];
606
- responseText = lastArticle.innerText || '';
573
+ responseRoot = articles[articles.length - 1];
574
+ responseText = responseRoot.innerText || '';
575
+ preciseRoot = true;
607
576
  }
608
577
 
609
578
  // Strategy 2: If no articles, look for the conversation container
610
579
  if (!responseText) {
611
580
  const convArea = document.querySelector('[data-testid="conversation"], [role="main"] > div > div');
612
581
  if (convArea) {
582
+ responseRoot = convArea;
613
583
  responseText = convArea.innerText || '';
584
+ preciseRoot = true;
614
585
  }
615
586
  }
616
587
 
617
588
  // Strategy 3: Fallback to looking for text after common Grok UI patterns
618
589
  if (!responseText || responseText.length < 10) {
619
590
  // Find content between user question and follow-up suggestions
620
- const mainArea = document.querySelector('main') || document.body;
621
- responseText = mainArea.innerText || bodyText;
591
+ responseRoot = document.querySelector('main') || document.body;
592
+ responseText = responseRoot.innerText || bodyText;
593
+ preciseRoot = false;
622
594
  }
623
595
 
596
+ // Completion state must come from the current response container. Page-wide markers can belong to an earlier turn and would make a new short response finish prematurely.
597
+ const currentStateText = preciseRoot && responseRoot ? (responseRoot.innerText || '') : '';
598
+ const thinkMatch = currentStateText.match(/Thought for (\\d+)s/i);
599
+ const thinkingDone = !!thinkMatch;
600
+ const thinkingSecs = thinkMatch ? parseInt(thinkMatch[1], 10) : null;
601
+ const isThinking = /\\bthinking\\.\\.\\./i.test(currentStateText) ||
602
+ /\\bSearching\\.\\.\\./i.test(currentStateText) ||
603
+ currentStateText.includes('Grok is thinking') ||
604
+ currentStateText.includes('is thinking...');
605
+
606
+ // Suggestion chips are the no-testid/no-aria-label buttons inside the response container. Only collect them from a precise container: a broad main/body fallback holds unrelated buttons that could erase a matching answer line.
607
+ const chipTexts = (preciseRoot && responseRoot ? Array.from(responseRoot.querySelectorAll('button')) : [])
608
+ .filter(function(b){ return !b.getAttribute('data-testid') && !b.getAttribute('aria-label'); })
609
+ .map(function(b){ return (b.innerText || '').trim(); })
610
+ .filter(function(t){ return t && !t.includes('\\n') && t.length <= 120; });
611
+
624
612
  return {
625
613
  bodyText: bodyText,
626
614
  responseText: responseText,
@@ -629,12 +617,13 @@ async function waitForResponse(cdp, timeoutMs = 300000, userPrompt = '') {
629
617
  thinkingDone: thinkingDone,
630
618
  thinkingSecs: thinkingSecs,
631
619
  isThinking: isThinking,
620
+ chipTexts: chipTexts,
632
621
  url: location.href
633
622
  };
634
623
  })()`);
635
624
 
636
625
  if (!snapshot || !snapshot.bodyText) {
637
- await delay(300);
626
+ await delay(300, signal);
638
627
  continue;
639
628
  }
640
629
 
@@ -653,16 +642,18 @@ async function waitForResponse(cdp, timeoutMs = 300000, userPrompt = '') {
653
642
  if (snapshot.thinkingDone && !thinkingComplete) {
654
643
  thinkingComplete = true;
655
644
  // Give a brief moment for final render, then we're done
656
- await delay(500);
645
+ await delay(500, signal);
657
646
  }
658
647
 
659
648
  // Extract the actual response text - try DOM-extracted first, fall back to body parsing
649
+ const chipTexts = snapshot.chipTexts || [];
650
+ lastChipTexts = chipTexts;
660
651
  let currentResponseText = '';
661
652
  if (snapshot.responseText && snapshot.responseText.length > 10) {
662
- currentResponseText = extractGrokResponse(snapshot.responseText, userPrompt) || '';
653
+ currentResponseText = extractGrokResponse(snapshot.responseText, userPrompt, chipTexts) || '';
663
654
  }
664
655
  if (!currentResponseText || currentResponseText.length < 5) {
665
- currentResponseText = extractGrokResponse(bodyText, userPrompt) || '';
656
+ currentResponseText = extractGrokResponse(bodyText, userPrompt, chipTexts) || '';
666
657
  }
667
658
 
668
659
  // Track RESPONSE text stability (more reliable than body text)
@@ -681,24 +672,24 @@ async function waitForResponse(cdp, timeoutMs = 300000, userPrompt = '') {
681
672
  }
682
673
 
683
674
  const stableMs = Date.now() - lastChangeAt;
684
- const noStopButton = !snapshot.hasStopBtn;
675
+ const noStopButton = !snapshot.hasStopBtn && !snapshot.isThinking;
685
676
 
686
677
  // Response is stable if the extracted response text hasn't changed
687
678
  // Use shorter thresholds since we're checking actual content, not noisy body text
688
679
  // 4 cycles (1.2s) + 1.5s minimum is enough for response stability
689
- const responseIsStable = responseStableCycles >= 4 && stableMs >= 1500 && currentResponseText.length > 10;
680
+ const responseIsStable = responseStableCycles >= 4 && stableMs >= 1500 && currentResponseText.trim().length > 0;
690
681
 
691
682
  // "Thought for Xs" is the strongest completion signal - response is definitely done
692
683
  const thinkingModelDone = snapshot.thinkingDone && noStopButton;
693
684
 
694
685
  // SIMPLE CHECK: If we have response content, no stop button, and stable for 3+ cycles
695
- const hasResponseNoStop = currentResponseText.length > 5 && noStopButton && responseStableCycles >= 3;
686
+ const hasResponseNoStop = currentResponseText.trim().length > 0 && noStopButton && responseStableCycles >= 3;
696
687
 
697
688
  // Response is complete when:
698
- // 1. Has meaningful response content (> 5 chars)
689
+ // 1. Has any non-empty extracted text
699
690
  // 2. No stop button
700
691
  // 3. Either: thinking done, response stable for 3+ cycles, OR stable for 4+ cycles with 1.5s
701
- const isDone = currentResponseText.length > 5 && noStopButton &&
692
+ const isDone = currentResponseText.trim().length > 0 && noStopButton &&
702
693
  (thinkingModelDone || hasResponseNoStop || responseIsStable);
703
694
 
704
695
  if (isDone) {
@@ -709,12 +700,12 @@ async function waitForResponse(cdp, timeoutMs = 300000, userPrompt = '') {
709
700
  };
710
701
  }
711
702
 
712
- await delay(300);
703
+ await delay(300, signal);
713
704
  }
714
705
 
715
706
  // Timeout - return whatever we have (partial response is better than nothing)
716
- const finalText = extractGrokResponse(previousText, userPrompt);
717
- if (finalText && finalText.length > 10) {
707
+ const finalText = extractGrokResponse(previousText, userPrompt, lastChipTexts);
708
+ if (finalText && finalText.trim().length > 0) {
718
709
  return {
719
710
  text: finalText,
720
711
  thinkingTime: thinkingTime,
@@ -741,20 +732,22 @@ async function query(options) {
741
732
  cdpEvaluate,
742
733
  cdpCommand,
743
734
  log = () => {},
735
+ signal,
744
736
  } = options;
737
+ throwIfAborted(signal);
745
738
 
746
739
  const startTime = Date.now();
747
740
  log("Starting Grok query");
748
741
 
749
742
  // Check cookies for X.com authentication
750
- const { cookies } = await getCookies();
743
+ const { cookies } = await raceAbort(getCookies, signal);
751
744
  if (!hasRequiredCookies(cookies)) {
752
745
  throw new Error("X.com login required - log in to x.com in Chrome first");
753
746
  }
754
747
  log(`Got ${cookies.length} cookies`);
755
748
 
756
749
  // Create tab
757
- const tabInfo = await createTab();
750
+ const tabInfo = await raceAbort(createTab, signal);
758
751
  const { tabId } = tabInfo || {};
759
752
 
760
753
  if (!tabId) {
@@ -762,8 +755,8 @@ async function query(options) {
762
755
  }
763
756
  log(`Created tab ${tabId}`);
764
757
 
765
- const cdp = (expr) => cdpEvaluate(tabId, expr);
766
- const inputCdp = (method, params) => cdpCommand(tabId, method, params);
758
+ const cdp = (expr) => raceAbort(() => cdpEvaluate(tabId, expr), signal);
759
+ const inputCdp = (method, params) => raceAbort(() => cdpCommand(tabId, method, params), signal);
767
760
 
768
761
  try {
769
762
  // Wait for page load
@@ -801,6 +794,7 @@ async function query(options) {
801
794
  warnings.push(`Requested model "${targetModel}" but got "${selectedModel}" - model may not be available`);
802
795
  }
803
796
  } catch (e) {
797
+ if (signal?.aborted) throw e;
804
798
  modelSelectionFailed = true;
805
799
  warnings.push(`Model selection failed: ${e.message}. Run 'surf grok --validate' to check available models.`);
806
800
  log(`Model selection failed: ${e.message}`);
@@ -818,6 +812,7 @@ async function query(options) {
818
812
  warnings.push(`DeepSearch toggle not found - feature may require X Premium or UI changed`);
819
813
  }
820
814
  } catch (e) {
815
+ if (signal?.aborted) throw e;
821
816
  warnings.push(`DeepSearch toggle failed: ${e.message}`);
822
817
  log(`DeepSearch toggle failed: ${e.message}`);
823
818
  }
@@ -832,7 +827,7 @@ async function query(options) {
832
827
  log("Submitted, waiting for response...");
833
828
 
834
829
  // Wait for response
835
- const response = await waitForResponse(cdp, timeout, prompt);
830
+ const response = await waitForResponse(cdp, timeout, prompt, signal);
836
831
  const thinkingInfo = response.thinkingTime ? ` (thought for ${response.thinkingTime}s)` : '';
837
832
  log(`Response: ${response.text.length} chars${thinkingInfo}${response.partial ? ' (partial)' : ''}`);
838
833
 
@@ -850,7 +845,11 @@ async function query(options) {
850
845
  tookMs: Date.now() - startTime,
851
846
  };
852
847
  } finally {
853
- await closeTab(tabId).catch(() => {});
848
+ try {
849
+ await closeTab(tabId);
850
+ } catch (error) {
851
+ log(`Failed to close Grok tab ${tabId}: ${error?.message || error}`);
852
+ }
854
853
  }
855
854
  }
856
855
 
@@ -865,7 +864,9 @@ async function validate(options) {
865
864
  closeTab,
866
865
  cdpEvaluate,
867
866
  log = () => {},
867
+ signal,
868
868
  } = options;
869
+ throwIfAborted(signal);
869
870
 
870
871
  const startTime = Date.now();
871
872
  log("Starting Grok validation");
@@ -884,7 +885,7 @@ async function validate(options) {
884
885
 
885
886
  // Check cookies
886
887
  try {
887
- const { cookies } = await getCookies();
888
+ const { cookies } = await raceAbort(getCookies, signal);
888
889
  result.authenticated = hasRequiredCookies(cookies);
889
890
  if (!result.authenticated) {
890
891
  result.errors.push("Not authenticated - log in to x.com in Chrome first");
@@ -892,6 +893,7 @@ async function validate(options) {
892
893
  }
893
894
  log("Cookies OK");
894
895
  } catch (e) {
896
+ if (signal?.aborted) throw e;
895
897
  result.errors.push(`Cookie check failed: ${e.message}`);
896
898
  return { ...result, tookMs: Date.now() - startTime };
897
899
  }
@@ -899,7 +901,7 @@ async function validate(options) {
899
901
  // Create tab
900
902
  let tabId;
901
903
  try {
902
- const tabInfo = await createTab();
904
+ const tabInfo = await raceAbort(createTab, signal);
903
905
  tabId = tabInfo?.tabId;
904
906
  if (!tabId) {
905
907
  result.errors.push("Failed to create tab");
@@ -907,15 +909,16 @@ async function validate(options) {
907
909
  }
908
910
  log(`Created tab ${tabId}`);
909
911
  } catch (e) {
912
+ if (signal?.aborted) throw e;
910
913
  result.errors.push(`Tab creation failed: ${e.message}`);
911
914
  return { ...result, tookMs: Date.now() - startTime };
912
915
  }
913
916
 
914
- const cdp = (expr) => cdpEvaluate(tabId, expr);
917
+ const cdp = (expr) => raceAbort(() => cdpEvaluate(tabId, expr), signal);
915
918
 
916
919
  try {
917
920
  // Wait for page load
918
- await waitForPageLoad(cdp);
921
+ await raceAbort(waitForPageLoad(cdp), signal);
919
922
  log("Page loaded");
920
923
 
921
924
  // Check login status
@@ -930,7 +933,7 @@ async function validate(options) {
930
933
  log(`Login: yes${result.premium ? ' (Premium)' : ''}`);
931
934
 
932
935
  // Wait for Grok UI
933
- await waitForGrokReady(cdp);
936
+ await raceAbort(waitForGrokReady(cdp), signal);
934
937
  log("Grok ready");
935
938
 
936
939
  // Check for input field
@@ -967,7 +970,7 @@ async function validate(options) {
967
970
  })()`);
968
971
 
969
972
  if (modelButtonClicked?.success) {
970
- await delay(500);
973
+ await abortableDelay(500, signal);
971
974
 
972
975
  // Scrape model options
973
976
  const modelScrape = await evaluate(cdp, `(() => {
@@ -1013,9 +1016,14 @@ async function validate(options) {
1013
1016
  }
1014
1017
 
1015
1018
  } catch (e) {
1019
+ if (signal?.aborted) throw e;
1016
1020
  result.errors.push(`Validation error: ${e.message}`);
1017
1021
  } finally {
1018
- await closeTab(tabId).catch(() => {});
1022
+ try {
1023
+ await closeTab(tabId);
1024
+ } catch (error) {
1025
+ log(`Failed to close Grok validation tab ${tabId}: ${error?.message || error}`);
1026
+ }
1019
1027
  }
1020
1028
 
1021
1029
  result.tookMs = Date.now() - startTime;
@@ -1067,6 +1075,7 @@ module.exports = {
1067
1075
  normalizeGrokModelLabel,
1068
1076
  getGrokModelMatchLabels,
1069
1077
  grokModelLabelsMatch,
1078
+ waitForResponse,
1070
1079
  GROK_URL,
1071
1080
  GROK_MODELS,
1072
1081
  DEFAULT_GROK_MODELS,
@@ -20,7 +20,7 @@ function normalizeModelString(model) {
20
20
  * @param {Function} log - Logging function (defaults to no-op for testing)
21
21
  * @returns {Array} Array of content objects with type and text/data
22
22
  */
23
- function formatToolContent(result, log = () => {}) {
23
+ function formatToolContent(result, log = () => {}, options = {}) {
24
24
  const text = (s) => [{ type: "text", text: s }];
25
25
 
26
26
  if (!result) return text("OK");
@@ -390,6 +390,7 @@ function formatToolContent(result, log = () => {}) {
390
390
 
391
391
  if (result.autoScreenshot) {
392
392
  const { path: ssPath, width, height } = result.autoScreenshot;
393
+ if (options.suppressImages) return text(`OK\nScreenshot saved: ${ssPath}`);
393
394
  try {
394
395
  const imgData = fs.readFileSync(ssPath);
395
396
  const base64 = imgData.toString("base64");
@@ -471,11 +472,16 @@ function mapComputerAction(args, tabId) {
471
472
  if (ref) return { type: "CLICK_REF", ref, button: "triple", ...baseMsg };
472
473
  return { type: "EXECUTE_TRIPLE_CLICK", x: coordinate?.[0], y: coordinate?.[1], modifiers, ...baseMsg };
473
474
 
474
- case "type":
475
+ case "type": {
475
476
  if (ref) {
476
477
  return { type: "FORM_FILL", data: [{ ref, value: text }], ...baseMsg };
477
478
  }
479
+ const typeSelector = a.selector || a.into;
480
+ if (typeSelector) {
481
+ return { type: "SMART_TYPE", selector: typeSelector, text, clear: a.clear ?? true, submit: a.submit ?? false, ...baseMsg };
482
+ }
478
483
  return { type: "EXECUTE_TYPE", text, ...baseMsg };
484
+ }
479
485
 
480
486
  case "key": {
481
487
  const keyValue = a.key || text;
@@ -726,7 +732,6 @@ function mapToolToMessage(tool, args, tabId) {
726
732
  type: "EXPORT_NETWORK_REQUESTS",
727
733
  har: a.har,
728
734
  jsonl: a.jsonl,
729
- output: a.output,
730
735
  ...baseMsg
731
736
  };
732
737
 
@@ -798,6 +803,12 @@ function mapToolToMessage(tool, args, tabId) {
798
803
  }
799
804
  return { type: "CLOSE_TAB", tabId: id, tabIds: ids };
800
805
  }
806
+ case "tab.move": {
807
+ const id = a.id || a.tab_id || a.tabId;
808
+ const ids = a.ids || a.tab_ids || a.tabIds;
809
+ const windowId = a["to-window"] || a.toWindow || a.window_id || a.windowId;
810
+ return { type: "TAB_MOVE", tabId: id, tabIds: ids, windowId, index: a.index };
811
+ }
801
812
  case "tab.name":
802
813
  return { type: "TABS_REGISTER", name: a.name, ...baseMsg };
803
814
  case "tab.unname":
@@ -890,18 +901,32 @@ function mapToolToMessage(tool, args, tabId) {
890
901
  case "upload":
891
902
  const files = a.files ? (typeof a.files === "string" ? a.files.split(",").map(f => f.trim()) : a.files) : [];
892
903
  return { type: "UPLOAD_FILE", ref: a.ref, files, ...baseMsg };
893
- case "page.read":
894
- return {
895
- type: "READ_PAGE",
896
- options: {
897
- filter: a.filter || "interactive",
898
- refId: a.ref,
904
+ case "page.read": {
905
+ let maxBytes;
906
+ if (a["max-bytes"] !== undefined) {
907
+ const raw = String(a["max-bytes"]).trim();
908
+ if (!/^\d+$/.test(raw) || raw === "0") {
909
+ throw new Error("max-bytes must be a positive integer");
910
+ }
911
+ maxBytes = parseInt(raw, 10);
912
+ if (!Number.isFinite(maxBytes) || maxBytes <= 0) {
913
+ throw new Error("max-bytes must be a positive integer");
914
+ }
915
+ }
916
+ return {
917
+ type: "READ_PAGE",
918
+ options: {
919
+ filter: a.filter || "interactive",
920
+ refId: a.ref,
899
921
  includeText: a["no-text"] !== true,
900
922
  depth: a.depth !== undefined ? parseInt(a.depth, 10) : undefined,
901
923
  compact: a.compact || false,
902
- },
903
- ...baseMsg
924
+ maxBytes,
925
+ forceFullSnapshot: a.compact === true || maxBytes !== undefined,
926
+ },
927
+ ...baseMsg
904
928
  };
929
+ }
905
930
  case "page.text":
906
931
  return { type: "GET_PAGE_TEXT", ...baseMsg };
907
932
  case "page.state":
@@ -1081,7 +1106,7 @@ function mapToolToMessage(tool, args, tabId) {
1081
1106
  return {
1082
1107
  type: "GEMINI_QUERY",
1083
1108
  query: a.query,
1084
- model: a.model || "gemini-3-pro",
1109
+ model: a.model || "gemini-3.1-pro",
1085
1110
  withPage: a["with-page"],
1086
1111
  file: a.file,
1087
1112
  generateImage: a["generate-image"],