@quanta-intellect/vessel-browser 0.1.10 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/out/main/index.js +1273 -458
- package/out/preload/content-script.js +76 -8
- package/out/renderer/assets/{index-CCVxW0YM.js → index-BUYEjb3N.js} +245 -101
- package/out/renderer/assets/{index-Cud0VqFQ.css → index-DOCQcMR5.css} +172 -0
- package/out/renderer/index.html +2 -2
- package/package.json +1 -1
package/out/main/index.js
CHANGED
|
@@ -6,13 +6,13 @@ const fs = require("fs");
|
|
|
6
6
|
const crypto = require("crypto");
|
|
7
7
|
const Anthropic = require("@anthropic-ai/sdk");
|
|
8
8
|
const OpenAI = require("openai");
|
|
9
|
+
const zod = require("zod");
|
|
9
10
|
const path$1 = require("node:path");
|
|
10
11
|
const node_crypto = require("node:crypto");
|
|
11
12
|
const http = require("node:http");
|
|
12
13
|
const os = require("node:os");
|
|
13
14
|
const mcp_js = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
14
15
|
const streamableHttp_js = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
15
|
-
const zod = require("zod");
|
|
16
16
|
const MAX_CUSTOM_HISTORY = 50;
|
|
17
17
|
class Tab {
|
|
18
18
|
id;
|
|
@@ -62,6 +62,26 @@ class Tab {
|
|
|
62
62
|
isReaderMode: false,
|
|
63
63
|
adBlockingEnabled: options?.adBlockingEnabled ?? true
|
|
64
64
|
};
|
|
65
|
+
this.view.webContents.on("before-input-event", (event, input) => {
|
|
66
|
+
if (!input.control && !input.meta) return;
|
|
67
|
+
const key = input.key.toLowerCase();
|
|
68
|
+
const wc = this.view.webContents;
|
|
69
|
+
if (input.type === "keyDown") {
|
|
70
|
+
if (key === "c") {
|
|
71
|
+
wc.copy();
|
|
72
|
+
event.preventDefault();
|
|
73
|
+
} else if (key === "v") {
|
|
74
|
+
wc.paste();
|
|
75
|
+
event.preventDefault();
|
|
76
|
+
} else if (key === "x") {
|
|
77
|
+
wc.cut();
|
|
78
|
+
event.preventDefault();
|
|
79
|
+
} else if (key === "a") {
|
|
80
|
+
wc.selectAll();
|
|
81
|
+
event.preventDefault();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
});
|
|
65
85
|
this.setupListeners();
|
|
66
86
|
if (url) {
|
|
67
87
|
this.lastCommittedUrl = url;
|
|
@@ -527,10 +547,12 @@ function resolveColor(color) {
|
|
|
527
547
|
}
|
|
528
548
|
const VESSEL_HIGHLIGHT_CSS = `
|
|
529
549
|
.__vessel-highlight {
|
|
530
|
-
|
|
531
|
-
outline
|
|
532
|
-
|
|
533
|
-
|
|
550
|
+
background: rgba(240, 198, 54, 0.3) !important;
|
|
551
|
+
outline: 2px solid rgba(240, 198, 54, 0.6) !important;
|
|
552
|
+
outline-offset: 1px !important;
|
|
553
|
+
border-radius: 2px !important;
|
|
554
|
+
box-shadow: 0 0 8px rgba(240, 198, 54, 0.3) !important;
|
|
555
|
+
transition: background 0.3s, outline-color 0.3s, box-shadow 0.3s;
|
|
534
556
|
}
|
|
535
557
|
.__vessel-highlight-text {
|
|
536
558
|
background: rgba(240, 198, 54, 0.3) !important;
|
|
@@ -553,6 +575,11 @@ const VESSEL_HIGHLIGHT_CSS = `
|
|
|
553
575
|
line-height: 1.3;
|
|
554
576
|
overflow-wrap: break-word;
|
|
555
577
|
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
|
|
578
|
+
opacity: 0;
|
|
579
|
+
transition: opacity 0.15s ease-in-out;
|
|
580
|
+
}
|
|
581
|
+
.__vessel-highlight-label.visible {
|
|
582
|
+
opacity: 1;
|
|
556
583
|
}
|
|
557
584
|
`;
|
|
558
585
|
async function highlightOnPage(wc, resolvedSelector, text, label, durationMs, color) {
|
|
@@ -583,14 +610,14 @@ async function highlightOnPage(wc, resolvedSelector, text, label, durationMs, co
|
|
|
583
610
|
if (!label) return null;
|
|
584
611
|
var anchor = label.__vesselAnchor;
|
|
585
612
|
if (!anchor || !anchor.isConnected) {
|
|
586
|
-
label.
|
|
613
|
+
label.classList.remove('visible');
|
|
587
614
|
return null;
|
|
588
615
|
}
|
|
589
616
|
var rect = anchor.getBoundingClientRect();
|
|
590
617
|
var viewportWidth = window.innerWidth || document.documentElement.clientWidth || 0;
|
|
591
618
|
var viewportHeight = window.innerHeight || document.documentElement.clientHeight || 0;
|
|
592
619
|
if (!viewportWidth || !viewportHeight || rect.width === 0 && rect.height === 0) {
|
|
593
|
-
label.
|
|
620
|
+
label.classList.remove('visible');
|
|
594
621
|
return null;
|
|
595
622
|
}
|
|
596
623
|
var margin = 8;
|
|
@@ -606,7 +633,7 @@ async function highlightOnPage(wc, resolvedSelector, text, label, durationMs, co
|
|
|
606
633
|
var visible = rect.bottom >= 0 && rect.top <= viewportHeight && rect.right >= 0 && rect.left <= viewportWidth;
|
|
607
634
|
label.style.top = top + 'px';
|
|
608
635
|
label.style.left = left + 'px';
|
|
609
|
-
label.
|
|
636
|
+
if (!visible) label.classList.remove('visible');
|
|
610
637
|
return {
|
|
611
638
|
left: left,
|
|
612
639
|
top: top,
|
|
@@ -655,9 +682,14 @@ async function highlightOnPage(wc, resolvedSelector, text, label, durationMs, co
|
|
|
655
682
|
(function() {
|
|
656
683
|
var el = document.querySelector(${JSON.stringify(resolvedSelector)});
|
|
657
684
|
if (!el) return 'Element not found';
|
|
685
|
+
// Remove any existing badge on this element to avoid duplicates
|
|
686
|
+
document.querySelectorAll('.__vessel-highlight-label[data-vessel-highlight]').forEach(function(b) {
|
|
687
|
+
if (b.__vesselAnchor === el) b.remove();
|
|
688
|
+
});
|
|
658
689
|
el.classList.add('__vessel-highlight');
|
|
690
|
+
el.style.setProperty('background', ${JSON.stringify(c.bg)}, 'important');
|
|
659
691
|
el.style.setProperty('outline-color', ${JSON.stringify(c.solid)}, 'important');
|
|
660
|
-
el.style.setProperty('box-shadow', '0 0
|
|
692
|
+
el.style.setProperty('box-shadow', '0 0 8px ' + ${JSON.stringify(c.glow)}, 'important');
|
|
661
693
|
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
662
694
|
var label = ${JSON.stringify(label || "")};
|
|
663
695
|
var badge = null;
|
|
@@ -671,6 +703,8 @@ async function highlightOnPage(wc, resolvedSelector, text, label, durationMs, co
|
|
|
671
703
|
badge.__vesselAnchor = el;
|
|
672
704
|
document.body.appendChild(badge);
|
|
673
705
|
window.__vesselHighlightLabelManager.positionAll();
|
|
706
|
+
el.addEventListener('mouseenter', function() { badge.classList.add('visible'); });
|
|
707
|
+
el.addEventListener('mouseleave', function() { badge.classList.remove('visible'); });
|
|
674
708
|
}
|
|
675
709
|
var duration = ${durationMs ?? 0};
|
|
676
710
|
if (duration > 0) {
|
|
@@ -691,6 +725,10 @@ async function highlightOnPage(wc, resolvedSelector, text, label, durationMs, co
|
|
|
691
725
|
var bgColor = ${JSON.stringify(c.bg)};
|
|
692
726
|
var labelBg = ${JSON.stringify(c.label)};
|
|
693
727
|
var labelText = ${JSON.stringify(c.text)};
|
|
728
|
+
// Remove any existing badges whose text matches to avoid duplicates
|
|
729
|
+
document.querySelectorAll('.__vessel-highlight-label[data-vessel-highlight]').forEach(function(b) {
|
|
730
|
+
if (b.textContent === ${JSON.stringify(label || "")}) b.remove();
|
|
731
|
+
});
|
|
694
732
|
var SKIP_TAGS = {SCRIPT:1,STYLE:1,NOSCRIPT:1,TEMPLATE:1,IFRAME:1,SVG:1};
|
|
695
733
|
// Collect matching text nodes first, then wrap — avoids TreeWalker
|
|
696
734
|
// seeing newly created nodes from surroundContents and re-matching.
|
|
@@ -747,6 +785,11 @@ async function highlightOnPage(wc, resolvedSelector, text, label, durationMs, co
|
|
|
747
785
|
badge.__vesselAnchor = firstMark;
|
|
748
786
|
document.body.appendChild(badge);
|
|
749
787
|
window.__vesselHighlightLabelManager.positionAll();
|
|
788
|
+
var marks = document.querySelectorAll('mark.__vessel-highlight-text[data-vessel-highlight]');
|
|
789
|
+
marks.forEach(function(m) {
|
|
790
|
+
m.addEventListener('mouseenter', function() { if (badge) { badge.__vesselAnchor = m; window.__vesselHighlightLabelManager.positionAll(); badge.classList.add('visible'); } });
|
|
791
|
+
m.addEventListener('mouseleave', function() { if (badge) badge.classList.remove('visible'); });
|
|
792
|
+
});
|
|
750
793
|
}
|
|
751
794
|
var duration = ${durationMs ?? 0};
|
|
752
795
|
if (duration > 0) {
|
|
@@ -1836,7 +1879,8 @@ const defaults = {
|
|
|
1836
1879
|
obsidianVaultPath: "",
|
|
1837
1880
|
approvalMode: "confirm-dangerous",
|
|
1838
1881
|
agentTranscriptMode: "summary",
|
|
1839
|
-
chatProvider: null
|
|
1882
|
+
chatProvider: null,
|
|
1883
|
+
maxToolIterations: 200
|
|
1840
1884
|
};
|
|
1841
1885
|
let settings = null;
|
|
1842
1886
|
let settingsIssues = [];
|
|
@@ -1902,6 +1946,28 @@ function setSetting(key, value) {
|
|
|
1902
1946
|
saveSettings();
|
|
1903
1947
|
return { ...settings };
|
|
1904
1948
|
}
|
|
1949
|
+
function enableClipboardShortcuts(view) {
|
|
1950
|
+
view.webContents.on("before-input-event", (event, input) => {
|
|
1951
|
+
if (!input.control && !input.meta) return;
|
|
1952
|
+
const key = input.key.toLowerCase();
|
|
1953
|
+
const wc = view.webContents;
|
|
1954
|
+
if (input.type === "keyDown") {
|
|
1955
|
+
if (key === "c") {
|
|
1956
|
+
wc.copy();
|
|
1957
|
+
event.preventDefault();
|
|
1958
|
+
} else if (key === "v") {
|
|
1959
|
+
wc.paste();
|
|
1960
|
+
event.preventDefault();
|
|
1961
|
+
} else if (key === "x") {
|
|
1962
|
+
wc.cut();
|
|
1963
|
+
event.preventDefault();
|
|
1964
|
+
} else if (key === "a") {
|
|
1965
|
+
wc.selectAll();
|
|
1966
|
+
event.preventDefault();
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
});
|
|
1970
|
+
}
|
|
1905
1971
|
const CHROME_HEIGHT = 110;
|
|
1906
1972
|
const DEFAULT_DEVTOOLS_PANEL_HEIGHT = 250;
|
|
1907
1973
|
const MIN_DEVTOOLS_PANEL = 120;
|
|
@@ -1954,6 +2020,9 @@ function createMainWindow(onTabStateChange) {
|
|
|
1954
2020
|
});
|
|
1955
2021
|
devtoolsPanelView.setBackgroundColor("#00000000");
|
|
1956
2022
|
mainWindow.contentView.addChildView(devtoolsPanelView);
|
|
2023
|
+
enableClipboardShortcuts(chromeView);
|
|
2024
|
+
enableClipboardShortcuts(sidebarView);
|
|
2025
|
+
enableClipboardShortcuts(devtoolsPanelView);
|
|
1957
2026
|
const settings2 = loadSettings();
|
|
1958
2027
|
const uiState = {
|
|
1959
2028
|
sidebarOpen: false,
|
|
@@ -3712,7 +3781,7 @@ function setMcpHealth(update) {
|
|
|
3712
3781
|
mcpStatusChangeListener?.(state$1.mcp.status);
|
|
3713
3782
|
}
|
|
3714
3783
|
}
|
|
3715
|
-
const
|
|
3784
|
+
const DEFAULT_MAX_ITERATIONS$1 = 200;
|
|
3716
3785
|
class AnthropicProvider {
|
|
3717
3786
|
client;
|
|
3718
3787
|
model;
|
|
@@ -3760,7 +3829,10 @@ class AnthropicProvider {
|
|
|
3760
3829
|
{ role: "user", content: userMessage }
|
|
3761
3830
|
];
|
|
3762
3831
|
try {
|
|
3763
|
-
|
|
3832
|
+
const maxIterations = loadSettings().maxToolIterations || DEFAULT_MAX_ITERATIONS$1;
|
|
3833
|
+
let iterationsUsed = 0;
|
|
3834
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
3835
|
+
iterationsUsed = i + 1;
|
|
3764
3836
|
const stream = this.client.messages.stream(
|
|
3765
3837
|
{
|
|
3766
3838
|
model: this.model,
|
|
@@ -3821,14 +3893,15 @@ class AnthropicProvider {
|
|
|
3821
3893
|
});
|
|
3822
3894
|
}
|
|
3823
3895
|
messages.push({ role: "assistant", content: assistantContent });
|
|
3824
|
-
if (
|
|
3896
|
+
if (toolUseBlocks.length === 0) {
|
|
3825
3897
|
break;
|
|
3826
3898
|
}
|
|
3827
3899
|
const toolResults = [];
|
|
3828
3900
|
for (const tb of toolUseBlocks) {
|
|
3829
3901
|
const argSummary = tb.input.url || tb.input.text || tb.input.direction || "";
|
|
3830
3902
|
onChunk(`
|
|
3831
|
-
|
|
3903
|
+
<<tool:${tb.name}${argSummary ? ":" + argSummary : ""}>>
|
|
3904
|
+
`);
|
|
3832
3905
|
const result = await onToolCall(tb.name, tb.input);
|
|
3833
3906
|
toolResults.push({
|
|
3834
3907
|
type: "tool_result",
|
|
@@ -3838,6 +3911,11 @@ class AnthropicProvider {
|
|
|
3838
3911
|
}
|
|
3839
3912
|
messages.push({ role: "user", content: toolResults });
|
|
3840
3913
|
}
|
|
3914
|
+
if (iterationsUsed >= maxIterations) {
|
|
3915
|
+
onChunk(`
|
|
3916
|
+
|
|
3917
|
+
[Reached maximum tool call limit (${maxIterations} steps). You can adjust this in Settings → Max Tool Iterations, or continue by sending another message.]`);
|
|
3918
|
+
}
|
|
3841
3919
|
} catch (err) {
|
|
3842
3920
|
if (err.name !== "AbortError") {
|
|
3843
3921
|
onChunk(`
|
|
@@ -3943,7 +4021,7 @@ const PROVIDERS = {
|
|
|
3943
4021
|
apiKeyHint: "Any OpenAI-compatible API endpoint"
|
|
3944
4022
|
}
|
|
3945
4023
|
};
|
|
3946
|
-
const
|
|
4024
|
+
const DEFAULT_MAX_ITERATIONS = 200;
|
|
3947
4025
|
function toOpenAITools(tools) {
|
|
3948
4026
|
return tools.map((t) => ({
|
|
3949
4027
|
type: "function",
|
|
@@ -4009,7 +4087,10 @@ class OpenAICompatProvider {
|
|
|
4009
4087
|
{ role: "user", content: userMessage }
|
|
4010
4088
|
];
|
|
4011
4089
|
try {
|
|
4012
|
-
|
|
4090
|
+
const maxIterations = loadSettings().maxToolIterations || DEFAULT_MAX_ITERATIONS;
|
|
4091
|
+
let iterationsUsed = 0;
|
|
4092
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
4093
|
+
iterationsUsed = i + 1;
|
|
4013
4094
|
let textAccum = "";
|
|
4014
4095
|
const toolCallAccums = {};
|
|
4015
4096
|
let finishReason = null;
|
|
@@ -4066,7 +4147,8 @@ class OpenAICompatProvider {
|
|
|
4066
4147
|
}
|
|
4067
4148
|
const argSummary = args.url || args.text || args.direction || "";
|
|
4068
4149
|
onChunk(`
|
|
4069
|
-
|
|
4150
|
+
<<tool:${tc.name}${argSummary ? ":" + argSummary : ""}>>
|
|
4151
|
+
`);
|
|
4070
4152
|
const result = await onToolCall(tc.name, args);
|
|
4071
4153
|
messages.push({
|
|
4072
4154
|
role: "tool",
|
|
@@ -4075,6 +4157,11 @@ class OpenAICompatProvider {
|
|
|
4075
4157
|
});
|
|
4076
4158
|
}
|
|
4077
4159
|
}
|
|
4160
|
+
if (iterationsUsed >= maxIterations) {
|
|
4161
|
+
onChunk(`
|
|
4162
|
+
|
|
4163
|
+
[Reached maximum tool call limit (${maxIterations} steps). You can adjust this in Settings → Max Tool Iterations, or continue by sending another message.]`);
|
|
4164
|
+
}
|
|
4078
4165
|
} catch (err) {
|
|
4079
4166
|
if (err.name !== "AbortError") {
|
|
4080
4167
|
onChunk(`
|
|
@@ -4633,6 +4720,11 @@ function buildScopedContext(page, mode) {
|
|
|
4633
4720
|
const largePageHint = formatLargePageHint(page);
|
|
4634
4721
|
if (largePageHint) sections.push(`**Reading Hint:** ${largePageHint}`);
|
|
4635
4722
|
sections.push("");
|
|
4723
|
+
const summaryIntent = analyzePageIntent(page);
|
|
4724
|
+
if (summaryIntent) {
|
|
4725
|
+
sections.push(summaryIntent);
|
|
4726
|
+
sections.push("");
|
|
4727
|
+
}
|
|
4636
4728
|
if ((page.pageIssues?.length ?? 0) > 0) {
|
|
4637
4729
|
sections.push("### Page Access Warnings");
|
|
4638
4730
|
sections.push(formatPageIssues(page.pageIssues ?? []));
|
|
@@ -4688,6 +4780,11 @@ function buildScopedContext(page, mode) {
|
|
|
4688
4780
|
sections.push(`**Title:** ${page.title}`);
|
|
4689
4781
|
sections.push(`**Viewport:** ${formatViewport(page)}`);
|
|
4690
4782
|
sections.push("");
|
|
4783
|
+
const interactivesIntent = analyzePageIntent(page);
|
|
4784
|
+
if (interactivesIntent) {
|
|
4785
|
+
sections.push(interactivesIntent);
|
|
4786
|
+
sections.push("");
|
|
4787
|
+
}
|
|
4691
4788
|
const interactivesHighlights = getHighlightsForPage(page.url);
|
|
4692
4789
|
if (interactivesHighlights.length > 0) {
|
|
4693
4790
|
sections.push("### Highlights & Annotations");
|
|
@@ -4864,6 +4961,62 @@ function buildScopedContext(page, mode) {
|
|
|
4864
4961
|
return buildStructuredContext(page);
|
|
4865
4962
|
}
|
|
4866
4963
|
}
|
|
4964
|
+
function analyzePageIntent(page) {
|
|
4965
|
+
const hints = [];
|
|
4966
|
+
const url = page.url.toLowerCase();
|
|
4967
|
+
(page.title || "").toLowerCase();
|
|
4968
|
+
const hasPasswordField = page.forms.some(
|
|
4969
|
+
(f) => f.fields.some((el) => el.inputType === "password")
|
|
4970
|
+
);
|
|
4971
|
+
const hasSearchInput = page.interactiveElements.some(
|
|
4972
|
+
(el) => el.inputType === "search" || el.name === "q" || el.name === "query" || el.name === "search" || (el.placeholder || "").toLowerCase().includes("search")
|
|
4973
|
+
) || page.forms.some(
|
|
4974
|
+
(f) => f.fields.some(
|
|
4975
|
+
(el) => el.inputType === "search" || el.name === "q" || el.name === "query"
|
|
4976
|
+
)
|
|
4977
|
+
);
|
|
4978
|
+
const formCount = page.forms.length;
|
|
4979
|
+
const hasCart = page.interactiveElements.some(
|
|
4980
|
+
(el) => (el.text || "").toLowerCase().includes("cart") || (el.text || "").toLowerCase().includes("checkout")
|
|
4981
|
+
) || url.includes("cart") || url.includes("checkout");
|
|
4982
|
+
const hasResults = page.interactiveElements.filter((el) => el.type === "link").length > 10;
|
|
4983
|
+
const hasPagination = page.interactiveElements.some(
|
|
4984
|
+
(el) => (el.text || "").toLowerCase() === "next" || el.text === "›" || el.text === "»" || (el.label || "").toLowerCase().includes("next page")
|
|
4985
|
+
);
|
|
4986
|
+
if (hasPasswordField) {
|
|
4987
|
+
hints.push("Page type: LOGIN/SIGNUP");
|
|
4988
|
+
hints.push("Suggested: vessel_login or vessel_fill_form → auto-fills credentials and submits");
|
|
4989
|
+
const userField = page.forms.flatMap((f) => f.fields).find(
|
|
4990
|
+
(el) => el.inputType === "email" || el.name === "email" || el.name === "username" || el.autocomplete === "username"
|
|
4991
|
+
);
|
|
4992
|
+
if (userField) {
|
|
4993
|
+
hints.push(`Username field: #${userField.index} [${userField.label || userField.name || userField.placeholder || "input"}]`);
|
|
4994
|
+
}
|
|
4995
|
+
} else if (hasSearchInput && !hasResults) {
|
|
4996
|
+
hints.push("Page type: SEARCH READY");
|
|
4997
|
+
hints.push("Suggested: vessel_search → auto-finds search box, types query, and submits");
|
|
4998
|
+
} else if (hasResults && hasSearchInput) {
|
|
4999
|
+
hints.push("Page type: SEARCH RESULTS");
|
|
5000
|
+
hints.push("Suggested: click a result link, or vessel_paginate for more results");
|
|
5001
|
+
if (hasPagination) hints.push("Pagination detected — vessel_paginate available");
|
|
5002
|
+
} else if (hasCart) {
|
|
5003
|
+
hints.push("Page type: SHOPPING/CHECKOUT");
|
|
5004
|
+
hints.push("Suggested: vessel_fill_form for payment/address fields");
|
|
5005
|
+
} else if (formCount > 0 && !hasPasswordField) {
|
|
5006
|
+
const totalFields = page.forms.reduce((n, f) => n + f.fields.length, 0);
|
|
5007
|
+
hints.push(`Page type: FORM (${formCount} form${formCount > 1 ? "s" : ""}, ${totalFields} fields)`);
|
|
5008
|
+
hints.push("Suggested: vessel_fill_form → fill all fields in one call");
|
|
5009
|
+
} else if (hasPagination) {
|
|
5010
|
+
hints.push("Page type: PAGINATED LIST");
|
|
5011
|
+
hints.push("Suggested: vessel_paginate to navigate between pages");
|
|
5012
|
+
} else if (page.content.length > 3e3 && page.interactiveElements.length < 10) {
|
|
5013
|
+
hints.push("Page type: ARTICLE/CONTENT");
|
|
5014
|
+
hints.push("Suggested: vessel_extract_content for readable text");
|
|
5015
|
+
}
|
|
5016
|
+
if (hints.length === 0) return "";
|
|
5017
|
+
return `### Page Intent (Speedee)
|
|
5018
|
+
${hints.join("\n")}`;
|
|
5019
|
+
}
|
|
4867
5020
|
function buildStructuredContext(page) {
|
|
4868
5021
|
const sections = [];
|
|
4869
5022
|
sections.push("## PAGE STRUCTURE");
|
|
@@ -4877,6 +5030,11 @@ function buildStructuredContext(page) {
|
|
|
4877
5030
|
if (page.byline) sections.push(`**Author:** ${page.byline}`);
|
|
4878
5031
|
if (page.excerpt) sections.push(`**Summary:** ${page.excerpt}`);
|
|
4879
5032
|
sections.push("");
|
|
5033
|
+
const pageIntent = analyzePageIntent(page);
|
|
5034
|
+
if (pageIntent) {
|
|
5035
|
+
sections.push(pageIntent);
|
|
5036
|
+
sections.push("");
|
|
5037
|
+
}
|
|
4880
5038
|
if ((page.pageIssues?.length ?? 0) > 0) {
|
|
4881
5039
|
sections.push("### Page Access Warnings");
|
|
4882
5040
|
sections.push(formatPageIssues(page.pageIssues ?? []));
|
|
@@ -4988,567 +5146,432 @@ function buildGeneralPrompt(query) {
|
|
|
4988
5146
|
user: query
|
|
4989
5147
|
};
|
|
4990
5148
|
}
|
|
4991
|
-
const
|
|
5149
|
+
const TOOL_DEFINITIONS = [
|
|
5150
|
+
// --- Tab Management ---
|
|
4992
5151
|
{
|
|
4993
5152
|
name: "current_tab",
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
type: "object",
|
|
4997
|
-
properties: {}
|
|
4998
|
-
}
|
|
5153
|
+
title: "Get Active Tab",
|
|
5154
|
+
description: "Get the browser tab the human is actively looking at right now. Use this instead of list_tabs when you only need the focused tab."
|
|
4999
5155
|
},
|
|
5000
5156
|
{
|
|
5001
5157
|
name: "list_tabs",
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
type: "object",
|
|
5005
|
-
properties: {}
|
|
5006
|
-
}
|
|
5158
|
+
title: "List Tabs",
|
|
5159
|
+
description: "List all open browser tabs with their IDs, titles, and URLs."
|
|
5007
5160
|
},
|
|
5008
5161
|
{
|
|
5009
5162
|
name: "switch_tab",
|
|
5163
|
+
title: "Switch Tab",
|
|
5010
5164
|
description: "Switch to a browser tab by tab ID, or by matching part of the title or URL.",
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
type: "string",
|
|
5017
|
-
description: "Case-insensitive partial match against tab title or URL"
|
|
5018
|
-
}
|
|
5019
|
-
}
|
|
5165
|
+
inputSchema: {
|
|
5166
|
+
tabId: zod.z.string().optional().describe("Exact tab ID to switch to"),
|
|
5167
|
+
match: zod.z.string().optional().describe(
|
|
5168
|
+
"Case-insensitive partial match against tab title or URL"
|
|
5169
|
+
)
|
|
5020
5170
|
}
|
|
5021
5171
|
},
|
|
5022
5172
|
{
|
|
5023
5173
|
name: "create_tab",
|
|
5174
|
+
title: "Create Tab",
|
|
5024
5175
|
description: "Open a new browser tab, optionally navigating to a URL.",
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
properties: {
|
|
5028
|
-
url: { type: "string", description: "Optional URL to open" }
|
|
5029
|
-
}
|
|
5176
|
+
inputSchema: {
|
|
5177
|
+
url: zod.z.string().optional().describe("Optional URL to open")
|
|
5030
5178
|
}
|
|
5031
5179
|
},
|
|
5180
|
+
// --- Navigation ---
|
|
5032
5181
|
{
|
|
5033
5182
|
name: "navigate",
|
|
5183
|
+
title: "Navigate",
|
|
5034
5184
|
description: "Navigate the browser to a URL.",
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
properties: {
|
|
5038
|
-
url: { type: "string", description: "The URL to navigate to" }
|
|
5039
|
-
},
|
|
5040
|
-
required: ["url"]
|
|
5185
|
+
inputSchema: {
|
|
5186
|
+
url: zod.z.string().describe("The URL to navigate to")
|
|
5041
5187
|
}
|
|
5042
5188
|
},
|
|
5043
5189
|
{
|
|
5044
5190
|
name: "go_back",
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
type: "object",
|
|
5048
|
-
properties: {}
|
|
5049
|
-
}
|
|
5191
|
+
title: "Go Back",
|
|
5192
|
+
description: "Go back to the previous page in browser history."
|
|
5050
5193
|
},
|
|
5051
5194
|
{
|
|
5052
5195
|
name: "go_forward",
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
type: "object",
|
|
5056
|
-
properties: {}
|
|
5057
|
-
}
|
|
5196
|
+
title: "Go Forward",
|
|
5197
|
+
description: "Go forward in browser history."
|
|
5058
5198
|
},
|
|
5059
5199
|
{
|
|
5060
5200
|
name: "reload",
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
type: "object",
|
|
5064
|
-
properties: {}
|
|
5065
|
-
}
|
|
5201
|
+
title: "Reload",
|
|
5202
|
+
description: "Reload the current page."
|
|
5066
5203
|
},
|
|
5204
|
+
// --- Interaction ---
|
|
5067
5205
|
{
|
|
5068
5206
|
name: "click",
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
type: "number",
|
|
5075
|
-
description: "The element index number from the page content"
|
|
5076
|
-
},
|
|
5077
|
-
selector: {
|
|
5078
|
-
type: "string",
|
|
5079
|
-
description: "CSS selector as fallback if index is not available"
|
|
5080
|
-
}
|
|
5081
|
-
}
|
|
5207
|
+
title: "Click Element",
|
|
5208
|
+
description: "Click an element on the page by its index number or CSS selector.",
|
|
5209
|
+
inputSchema: {
|
|
5210
|
+
index: zod.z.number().optional().describe("Element index from the page content listing"),
|
|
5211
|
+
selector: zod.z.string().optional().describe("CSS selector as fallback")
|
|
5082
5212
|
}
|
|
5083
5213
|
},
|
|
5084
5214
|
{
|
|
5085
5215
|
name: "type_text",
|
|
5216
|
+
title: "Type Text",
|
|
5086
5217
|
description: "Type text into an input field or textarea. Clears existing content first.",
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
type: "string",
|
|
5095
|
-
enum: ["default", "keystroke"],
|
|
5096
|
-
description: '"default" sets value directly and fires input+change events. "keystroke" simulates character-by-character key events for apps that validate on keypress.'
|
|
5097
|
-
}
|
|
5098
|
-
},
|
|
5099
|
-
required: ["text"]
|
|
5218
|
+
inputSchema: {
|
|
5219
|
+
index: zod.z.number().optional().describe("The element index number"),
|
|
5220
|
+
selector: zod.z.string().optional().describe("CSS selector as fallback"),
|
|
5221
|
+
text: zod.z.string().describe("The text to type"),
|
|
5222
|
+
mode: zod.z.enum(["default", "keystroke"]).optional().describe(
|
|
5223
|
+
'"default" sets value directly. "keystroke" simulates character-by-character key events.'
|
|
5224
|
+
)
|
|
5100
5225
|
}
|
|
5101
5226
|
},
|
|
5102
5227
|
{
|
|
5103
5228
|
name: "select_option",
|
|
5229
|
+
title: "Select Option",
|
|
5104
5230
|
description: "Select an option in a dropdown by visible label or option value.",
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
description: "The select element index number"
|
|
5111
|
-
},
|
|
5112
|
-
selector: { type: "string", description: "CSS selector as fallback" },
|
|
5113
|
-
label: {
|
|
5114
|
-
type: "string",
|
|
5115
|
-
description: "Visible option label to match"
|
|
5116
|
-
},
|
|
5117
|
-
value: {
|
|
5118
|
-
type: "string",
|
|
5119
|
-
description: "Option value attribute to match"
|
|
5120
|
-
}
|
|
5121
|
-
}
|
|
5231
|
+
inputSchema: {
|
|
5232
|
+
index: zod.z.number().optional().describe("The select element index number"),
|
|
5233
|
+
selector: zod.z.string().optional().describe("CSS selector as fallback"),
|
|
5234
|
+
label: zod.z.string().optional().describe("Visible option label to match"),
|
|
5235
|
+
value: zod.z.string().optional().describe("Option value attribute to match")
|
|
5122
5236
|
}
|
|
5123
5237
|
},
|
|
5124
5238
|
{
|
|
5125
5239
|
name: "submit_form",
|
|
5240
|
+
title: "Submit Form",
|
|
5126
5241
|
description: "Submit a form using a field index, submit button index, form selector, or button selector.",
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
index: {
|
|
5131
|
-
type: "number",
|
|
5132
|
-
description: "Index of a field or submit button inside the target form"
|
|
5133
|
-
},
|
|
5134
|
-
selector: {
|
|
5135
|
-
type: "string",
|
|
5136
|
-
description: "Form or submit button selector"
|
|
5137
|
-
}
|
|
5138
|
-
}
|
|
5242
|
+
inputSchema: {
|
|
5243
|
+
index: zod.z.number().optional().describe("Index of a form field or submit button"),
|
|
5244
|
+
selector: zod.z.string().optional().describe("Form or submit button selector")
|
|
5139
5245
|
}
|
|
5140
5246
|
},
|
|
5141
5247
|
{
|
|
5142
5248
|
name: "press_key",
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
description: "Keyboard key, for example Enter or Escape"
|
|
5150
|
-
},
|
|
5151
|
-
index: { type: "number", description: "Element index to focus first" },
|
|
5152
|
-
selector: {
|
|
5153
|
-
type: "string",
|
|
5154
|
-
description: "CSS selector to focus first"
|
|
5155
|
-
}
|
|
5156
|
-
},
|
|
5157
|
-
required: ["key"]
|
|
5249
|
+
title: "Press Key",
|
|
5250
|
+
description: "Press a keyboard key, optionally after focusing an element.",
|
|
5251
|
+
inputSchema: {
|
|
5252
|
+
key: zod.z.string().describe("Keyboard key such as Enter or Escape"),
|
|
5253
|
+
index: zod.z.number().optional().describe("Element index to focus first"),
|
|
5254
|
+
selector: zod.z.string().optional().describe("CSS selector to focus first")
|
|
5158
5255
|
}
|
|
5159
5256
|
},
|
|
5160
5257
|
{
|
|
5161
5258
|
name: "scroll",
|
|
5259
|
+
title: "Scroll",
|
|
5162
5260
|
description: "Scroll the page up or down.",
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
direction: {
|
|
5167
|
-
type: "string",
|
|
5168
|
-
enum: ["up", "down"],
|
|
5169
|
-
description: "Scroll direction"
|
|
5170
|
-
},
|
|
5171
|
-
amount: {
|
|
5172
|
-
type: "number",
|
|
5173
|
-
description: "Pixels to scroll (default 500)"
|
|
5174
|
-
}
|
|
5175
|
-
},
|
|
5176
|
-
required: ["direction"]
|
|
5261
|
+
inputSchema: {
|
|
5262
|
+
direction: zod.z.enum(["up", "down"]).describe("Scroll direction"),
|
|
5263
|
+
amount: zod.z.number().optional().describe("Pixels to scroll (default 500)")
|
|
5177
5264
|
}
|
|
5178
5265
|
},
|
|
5179
5266
|
{
|
|
5180
5267
|
name: "hover",
|
|
5268
|
+
title: "Hover Element",
|
|
5181
5269
|
description: "Move the mouse pointer over an element to trigger hover states, tooltips, or dropdown menus.",
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
index: { type: "number", description: "Element index number" },
|
|
5186
|
-
selector: { type: "string", description: "CSS selector as fallback" }
|
|
5187
|
-
}
|
|
5270
|
+
inputSchema: {
|
|
5271
|
+
index: zod.z.number().optional().describe("Element index number"),
|
|
5272
|
+
selector: zod.z.string().optional().describe("CSS selector as fallback")
|
|
5188
5273
|
}
|
|
5189
5274
|
},
|
|
5190
5275
|
{
|
|
5191
5276
|
name: "focus",
|
|
5277
|
+
title: "Focus Element",
|
|
5192
5278
|
description: "Focus an input, button, or interactive element. Useful before pressing keys or to trigger focus-dependent UI.",
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
index: { type: "number", description: "Element index number" },
|
|
5197
|
-
selector: { type: "string", description: "CSS selector as fallback" }
|
|
5198
|
-
}
|
|
5279
|
+
inputSchema: {
|
|
5280
|
+
index: zod.z.number().optional().describe("Element index number"),
|
|
5281
|
+
selector: zod.z.string().optional().describe("CSS selector as fallback")
|
|
5199
5282
|
}
|
|
5200
5283
|
},
|
|
5284
|
+
// --- Page & Content ---
|
|
5201
5285
|
{
|
|
5202
5286
|
name: "set_ad_blocking",
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
type: "string",
|
|
5213
|
-
description: "Exact tab ID to target instead of the active tab"
|
|
5214
|
-
},
|
|
5215
|
-
match: {
|
|
5216
|
-
type: "string",
|
|
5217
|
-
description: "Case-insensitive partial match against tab title or URL"
|
|
5218
|
-
},
|
|
5219
|
-
reload: {
|
|
5220
|
-
type: "boolean",
|
|
5221
|
-
description: "Reload the tab after changing the setting (default true)"
|
|
5222
|
-
}
|
|
5223
|
-
},
|
|
5224
|
-
required: ["enabled"]
|
|
5287
|
+
title: "Set Ad Blocking",
|
|
5288
|
+
description: "Enable or disable ad blocking for the active tab or a matched tab. Reload after changes unless reload is false.",
|
|
5289
|
+
inputSchema: {
|
|
5290
|
+
enabled: zod.z.boolean().describe("Whether ad blocking should be enabled for the tab"),
|
|
5291
|
+
tabId: zod.z.string().optional().describe("Exact tab ID to target instead of the active tab"),
|
|
5292
|
+
match: zod.z.string().optional().describe(
|
|
5293
|
+
"Case-insensitive partial match against tab title or URL"
|
|
5294
|
+
),
|
|
5295
|
+
reload: zod.z.boolean().optional().describe("Reload the tab after changing (default true)")
|
|
5225
5296
|
}
|
|
5226
5297
|
},
|
|
5227
5298
|
{
|
|
5228
5299
|
name: "dismiss_popup",
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
type: "object",
|
|
5232
|
-
properties: {}
|
|
5233
|
-
}
|
|
5300
|
+
title: "Dismiss Popup",
|
|
5301
|
+
description: "Dismiss a modal, popup, newsletter gate, cookie banner, or overlay using common close/decline actions."
|
|
5234
5302
|
},
|
|
5235
5303
|
{
|
|
5236
5304
|
name: "read_page",
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
type: "object",
|
|
5240
|
-
properties: {}
|
|
5241
|
-
}
|
|
5305
|
+
title: "Read Page",
|
|
5306
|
+
description: "Re-read the current page content. Includes active text selection and visible unsaved highlights. Use after navigation or interaction to see updated content."
|
|
5242
5307
|
},
|
|
5243
5308
|
{
|
|
5244
5309
|
name: "wait_for",
|
|
5310
|
+
title: "Wait For",
|
|
5245
5311
|
description: "Wait for a text string or CSS selector to appear on the page before continuing.",
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
type: "string",
|
|
5251
|
-
description: "Text that should appear in the page body"
|
|
5252
|
-
},
|
|
5253
|
-
selector: {
|
|
5254
|
-
type: "string",
|
|
5255
|
-
description: "CSS selector that should match an element"
|
|
5256
|
-
},
|
|
5257
|
-
timeoutMs: {
|
|
5258
|
-
type: "number",
|
|
5259
|
-
description: "Maximum time to wait in milliseconds (default 5000)"
|
|
5260
|
-
}
|
|
5261
|
-
}
|
|
5312
|
+
inputSchema: {
|
|
5313
|
+
text: zod.z.string().optional().describe("Text that should appear in the page body"),
|
|
5314
|
+
selector: zod.z.string().optional().describe("CSS selector that should match an element"),
|
|
5315
|
+
timeoutMs: zod.z.number().optional().describe("Maximum time to wait in milliseconds (default 5000)")
|
|
5262
5316
|
}
|
|
5263
5317
|
},
|
|
5318
|
+
// --- Checkpoints & Sessions ---
|
|
5264
5319
|
{
|
|
5265
5320
|
name: "create_checkpoint",
|
|
5321
|
+
title: "Create Checkpoint",
|
|
5266
5322
|
description: "Capture the current browser session as a named checkpoint for later recovery.",
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
name: {
|
|
5271
|
-
type: "string",
|
|
5272
|
-
description: "Short checkpoint name"
|
|
5273
|
-
},
|
|
5274
|
-
note: {
|
|
5275
|
-
type: "string",
|
|
5276
|
-
description: "Optional note about why this checkpoint matters"
|
|
5277
|
-
}
|
|
5278
|
-
}
|
|
5323
|
+
inputSchema: {
|
|
5324
|
+
name: zod.z.string().optional().describe("Short checkpoint name"),
|
|
5325
|
+
note: zod.z.string().optional().describe("Optional note about why this checkpoint matters")
|
|
5279
5326
|
}
|
|
5280
5327
|
},
|
|
5281
5328
|
{
|
|
5282
5329
|
name: "restore_checkpoint",
|
|
5330
|
+
title: "Restore Checkpoint",
|
|
5283
5331
|
description: "Restore a previously captured checkpoint by name or ID.",
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
checkpointId: {
|
|
5288
|
-
type: "string",
|
|
5289
|
-
description: "Exact checkpoint ID"
|
|
5290
|
-
},
|
|
5291
|
-
name: {
|
|
5292
|
-
type: "string",
|
|
5293
|
-
description: "Checkpoint name to match if ID is unknown"
|
|
5294
|
-
}
|
|
5295
|
-
}
|
|
5332
|
+
inputSchema: {
|
|
5333
|
+
checkpointId: zod.z.string().optional().describe("Exact checkpoint ID"),
|
|
5334
|
+
name: zod.z.string().optional().describe("Checkpoint name to match if ID is unknown")
|
|
5296
5335
|
}
|
|
5297
5336
|
},
|
|
5298
5337
|
{
|
|
5299
5338
|
name: "save_session",
|
|
5339
|
+
title: "Save Session",
|
|
5300
5340
|
description: "Persist the current browser cookies, localStorage, and tab layout under a reusable session name.",
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
properties: {
|
|
5304
|
-
name: {
|
|
5305
|
-
type: "string",
|
|
5306
|
-
description: "Session name such as github-logged-in"
|
|
5307
|
-
}
|
|
5308
|
-
},
|
|
5309
|
-
required: ["name"]
|
|
5341
|
+
inputSchema: {
|
|
5342
|
+
name: zod.z.string().describe("Session name such as github-logged-in")
|
|
5310
5343
|
}
|
|
5311
5344
|
},
|
|
5312
5345
|
{
|
|
5313
5346
|
name: "load_session",
|
|
5347
|
+
title: "Load Session",
|
|
5314
5348
|
description: "Load a previously saved named session, restoring cookies, localStorage, and saved tabs.",
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
properties: {
|
|
5318
|
-
name: {
|
|
5319
|
-
type: "string",
|
|
5320
|
-
description: "Previously saved session name"
|
|
5321
|
-
}
|
|
5322
|
-
},
|
|
5323
|
-
required: ["name"]
|
|
5349
|
+
inputSchema: {
|
|
5350
|
+
name: zod.z.string().describe("Previously saved session name")
|
|
5324
5351
|
}
|
|
5325
5352
|
},
|
|
5326
5353
|
{
|
|
5327
5354
|
name: "list_sessions",
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
type: "object",
|
|
5331
|
-
properties: {}
|
|
5332
|
-
}
|
|
5355
|
+
title: "List Sessions",
|
|
5356
|
+
description: "List previously saved named browser sessions with cookie and storage counts."
|
|
5333
5357
|
},
|
|
5334
5358
|
{
|
|
5335
5359
|
name: "delete_session",
|
|
5360
|
+
title: "Delete Session",
|
|
5336
5361
|
description: "Delete a previously saved named browser session.",
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
properties: {
|
|
5340
|
-
name: {
|
|
5341
|
-
type: "string",
|
|
5342
|
-
description: "Saved session name to delete"
|
|
5343
|
-
}
|
|
5344
|
-
},
|
|
5345
|
-
required: ["name"]
|
|
5362
|
+
inputSchema: {
|
|
5363
|
+
name: zod.z.string().describe("Saved session name to delete")
|
|
5346
5364
|
}
|
|
5347
5365
|
},
|
|
5366
|
+
// --- Bookmarks ---
|
|
5348
5367
|
{
|
|
5349
5368
|
name: "list_bookmarks",
|
|
5369
|
+
title: "List Bookmarks",
|
|
5350
5370
|
description: "List bookmark folders and saved pages. Optionally filter by folder name or ID.",
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
folderId: {
|
|
5355
|
-
type: "string",
|
|
5356
|
-
description: "Exact bookmark folder ID to filter by"
|
|
5357
|
-
},
|
|
5358
|
-
folderName: {
|
|
5359
|
-
type: "string",
|
|
5360
|
-
description: "Exact bookmark folder name to filter by"
|
|
5361
|
-
}
|
|
5362
|
-
}
|
|
5371
|
+
inputSchema: {
|
|
5372
|
+
folderId: zod.z.string().optional().describe("Exact bookmark folder ID to filter by"),
|
|
5373
|
+
folderName: zod.z.string().optional().describe("Exact bookmark folder name to filter by")
|
|
5363
5374
|
}
|
|
5364
5375
|
},
|
|
5365
5376
|
{
|
|
5366
5377
|
name: "search_bookmarks",
|
|
5378
|
+
title: "Search Bookmarks",
|
|
5367
5379
|
description: "Search bookmarks by title, URL, note, folder name, or folder summary.",
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
properties: {
|
|
5371
|
-
query: {
|
|
5372
|
-
type: "string",
|
|
5373
|
-
description: "Search term to match against saved bookmarks"
|
|
5374
|
-
}
|
|
5375
|
-
},
|
|
5376
|
-
required: ["query"]
|
|
5380
|
+
inputSchema: {
|
|
5381
|
+
query: zod.z.string().describe("Search term to match against saved bookmarks")
|
|
5377
5382
|
}
|
|
5378
5383
|
},
|
|
5379
5384
|
{
|
|
5380
5385
|
name: "create_bookmark_folder",
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
type: "string",
|
|
5387
|
-
description: "Folder name to create"
|
|
5388
|
-
},
|
|
5389
|
-
summary: {
|
|
5390
|
-
type: "string",
|
|
5391
|
-
description: "Optional one-sentence summary shown in the UI for this folder"
|
|
5392
|
-
}
|
|
5393
|
-
},
|
|
5394
|
-
required: ["name"]
|
|
5386
|
+
title: "Create Bookmark Folder",
|
|
5387
|
+
description: "Create a bookmark folder for organizing saved pages. Returns existing folder if the same name exists.",
|
|
5388
|
+
inputSchema: {
|
|
5389
|
+
name: zod.z.string().describe("Folder name to create"),
|
|
5390
|
+
summary: zod.z.string().optional().describe("Optional one-sentence summary for this folder")
|
|
5395
5391
|
}
|
|
5396
5392
|
},
|
|
5397
5393
|
{
|
|
5398
5394
|
name: "save_bookmark",
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
type: "number",
|
|
5413
|
-
description: "Element index of a link on the current page to bookmark without opening it."
|
|
5414
|
-
},
|
|
5415
|
-
selector: {
|
|
5416
|
-
type: "string",
|
|
5417
|
-
description: "CSS selector of a link on the current page to bookmark without opening it."
|
|
5418
|
-
},
|
|
5419
|
-
folderId: {
|
|
5420
|
-
type: "string",
|
|
5421
|
-
description: "Folder ID to save into"
|
|
5422
|
-
},
|
|
5423
|
-
folderName: {
|
|
5424
|
-
type: "string",
|
|
5425
|
-
description: "Folder name to save into. Created automatically if missing."
|
|
5426
|
-
},
|
|
5427
|
-
folderSummary: {
|
|
5428
|
-
type: "string",
|
|
5429
|
-
description: "Optional summary used if a new folder is created"
|
|
5430
|
-
},
|
|
5431
|
-
createFolderIfMissing: {
|
|
5432
|
-
type: "boolean",
|
|
5433
|
-
description: "Create folderName automatically when it does not exist"
|
|
5434
|
-
},
|
|
5435
|
-
note: {
|
|
5436
|
-
type: "string",
|
|
5437
|
-
description: "Optional note about why the page was saved"
|
|
5438
|
-
},
|
|
5439
|
-
onDuplicate: {
|
|
5440
|
-
type: "string",
|
|
5441
|
-
enum: ["ask", "update", "duplicate"],
|
|
5442
|
-
description: 'How to handle an existing bookmark with the same URL in the same folder: "ask" (default), "update", or "duplicate".'
|
|
5443
|
-
}
|
|
5444
|
-
}
|
|
5395
|
+
title: "Save Bookmark",
|
|
5396
|
+
description: "Save the current page, a specified URL, or a link target from the current page as a bookmark.",
|
|
5397
|
+
inputSchema: {
|
|
5398
|
+
url: zod.z.string().optional().describe("URL to save. Omit to save the current page."),
|
|
5399
|
+
title: zod.z.string().optional().describe("Title for the bookmark"),
|
|
5400
|
+
index: zod.z.number().optional().describe("Element index of a link to bookmark without opening"),
|
|
5401
|
+
selector: zod.z.string().optional().describe("CSS selector of a link to bookmark without opening"),
|
|
5402
|
+
folderId: zod.z.string().optional().describe("Folder ID to save into"),
|
|
5403
|
+
folderName: zod.z.string().optional().describe("Folder name to save into. Created automatically if missing."),
|
|
5404
|
+
folderSummary: zod.z.string().optional().describe("Optional summary used if a new folder is created"),
|
|
5405
|
+
createFolderIfMissing: zod.z.boolean().optional().describe("Create folderName automatically when it does not exist"),
|
|
5406
|
+
note: zod.z.string().optional().describe("Optional note about why the page was saved"),
|
|
5407
|
+
onDuplicate: zod.z.enum(["ask", "update", "duplicate"]).optional().describe("How to handle duplicate URLs in the same folder")
|
|
5445
5408
|
}
|
|
5446
5409
|
},
|
|
5447
5410
|
{
|
|
5448
5411
|
name: "organize_bookmark",
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
description: "Optional title when saving a new bookmark or retitling an existing one"
|
|
5464
|
-
},
|
|
5465
|
-
index: {
|
|
5466
|
-
type: "number",
|
|
5467
|
-
description: "Element index of a link on the current page to organize without opening it."
|
|
5468
|
-
},
|
|
5469
|
-
selector: {
|
|
5470
|
-
type: "string",
|
|
5471
|
-
description: "CSS selector of a link on the current page to organize without opening it."
|
|
5472
|
-
},
|
|
5473
|
-
folderId: {
|
|
5474
|
-
type: "string",
|
|
5475
|
-
description: "Exact bookmark folder ID target"
|
|
5476
|
-
},
|
|
5477
|
-
folderName: {
|
|
5478
|
-
type: "string",
|
|
5479
|
-
description: "Folder name target. Created automatically if missing"
|
|
5480
|
-
},
|
|
5481
|
-
folderSummary: {
|
|
5482
|
-
type: "string",
|
|
5483
|
-
description: "Optional summary used if a new folder is created"
|
|
5484
|
-
},
|
|
5485
|
-
createFolderIfMissing: {
|
|
5486
|
-
type: "boolean",
|
|
5487
|
-
description: "Create folderName automatically when it does not exist"
|
|
5488
|
-
},
|
|
5489
|
-
note: {
|
|
5490
|
-
type: "string",
|
|
5491
|
-
description: "Optional note to attach or update on the bookmark"
|
|
5492
|
-
},
|
|
5493
|
-
archive: {
|
|
5494
|
-
type: "boolean",
|
|
5495
|
-
description: 'If true, organize into the default "Archive" folder'
|
|
5496
|
-
}
|
|
5497
|
-
}
|
|
5412
|
+
title: "Organize Bookmark",
|
|
5413
|
+
description: "Move an existing bookmark or save the current page into a folder, creating the folder if needed.",
|
|
5414
|
+
inputSchema: {
|
|
5415
|
+
bookmarkId: zod.z.string().optional().describe("Existing bookmark ID to move"),
|
|
5416
|
+
url: zod.z.string().optional().describe("URL to organize"),
|
|
5417
|
+
title: zod.z.string().optional().describe("Optional title"),
|
|
5418
|
+
index: zod.z.number().optional().describe("Element index of a link to organize"),
|
|
5419
|
+
selector: zod.z.string().optional().describe("CSS selector of a link to organize"),
|
|
5420
|
+
folderId: zod.z.string().optional().describe("Target folder ID"),
|
|
5421
|
+
folderName: zod.z.string().optional().describe("Target folder name. Created automatically if missing"),
|
|
5422
|
+
folderSummary: zod.z.string().optional().describe("Optional summary for new folder"),
|
|
5423
|
+
createFolderIfMissing: zod.z.boolean().optional().describe("Create folderName automatically when it does not exist"),
|
|
5424
|
+
note: zod.z.string().optional().describe("Optional note"),
|
|
5425
|
+
archive: zod.z.boolean().optional().describe('If true, organize into the default "Archive" folder')
|
|
5498
5426
|
}
|
|
5499
5427
|
},
|
|
5500
5428
|
{
|
|
5501
5429
|
name: "archive_bookmark",
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
type: "string",
|
|
5512
|
-
description: "URL to archive. Omit to use the current page, or provide index/selector to archive a link target from the page."
|
|
5513
|
-
},
|
|
5514
|
-
title: {
|
|
5515
|
-
type: "string",
|
|
5516
|
-
description: "Optional title when saving a new archived bookmark"
|
|
5517
|
-
},
|
|
5518
|
-
index: {
|
|
5519
|
-
type: "number",
|
|
5520
|
-
description: "Element index of a link on the current page to archive without opening it."
|
|
5521
|
-
},
|
|
5522
|
-
selector: {
|
|
5523
|
-
type: "string",
|
|
5524
|
-
description: "CSS selector of a link on the current page to archive without opening it."
|
|
5525
|
-
},
|
|
5526
|
-
note: {
|
|
5527
|
-
type: "string",
|
|
5528
|
-
description: "Optional note about why the page was archived"
|
|
5529
|
-
}
|
|
5530
|
-
}
|
|
5430
|
+
title: "Archive Bookmark",
|
|
5431
|
+
description: 'Archive the current page, a URL, a link target, or an existing bookmark into the "Archive" folder.',
|
|
5432
|
+
inputSchema: {
|
|
5433
|
+
bookmarkId: zod.z.string().optional().describe("Existing bookmark ID to archive"),
|
|
5434
|
+
url: zod.z.string().optional().describe("URL to archive"),
|
|
5435
|
+
title: zod.z.string().optional().describe("Optional title"),
|
|
5436
|
+
index: zod.z.number().optional().describe("Element index of a link to archive"),
|
|
5437
|
+
selector: zod.z.string().optional().describe("CSS selector of a link to archive"),
|
|
5438
|
+
note: zod.z.string().optional().describe("Optional note")
|
|
5531
5439
|
}
|
|
5532
5440
|
},
|
|
5533
5441
|
{
|
|
5534
5442
|
name: "open_bookmark",
|
|
5443
|
+
title: "Open Bookmark",
|
|
5535
5444
|
description: "Open a saved bookmark by its bookmark ID. Optionally open it in a new tab.",
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5445
|
+
inputSchema: {
|
|
5446
|
+
bookmarkId: zod.z.string().describe("Exact bookmark ID to open"),
|
|
5447
|
+
newTab: zod.z.boolean().optional().describe("Open in a new tab instead of the current tab")
|
|
5448
|
+
}
|
|
5449
|
+
},
|
|
5450
|
+
// --- Highlights ---
|
|
5451
|
+
{
|
|
5452
|
+
name: "highlight",
|
|
5453
|
+
title: "Highlight Element",
|
|
5454
|
+
description: "Visually highlight an element or text on the page for the user. Use to draw attention to specific content. Highlights persist until cleared.",
|
|
5455
|
+
inputSchema: {
|
|
5456
|
+
index: zod.z.number().optional().describe("Element index from page content to highlight"),
|
|
5457
|
+
selector: zod.z.string().optional().describe("CSS selector of element to highlight"),
|
|
5458
|
+
text: zod.z.string().optional().describe("Text to find and highlight on the page (all occurrences)"),
|
|
5459
|
+
label: zod.z.string().optional().describe("Annotation label to display near the highlight"),
|
|
5460
|
+
durationMs: zod.z.number().optional().describe("Auto-clear after this many milliseconds (omit for permanent)"),
|
|
5461
|
+
color: zod.z.enum(["yellow", "red", "green", "blue", "purple", "orange"]).optional().describe("Highlight color (default yellow)")
|
|
5462
|
+
}
|
|
5463
|
+
},
|
|
5464
|
+
{
|
|
5465
|
+
name: "clear_highlights",
|
|
5466
|
+
title: "Clear Highlights",
|
|
5467
|
+
description: "Remove all visual highlights from the current page."
|
|
5468
|
+
},
|
|
5469
|
+
// --- Speedee System: Flow State ---
|
|
5470
|
+
{
|
|
5471
|
+
name: "flow_start",
|
|
5472
|
+
title: "Start Workflow",
|
|
5473
|
+
description: "Begin tracking a multi-step web workflow. Vessel will show progress after every action so you always know where you are.",
|
|
5474
|
+
inputSchema: {
|
|
5475
|
+
goal: zod.z.string().describe("What this workflow accomplishes (e.g. 'Purchase item from Amazon')"),
|
|
5476
|
+
steps: zod.z.array(zod.z.string()).describe(
|
|
5477
|
+
"Ordered list of step labels (e.g. ['Log in', 'Search', 'Select item', 'Checkout'])"
|
|
5478
|
+
)
|
|
5479
|
+
}
|
|
5480
|
+
},
|
|
5481
|
+
{
|
|
5482
|
+
name: "flow_advance",
|
|
5483
|
+
title: "Advance Workflow Step",
|
|
5484
|
+
description: "Mark the current workflow step as done and move to the next one.",
|
|
5485
|
+
inputSchema: {
|
|
5486
|
+
detail: zod.z.string().optional().describe("Brief note about what was accomplished")
|
|
5487
|
+
}
|
|
5488
|
+
},
|
|
5489
|
+
{
|
|
5490
|
+
name: "flow_status",
|
|
5491
|
+
title: "Workflow Status",
|
|
5492
|
+
description: "Check the current workflow progress."
|
|
5493
|
+
},
|
|
5494
|
+
{
|
|
5495
|
+
name: "flow_end",
|
|
5496
|
+
title: "End Workflow",
|
|
5497
|
+
description: "Clear the active workflow tracker."
|
|
5498
|
+
},
|
|
5499
|
+
// --- Speedee System: Suggestion Engine ---
|
|
5500
|
+
{
|
|
5501
|
+
name: "suggest",
|
|
5502
|
+
title: "What Should I Do?",
|
|
5503
|
+
description: "Analyze the current page and return the most relevant tools and suggested next actions. Call this when unsure what to do."
|
|
5504
|
+
},
|
|
5505
|
+
// --- Speedee System: Composable Macros ---
|
|
5506
|
+
{
|
|
5507
|
+
name: "fill_form",
|
|
5508
|
+
title: "Fill Form",
|
|
5509
|
+
description: "Fill multiple form fields at once. Much faster than calling type_text for each field individually.",
|
|
5510
|
+
inputSchema: {
|
|
5511
|
+
fields: zod.z.array(
|
|
5512
|
+
zod.z.object({
|
|
5513
|
+
index: zod.z.number().optional().describe("Element index from page content"),
|
|
5514
|
+
selector: zod.z.string().optional().describe("CSS selector fallback"),
|
|
5515
|
+
value: zod.z.string().describe("Value to enter")
|
|
5516
|
+
})
|
|
5517
|
+
).describe("Fields to fill"),
|
|
5518
|
+
submit: zod.z.boolean().optional().describe("Submit the form after filling (default false)")
|
|
5519
|
+
}
|
|
5520
|
+
},
|
|
5521
|
+
{
|
|
5522
|
+
name: "login",
|
|
5523
|
+
title: "Login",
|
|
5524
|
+
description: "Compound action: navigate to a login page, fill credentials, and submit. Handles the full login flow in one call.",
|
|
5525
|
+
inputSchema: {
|
|
5526
|
+
url: zod.z.string().optional().describe("Login page URL (skip if already on login page)"),
|
|
5527
|
+
username: zod.z.string().describe("Username or email"),
|
|
5528
|
+
password: zod.z.string().describe("Password"),
|
|
5529
|
+
username_selector: zod.z.string().optional().describe("CSS selector for username field (auto-detected if omitted)"),
|
|
5530
|
+
password_selector: zod.z.string().optional().describe("CSS selector for password field (auto-detected if omitted)"),
|
|
5531
|
+
submit_selector: zod.z.string().optional().describe("CSS selector for submit button (auto-detected if omitted)")
|
|
5532
|
+
}
|
|
5533
|
+
},
|
|
5534
|
+
{
|
|
5535
|
+
name: "search",
|
|
5536
|
+
title: "Search",
|
|
5537
|
+
description: "Find a search box on the current page, type a query, and submit. Returns the resulting page state.",
|
|
5538
|
+
inputSchema: {
|
|
5539
|
+
query: zod.z.string().describe("Search query text"),
|
|
5540
|
+
selector: zod.z.string().optional().describe("CSS selector for search input (auto-detected if omitted)")
|
|
5541
|
+
}
|
|
5542
|
+
},
|
|
5543
|
+
{
|
|
5544
|
+
name: "paginate",
|
|
5545
|
+
title: "Paginate",
|
|
5546
|
+
description: "Navigate to the next or previous page of results. Auto-detects pagination controls.",
|
|
5547
|
+
inputSchema: {
|
|
5548
|
+
direction: zod.z.enum(["next", "prev"]).describe("Pagination direction"),
|
|
5549
|
+
selector: zod.z.string().optional().describe("CSS selector for pagination link (auto-detected if omitted)")
|
|
5549
5550
|
}
|
|
5550
5551
|
}
|
|
5551
5552
|
];
|
|
5553
|
+
function toAnthropicTools(defs) {
|
|
5554
|
+
return defs.filter((d) => !d.mcpOnly).map((d) => {
|
|
5555
|
+
let inputSchema;
|
|
5556
|
+
if (d.inputSchema) {
|
|
5557
|
+
const jsonSchema = zod.z.toJSONSchema(zod.z.object(d.inputSchema));
|
|
5558
|
+
delete jsonSchema.$schema;
|
|
5559
|
+
delete jsonSchema.additionalProperties;
|
|
5560
|
+
inputSchema = jsonSchema;
|
|
5561
|
+
} else {
|
|
5562
|
+
inputSchema = {
|
|
5563
|
+
type: "object",
|
|
5564
|
+
properties: {}
|
|
5565
|
+
};
|
|
5566
|
+
}
|
|
5567
|
+
return {
|
|
5568
|
+
name: d.name,
|
|
5569
|
+
description: d.description,
|
|
5570
|
+
input_schema: inputSchema
|
|
5571
|
+
};
|
|
5572
|
+
});
|
|
5573
|
+
}
|
|
5574
|
+
const AGENT_TOOLS = toAnthropicTools(TOOL_DEFINITIONS);
|
|
5552
5575
|
function trimText(value) {
|
|
5553
5576
|
return typeof value === "string" ? value.trim() : "";
|
|
5554
5577
|
}
|
|
@@ -6643,6 +6666,17 @@ async function describeElementForClick$1(wc, selector) {
|
|
|
6643
6666
|
};
|
|
6644
6667
|
}
|
|
6645
6668
|
async function clickResolvedSelector$1(wc, selector) {
|
|
6669
|
+
if (selector.startsWith("__vessel_idx:")) {
|
|
6670
|
+
const idx = Number(selector.slice("__vessel_idx:".length));
|
|
6671
|
+
const beforeUrl2 = wc.getURL();
|
|
6672
|
+
const result = await wc.executeJavaScript(
|
|
6673
|
+
`window.__vessel?.interactByIndex?.(${idx}, "click") || "Error: interactByIndex not available"`
|
|
6674
|
+
);
|
|
6675
|
+
if (typeof result === "string" && result.startsWith("Error")) return result;
|
|
6676
|
+
await waitForPotentialNavigation$1(wc, beforeUrl2);
|
|
6677
|
+
const afterUrl2 = wc.getURL();
|
|
6678
|
+
return afterUrl2 !== beforeUrl2 ? `${result} -> ${afterUrl2}` : result;
|
|
6679
|
+
}
|
|
6646
6680
|
const beforeUrl = wc.getURL();
|
|
6647
6681
|
const elInfo = await describeElementForClick$1(wc, selector);
|
|
6648
6682
|
if ("error" in elInfo) return `Error: ${elInfo.error}`;
|
|
@@ -6750,6 +6784,10 @@ async function dismissPopup$1(wc) {
|
|
|
6750
6784
|
document.querySelectorAll("dialog, [role='dialog'], [role='alertdialog'], [aria-modal='true']").forEach((el) => {
|
|
6751
6785
|
if (isVisible(el)) nodes.push(el);
|
|
6752
6786
|
});
|
|
6787
|
+
// Detect known consent manager containers by ID/class patterns
|
|
6788
|
+
document.querySelectorAll("#onetrust-consent-sdk, #onetrust-banner-sdk, [id*='onetrust'], [class*='onetrust'], #CybotCookiebotDialog, #truste-consent-track, [id*='cookie-banner'], [id*='consent-banner'], [class*='cookie-consent'], [class*='consent-banner'], [id*='gdpr'], [class*='gdpr']").forEach((el) => {
|
|
6789
|
+
if (el instanceof HTMLElement && isVisible(el)) nodes.push(el);
|
|
6790
|
+
});
|
|
6753
6791
|
document.querySelectorAll("body *").forEach((el) => {
|
|
6754
6792
|
if (!(el instanceof HTMLElement) || !isVisible(el)) return;
|
|
6755
6793
|
const style = window.getComputedStyle(el);
|
|
@@ -6779,14 +6817,20 @@ async function dismissPopup$1(wc) {
|
|
|
6779
6817
|
el.textContent ||
|
|
6780
6818
|
el.getAttribute("value"),
|
|
6781
6819
|
).toLowerCase();
|
|
6782
|
-
const classText = text(el.className).toLowerCase();
|
|
6820
|
+
const classText = text(typeof el.className === "string" ? el.className : "").toLowerCase();
|
|
6783
6821
|
const idText = text(el.id).toLowerCase();
|
|
6822
|
+
const combined = classText + " " + idText;
|
|
6784
6823
|
let score = rooted ? 30 : 0;
|
|
6785
6824
|
if (/^x$|^×$/.test(label)) score += 120;
|
|
6786
|
-
if (/no thanks|no, thanks|not now|maybe later|dismiss|close|skip|cancel|continue without|no thank you/.test(label)) score += 100;
|
|
6787
|
-
if (/close|dismiss|modal-close|overlay-close/.test(
|
|
6825
|
+
if (/no thanks|no, thanks|not now|maybe later|dismiss|close|skip|cancel|continue without|no thank you|reject|decline/.test(label)) score += 100;
|
|
6826
|
+
if (/close|dismiss|modal-close|overlay-close/.test(combined)) score += 90;
|
|
6827
|
+
// Known consent manager dismiss/reject buttons get a big boost
|
|
6828
|
+
if (/onetrust-close|onetrust-reject|cookie.*close|consent.*close|cookie.*reject|consent.*reject/.test(combined)) score += 110;
|
|
6829
|
+
// OneTrust "Accept" is valid for dismissing the banner (user just wants it gone)
|
|
6830
|
+
if (/onetrust-accept|cookie.*accept|consent.*accept/.test(combined)) score += 80;
|
|
6788
6831
|
if (el.getAttribute("aria-label")) score += 20;
|
|
6789
|
-
|
|
6832
|
+
// Penalize general accept/subscribe buttons that aren't consent-related
|
|
6833
|
+
if (/accept|continue|submit|sign up|subscribe|join|start|next/.test(label) && !/cookie|consent|onetrust/.test(combined)) score -= 80;
|
|
6790
6834
|
const rect = el.getBoundingClientRect();
|
|
6791
6835
|
if (rect.top < 120) score += 10;
|
|
6792
6836
|
if (rect.right > (window.innerWidth || 0) - 120) score += 15;
|
|
@@ -6802,13 +6846,26 @@ async function dismissPopup$1(wc) {
|
|
|
6802
6846
|
if (!(el instanceof HTMLElement) || !isVisible(el)) return;
|
|
6803
6847
|
const candidateSelector = selectorFor(el);
|
|
6804
6848
|
if (!candidateSelector) return;
|
|
6805
|
-
|
|
6849
|
+
var label = text(
|
|
6806
6850
|
el.getAttribute("aria-label") ||
|
|
6807
6851
|
el.getAttribute("title") ||
|
|
6808
6852
|
el.textContent ||
|
|
6809
6853
|
el.getAttribute("value"),
|
|
6810
6854
|
);
|
|
6811
|
-
|
|
6855
|
+
// Don't skip empty-label buttons from known consent managers
|
|
6856
|
+
if (!label) {
|
|
6857
|
+
var idLower = (el.id || "").toLowerCase();
|
|
6858
|
+
var classLower = (typeof el.className === "string" ? el.className : "").toLowerCase();
|
|
6859
|
+
var combined = idLower + " " + classLower;
|
|
6860
|
+
if (/onetrust|consent|cookie|banner|gdpr|trustarc|cookiebot/.test(combined)) {
|
|
6861
|
+
label = idLower.includes("accept") ? "Accept cookies"
|
|
6862
|
+
: idLower.includes("reject") ? "Reject cookies"
|
|
6863
|
+
: idLower.includes("close") || classLower.includes("close") ? "Close"
|
|
6864
|
+
: "Consent button";
|
|
6865
|
+
} else {
|
|
6866
|
+
return;
|
|
6867
|
+
}
|
|
6868
|
+
}
|
|
6812
6869
|
results.push({
|
|
6813
6870
|
selector: candidateSelector,
|
|
6814
6871
|
label: label.slice(0, 120),
|
|
@@ -6877,7 +6934,11 @@ async function resolveSelector$1(wc, index, selector) {
|
|
|
6877
6934
|
`
|
|
6878
6935
|
);
|
|
6879
6936
|
if (typeof authoritativeSelector === "string" && authoritativeSelector) {
|
|
6880
|
-
|
|
6937
|
+
const resolves = await wc.executeJavaScript(
|
|
6938
|
+
`!!document.querySelector(${JSON.stringify(authoritativeSelector)})`
|
|
6939
|
+
);
|
|
6940
|
+
if (resolves) return authoritativeSelector;
|
|
6941
|
+
return `__vessel_idx:${index}`;
|
|
6881
6942
|
}
|
|
6882
6943
|
const page = await extractContent(wc);
|
|
6883
6944
|
const extractedSelector = findSelectorByIndex(page, index);
|
|
@@ -6978,10 +7039,20 @@ function isDangerousAction$1(name) {
|
|
|
6978
7039
|
"create_tab",
|
|
6979
7040
|
"switch_tab",
|
|
6980
7041
|
"restore_checkpoint",
|
|
6981
|
-
"load_session"
|
|
7042
|
+
"load_session",
|
|
7043
|
+
"login",
|
|
7044
|
+
"fill_form",
|
|
7045
|
+
"search",
|
|
7046
|
+
"paginate"
|
|
6982
7047
|
].includes(name);
|
|
6983
7048
|
}
|
|
6984
7049
|
async function setElementValue$1(wc, selector, value) {
|
|
7050
|
+
if (selector.startsWith("__vessel_idx:")) {
|
|
7051
|
+
const idx = Number(selector.slice("__vessel_idx:".length));
|
|
7052
|
+
return wc.executeJavaScript(
|
|
7053
|
+
`window.__vessel?.interactByIndex?.(${idx}, "value", ${JSON.stringify(value)}) || "Error: interactByIndex not available"`
|
|
7054
|
+
);
|
|
7055
|
+
}
|
|
6985
7056
|
return wc.executeJavaScript(`
|
|
6986
7057
|
(function() {
|
|
6987
7058
|
const el = document.querySelector(${JSON.stringify(selector)});
|
|
@@ -7407,9 +7478,12 @@ async function getPostActionState$1(ctx, name) {
|
|
|
7407
7478
|
"click",
|
|
7408
7479
|
"submit_form",
|
|
7409
7480
|
"reload",
|
|
7410
|
-
"press_key"
|
|
7481
|
+
"press_key",
|
|
7482
|
+
"login",
|
|
7483
|
+
"search",
|
|
7484
|
+
"paginate"
|
|
7411
7485
|
];
|
|
7412
|
-
const interactActions = ["type_text", "select_option", "hover", "focus"];
|
|
7486
|
+
const interactActions = ["type_text", "select_option", "hover", "focus", "fill_form"];
|
|
7413
7487
|
const tabActions = [
|
|
7414
7488
|
"create_tab",
|
|
7415
7489
|
"switch_tab",
|
|
@@ -7479,7 +7553,12 @@ async function executeAction(name, args, ctx) {
|
|
|
7479
7553
|
"save_bookmark",
|
|
7480
7554
|
"organize_bookmark",
|
|
7481
7555
|
"archive_bookmark",
|
|
7482
|
-
"open_bookmark"
|
|
7556
|
+
"open_bookmark",
|
|
7557
|
+
"flow_start",
|
|
7558
|
+
"flow_advance",
|
|
7559
|
+
"flow_status",
|
|
7560
|
+
"flow_end",
|
|
7561
|
+
"suggest"
|
|
7483
7562
|
].includes(name)) {
|
|
7484
7563
|
return "Error: No active tab";
|
|
7485
7564
|
}
|
|
@@ -7964,12 +8043,251 @@ ${truncated}`;
|
|
|
7964
8043
|
if (!wc) return "Error: No active tab";
|
|
7965
8044
|
return clearHighlights(wc);
|
|
7966
8045
|
}
|
|
8046
|
+
// --- Speedee System ---
|
|
8047
|
+
case "flow_start": {
|
|
8048
|
+
const goal = typeof args.goal === "string" ? args.goal : "";
|
|
8049
|
+
const steps = Array.isArray(args.steps) ? args.steps.map(String) : [];
|
|
8050
|
+
if (!goal || steps.length === 0) return "Error: goal and steps are required";
|
|
8051
|
+
const flow = ctx.runtime.startFlow(goal, steps, wc?.getURL());
|
|
8052
|
+
return `Flow started: ${flow.goal}
|
|
8053
|
+
${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`;
|
|
8054
|
+
}
|
|
8055
|
+
case "flow_advance": {
|
|
8056
|
+
const flow = ctx.runtime.advanceFlow(
|
|
8057
|
+
typeof args.detail === "string" ? args.detail : void 0
|
|
8058
|
+
);
|
|
8059
|
+
if (!flow) return "No active flow to advance";
|
|
8060
|
+
return `Step completed.${ctx.runtime.getFlowContext()}`;
|
|
8061
|
+
}
|
|
8062
|
+
case "flow_status": {
|
|
8063
|
+
const flow = ctx.runtime.getFlowState();
|
|
8064
|
+
if (!flow) return "No active workflow.";
|
|
8065
|
+
return ctx.runtime.getFlowContext();
|
|
8066
|
+
}
|
|
8067
|
+
case "flow_end": {
|
|
8068
|
+
ctx.runtime.clearFlow();
|
|
8069
|
+
return "Workflow ended.";
|
|
8070
|
+
}
|
|
8071
|
+
case "suggest": {
|
|
8072
|
+
if (!wc) return "No active tab. Use navigate to open a page.";
|
|
8073
|
+
let page;
|
|
8074
|
+
try {
|
|
8075
|
+
page = await extractContent(wc);
|
|
8076
|
+
} catch {
|
|
8077
|
+
return "Could not read page. Try navigate to a working URL.";
|
|
8078
|
+
}
|
|
8079
|
+
const suggestions = [];
|
|
8080
|
+
suggestions.push(`Page: ${page.title || "(untitled)"}`);
|
|
8081
|
+
suggestions.push(`URL: ${page.url}`);
|
|
8082
|
+
suggestions.push("");
|
|
8083
|
+
const flowCtx2 = ctx.runtime.getFlowContext();
|
|
8084
|
+
if (flowCtx2) {
|
|
8085
|
+
suggestions.push(flowCtx2);
|
|
8086
|
+
suggestions.push("");
|
|
8087
|
+
}
|
|
8088
|
+
const hasPasswordField = page.forms.some(
|
|
8089
|
+
(f) => f.fields.some((el) => el.inputType === "password")
|
|
8090
|
+
);
|
|
8091
|
+
const hasSearchInput = page.interactiveElements.some(
|
|
8092
|
+
(el) => el.inputType === "search" || el.name === "q" || el.name === "query" || (el.placeholder || "").toLowerCase().includes("search")
|
|
8093
|
+
);
|
|
8094
|
+
const formCount = page.forms.length;
|
|
8095
|
+
const totalFields = page.forms.reduce((n, f) => n + f.fields.length, 0);
|
|
8096
|
+
const linkCount = page.interactiveElements.filter((el) => el.type === "link").length;
|
|
8097
|
+
const hasPagination = page.interactiveElements.some(
|
|
8098
|
+
(el) => (el.text || "").toLowerCase() === "next" || el.text === "›" || el.text === "»"
|
|
8099
|
+
);
|
|
8100
|
+
const hasOverlays = page.overlays.some((o) => o.blocksInteraction);
|
|
8101
|
+
if (hasOverlays) {
|
|
8102
|
+
suggestions.push("BLOCKING OVERLAY detected — dismiss it first:");
|
|
8103
|
+
suggestions.push(" → dismiss_popup or click on close/accept button");
|
|
8104
|
+
suggestions.push("");
|
|
8105
|
+
}
|
|
8106
|
+
if (hasPasswordField) {
|
|
8107
|
+
suggestions.push("LOGIN PAGE detected:");
|
|
8108
|
+
suggestions.push(" → login(username, password) — handles the full flow");
|
|
8109
|
+
suggestions.push(" → Or fill_form + submit_form for manual control");
|
|
8110
|
+
} else if (hasSearchInput && linkCount < 10) {
|
|
8111
|
+
suggestions.push("SEARCH PAGE detected:");
|
|
8112
|
+
suggestions.push(" → search(query) — finds the box, types, submits");
|
|
8113
|
+
} else if (hasSearchInput && linkCount >= 10) {
|
|
8114
|
+
suggestions.push("SEARCH RESULTS detected:");
|
|
8115
|
+
suggestions.push(" → click on a result link");
|
|
8116
|
+
if (hasPagination) suggestions.push(" → paginate('next') for more results");
|
|
8117
|
+
} else if (formCount > 0) {
|
|
8118
|
+
suggestions.push(`FORM detected (${totalFields} fields):`);
|
|
8119
|
+
suggestions.push(" → fill_form(fields) — fill all fields at once");
|
|
8120
|
+
} else if (hasPagination) {
|
|
8121
|
+
suggestions.push("PAGINATED CONTENT:");
|
|
8122
|
+
suggestions.push(" → read_page to read this page");
|
|
8123
|
+
suggestions.push(" → paginate('next') for the next page");
|
|
8124
|
+
} else if (page.content.length > 3e3 && page.interactiveElements.length < 10) {
|
|
8125
|
+
suggestions.push("ARTICLE/CONTENT page:");
|
|
8126
|
+
suggestions.push(" → read_page for readable text");
|
|
8127
|
+
suggestions.push(" → scroll to see more");
|
|
8128
|
+
} else {
|
|
8129
|
+
suggestions.push("GENERAL PAGE:");
|
|
8130
|
+
suggestions.push(" → read_page to understand the page structure");
|
|
8131
|
+
suggestions.push(" → click on any element by index");
|
|
8132
|
+
suggestions.push(" → navigate to go somewhere new");
|
|
8133
|
+
}
|
|
8134
|
+
suggestions.push("");
|
|
8135
|
+
suggestions.push(`Available: ${page.interactiveElements.length} interactive elements, ${formCount} forms, ${linkCount} links`);
|
|
8136
|
+
return suggestions.join("\n");
|
|
8137
|
+
}
|
|
8138
|
+
case "fill_form": {
|
|
8139
|
+
if (!wc) return "Error: No active tab";
|
|
8140
|
+
const fields = Array.isArray(args.fields) ? args.fields : [];
|
|
8141
|
+
if (fields.length === 0) return "Error: No fields provided";
|
|
8142
|
+
const results = [];
|
|
8143
|
+
for (const field of fields) {
|
|
8144
|
+
const sel = await resolveSelector$1(wc, field.index, field.selector);
|
|
8145
|
+
if (!sel) {
|
|
8146
|
+
results.push(`Skipped: no selector for index=${field.index}`);
|
|
8147
|
+
continue;
|
|
8148
|
+
}
|
|
8149
|
+
const result2 = await setElementValue$1(wc, sel, String(field.value || ""));
|
|
8150
|
+
results.push(result2);
|
|
8151
|
+
}
|
|
8152
|
+
if (args.submit) {
|
|
8153
|
+
const firstSel = await resolveSelector$1(wc, fields[0]?.index, fields[0]?.selector);
|
|
8154
|
+
if (firstSel) {
|
|
8155
|
+
const beforeUrl = wc.getURL();
|
|
8156
|
+
const submitResult = await submitForm$1(wc, { selector: firstSel });
|
|
8157
|
+
await waitForPotentialNavigation$1(wc, beforeUrl);
|
|
8158
|
+
const afterUrl = wc.getURL();
|
|
8159
|
+
results.push(
|
|
8160
|
+
afterUrl !== beforeUrl ? `Submitted → ${afterUrl}` : submitResult
|
|
8161
|
+
);
|
|
8162
|
+
}
|
|
8163
|
+
}
|
|
8164
|
+
return `Filled ${results.length} field(s):
|
|
8165
|
+
${results.join("\n")}`;
|
|
8166
|
+
}
|
|
8167
|
+
case "login": {
|
|
8168
|
+
if (!wc) return "Error: No active tab";
|
|
8169
|
+
const steps = [];
|
|
8170
|
+
if (typeof args.url === "string" && args.url.trim()) {
|
|
8171
|
+
const id = ctx.tabManager.getActiveTabId();
|
|
8172
|
+
ctx.tabManager.navigateTab(id, args.url);
|
|
8173
|
+
await waitForLoad$1(wc);
|
|
8174
|
+
steps.push(`Navigated to ${wc.getURL()}`);
|
|
8175
|
+
}
|
|
8176
|
+
const userSel = args.username_selector || await wc.executeJavaScript(`
|
|
8177
|
+
(function() {
|
|
8178
|
+
var el = document.querySelector('input[type="email"], input[name="email"], input[name="username"], input[name="user"], input[autocomplete="username"], input[autocomplete="email"], input[type="text"]:not([name="search"]):not([name="q"])');
|
|
8179
|
+
return el ? (el.id ? '#' + CSS.escape(el.id) : el.name ? 'input[name="' + el.name + '"]' : null) : null;
|
|
8180
|
+
})()
|
|
8181
|
+
`);
|
|
8182
|
+
if (!userSel) return "Error: Could not find username/email field. Try providing username_selector.";
|
|
8183
|
+
const passSel = args.password_selector || await wc.executeJavaScript(`
|
|
8184
|
+
(function() {
|
|
8185
|
+
var el = document.querySelector('input[type="password"]');
|
|
8186
|
+
return el ? (el.id ? '#' + CSS.escape(el.id) : el.name ? 'input[name="' + el.name + '"]' : null) : null;
|
|
8187
|
+
})()
|
|
8188
|
+
`);
|
|
8189
|
+
if (!passSel) return "Error: Could not find password field. Try providing password_selector.";
|
|
8190
|
+
const userResult = await setElementValue$1(wc, userSel, String(args.username || ""));
|
|
8191
|
+
steps.push(userResult);
|
|
8192
|
+
const passResult = await setElementValue$1(wc, passSel, String(args.password || ""));
|
|
8193
|
+
steps.push(passResult);
|
|
8194
|
+
const beforeUrl = wc.getURL();
|
|
8195
|
+
if (args.submit_selector) {
|
|
8196
|
+
await clickResolvedSelector$1(wc, args.submit_selector);
|
|
8197
|
+
} else {
|
|
8198
|
+
const clicked = await wc.executeJavaScript(`
|
|
8199
|
+
(function() {
|
|
8200
|
+
var btn = document.querySelector('button[type="submit"], input[type="submit"], form button:not([type="button"])');
|
|
8201
|
+
if (btn) { btn.click(); return true; }
|
|
8202
|
+
var form = document.querySelector('input[type="password"]')?.closest('form');
|
|
8203
|
+
if (form) { form.requestSubmit ? form.requestSubmit() : form.submit(); return true; }
|
|
8204
|
+
return false;
|
|
8205
|
+
})()
|
|
8206
|
+
`);
|
|
8207
|
+
if (!clicked) return steps.join("\n") + "\nWarning: Could not find submit button. Credentials filled but form not submitted.";
|
|
8208
|
+
}
|
|
8209
|
+
await waitForPotentialNavigation$1(wc, beforeUrl);
|
|
8210
|
+
const afterUrl = wc.getURL();
|
|
8211
|
+
steps.push(
|
|
8212
|
+
afterUrl !== beforeUrl ? `Submitted → ${afterUrl}` : "Form submitted (same page)"
|
|
8213
|
+
);
|
|
8214
|
+
return `Login flow complete:
|
|
8215
|
+
${steps.join("\n")}`;
|
|
8216
|
+
}
|
|
8217
|
+
case "search": {
|
|
8218
|
+
if (!wc) return "Error: No active tab";
|
|
8219
|
+
const searchSel = args.selector || await wc.executeJavaScript(`
|
|
8220
|
+
(function() {
|
|
8221
|
+
var el = document.querySelector('input[type="search"], input[name="q"], input[name="query"], input[name="search"], input[role="searchbox"], input[aria-label*="search" i], input[placeholder*="search" i]');
|
|
8222
|
+
if (!el) {
|
|
8223
|
+
var inputs = document.querySelectorAll('input[type="text"]');
|
|
8224
|
+
for (var i = 0; i < inputs.length; i++) {
|
|
8225
|
+
var form = inputs[i].closest('form');
|
|
8226
|
+
if (form && (form.getAttribute('role') === 'search' || form.action?.includes('search'))) {
|
|
8227
|
+
el = inputs[i];
|
|
8228
|
+
break;
|
|
8229
|
+
}
|
|
8230
|
+
}
|
|
8231
|
+
}
|
|
8232
|
+
return el ? (el.id ? '#' + CSS.escape(el.id) : el.name ? 'input[name="' + el.name + '"]' : null) : null;
|
|
8233
|
+
})()
|
|
8234
|
+
`);
|
|
8235
|
+
if (!searchSel) return "Error: Could not find search input. Try providing a selector.";
|
|
8236
|
+
await setElementValue$1(wc, searchSel, String(args.query || ""));
|
|
8237
|
+
await wc.executeJavaScript(`
|
|
8238
|
+
(function() {
|
|
8239
|
+
var el = document.querySelector(${JSON.stringify(searchSel)});
|
|
8240
|
+
if (el) el.focus();
|
|
8241
|
+
})()
|
|
8242
|
+
`);
|
|
8243
|
+
await sleep$1(50);
|
|
8244
|
+
const beforeUrl = wc.getURL();
|
|
8245
|
+
wc.sendInputEvent({ type: "keyDown", keyCode: "Return" });
|
|
8246
|
+
await sleep$1(16);
|
|
8247
|
+
wc.sendInputEvent({ type: "keyUp", keyCode: "Return" });
|
|
8248
|
+
await waitForPotentialNavigation$1(wc, beforeUrl);
|
|
8249
|
+
const afterUrl = wc.getURL();
|
|
8250
|
+
return afterUrl !== beforeUrl ? `Searched "${args.query}" → ${afterUrl}` : `Searched "${args.query}" (same page — results may have loaded dynamically)`;
|
|
8251
|
+
}
|
|
8252
|
+
case "paginate": {
|
|
8253
|
+
if (!wc) return "Error: No active tab";
|
|
8254
|
+
const beforeUrl = wc.getURL();
|
|
8255
|
+
if (args.selector) {
|
|
8256
|
+
return clickResolvedSelector$1(wc, args.selector);
|
|
8257
|
+
}
|
|
8258
|
+
const isNext = args.direction === "next";
|
|
8259
|
+
const clicked = await wc.executeJavaScript(`
|
|
8260
|
+
(function() {
|
|
8261
|
+
var patterns = ${isNext ? '["next", "Next", "›", "»", "→", ">", "Next Page", "Load More"]' : '["prev", "Prev", "Previous", "‹", "«", "←", "<", "Previous Page"]'};
|
|
8262
|
+
var links = document.querySelectorAll('a, button');
|
|
8263
|
+
for (var i = 0; i < links.length; i++) {
|
|
8264
|
+
var el = links[i];
|
|
8265
|
+
var text = (el.textContent || '').trim();
|
|
8266
|
+
var ariaLabel = (el.getAttribute('aria-label') || '').toLowerCase();
|
|
8267
|
+
var rel = (el.getAttribute('rel') || '').toLowerCase();
|
|
8268
|
+
if (rel === '${isNext ? "next" : "prev"}') { el.click(); return true; }
|
|
8269
|
+
for (var j = 0; j < patterns.length; j++) {
|
|
8270
|
+
if (text === patterns[j] || ariaLabel.includes(patterns[j].toLowerCase())) {
|
|
8271
|
+
el.click();
|
|
8272
|
+
return true;
|
|
8273
|
+
}
|
|
8274
|
+
}
|
|
8275
|
+
}
|
|
8276
|
+
return false;
|
|
8277
|
+
})()
|
|
8278
|
+
`);
|
|
8279
|
+
if (!clicked) return `Error: Could not find ${args.direction} pagination control. Try providing a selector.`;
|
|
8280
|
+
await waitForPotentialNavigation$1(wc, beforeUrl);
|
|
8281
|
+
const afterUrl = wc.getURL();
|
|
8282
|
+
return afterUrl !== beforeUrl ? `Paginated ${args.direction} → ${afterUrl}` : `Clicked ${args.direction} (page may have updated dynamically)`;
|
|
8283
|
+
}
|
|
7967
8284
|
default:
|
|
7968
8285
|
return `Unknown tool: ${name}`;
|
|
7969
8286
|
}
|
|
7970
8287
|
}
|
|
7971
8288
|
});
|
|
7972
|
-
|
|
8289
|
+
const flowCtx = ctx.runtime.getFlowContext();
|
|
8290
|
+
return result + await getPostActionState$1(ctx, name) + flowCtx;
|
|
7973
8291
|
}
|
|
7974
8292
|
async function handleAIQuery(query, provider, activeWebContents, onChunk, onEnd, tabManager, runtime, history) {
|
|
7975
8293
|
const lowerQuery = query.toLowerCase().trim();
|
|
@@ -7981,8 +8299,20 @@ async function handleAIQuery(query, provider, activeWebContents, onChunk, onEnd,
|
|
|
7981
8299
|
const truncated = pageContent.content.length > 2e4 ? pageContent.content.slice(0, 2e4) + "\n[Content truncated...]" : pageContent.content;
|
|
7982
8300
|
const runtimeState = runtime.getState();
|
|
7983
8301
|
const recentCheckpoints = runtimeState.checkpoints.slice(-3).map((item) => `- ${item.name} (${item.id})`).join("\n");
|
|
8302
|
+
const activeTabTitle = pageContent.title || "(untitled)";
|
|
8303
|
+
const activeTabUrl = pageContent.url || activeWebContents.getURL();
|
|
8304
|
+
const allTabs = tabManager.getAllStates();
|
|
8305
|
+
const activeTabId = tabManager.getActiveTabId();
|
|
8306
|
+
const tabSummary = allTabs.length > 1 ? `
|
|
8307
|
+
All open tabs: ${allTabs.map((t) => `${t.id === activeTabId ? "→ " : ""}${t.title || "New Tab"} (${t.url})`).join(" | ")}` : "";
|
|
7984
8308
|
const systemPrompt = `You are Vessel, an AI agent embedded in a web browser. You can see the current page and interact with it using tools.
|
|
7985
8309
|
|
|
8310
|
+
THE USER IS CURRENTLY LOOKING AT:
|
|
8311
|
+
Title: ${activeTabTitle}
|
|
8312
|
+
URL: ${activeTabUrl}${tabSummary}
|
|
8313
|
+
|
|
8314
|
+
When the user says "this page", "this article", "this site", or asks about what they're viewing, they mean the page above. The content below is from that page — answer directly without needing to call read_page or current_tab first.
|
|
8315
|
+
|
|
7986
8316
|
Current page context:
|
|
7987
8317
|
${structuredContext}
|
|
7988
8318
|
|
|
@@ -8013,7 +8343,9 @@ Instructions:
|
|
|
8013
8343
|
- If the page context reports a rate limit, human verification, or access warning, stop using that page and switch to a different source.
|
|
8014
8344
|
- Reference interactive elements by their index number (shown as [#N] in the listings above).
|
|
8015
8345
|
- Be concise. Explain what you're doing as you go.
|
|
8016
|
-
- For simple questions about the page, just answer directly without using tools
|
|
8346
|
+
- For simple questions about the page, just answer directly without using tools.
|
|
8347
|
+
- You have a highlight tool that visually marks elements on the page for the user. Use it when the user asks you to highlight, mark, or draw attention to specific content. Colors: yellow (default), red (errors), green (success), blue (info), purple (important), orange (warnings).
|
|
8348
|
+
- After completing a task or answering a question, offer 1-2 brief, natural follow-up suggestions that make sense in context (e.g. "Want me to highlight any of these?" or "I can save these to a bookmark folder if you'd like"). Keep suggestions short and conversational — don't list every possible action.`;
|
|
8017
8349
|
const actionCtx = { tabManager, runtime };
|
|
8018
8350
|
await provider.streamAgentQuery(
|
|
8019
8351
|
systemPrompt,
|
|
@@ -9130,6 +9462,17 @@ async function describeElementForClick(wc, selector) {
|
|
|
9130
9462
|
};
|
|
9131
9463
|
}
|
|
9132
9464
|
async function clickResolvedSelector(wc, selector) {
|
|
9465
|
+
if (selector.startsWith("__vessel_idx:")) {
|
|
9466
|
+
const idx = Number(selector.slice("__vessel_idx:".length));
|
|
9467
|
+
const beforeUrl2 = wc.getURL();
|
|
9468
|
+
const result = await wc.executeJavaScript(
|
|
9469
|
+
`window.__vessel?.interactByIndex?.(${idx}, "click") || "Error: interactByIndex not available"`
|
|
9470
|
+
);
|
|
9471
|
+
if (typeof result === "string" && result.startsWith("Error")) return result;
|
|
9472
|
+
await waitForPotentialNavigation(wc, beforeUrl2);
|
|
9473
|
+
const afterUrl2 = wc.getURL();
|
|
9474
|
+
return afterUrl2 !== beforeUrl2 ? `${result} -> ${afterUrl2}` : result;
|
|
9475
|
+
}
|
|
9133
9476
|
const beforeUrl = wc.getURL();
|
|
9134
9477
|
const elInfo = await describeElementForClick(wc, selector);
|
|
9135
9478
|
if ("error" in elInfo) return `Error: ${elInfo.error}`;
|
|
@@ -9237,6 +9580,10 @@ async function dismissPopup(wc) {
|
|
|
9237
9580
|
document.querySelectorAll("dialog, [role='dialog'], [role='alertdialog'], [aria-modal='true']").forEach((el) => {
|
|
9238
9581
|
if (isVisible(el)) nodes.push(el);
|
|
9239
9582
|
});
|
|
9583
|
+
// Detect known consent manager containers
|
|
9584
|
+
document.querySelectorAll("#onetrust-consent-sdk, #onetrust-banner-sdk, [id*='onetrust'], [class*='onetrust'], #CybotCookiebotDialog, #truste-consent-track, [id*='cookie-banner'], [id*='consent-banner'], [class*='cookie-consent'], [class*='consent-banner'], [id*='gdpr'], [class*='gdpr']").forEach((el) => {
|
|
9585
|
+
if (el instanceof HTMLElement && isVisible(el)) nodes.push(el);
|
|
9586
|
+
});
|
|
9240
9587
|
document.querySelectorAll("body *").forEach((el) => {
|
|
9241
9588
|
if (!(el instanceof HTMLElement) || !isVisible(el)) return;
|
|
9242
9589
|
const style = window.getComputedStyle(el);
|
|
@@ -9266,14 +9613,17 @@ async function dismissPopup(wc) {
|
|
|
9266
9613
|
el.textContent ||
|
|
9267
9614
|
el.getAttribute("value"),
|
|
9268
9615
|
).toLowerCase();
|
|
9269
|
-
const classText = text(el.className).toLowerCase();
|
|
9616
|
+
const classText = text(typeof el.className === "string" ? el.className : "").toLowerCase();
|
|
9270
9617
|
const idText = text(el.id).toLowerCase();
|
|
9618
|
+
const combined = classText + " " + idText;
|
|
9271
9619
|
let score = rooted ? 30 : 0;
|
|
9272
9620
|
if (/^x$|^×$/.test(label)) score += 120;
|
|
9273
|
-
if (/no thanks|no, thanks|not now|maybe later|dismiss|close|skip|cancel|continue without|no thank you/.test(label)) score += 100;
|
|
9274
|
-
if (/close|dismiss|modal-close|overlay-close/.test(
|
|
9621
|
+
if (/no thanks|no, thanks|not now|maybe later|dismiss|close|skip|cancel|continue without|no thank you|reject|decline/.test(label)) score += 100;
|
|
9622
|
+
if (/close|dismiss|modal-close|overlay-close/.test(combined)) score += 90;
|
|
9623
|
+
if (/onetrust-close|onetrust-reject|cookie.*close|consent.*close|cookie.*reject|consent.*reject/.test(combined)) score += 110;
|
|
9624
|
+
if (/onetrust-accept|cookie.*accept|consent.*accept/.test(combined)) score += 80;
|
|
9275
9625
|
if (el.getAttribute("aria-label")) score += 20;
|
|
9276
|
-
if (/accept|continue|submit|sign up|subscribe|join|start|next/.test(label)) score -= 80;
|
|
9626
|
+
if (/accept|continue|submit|sign up|subscribe|join|start|next/.test(label) && !/cookie|consent|onetrust/.test(combined)) score -= 80;
|
|
9277
9627
|
const rect = el.getBoundingClientRect();
|
|
9278
9628
|
if (rect.top < 120) score += 10;
|
|
9279
9629
|
if (rect.right > (window.innerWidth || 0) - 120) score += 15;
|
|
@@ -9289,13 +9639,25 @@ async function dismissPopup(wc) {
|
|
|
9289
9639
|
if (!(el instanceof HTMLElement) || !isVisible(el)) return;
|
|
9290
9640
|
const candidateSelector = selectorFor(el);
|
|
9291
9641
|
if (!candidateSelector) return;
|
|
9292
|
-
|
|
9642
|
+
var label = text(
|
|
9293
9643
|
el.getAttribute("aria-label") ||
|
|
9294
9644
|
el.getAttribute("title") ||
|
|
9295
9645
|
el.textContent ||
|
|
9296
9646
|
el.getAttribute("value"),
|
|
9297
9647
|
);
|
|
9298
|
-
if (!label)
|
|
9648
|
+
if (!label) {
|
|
9649
|
+
var idLower = (el.id || "").toLowerCase();
|
|
9650
|
+
var classLower = (typeof el.className === "string" ? el.className : "").toLowerCase();
|
|
9651
|
+
var combined = idLower + " " + classLower;
|
|
9652
|
+
if (/onetrust|consent|cookie|banner|gdpr|trustarc|cookiebot/.test(combined)) {
|
|
9653
|
+
label = idLower.includes("accept") ? "Accept cookies"
|
|
9654
|
+
: idLower.includes("reject") ? "Reject cookies"
|
|
9655
|
+
: idLower.includes("close") || classLower.includes("close") ? "Close"
|
|
9656
|
+
: "Consent button";
|
|
9657
|
+
} else {
|
|
9658
|
+
return;
|
|
9659
|
+
}
|
|
9660
|
+
}
|
|
9299
9661
|
results.push({
|
|
9300
9662
|
selector: candidateSelector,
|
|
9301
9663
|
label: label.slice(0, 120),
|
|
@@ -9362,7 +9724,11 @@ function isDangerousAction(name) {
|
|
|
9362
9724
|
"create_tab",
|
|
9363
9725
|
"switch_tab",
|
|
9364
9726
|
"close_tab",
|
|
9365
|
-
"restore_checkpoint"
|
|
9727
|
+
"restore_checkpoint",
|
|
9728
|
+
"login",
|
|
9729
|
+
"fill_form",
|
|
9730
|
+
"search",
|
|
9731
|
+
"paginate"
|
|
9366
9732
|
].includes(name);
|
|
9367
9733
|
}
|
|
9368
9734
|
function getTabByMatch(tabManager, match) {
|
|
@@ -9439,7 +9805,8 @@ async function withAction(runtime, tabManager, name, args, executor) {
|
|
|
9439
9805
|
executor
|
|
9440
9806
|
});
|
|
9441
9807
|
const stateInfo = await getPostActionState(tabManager, name);
|
|
9442
|
-
|
|
9808
|
+
const flowCtx = runtime.getFlowContext();
|
|
9809
|
+
return asTextResponse(result + stateInfo + flowCtx);
|
|
9443
9810
|
} catch (error) {
|
|
9444
9811
|
return asTextResponse(
|
|
9445
9812
|
`Error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
@@ -9447,6 +9814,12 @@ async function withAction(runtime, tabManager, name, args, executor) {
|
|
|
9447
9814
|
}
|
|
9448
9815
|
}
|
|
9449
9816
|
async function setElementValue(wc, selector, value) {
|
|
9817
|
+
if (selector.startsWith("__vessel_idx:")) {
|
|
9818
|
+
const idx = Number(selector.slice("__vessel_idx:".length));
|
|
9819
|
+
return wc.executeJavaScript(
|
|
9820
|
+
`window.__vessel?.interactByIndex?.(${idx}, "value", ${JSON.stringify(value)}) || "Error: interactByIndex not available"`
|
|
9821
|
+
);
|
|
9822
|
+
}
|
|
9450
9823
|
return wc.executeJavaScript(`
|
|
9451
9824
|
(function() {
|
|
9452
9825
|
const el = document.querySelector(${JSON.stringify(selector)});
|
|
@@ -11794,6 +12167,382 @@ ${JSON.stringify(otherHighlights, null, 2)}`
|
|
|
11794
12167
|
);
|
|
11795
12168
|
}
|
|
11796
12169
|
);
|
|
12170
|
+
server.registerTool(
|
|
12171
|
+
"vessel_flow_start",
|
|
12172
|
+
{
|
|
12173
|
+
title: "Start Workflow",
|
|
12174
|
+
description: "Begin tracking a multi-step web workflow. Vessel will show progress after every action so you always know where you are in the flow.",
|
|
12175
|
+
inputSchema: {
|
|
12176
|
+
goal: zod.z.string().describe("What this workflow accomplishes (e.g. 'Purchase item from Amazon')"),
|
|
12177
|
+
steps: zod.z.array(zod.z.string()).describe("Ordered list of step labels (e.g. ['Log in', 'Search', 'Select item', 'Checkout'])")
|
|
12178
|
+
}
|
|
12179
|
+
},
|
|
12180
|
+
async ({ goal, steps }) => {
|
|
12181
|
+
const tab = tabManager.getActiveTab();
|
|
12182
|
+
const flow = runtime.startFlow(goal, steps, tab?.view.webContents.getURL());
|
|
12183
|
+
return asTextResponse(
|
|
12184
|
+
`Flow started: ${flow.goal}
|
|
12185
|
+
${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
|
|
12186
|
+
);
|
|
12187
|
+
}
|
|
12188
|
+
);
|
|
12189
|
+
server.registerTool(
|
|
12190
|
+
"vessel_flow_advance",
|
|
12191
|
+
{
|
|
12192
|
+
title: "Advance Workflow Step",
|
|
12193
|
+
description: "Mark the current workflow step as done and move to the next one. Call this after completing each step.",
|
|
12194
|
+
inputSchema: {
|
|
12195
|
+
detail: zod.z.string().optional().describe("Brief note about what was accomplished")
|
|
12196
|
+
}
|
|
12197
|
+
},
|
|
12198
|
+
async ({ detail }) => {
|
|
12199
|
+
const flow = runtime.advanceFlow(detail);
|
|
12200
|
+
if (!flow) return asTextResponse("No active flow to advance");
|
|
12201
|
+
const ctx = runtime.getFlowContext();
|
|
12202
|
+
return asTextResponse(`Step completed.${ctx}`);
|
|
12203
|
+
}
|
|
12204
|
+
);
|
|
12205
|
+
server.registerTool(
|
|
12206
|
+
"vessel_flow_status",
|
|
12207
|
+
{
|
|
12208
|
+
title: "Workflow Status",
|
|
12209
|
+
description: "Check the current workflow progress."
|
|
12210
|
+
},
|
|
12211
|
+
async () => {
|
|
12212
|
+
const flow = runtime.getFlowState();
|
|
12213
|
+
if (!flow) return asTextResponse("No active workflow.");
|
|
12214
|
+
return asTextResponse(runtime.getFlowContext());
|
|
12215
|
+
}
|
|
12216
|
+
);
|
|
12217
|
+
server.registerTool(
|
|
12218
|
+
"vessel_flow_end",
|
|
12219
|
+
{
|
|
12220
|
+
title: "End Workflow",
|
|
12221
|
+
description: "Clear the active workflow tracker."
|
|
12222
|
+
},
|
|
12223
|
+
async () => {
|
|
12224
|
+
runtime.clearFlow();
|
|
12225
|
+
return asTextResponse("Workflow ended.");
|
|
12226
|
+
}
|
|
12227
|
+
);
|
|
12228
|
+
server.registerTool(
|
|
12229
|
+
"vessel_suggest",
|
|
12230
|
+
{
|
|
12231
|
+
title: "What Should I Do?",
|
|
12232
|
+
description: "Analyze the current page and return the most relevant tools and suggested next actions. Call this when you're unsure what to do next — it reads the page context and tells you the optimal approach."
|
|
12233
|
+
},
|
|
12234
|
+
async () => {
|
|
12235
|
+
const tab = tabManager.getActiveTab();
|
|
12236
|
+
if (!tab) return asTextResponse("No active tab. Use vessel_navigate to open a page.");
|
|
12237
|
+
const wc = tab.view.webContents;
|
|
12238
|
+
let page;
|
|
12239
|
+
try {
|
|
12240
|
+
page = await extractContent(wc);
|
|
12241
|
+
} catch {
|
|
12242
|
+
return asTextResponse("Could not read page. Try vessel_navigate to a working URL.");
|
|
12243
|
+
}
|
|
12244
|
+
const suggestions = [];
|
|
12245
|
+
suggestions.push(`Page: ${page.title || "(untitled)"}`);
|
|
12246
|
+
suggestions.push(`URL: ${page.url}`);
|
|
12247
|
+
suggestions.push("");
|
|
12248
|
+
const flowCtx = runtime.getFlowContext();
|
|
12249
|
+
if (flowCtx) {
|
|
12250
|
+
suggestions.push(flowCtx);
|
|
12251
|
+
suggestions.push("");
|
|
12252
|
+
}
|
|
12253
|
+
page.url.toLowerCase();
|
|
12254
|
+
const hasPasswordField = page.forms.some(
|
|
12255
|
+
(f) => f.fields.some((el) => el.inputType === "password")
|
|
12256
|
+
);
|
|
12257
|
+
const hasSearchInput = page.interactiveElements.some(
|
|
12258
|
+
(el) => el.inputType === "search" || el.name === "q" || el.name === "query" || (el.placeholder || "").toLowerCase().includes("search")
|
|
12259
|
+
);
|
|
12260
|
+
const formCount = page.forms.length;
|
|
12261
|
+
const totalFields = page.forms.reduce((n, f) => n + f.fields.length, 0);
|
|
12262
|
+
const linkCount = page.interactiveElements.filter((el) => el.type === "link").length;
|
|
12263
|
+
const hasPagination = page.interactiveElements.some(
|
|
12264
|
+
(el) => (el.text || "").toLowerCase() === "next" || el.text === "›" || el.text === "»"
|
|
12265
|
+
);
|
|
12266
|
+
const hasOverlays = page.overlays.some((o) => o.blocksInteraction);
|
|
12267
|
+
if (hasOverlays) {
|
|
12268
|
+
suggestions.push("⚠ BLOCKING OVERLAY detected — dismiss it first:");
|
|
12269
|
+
suggestions.push(" → vessel_dismiss_popup or vessel_click on close/accept button");
|
|
12270
|
+
suggestions.push("");
|
|
12271
|
+
}
|
|
12272
|
+
if (hasPasswordField) {
|
|
12273
|
+
suggestions.push("🔑 LOGIN PAGE detected:");
|
|
12274
|
+
suggestions.push(" → vessel_login(username, password) — handles the full flow");
|
|
12275
|
+
suggestions.push(" → Or vessel_fill_form + vessel_submit_form for manual control");
|
|
12276
|
+
} else if (hasSearchInput && linkCount < 10) {
|
|
12277
|
+
suggestions.push("🔍 SEARCH PAGE detected:");
|
|
12278
|
+
suggestions.push(" → vessel_search(query) — finds the box, types, submits");
|
|
12279
|
+
} else if (hasSearchInput && linkCount >= 10) {
|
|
12280
|
+
suggestions.push("📋 SEARCH RESULTS detected:");
|
|
12281
|
+
suggestions.push(" → vessel_click on a result link");
|
|
12282
|
+
if (hasPagination) {
|
|
12283
|
+
suggestions.push(" → vessel_paginate('next') for more results");
|
|
12284
|
+
}
|
|
12285
|
+
} else if (formCount > 0) {
|
|
12286
|
+
suggestions.push(`📝 FORM detected (${totalFields} fields):`);
|
|
12287
|
+
suggestions.push(" → vessel_fill_form(fields) — fill all fields at once");
|
|
12288
|
+
suggestions.push(" → Or vessel_type for individual fields");
|
|
12289
|
+
} else if (hasPagination) {
|
|
12290
|
+
suggestions.push("📄 PAGINATED CONTENT:");
|
|
12291
|
+
suggestions.push(" → vessel_extract_content to read this page");
|
|
12292
|
+
suggestions.push(" → vessel_paginate('next') for the next page");
|
|
12293
|
+
} else if (page.content.length > 3e3 && page.interactiveElements.length < 10) {
|
|
12294
|
+
suggestions.push("📖 ARTICLE/CONTENT page:");
|
|
12295
|
+
suggestions.push(" → vessel_extract_content for readable text");
|
|
12296
|
+
suggestions.push(" → vessel_scroll to see more");
|
|
12297
|
+
} else {
|
|
12298
|
+
suggestions.push("🌐 GENERAL PAGE:");
|
|
12299
|
+
suggestions.push(" → vessel_extract_content to understand the page structure");
|
|
12300
|
+
suggestions.push(" → vessel_click on any element by index");
|
|
12301
|
+
suggestions.push(" → vessel_navigate to go somewhere new");
|
|
12302
|
+
}
|
|
12303
|
+
suggestions.push("");
|
|
12304
|
+
suggestions.push(`Available: ${page.interactiveElements.length} interactive elements, ${formCount} forms, ${linkCount} links`);
|
|
12305
|
+
return asTextResponse(suggestions.join("\n"));
|
|
12306
|
+
}
|
|
12307
|
+
);
|
|
12308
|
+
server.registerTool(
|
|
12309
|
+
"vessel_fill_form",
|
|
12310
|
+
{
|
|
12311
|
+
title: "Fill Form",
|
|
12312
|
+
description: "Fill multiple form fields at once. Provide a map of field identifiers to values. Fields are matched by index, name, label, or placeholder. Much faster than calling type for each field individually.",
|
|
12313
|
+
inputSchema: {
|
|
12314
|
+
fields: zod.z.array(
|
|
12315
|
+
zod.z.object({
|
|
12316
|
+
index: zod.z.number().optional().describe("Element index from page content"),
|
|
12317
|
+
selector: zod.z.string().optional().describe("CSS selector fallback"),
|
|
12318
|
+
value: zod.z.string().describe("Value to enter")
|
|
12319
|
+
})
|
|
12320
|
+
).describe("Fields to fill"),
|
|
12321
|
+
submit: zod.z.boolean().optional().describe("Submit the form after filling (default false)")
|
|
12322
|
+
}
|
|
12323
|
+
},
|
|
12324
|
+
async ({ fields, submit }) => {
|
|
12325
|
+
const tab = tabManager.getActiveTab();
|
|
12326
|
+
if (!tab) return asTextResponse("Error: No active tab");
|
|
12327
|
+
return withAction(
|
|
12328
|
+
runtime,
|
|
12329
|
+
tabManager,
|
|
12330
|
+
"fill_form",
|
|
12331
|
+
{ fieldCount: fields.length, submit },
|
|
12332
|
+
async () => {
|
|
12333
|
+
const wc = tab.view.webContents;
|
|
12334
|
+
const results = [];
|
|
12335
|
+
for (const field of fields) {
|
|
12336
|
+
const sel = await resolveSelector(wc, field.index, field.selector);
|
|
12337
|
+
if (!sel) {
|
|
12338
|
+
results.push(`Skipped: no selector for index=${field.index}`);
|
|
12339
|
+
continue;
|
|
12340
|
+
}
|
|
12341
|
+
const result = await setElementValue(wc, sel, field.value);
|
|
12342
|
+
results.push(result);
|
|
12343
|
+
}
|
|
12344
|
+
if (submit) {
|
|
12345
|
+
const firstSel = await resolveSelector(wc, fields[0]?.index, fields[0]?.selector);
|
|
12346
|
+
if (firstSel) {
|
|
12347
|
+
const beforeUrl = wc.getURL();
|
|
12348
|
+
const submitResult = await submitForm(wc, void 0, firstSel);
|
|
12349
|
+
await waitForPotentialNavigation(wc, beforeUrl);
|
|
12350
|
+
const afterUrl = wc.getURL();
|
|
12351
|
+
results.push(
|
|
12352
|
+
afterUrl !== beforeUrl ? `Submitted → ${afterUrl}` : submitResult
|
|
12353
|
+
);
|
|
12354
|
+
}
|
|
12355
|
+
}
|
|
12356
|
+
return `Filled ${results.length} field(s):
|
|
12357
|
+
${results.join("\n")}`;
|
|
12358
|
+
}
|
|
12359
|
+
);
|
|
12360
|
+
}
|
|
12361
|
+
);
|
|
12362
|
+
server.registerTool(
|
|
12363
|
+
"vessel_login",
|
|
12364
|
+
{
|
|
12365
|
+
title: "Login",
|
|
12366
|
+
description: "Compound action: navigate to a login page, fill credentials, and submit. Handles the full login flow in one call.",
|
|
12367
|
+
inputSchema: {
|
|
12368
|
+
url: zod.z.string().optional().describe("Login page URL (skip if already on login page)"),
|
|
12369
|
+
username: zod.z.string().describe("Username or email"),
|
|
12370
|
+
password: zod.z.string().describe("Password"),
|
|
12371
|
+
username_selector: zod.z.string().optional().describe("CSS selector for username field (auto-detected if omitted)"),
|
|
12372
|
+
password_selector: zod.z.string().optional().describe("CSS selector for password field (auto-detected if omitted)"),
|
|
12373
|
+
submit_selector: zod.z.string().optional().describe("CSS selector for submit button (auto-detected if omitted)")
|
|
12374
|
+
}
|
|
12375
|
+
},
|
|
12376
|
+
async ({ url, username, password, username_selector, password_selector, submit_selector }) => {
|
|
12377
|
+
const tab = tabManager.getActiveTab();
|
|
12378
|
+
if (!tab) return asTextResponse("Error: No active tab");
|
|
12379
|
+
return withAction(
|
|
12380
|
+
runtime,
|
|
12381
|
+
tabManager,
|
|
12382
|
+
"login",
|
|
12383
|
+
{ url, username: username.slice(0, 3) + "***" },
|
|
12384
|
+
async () => {
|
|
12385
|
+
const wc = tab.view.webContents;
|
|
12386
|
+
const steps = [];
|
|
12387
|
+
if (url) {
|
|
12388
|
+
const id = tabManager.getActiveTabId();
|
|
12389
|
+
tabManager.navigateTab(id, url);
|
|
12390
|
+
await waitForLoad(wc);
|
|
12391
|
+
steps.push(`Navigated to ${wc.getURL()}`);
|
|
12392
|
+
}
|
|
12393
|
+
const userSel = username_selector || await wc.executeJavaScript(`
|
|
12394
|
+
(function() {
|
|
12395
|
+
var el = document.querySelector('input[type="email"], input[name="email"], input[name="username"], input[name="user"], input[autocomplete="username"], input[autocomplete="email"], input[type="text"]:not([name="search"]):not([name="q"])');
|
|
12396
|
+
return el ? (el.id ? '#' + CSS.escape(el.id) : el.name ? 'input[name="' + el.name + '"]' : null) : null;
|
|
12397
|
+
})()
|
|
12398
|
+
`);
|
|
12399
|
+
if (!userSel) return "Error: Could not find username/email field. Try providing username_selector.";
|
|
12400
|
+
const passSel = password_selector || await wc.executeJavaScript(`
|
|
12401
|
+
(function() {
|
|
12402
|
+
var el = document.querySelector('input[type="password"]');
|
|
12403
|
+
return el ? (el.id ? '#' + CSS.escape(el.id) : el.name ? 'input[name="' + el.name + '"]' : null) : null;
|
|
12404
|
+
})()
|
|
12405
|
+
`);
|
|
12406
|
+
if (!passSel) return "Error: Could not find password field. Try providing password_selector.";
|
|
12407
|
+
const userResult = await setElementValue(wc, userSel, username);
|
|
12408
|
+
steps.push(userResult);
|
|
12409
|
+
const passResult = await setElementValue(wc, passSel, password);
|
|
12410
|
+
steps.push(passResult);
|
|
12411
|
+
const beforeUrl = wc.getURL();
|
|
12412
|
+
if (submit_selector) {
|
|
12413
|
+
await clickResolvedSelector(wc, submit_selector);
|
|
12414
|
+
} else {
|
|
12415
|
+
const clicked = await wc.executeJavaScript(`
|
|
12416
|
+
(function() {
|
|
12417
|
+
var btn = document.querySelector('button[type="submit"], input[type="submit"], form button:not([type="button"])');
|
|
12418
|
+
if (btn) { btn.click(); return true; }
|
|
12419
|
+
var form = document.querySelector('input[type="password"]')?.closest('form');
|
|
12420
|
+
if (form) { form.requestSubmit ? form.requestSubmit() : form.submit(); return true; }
|
|
12421
|
+
return false;
|
|
12422
|
+
})()
|
|
12423
|
+
`);
|
|
12424
|
+
if (!clicked) return steps.join("\n") + "\nWarning: Could not find submit button. Credentials filled but form not submitted.";
|
|
12425
|
+
}
|
|
12426
|
+
await waitForPotentialNavigation(wc, beforeUrl);
|
|
12427
|
+
const afterUrl = wc.getURL();
|
|
12428
|
+
steps.push(
|
|
12429
|
+
afterUrl !== beforeUrl ? `Submitted → ${afterUrl}` : "Form submitted (same page)"
|
|
12430
|
+
);
|
|
12431
|
+
return `Login flow complete:
|
|
12432
|
+
${steps.join("\n")}`;
|
|
12433
|
+
}
|
|
12434
|
+
);
|
|
12435
|
+
}
|
|
12436
|
+
);
|
|
12437
|
+
server.registerTool(
|
|
12438
|
+
"vessel_search",
|
|
12439
|
+
{
|
|
12440
|
+
title: "Search",
|
|
12441
|
+
description: "Compound action: find a search box on the current page, type a query, and submit. Returns the resulting page state.",
|
|
12442
|
+
inputSchema: {
|
|
12443
|
+
query: zod.z.string().describe("Search query text"),
|
|
12444
|
+
selector: zod.z.string().optional().describe("CSS selector for search input (auto-detected if omitted)")
|
|
12445
|
+
}
|
|
12446
|
+
},
|
|
12447
|
+
async ({ query, selector }) => {
|
|
12448
|
+
const tab = tabManager.getActiveTab();
|
|
12449
|
+
if (!tab) return asTextResponse("Error: No active tab");
|
|
12450
|
+
return withAction(
|
|
12451
|
+
runtime,
|
|
12452
|
+
tabManager,
|
|
12453
|
+
"search",
|
|
12454
|
+
{ query },
|
|
12455
|
+
async () => {
|
|
12456
|
+
const wc = tab.view.webContents;
|
|
12457
|
+
const searchSel = selector || await wc.executeJavaScript(`
|
|
12458
|
+
(function() {
|
|
12459
|
+
var el = document.querySelector('input[type="search"], input[name="q"], input[name="query"], input[name="search"], input[role="searchbox"], input[aria-label*="search" i], input[placeholder*="search" i]');
|
|
12460
|
+
if (!el) {
|
|
12461
|
+
var inputs = document.querySelectorAll('input[type="text"]');
|
|
12462
|
+
for (var i = 0; i < inputs.length; i++) {
|
|
12463
|
+
var form = inputs[i].closest('form');
|
|
12464
|
+
if (form && (form.getAttribute('role') === 'search' || form.action?.includes('search'))) {
|
|
12465
|
+
el = inputs[i];
|
|
12466
|
+
break;
|
|
12467
|
+
}
|
|
12468
|
+
}
|
|
12469
|
+
}
|
|
12470
|
+
return el ? (el.id ? '#' + CSS.escape(el.id) : el.name ? 'input[name="' + el.name + '"]' : null) : null;
|
|
12471
|
+
})()
|
|
12472
|
+
`);
|
|
12473
|
+
if (!searchSel) return "Error: Could not find search input. Try providing a selector.";
|
|
12474
|
+
await setElementValue(wc, searchSel, query);
|
|
12475
|
+
await wc.executeJavaScript(`
|
|
12476
|
+
(function() {
|
|
12477
|
+
var el = document.querySelector(${JSON.stringify(searchSel)});
|
|
12478
|
+
if (el) el.focus();
|
|
12479
|
+
})()
|
|
12480
|
+
`);
|
|
12481
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
12482
|
+
const beforeUrl = wc.getURL();
|
|
12483
|
+
wc.sendInputEvent({ type: "keyDown", keyCode: "Return" });
|
|
12484
|
+
await new Promise((r) => setTimeout(r, 16));
|
|
12485
|
+
wc.sendInputEvent({ type: "keyUp", keyCode: "Return" });
|
|
12486
|
+
await waitForPotentialNavigation(wc, beforeUrl);
|
|
12487
|
+
const afterUrl = wc.getURL();
|
|
12488
|
+
return afterUrl !== beforeUrl ? `Searched "${query}" → ${afterUrl}` : `Searched "${query}" (same page — results may have loaded dynamically)`;
|
|
12489
|
+
}
|
|
12490
|
+
);
|
|
12491
|
+
}
|
|
12492
|
+
);
|
|
12493
|
+
server.registerTool(
|
|
12494
|
+
"vessel_paginate",
|
|
12495
|
+
{
|
|
12496
|
+
title: "Paginate",
|
|
12497
|
+
description: "Navigate to the next or previous page of results. Auto-detects pagination controls.",
|
|
12498
|
+
inputSchema: {
|
|
12499
|
+
direction: zod.z.enum(["next", "prev"]).describe("Pagination direction"),
|
|
12500
|
+
selector: zod.z.string().optional().describe("CSS selector for the pagination link (auto-detected if omitted)")
|
|
12501
|
+
}
|
|
12502
|
+
},
|
|
12503
|
+
async ({ direction, selector }) => {
|
|
12504
|
+
const tab = tabManager.getActiveTab();
|
|
12505
|
+
if (!tab) return asTextResponse("Error: No active tab");
|
|
12506
|
+
return withAction(
|
|
12507
|
+
runtime,
|
|
12508
|
+
tabManager,
|
|
12509
|
+
"paginate",
|
|
12510
|
+
{ direction },
|
|
12511
|
+
async () => {
|
|
12512
|
+
const wc = tab.view.webContents;
|
|
12513
|
+
const beforeUrl = wc.getURL();
|
|
12514
|
+
if (selector) {
|
|
12515
|
+
return clickResolvedSelector(wc, selector);
|
|
12516
|
+
}
|
|
12517
|
+
const isNext = direction === "next";
|
|
12518
|
+
const clicked = await wc.executeJavaScript(`
|
|
12519
|
+
(function() {
|
|
12520
|
+
var patterns = ${isNext ? '["next", "Next", "›", "»", "→", ">", "Next Page", "Load More"]' : '["prev", "Prev", "Previous", "‹", "«", "←", "<", "Previous Page"]'};
|
|
12521
|
+
var links = document.querySelectorAll('a, button');
|
|
12522
|
+
for (var i = 0; i < links.length; i++) {
|
|
12523
|
+
var el = links[i];
|
|
12524
|
+
var text = (el.textContent || '').trim();
|
|
12525
|
+
var ariaLabel = (el.getAttribute('aria-label') || '').toLowerCase();
|
|
12526
|
+
var rel = (el.getAttribute('rel') || '').toLowerCase();
|
|
12527
|
+
if (rel === '${isNext ? "next" : "prev"}') { el.click(); return true; }
|
|
12528
|
+
for (var j = 0; j < patterns.length; j++) {
|
|
12529
|
+
if (text === patterns[j] || ariaLabel.includes(patterns[j].toLowerCase())) {
|
|
12530
|
+
el.click();
|
|
12531
|
+
return true;
|
|
12532
|
+
}
|
|
12533
|
+
}
|
|
12534
|
+
}
|
|
12535
|
+
return false;
|
|
12536
|
+
})()
|
|
12537
|
+
`);
|
|
12538
|
+
if (!clicked) return `Error: Could not find ${direction} pagination control. Try providing a selector.`;
|
|
12539
|
+
await waitForPotentialNavigation(wc, beforeUrl);
|
|
12540
|
+
const afterUrl = wc.getURL();
|
|
12541
|
+
return afterUrl !== beforeUrl ? `Paginated ${direction} → ${afterUrl}` : `Clicked ${direction} (page may have updated dynamically)`;
|
|
12542
|
+
}
|
|
12543
|
+
);
|
|
12544
|
+
}
|
|
12545
|
+
);
|
|
11797
12546
|
}
|
|
11798
12547
|
function waitForLoad(wc, timeout = 1e4) {
|
|
11799
12548
|
return new Promise((resolve) => {
|
|
@@ -11843,7 +12592,11 @@ async function resolveSelector(wc, index, selector) {
|
|
|
11843
12592
|
`
|
|
11844
12593
|
);
|
|
11845
12594
|
if (typeof authoritativeSelector === "string" && authoritativeSelector) {
|
|
11846
|
-
|
|
12595
|
+
const resolves = await wc.executeJavaScript(
|
|
12596
|
+
`!!document.querySelector(${JSON.stringify(authoritativeSelector)})`
|
|
12597
|
+
);
|
|
12598
|
+
if (resolves) return authoritativeSelector;
|
|
12599
|
+
return `__vessel_idx:${index}`;
|
|
11847
12600
|
}
|
|
11848
12601
|
const page = await extractContent(wc);
|
|
11849
12602
|
const extractedSelector = findSelectorByIndex(page, index);
|
|
@@ -12396,7 +13149,8 @@ function sanitizePersistence(persisted) {
|
|
|
12396
13149
|
actions: Array.isArray(persisted?.actions) ? persisted.actions.slice(-120) : [],
|
|
12397
13150
|
checkpoints: Array.isArray(persisted?.checkpoints) ? persisted.checkpoints.slice(-20) : [],
|
|
12398
13151
|
transcript: [],
|
|
12399
|
-
mcpStatus: "stopped"
|
|
13152
|
+
mcpStatus: "stopped",
|
|
13153
|
+
flowState: null
|
|
12400
13154
|
};
|
|
12401
13155
|
}
|
|
12402
13156
|
class AgentRuntime {
|
|
@@ -12515,6 +13269,67 @@ class AgentRuntime {
|
|
|
12515
13269
|
this.emit();
|
|
12516
13270
|
return this.getState();
|
|
12517
13271
|
}
|
|
13272
|
+
// --- Speedee Flow State ---
|
|
13273
|
+
startFlow(goal, steps, startUrl) {
|
|
13274
|
+
const flow = {
|
|
13275
|
+
id: node_crypto.randomUUID(),
|
|
13276
|
+
goal,
|
|
13277
|
+
steps: steps.map((label) => ({ label, status: "pending" })),
|
|
13278
|
+
currentStepIndex: 0,
|
|
13279
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13280
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13281
|
+
startUrl
|
|
13282
|
+
};
|
|
13283
|
+
this.state.flowState = flow;
|
|
13284
|
+
this.emit();
|
|
13285
|
+
return clone(flow);
|
|
13286
|
+
}
|
|
13287
|
+
advanceFlow(detail) {
|
|
13288
|
+
const flow = this.state.flowState;
|
|
13289
|
+
if (!flow) return null;
|
|
13290
|
+
const step = flow.steps[flow.currentStepIndex];
|
|
13291
|
+
if (step) {
|
|
13292
|
+
step.status = "done";
|
|
13293
|
+
step.detail = detail;
|
|
13294
|
+
}
|
|
13295
|
+
flow.currentStepIndex = Math.min(flow.currentStepIndex + 1, flow.steps.length);
|
|
13296
|
+
flow.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
13297
|
+
this.emit();
|
|
13298
|
+
return clone(flow);
|
|
13299
|
+
}
|
|
13300
|
+
failFlowStep(detail) {
|
|
13301
|
+
const flow = this.state.flowState;
|
|
13302
|
+
if (!flow) return null;
|
|
13303
|
+
const step = flow.steps[flow.currentStepIndex];
|
|
13304
|
+
if (step) {
|
|
13305
|
+
step.status = "failed";
|
|
13306
|
+
step.detail = detail;
|
|
13307
|
+
}
|
|
13308
|
+
flow.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
13309
|
+
this.emit();
|
|
13310
|
+
return clone(flow);
|
|
13311
|
+
}
|
|
13312
|
+
getFlowState() {
|
|
13313
|
+
return this.state.flowState ? clone(this.state.flowState) : null;
|
|
13314
|
+
}
|
|
13315
|
+
clearFlow() {
|
|
13316
|
+
this.state.flowState = null;
|
|
13317
|
+
this.emit();
|
|
13318
|
+
}
|
|
13319
|
+
getFlowContext() {
|
|
13320
|
+
const flow = this.state.flowState;
|
|
13321
|
+
if (!flow) return "";
|
|
13322
|
+
const progress = flow.steps.map((s, i) => {
|
|
13323
|
+
const marker = s.status === "done" ? "✓" : s.status === "failed" ? "✗" : s.status === "skipped" ? "-" : i === flow.currentStepIndex ? "→" : " ";
|
|
13324
|
+
const detail = s.detail ? ` (${s.detail})` : "";
|
|
13325
|
+
return `[${marker}] ${s.label}${detail}`;
|
|
13326
|
+
}).join("\n");
|
|
13327
|
+
return `
|
|
13328
|
+
--- Active Flow ---
|
|
13329
|
+
Goal: ${flow.goal}
|
|
13330
|
+
${progress}
|
|
13331
|
+
---`;
|
|
13332
|
+
}
|
|
12518
13333
|
async runControlledAction({
|
|
12519
13334
|
source,
|
|
12520
13335
|
name,
|